C++基础知识[1]——智能指针

自动资源释放

  考虑下面代码:

void f()
{
    vector<string> v;
    string s;

    while(cin >> s)
        v.push_back(s);

    string *p = new string[v.size()];
    
    // remaining processing
    
    delete [] p;
}

  在正常执行的情况下,函数中 delete 语句释放数组,函数结束时自动撤销 vector。

  但如果函数内部发生异常,delete 语句执行前程序就停止执行,那么数组将不会被释放。除此之外,漏写 delete 也是很常见的情况。而不管什么异常,都保证能运行 vector 析构函数。

  故一种使程序更为异常安全的的技术出现了,即"即使发生异常,程序也能正确操作"。在这个例子中即"如果发生异常,被分配的任何资源都适当地释放"。

方法一 try-catch

void f()
{
    vector<string> v;
    string *p = new string[v.size()];
    
    try{
        // remaining processing
        // excepion occurs here
    }
    catch{
        delete [] p;
    }
    
    // ...
}

方法二 设计资源管理类

class Resource{
private:
    string *r;
public:
    Resource(size_t string_size): r(new string[string_size]) {}
    ~Resource() {if(r) delete [] r;}
}

void f()
{
    vector<string> v;
    Resource res(v.size();)
    // excepion occurs here

    // ...
}

智能指针

  对于编译器来说,智能指针实际上是一个栈对象,并非指针类型,在栈对象生命期即将结束时,智能指针通过析构函数释放有它管理的堆内存。所有智能指针都重载了 operator-> 操作符,直接返回对象的引用,用以操作对象。访问智能指针原来的方法则使用 . 操作符。

  访问智能指针包含的裸指针则可以用 get() 函数。由于智能指针是一个对象,所以 if(smart_ptr) 永远为真,要判断智能指针的裸指针是否为空,正确的方式是:if(smart_ptr.get())

  智能指针包含了 reset() 方法,如果不传递参数(或者传递 NULL),则智能指针会释放当前管理的内存。如果传递一个对象,则智能指针会释放当前对象,来管理新传入的对象。

  PS:相关的智能指针在 memory 头文件中定义,在 std 命名空间中。

auto_ptr

  auto_ptr 是一个接受类型形参的模板。

  auto_ptr 对象只能保存一个指向对象的指针,并且不能用于指向动态分配的数组。

  需要注意 auto_ptr 在复制和赋值的时候有特殊的行为(会将所有权转移,是破坏性操作,所以也不能存储在容器中)。

  样例:

auto_ptr<string> ap1(new string("Hello"));
ap1.reset(new string("Hello"))  //如果之前有指向的对象,会先删除再绑定新的
auto_ptr<string> ap2 = new string("World"); //error,必须直接初始化
auto_ptr<string> ap2 = ap1; //将所有权从ap2转给ap1,并且使ap1变为未绑定状态
ap2.reset(ap1); //同上一条
ap1.get();  //返回裸指针
ap1->c_str();   //使用对象

  另外 auto_ptr 在 C++11 中已被弃用,不推荐使用。

warning: 'template<class> class std::auto_ptr' is deprecated [-Wdeprecated-declarations]

  在下面的例子中,p2 夺取了 p1 的内存管理权,导致 p1 悬空,最后导致崩溃。

std::auto_ptr<string> p1(new string("Hello"));
if (p1.get()) {
    std::auto_ptr<string> p2;
    p2 = p1;
    p2->c_str();
    p1->c_str();    //crash here
}

  在下面的例子中,调用 release 后对象没有被析构,反而导致了内存泄漏。

//error
std::auto_ptr<string> p1(new string("Hello"));
if (p1.get()) {
    p1.release();
}

//right
std::auto_ptr<string> p1(new string("Hello"));
if (p1.get()) {
    string *temp = p1.release();
    delete temp;    //手动释放
}

//right
std::auto_ptr<string> p1(new string("Hello"));
if (p1.get()) {
    p1.reset();     //自动释放
}

