Skip to the content.

从C++的RAII理解智能指针的思路(一)


Contact me:

Blog -> https://cugtyt.github.io/blog/index
Email -> cugtyt@qq.com
GitHub -> Cugtyt@GitHub


假设我们有如下代码:

class MyClass;

void fun() {
    MyClass mc;
    // do sth. with mc
}

在fun函数结束的时候mc还存在吗?当然,这是c++的基本知识,我们创建mc的时候调用构造函数,离开作用域就调用析构函数,所以mc已经不存在了。

void fun() {
    int* iarr = new int[10];
    // do sth. wirh iarr
}

在fun函数结束的时候iarr还在吗?当然,iarr不在了,可是它申请的空间还是没有释放,这造成了内存泄漏。

在C++中,这种在生命周期结束时释放资源的方法被称作资源获取即初始化(Resource Acquisition Is Initialization (RAII))。我们能不能把这个特性用到资源管理上呢?

class Unique {
public:
    Unique(int n) {
        std::cout << "Unique" << std::endl;
        ptr = new int[n];
    }
    ~Unique() {
        std::cout << "~Unique" << std::endl;
        delete []ptr;
    }
    // other func
private:
    int *ptr;
};

int main() {
    Unique u1(10);
    // do sth. with u
}

Output:
    Unique
    ~Unique

这样一个最为粗糙和简陋的管理方法就出现了,在函数结束的时候内存也释放了,当然我们拿int数组作为例子。很好,但是只能适用简单情况,很多问题不能处理,比如,我们不能随意把ptr暴露出来。如果外面获取了ptr,把内存释放掉了,那么等我们释放的时候就是第二次释放了,会出问题的。同样也不能实现内存共享,对象复制也受到影响等。

Unique u1{10};
Unique u2 = u1;     // !!!

Output:
    Unique
    ~Unique
    ~Unique

上面的代码会造成两个指针同时指向一片区域,调用构造函数就二次释放了,我们需要避免它。我们可以简单粗暴的把赋值构造函数和拷贝构造函数delete就可以解决意外复制的情况:

class Unique {
public:
...
    Unique(const Unique&) = delete;
    Unique& operator=(const Unique&) = delete;
...
}

这样,没有了拷贝,如果我们为了需要转移所有权,可以在函数里面写入对指针的判断:

class Unique {
public:
    Unique(int n) {
        std::cout << "Unique" << std::endl;
        ptr = new int[n];
    }
    Unique(const Unique& u) {
        this->ptr = u.ptr;
        u.ptr = std::nullptr;
    }
    Unique& operator=(const Unique&) {/*与上面函数一样*/}
    ~Unique() {
        std::cout << "~Unique" << std::endl;
        if (ptr) {
            delete []ptr;
        }
    }
    // other func
private:
    int *ptr;
};

int main() {
    Unique u1(10);
    Unique u2(u1);
}

虽然两次析构函数都会调用,但是内存正确释放一次,我们实现了unique_ptr的基本思路。注意这是模型,仅供理解,和c++内部实现并不一样。

那么shared_ptr,weak_ptr呢,后面我们继续讨论。