C++智能指针学习

智能指针简介

犹记得在学校期间学习C++ 时,老师千叮咛万嘱咐一定要注意资源的分配和释放,new 和 delete 一定要配对出现,因为这里很容易出现内存泄漏的问题,而且还会有野指针的问题。当new 和 delete 完全配对出现时,如果程序过早退出,又需要在每一个退出点做资源释放的delete 操作。

C++ 98 时开始出现智能指针auto_ptr,后来C++11 中将auto_ptr 又给废弃掉了,取而代之的是unique_ptr、shared_ptr 和 weak_ptr。

智能指针介绍

auto_ptr (官方文档

class Test {
public:
    Test(int value = 0) : m_value(value) {}
    ~Test() { cout << "Enter in destructor.\n"; }

public:
    int m_value;
};

void fun() {
    auto_ptr<Test> ptr(new Test(1));
    if (1 == ptr->m_value) {
        return;
    }
}

int main() {
    fun();
    cout << "Will exit main function.\n";
    return 0;
}

从这个示例中可以看出,在fun() 退出时,ptr 在离开作用域时调用析构函数自动释放了,而没有出现普通指针的内存泄漏问题。从这个示例看,auto_ptr 还是很方便的。

但是auto_ptr 之所以在C++11 中被抛弃,还是有很多先天的缺陷的。

缺陷一:

当把一个auto_ptr 赋值给另一个auto_ptr 时,它的所有权(ownship) 也就转移了,原先的auto_ptr 就不再掌控它所分配的内存空间了。

void fun() {
    auto_ptr<Test> ptr1(new Test(1));
    auto_ptr<Test> ptr2 = ptr1;
    cout << ptr2->m_value << endl;
    cout << ptr1->m_value << endl;  // Access violation reading location
}

这里访问ptr1 就出错了。

缺陷二:

auto_ptr 不能指向一组对象,即auto_ptr 内部使用的是delete,而非delete[]。

void fun() { 
	auto_ptr<Test> ptr1(new Test[5]);
}

这里就会出错,出错的地方是:

~auto_ptr() _NOEXCEPT
{	// destroy the object
	delete _Myptr;
}
缺陷三:

auto_ptr 与标准容器(vector, map …) 不兼容。

unique_ptr (官方文档

unique_ptr 的出现主要是为了替代C++98 的auto_ptr。正如其名字所示,资源智能唯一地被一个unique_ptr 占有,当离开作用域时,所包含的资源释放。

对于auto_ptr 的缺陷一,

void fun() {
    unique_ptr<Test> ptr1(new Test(1));
    unique_ptr<Test> ptr2 = ptr1;
    cout << ptr2->m_value << endl;
    cout << ptr1->m_value << endl;
}

这里会编译出错,因为unique_ptr 不提供复制语义,无论是拷贝构造还是赋值;但是它提供了移动语义(move semantics),当然move后,原unique_ptr 就不再占有资源了。

注:

unique_ptr<Test> fun() {
    unique_ptr<Test> ptr1(new Test(1));
    return ptr1;
}

对于上文的sample,无论是auto_ptr 还是unique_ptr,都是ok的,因为这里符合保持唯一占有资源的原则。

对于auto_ptr 的缺陷二,unique_ptr 提供了创建一组对象的特殊功能,

void fun() { 
	unique_ptr<Test> ptr1(new Test[5]);
}

这里就不会出现问题。

要注意官方文档中的release 接口,该接口并不释放资源,而是将所有权放弃掉,那么如果调用了release 接口,一定要确保该资源在其他地方释放。

shared_ptr (官方文档

shared_ptr 名如其名,它允许多个shared_ptr 共享同一块内存。即多个指针可以同时指向一个对象,当最后一个指针离开其作用域时,内存就会自动释放了。

shared_ptr 是采用计数机制来标记资源被多少个指针所共享。从shared_ptr 的官方文档中可以看到它提供的接口就比上面两个智能指针多了好多。

例如在创建shared_ptr时,除了普通的new 以为,还可以通过make_shared 的方式更高效的创建shared_ptr。

void fun() {
    shared_ptr<Test> ptr1(new Test(1));
    shared_ptr<Test> ptr2 = make_shared<Test>(2);
}

在创建shared_ptr 时,可以发现shared_ptr 并未像unique_ptr 那样提供创建一组对象的方法,那么是不是shared_ptr 就不能创建一组对象呢?

shared_ptr 可以自定义资源释放的方法,如果需要创建一组对象时,就必须要传递自定义资源释放方法,不然程序就会crash 掉。常见的方法是使用lamda 表达式去定义自定义资源释放的方法。

void fun() {
    shared_ptr<Test> ptr1(new Test[5], [](Test* p) { delete[] p; });
}

shared_ptr 非常方便,但使用时也必须非常小心。

主要注意事项:

  1. 不要滥用shared_ptr,如果是唯一指针,尽量用unique_ptr,即是性能的考虑,也防止被未知的共享出去
  2. shared_ptr 并不是线程安全的,不要想当然以为是共享资源就自然可以用于多线程
  3. 尽量使用make_shared 来创建初始化shared_ptr
  4. 使用delete 删除shared_ptr 使用的裸指针(通过get 获得的指针)
  5. 指向指针数组时没有使用自定义的删除方法
  6. 使用了循环引用,那么就无法释放资源了(死锁)

weak_ptr (官方文档

上文中提到了shared_ptr 如果出现循环引用,就会出现死锁问题。而weak_ptr 的出现就是为了解决该问题的。但是weak_ptr 非常麻烦。

weak_ptr 不能独立存在,需要绑定一个shared_ptr,但是它既不拥有该内存空间,也不会改变shared_ptr 的引用计数。当shared_ptr 指向的内存不再有效后,对应的weak_ptr 也就为空了。

weak_ptr 通过lock 接口将weak_ptr 升级为shared_ptr,这样就可以像shared_ptr一样使用该内存。

通过一个sample 看一下。

常见shared_ptr 死锁:

class ClassB;

class ClassA {
public:
    ClassA() { cout << "ClassA Constructor..." << endl; }
    ~ClassA() { cout << "ClassA Destructor..." << endl; }
    shared_ptr<ClassB> pb;
};

class ClassB {
public:
    ClassB() { cout << "ClassB Constructor..." << endl; }
    ~ClassB() { cout << "ClassB Destructor..." << endl; }
    shared_ptr<ClassA> pa;
};

void fun() {
    shared_ptr<ClassA> spa = make_shared<ClassA>();
    shared_ptr<ClassB> spb = make_shared<ClassB>();
    spa->pb = spb;
    spb->pa = spa;
}

int main() {
    fun();
    cout << "Will exit main function.\n";
    return 0;
}

这个sample 就不会释放资源。如果使用weak_ptr,fun 运行结束会自动释放资源。

class ClassB;

class ClassA {
public:
    ClassA() { cout << "ClassA Constructor..." << endl; }
    ~ClassA() { cout << "ClassA Destructor..." << endl; }
    weak_ptr<ClassB> pb;
};

class ClassB {
public:
    ClassB() { cout << "ClassB Constructor..." << endl; }
    ~ClassB() { cout << "ClassB Destructor..." << endl; }
    weak_ptr<ClassA> pa;
};

void fun() {
    shared_ptr<ClassA> spa = make_shared<ClassA>();
    shared_ptr<ClassB> spb = make_shared<ClassB>();
    spa->pb = spb;
    spb->pa = spa;
}

int main() {
    fun();
    cout << "Will exit main function.\n";
    return 0;
}

智能指针性能

智能指针是C++ RAII 的一种应用,从性能上一般的理解是要比普通指针差些,但是实际上unique_ptr 的性能与普通指针的性能差异非常小;而shared_ptr 因为引入了引用计数的原因,性能相对而言稍大(大约是普通指针的两倍),但是对于一般使用,这些性能差异非常小。尤其是unique_ptr 更是与普通指针没有什么区别。