shared_ptr

  shared_ptr 允许多个指针指向同一对象。

shared_ptr<int> p1 = make_shared<int>(42);
auto p2 = make_shared<int>();
shared_ptr<int> p3(new int(23));
shared_ptr<int> p4(p1, func_d); //p4将调用func_d来代替delete
shared_ptr<int> p5(u); //u为unique_ptr,p5接管u的所有权后将u置空

if(!p1.unique()){
    p1.reset(new int(243));
    *p1 += val; //自己是唯一用户,可以改变对象的值
}

  当 shared_ptr 被销毁时,将递减其引用计数并检查它是否为0,以决定是否销毁所指向的对象及释放内存。

shared_ptr<int> func(T arg){
    shared_ptr<int> p = make_shared<int>(123);
    //...
    return p;   //返回p时,引用计数递增
}   //p离开作用域,但他指向的内存不会被释放

  PS:最好不要混合使用普通指针和智能指针,原因如下:

//program 1
void process(shared_ptr<int> ptr){
    //use ptr
}   //离开作用域,ptr被销毁

int *x(new int(123));
process(x); //error 无法将 int* 转化为 shared_ptr<int>
process(shared_ptr<int>(x));    //合法,但内存会被释放
int a = *x; //error 未定义。此时x指向的空间已被释放,是一个空悬指针
//program 2
shared_ptr<int> p(new int(123));
int *x = p.get();
{//新程序块
    shared_ptr<int>(q);
}//程序块结束,q被销毁,指向的内存也被释放
int a = *x; //error 未定义。x指向的内存已被释放
//另外整个程序块结束时,p销毁时这块内存会被二次delete

  指向动态数组的 shared_ptr。与 unique_ptr 不同,shared_ptr 不直接支持管理动态数组,必须提供自定的删除器。

shared_ptr<int> sp(new int[10], [](int *p){delete[] p;});

for(size_t i = 0; i < 10; ++i)
    *(sp.get() + i) = i;

unique_ptr

  一个 unique_ptr “拥有"它所指向的对象。某个时刻只能有一个 unique_ptr 指向一个给定对象。

  与 shared_ptr 不同的是,没有类似 make_shared 的标准库函数返回一个 unique_ptr。定义时必须绑定到一个 new 返回的指针上。

  PS:unique_ptr 不支持普通的拷贝或赋值。

unique_ptr<string> p1(new string("Hello"));
unique_ptr<string> p2(p1);  //error
unique_ptr<string> p3 = p1; //error

unique_ptr<string> p4(p1.release());
p4.reset(p1.release());

p1.release();   //error p1不会释放内存,且丢失了指针
auto p = p1.release();  //但须记得 delete(p)

//但有一个例外:可以拷贝或赋值一个将要被销毁的unique_ptr
unique_ptr<int> clone(int p){
    return unique_ptr<int>(new int(p));
}

unique_ptr<int> clone(int p){
    unique_ptr<int> ret(new int(p));
    return ret;
}

  但向 unique_ptr 传递删除器的方式与 shared_ptr 不同。

void f(destination &d){
    connection c = connect(&d);
    unique_ptr<connection, decltype(end_connection)*> p(&c, end_connection);
}

  指向动态数组的 unique_ptr

unique_ptr<int[]> up(new int[10]{});

for(size_t i = 0; i < 10; ++i)
    up[i] = i;

weak_ptr

  weak_ptr 是一种不控制所指向对象生存期的智能指针,指向一个由 shared_ptr 管理的对象。将一个 weak_ptr 绑定到 shared_ptr 不会改变 shared_ptr 的引用计数。一旦最后一个指向对象的 shared_ptr 被销毁,对象就会被释放。

auto p = make_shared<int>(123);
weak_ptr<int> wp(p);

if(shared_ptr<int> sp = wp.lock()){
    //在if中sp和wp共享对象
    (*sp)++;
    //...
}

参考资料:

《C++ Primer 中文版 第4版》
《C++ Primer 中文版 第5版》
C++ 智能指针详解
详解C++11智能指针