写在前面 在本次系列课程开始之前,请注意以下事项:
重要 完全摒弃C-Style编程思维,将现代化C++当作一门全新的编程语言
本次课程所有的实验代码基于C++14,这也是综合实用性 和兼容性 ,总体相对均衡的选择
下面正式开始
何为智能指针 相比传统裸指针需开发者手动申请、释放的背景下,智能指针基于C++类的构造、析构函数(RAII范式)机制,管理内存的生命周期,无需开发者介入管理(或者说低介入)指针的申请、释放。
C++14的标准库memory提供了三种推荐使用的智能指针: std::shared_ptr: 共享指针,基于引用计数的内存生命周期管理
std::unique_ptr: 独享指针,禁止拷贝,可通过std::move的方式转移指针所有权
std::weak_ptr: 与shared_ptr搭配使用,不增加其引用计数
下面就Foo对象,作为代表类,阐述智能指针的使用方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <iostream> #include <memory> class Foo {public : Foo () { std::cout << "Foo construct." << std::endl; } ~Foo () { std::cout << "Foo desctruct." << std::endl; } void DoSomething () { std::cout << "Foo DoSomething" << std::endl; } };
智能指针可以像普通裸指针一样访问。
1 2 3 4 5 6 7 8 9 10 std::shared_ptr<Foo> f (new Foo) ;if (f != nullptr ) { std::cout << "f is not nullptr" << std::endl; } f->DoSomething (); auto &raw = *f;raw.DoSomething ();
Foo construct.
f is not nullptr
Foo DoSomething
Foo DoSomething
std::shared_ptr std::shared_ptr指针的构造方式有: 1 2 3 4 5 6 7 std::shared_ptr<Foo> ptr0; std::shared_ptr<Foo> ptr1 (new Foo()) ;std::shared_ptr<Foo> ptr2 = std::make_shared <Foo>();
Foo construct.
Foo construct.
std::shared_ptr引用计数 std::shared_ptr本质上是在内部维护了一个对该指针指向对象的引用计数(这个引用计数是放在堆内存上申请的,这块内存和被管理的内存一起释放),在发生指针拷贝时,引用计数加一,任意副本发生析构时,则相应引用计数减一。
值的称赞的是,std::shared_ptr的引用计数管理,是线程安全的,你可以在多线程场景安全的使用。
std::shared_ptr在典型场景的引用计数变化如下:
1 2 3 4 { std::shared_ptr<Foo> foo0 = std::make_shared <Foo>(); std::cout << "构造后,引用计数为" << foo0. use_count () << std::endl; }
Foo construct.
构造后,引用计数为1
Foo desctruct.
1 2 3 4 5 6 { std::shared_ptr<Foo> foo0 = std::make_shared <Foo>(); std::shared_ptr<Foo> foo1 (foo0) ; std::cout << "拷贝构造,新对象引用计数为" << foo1. use_count () << std::endl; std::cout << "拷贝构造,旧对象引用计数为" << foo0. use_count () << std::endl; }
Foo construct.
拷贝构造,新对象引用计数为2
拷贝构造,旧对象引用计数为2
Foo desctruct.
1 2 3 4 5 6 7 { std::shared_ptr<Foo> foo0 = std::make_shared <Foo>(); std::shared_ptr<Foo> foo1 (std::move(foo0)) ; std::cout << "移动构造,新对象引用计数为" << foo1. use_count () << std::endl; }
Foo construct.
移动构造,新对象引用计数为1
Foo desctruct.
1 2 3 4 5 6 7 { std::shared_ptr<Foo> foo0 = std::make_shared <Foo>(); std::shared_ptr<Foo> foo1 (foo0) ; std::cout << "拷贝构造,新对象引用计数为" << foo1. use_count () << std::endl; foo1 = nullptr ; std::cout << "指针副本重新赋值后,引用计数为" << foo0. use_count () << std::endl; }
Foo construct.
拷贝构造,新对象引用计数为2
指针副本重新赋值后,引用计数为1
Foo desctruct.
std::shared_ptr指向内存的释放 std::shared_ptr在两种场景下会触发内存的释放:
引用计数为1且指针对象触发了析构函数。
引用计数为1且用户对这个指针重新赋值。 实际上析构、重新赋值都会触发引用减1
std::shared_ptr支持自定义的内存释放器:
1 2 3 4 5 6 { std::shared_ptr<Foo> foo4 (new Foo(), [](Foo *p){ std::cout << "delete by lambda" << std::endl; delete p; }) ;}
Foo construct.
delete by lambda
Foo desctruct.
std::unique_ptr 独享指针的拷贝构造、拷贝赋值是显式删除的。只能通过std::move传递指针的所有权。
1 2 3 std::unique_ptr<Foo> foo5 (new Foo()) ;std::unique_ptr<Foo> foo6 = foo5;
[1minput_line_16:4:22: [0m[0;1;31merror: [0m[1mcall to deleted constructor of 'std::unique_ptr<Foo>'[0m
std::unique_ptr<Foo> foo6 = foo5;
[0;1;32m ^ ~~~~
[0m[1m/root/miniconda3/envs/cling/bin/../lib/gcc/../../x86_64-conda-linux-gnu/include/c++/10.4.0/bits/unique_ptr.h:468:7: [0m[0;1;30mnote: [0m'unique_ptr' has been explicitly marked deleted here[0m
unique_ptr(const unique_ptr&) = delete;
[0;1;32m ^
[0m
Interpreter Error:
请注意,所有权转移后,std::unique_ptr将在被转移后的作用域中生存,这个作用域结束,则会触发析构。如下面的例子:
1 2 3 4 5 6 7 8 void UniquePtrTobeMovedTo (std::unique_ptr<Foo> &&foo) { std::unique_ptr<Foo> b (std::forward<std::unique_ptr<Foo> &&>(foo)) ; std::cout << "foo pointer moved to here, and it will be destructed in this scope." << std::endl; } std::unique_ptr<Foo> foo7 (new Foo()) ;UniquePtrTobeMovedTo (std::move (foo7));
Foo construct.
foo pointer moved to here, and it will be destructed in this scope.
Foo desctruct.
std::weak_ptr 弱引用指针,与std::shared_ptr搭配使用,可在不增加引用计数的前提下,对std::shared_ptr进行拷贝。
1 2 3 std::shared_ptr<Foo> foo9 (new Foo()) ;std::weak_ptr<Foo> wfoo = foo9; std::cout << "赋值给std::weak_ptr后,引用计数为" << foo9. use_count ();
Foo construct.
赋值给std::weak_ptr后,引用计数为1
涉及引用计数管理的GC机制,都会有相同的场景,A持有B的引用,B持有A的引用,导致引用计数永远无法为0. 当然,这样两个对象之间相互引用,是非常不好的编程实践,我们在开发时应尽量避免。
我用一个简单的例子来讲述std::shared_ptr的循环引用问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class A ;class B ;class A {public : A () { std::cout << "A constructed" << std::endl; } ~A () { std::cout << "A desctructed" << std::endl; } std::shared_ptr<B> b; }; class B {public : B () { std::cout << "B constructed" << std::endl; } ~B () { std::cout << "B desctructed" << std::endl; } std::shared_ptr<A> a; };
1 2 3 4 5 6 7 8 9 10 11 { std::shared_ptr<A> pointerA (new A()) ; std::shared_ptr<B> pointerB (new B()) ; pointerA->b = pointerB; pointerB->a = pointerA; std::cout << "pointerA引用计数为" << pointerA.use_count () << std::endl; std::cout << "pointerB引用计数为" << pointerB.use_count () << std::endl; }
A constructed
B constructed
pointerA引用计数为2
pointerB引用计数为2
可见,由于循环引用的原因,导致错误计数,产生内存泄漏。
解决循环引用也相当简单,将其中一个对象的引用指针改为std::weak_ptr
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class C ;class D ;class C {public : C () { std::cout << "C constructed" << std::endl; } ~C () { std::cout << "C desctructed" << std::endl; } std::shared_ptr<D> d; }; class D {public : D () { std::cout << "D constructed" << std::endl; } ~D () { std::cout << "D desctructed" << std::endl; } std::weak_ptr<C> c; };
1 2 3 4 5 6 7 8 9 10 11 { std::shared_ptr<C> pointerC (new C()) ; std::shared_ptr<D> pointerD (new D()) ; pointerC->d = pointerD; pointerD->c = pointerC; std::cout << "pointerC引用计数为" << pointerC.use_count () << std::endl; std::cout << "pointerD引用计数为" << pointerD.use_count () << std::endl; }
C constructed
D constructed
pointerC引用计数为1
pointerD引用计数为2
C desctructed
D desctructed
指针设计思路应当是这样的:
如果指针指向对象不应被多个对象共享,则尽可能使用std::unique_ptr
如果指针不得不被多个对象共享,则应当尽量避免多个对象之间出现循环引用
如果循环引用不可避免(如:天杀的祖传代码),则循环引用中的对象,应持有的时对方std::weak_ptr指针,以实现引用计数不增加,从而正常释放。