如果你熟悉 Java 或者 C# ,那么以下代码你肯定不陌生
class Students { public int value = 0; } void main() { Students a = new Students(); Students b = a; a.value = 10; //此时,变量 a 和变量 b 的 value 值都是 10 }
很简单,生成了一个Students对象,变量 a 和变量 b 都是对该对象的引用;但是在C++ 中,结果并不是这样
class Students { public: int value = 0; }; void main() { Students a; Students b = a; a.value = 10; //此时,变量 a 的 value 是10,变量 b 的 value 仍是 0 }
编译器会为 class 默认生成 copy constructor 和 assignment operator,C++ 对象默认是可以拷贝的 ,因此 b = a,相当于拷贝了一份全新的对象。那要怎么实现类似Java 或 C# 的效果呢?C++ 就发明了指针这么个东西,它就是指向对象实际存储在内存中的地址,用法如下:
int main() { Students* a = new Students(); Students* b = a; a->value = 5; //此时,变量 a 和 b 的value 都是 5 //delete a; }
new做的事情就是,调用底层 malloc 获取 Students 类型大小的内存地址(一般会对字节补齐),然后调用 Students 的构造函数(如果没有,编译器会自动补全空构造),返回该对象的内存地址。从此,内存上的这一块地址就永远被占用,除非调用 delete 函数释放。但如果 delete 过早,其他函数还在用这个指针,就可能导致程序崩溃
void foo(Students* ptr) { ptr->value = 10; //crash } void main() { Students* ptr = new Students(); delete ptr; foo(ptr); }
使用裸指针(raw pointer)最大的问题就是,你不知道它当前的所有权(ownership)属于谁,因此也就很难判断应该在哪个位置 delete 它。C++ 编委会的巨佬们本着为了让程序员少掉头发的原则,设计了一个封装指针的类,同时赋予它一个响亮的名字: 智能指针( Smart Pointers )
既然也叫 “指针”,那么就要求它用起来跟普通指针没区别。因此这个封装裸指针的类重载了很多操作符,比如:* ,& ,bool ,== 等。针对各种使用场景,一共设计了三种智能指针类 :
- std::unique_ptr(single ownership)
- std::shared_ptr (shared ownership)
- std::weak_ptr ( no ownership ,与 shared_ptr 伴生关系)
针对上面会崩溃的问题,我们代码改下如下:
void foo(std::unique_ptr<Students> ptr) { ptr->value = 10; //no crash } void main() { auto ptr =std::make_unique<Students>(); foo(std::move(ptr)); } // no memory leak
崩溃解决,同时也不会造成内存泄漏。构造 unique_ptr 的官方推荐的写法是: auto ptr = std::make_unique<class>(); 转移 ownership 的写法是 std::move(ptr); 为了能更详细的理解,我们来写个小实例,并打印相关讯息:
class Students { public: Students(){ std::cout << "Students初始化创建" << std::endl; } ~Students() { std::cout << "Students析构销毁" << std::endl; } }; int foo(std::unique_ptr<Students> ptr2) { std::cout << "进入foo函数" << std::endl; std::cout << "退出foo函数" << std::endl; } int main() { std::cout << "进入main函数" << std::endl; //注释段1 //{ //std::cout << "进入main函数scope1" << std::endl; //auto ptr1 = std::make_unique<Students>(); //std::cout << "退出main函数scope1" << std::endl; //} //注释段2 //auto ptr2 = std::make_unique<Students>(); //foo(std::move(ptr2)); std::cout << "退出main函数" << std::endl; }
打开注释段1,打印如下左边;打开注释段2,打印如下图右边

可以看出 ptr1 的 ownership 属于 block scope,scope代码执行完以后, ptr1 自动执行析构函数;ptr2 的 ownership 一开始属于 main 函数, 通过 std::move 转移给 foo 函数,因此一旦 foo 函数生命周期结束,ptr2 也会自动执行析构函数
看似很完美,然而 unique 的意思是唯一,也就是说这种智能指针的 ownership 只能唯一,因此如下代码,就会崩溃:
void foo(std::unique_ptr<Students> ptr) { ptr->value = 10; } void bar(std::unique_ptr<Students> ptr) { ptr->value = 10; //ptr is empty, crash } int main() { auto ptr = std::make_unique<Students>(); foo(std::move(ptr)); bar(std::move(ptr)); }
原因是什么呢?std::move(ptr) 的意思就是把 ptr 的 ownership 从 main 函数转移给 foo 函数,转移后 ptr 也就没有 ownership了,自动析构了; 针对这种情况,我们就需要用到 shared_ptr , 使用规范如下:

跟 unique_ptr 相比,多了一个use_count。因为它就是用来记录,当前有多少个 ownership,为什么要记录个数呢?因为只有确定它最后一个 ownership 的生命周期结束以后,它才能自动析构。针对上面那个问题,我们用shared_ptr改写:
void foo(const std::shared_ptr<Students> ptr) { puts("enter foo"); std::cout << ptr.use_count() << std::endl; puts("exit foo"); } int main() { puts("enter main"); const auto ptr = std::make_shared<Students>(); std::cout << ptr.use_count() <<std::endl; { puts("enter scope"); auto ptr_scope1 = ptr; std::cout << ptr.use_count() << std::endl; auto ptr_scope2 = std::move(ptr_scope1);//transfer ownership std::cout << ptr.use_count() << std::endl; puts("exit scope"); } std::cout << ptr.use_count() << std::endl; foo(ptr); std::cout << ptr.use_count() << std::endl; puts("exit main"); }
打印结果如下:

也许下面这张图,你能理解的更透彻

shared_ptr 的 use_count 的增减做了防 data_race ,因此它默认支持多线程。目前来看,unique_ptr 和 shared_ptr 好像可以解决所有问题,为什么还需要一个 weak_ptr 呢?那么来看下面的情况:
class Students { public: Students() { puts("Students create"); } ~Students() { puts("Students destroy"); } std::shared_ptr<Students> next; //std::weak_ptr<Students> next; }; int main() { auto ptr_1 = std::make_shared<Students>(); auto ptr_2 = std::make_shared<Students>(); ptr_1->next = ptr_2; ptr_2->next = ptr_1; std::cout << ptr_1.use_count() << std::endl; std::cout << ptr_2.use_count() << std::endl; } //print: //Student create //Student create //2 //2
哦豁,并没有打印 Student destroy;也即是说出现了 memory leak;智能指针失灵了?问题就出在赋值的那两句代码导致 use_count = 2,main函数退出后,use_count 仍然为1,因此不会执行析构。因此如果有一种指针,我想用shared_ptr,但又不想它 use_count 有变化,只是在用的时候,检查一下它还在不在,那不就完美了吗?因此,上面的注释打开,就能正确析构。详细用法如下:
auto ptr = std::make_shared<Students>(); std::cout << ptr.use_count() << std::endl; { //必须从 shared_ptr 构造(不会增加 shared_ptr 的 use_count) std::weak_ptr<Students> ptr_w = ptr; //必须从weak_ptr 重新转换成 shared_ptr 再使用 //ptr的 use_cunt + 1 std::shared_ptr<Students> ptr_w2s = ptr_w.lock(); if(ptr_w2s) { std::cout << ptr.use_count() << std::endl; } } std::cout << ptr.use_count() << std::endl; //print: 1 2 1
打印结果与预期的结果一致。因此,很多时候,为了避免循环引用,一般shared_ptr 和 weak_ptr 配合使用。官方给出的使用建议是:
- 尽量使用智能指针替代裸指针
- 优先使用std::unique_ptr 而不是 shared_ptr (可以想想为什么?)
- 尽可能 std::move(share_ptr)而不是直接 foo(share_ptr) (理由同上)
内容参考: