内存控制这一大毒瘤,几乎一直伴随着C/C++工程师。随着计算机技术的发展,在boost准标准库的推动下,C++11终于将unique_ptr、shared_ptr、weak_ptr这几类智能指针纳入C++中。当然,在这之前还有一种auto_ptr智能指针,不过由于它的设计存在较大问题(比如:auto_ptr与STL不兼容),现阶段已经很少能看到它的出现了。
感谢@毛毛是我的小可爱 在评论中指出“ 标题说的还是C++14,文章开头就跑出C++11。请严谨一点!”。
这里说明一下,(1)上文中提到C++11
出现了智能指针unique_ptr、shared_ptr、weak_ptr,(2)但是文中的代码示例使用了make_unique()
和std::move()
,这些为C++14
中的内容。
智能指针实质是一个对象,行为表现的却像一个指针。
shared_ptr和unique_ptr之间的区别在于:shared_ptr是引用计数的智能指针,而unique_ptr不是。这意味着,可以有多个shared_ptr实例指向同一块动态分配的内存,当最后一个shared_ptr离开作用域时,才会释放这块内存。shared_ptr也是线程安全的。 另一方面,unique_ptr意味着所有权。单个unique_ptr离开作用域时,会立即释放底层内存。
2018-2-28更新 :感谢@奔跑的哇牛 在留言中说到shared_ptr
本身不是线程安全的。是的,shared_ptr
本身不是线程安全的。陈硕的书中也明确提到了,shared_ptr
的计数功能是原子的,但对象的读写不是原子的。c++
标准也只是保证的是weak_ptr
的lock()
指针提升是线程安全的。所以,要实现线程安全,可能需要weak_ptr
与shared_ptr
配合使用,详见陈硕的多线程书籍。
也可以参考:http://www.pandademo.com/2017/08/thread-safety-of-shared_ptr-and-weak_ptr/
默认的智能指针应该是unique_ptr。只有需要共享资源时,才使用shared_ptr。
这两个智能指针都需要包含
头文件。
在开始本文之前,首先给出一个类。因为下文中,为了演示智能指针的使用方式,在较多时候都有用到这个类demo。
#include
#include //std::move()
class demo {
public:
demo() : uptr(std::make_unique<int[]>(10)){
printf("demo\n");
for (int i = 0; i < 10; ++i){
uptr[i] = i;
}
}
~demo(){
printf("~demo\n");
}
void show(){
printf("%d\n", uptr[9]);
}
private:
std::unique_ptr<int[]> uptr;
};
unique_ptr是唯一的,适用于存储动态分配的旧C风格的数组。auto关键字会自动识别指针类型,当与make_unique配合使用时,即表示unique_ptr智能指针。
void my_unique_ptr(){
auto uptr = std::make_unique<int[]>(10);
uptr[5] = 17;
printf("%d\n", uptr[5]);
}
这里应该总是使用auto/make_unique写法。除非编译器不支持的情况下,可以这样写,
std::unique_ptr<int[]> uptr(new int[10]);
//std::unique_ptr uptr = new int[10];//error
注意,上文中,被注释的那种写法是不被接受的。再整理一下,unique_ptr可以有以下使用方式,
void my_unique_ptr2(){
auto uptr = std::make_unique<demo>();
uptr->show();
std::unique_ptr<demo> uptr2(new demo());///等效写法
uptr2->show();
}
另外,需要说明的是,unique_ptr无法使用拷贝构造函数的,上文已经提到过了,这里再给出一个示例,
void my_unique_ptr3(){
auto uptr = std::make_unique<int>(42);
printf("%d\n", *uptr);//42
///std::unique_ptr uptr1 = uptr; ///unique_ptr无拷贝构造函数
///std::unique_ptr uptr1(uptr); ///等效写法
std::unique_ptr<int> uptr2 = std::move(uptr);
///printf("%d\n",*uptr); ///error 所有权已交给uptr2
printf("%d\n", *uptr2);
}
既然,无法使用拷贝构造函数,那么就无法直接使用赋值“=”来转移指针所有权。但是C++14设计者给开了另外一扇门:std::move。它被包含在
头文件中。
shared_ptr用法与unique_ptr类似。如果编译器支持,你应该总是使用auto/make_shared的写法,它比直接创建shared_ptr更高效。
void my_shared_ptr(){
///auto sptr = std::make_shared(10); //error
///sptr[6] = 20;
auto sptr = std::make_shared<demo>();
sptr->show();
}
上文中已经提到了,智能指针家族中,unique_ptr是唯一可以适用于旧C风格数组的指针,shared_ptr等其他智能指针不能。
shared_ptr除了用于管理纯粹的内存之外还可以用于其他的目的,比如管理FILE、SOCKET等,极大的增加了编程的方便性。
void auto_run_fun(FILE* f){
printf("auto running.\n");
fclose(f);
}
void my_shared_ptr2(){
FILE* f = fopen("data.txt","w");
std::shared_ptr<FILE> file_ptr(f, auto_run_fun);
}
由于shared_ptr是引用计数的,这里需要极为注意的一点是:糟糕!只调用一次构造函数,却调用了两次析构函数。
正确的使用方式应该是使用make_shared和拷贝构造函数建立副本。范例如下,
void my_shared_ptr3(){
/*demo* d = new demo();
std::shared_ptr sptr1(d);
std::shared_ptr sptr2(d);//~demo() error
*/
auto sptr3 = std::make_shared<demo>();
std::shared_ptr<demo> sptr4(sptr3); ///shared_ptr拷贝构造函数
}
shared_ptr引用计数,完全可以返回一个子函数的指针。 在以往的认知中,子函数中的栈空间上的内存是无法返回的,而子函数中堆空间上的内存是可以返回的(同时,还必须注意手动释放它,否则必然内存泄漏)。
std::shared_ptr<demo> my_shared_ptr4(){
auto sptr = std::make_shared<demo>();
return sptr;
}
那么,对于以上代码的返回值,下文中这样子使用它,也是非常正确的。
my_shared_ptr4()->show();
智能指针,完全继承了JAVA和C#中内存托管的风格,而且智能指针在很多情况下完全可以具体推算出:它会在何时被释放。
关于shared_ptr更多使用方式,推荐《C++14 N叉树使用shared_ptr智能指针》 一文。
weak_ptr是shared_ptr的黄金伙伴。从上文知道shared_ptr与shared_ptr之间,每拷贝一次,引用计数就会+1,而如果使用weak_ptr则不会出现这个现象。
如果将一个shared_ptr指针赋值给weak_ptr指针,对shared_ptr指针本身不会造成任何影响。对于weak_ptr指针来说,却可以通过一些方法来探测被赋值过来的shared_ptr指针的有效性,同时weak_ptr指针也可以间接操纵shared_ptr指针。以下主要介绍两个方法:
///weak_ptr -> shared_ptr
void my_weak_ptr(){
std::weak_ptr<demo> wptr;
{
auto sptr = std::make_shared<demo>();
wptr = sptr;
auto sptr2 = wptr.lock();
if (!wptr.expired()){///等价于sptr2 != nullptr
printf("shared_ptr ok\n");
sptr2->show();
}
}
if (wptr.expired()){
printf("shared_ptr deleted\n");
}
}
以上代码中,之所以要加个大括号{}在文中,主要是:为了利用变量的作用域原理。 让shared_ptr指针离开{}作用域后,立即被释放。
参考文献:
Marc, Gregoire. C++高级编程(第3版)[M]. 北京:清华大学出版社, 2015. 630-636