C++ 智能指针
C++ 智能指针
unique_ptr
大概长这个样子
//大概长这个样子(化简版) template<class T> class unique_ptr{ T* ptr; };
unique_ptr是独占性智能指针,
- 某个时刻只能有一个uniqueptr指向一个给定对象;
- 当uniqueptr被销毁时,它所指向的对象也被销毁(自动对所指向对象调用delete);
- uniqueptr不支持普通的拷贝和赋值操作;
- 如果真的需要转移所有权(独占权),需要用std::move(std::unique_ptr对象)语法,转移所有权后如果仍使用 原有指针调用 会导致崩溃。
例如:
class Movie { public: Movie(string name) { this->name = name; cout << "let's watch moive " << name << endl; } ~Movie() {cout << "movie " << name << " end" << endl;} void getName() {cout<< "name=" << name << endl;} private: string name; }; void test_unique_ptr() { unique_ptr<Movie> p1(new Movie("ne za")); unique_ptr<Movie> p2(new Movie("the lord of the ring")); //unique_ptr<Movie> p3 = p1; // Error! 不支持赋值操作 unique_ptr<Movie> p3 = move(p1); //p1->getName(); // Error! 转移给p3以后,p1不再指向原对象 p3->getName(); }
也可以用release方法来转移对象所有权,例如:
void test_unique_ptr2() { unique_ptr<Movie> p1(new Movie("ne za")); unique_ptr<Movie> p2(new Movie("the lord of the ring")); p2.reset(p1.release()); if (p1 == nullptr) { cout << "p1 is nullptr" << endl; } p2.reset(); if (p2 == nullptr) { cout << "p2 is nullptr" << endl; } }
p1.release() 方法转移其对象控制权(但并不会释放对象),并将p1赋空;
p1.reset(p2) 方法释放p1指向的对象,并重新指向p2所指向的对象;
p1.reset() 方法释放p1指向的对象,并赋空;
shared_ptr
shared_ptr 是在使用引用计数的机制上提供了可以共享所有权的智能指针。
大概长这个样子
//大概长这个样子(化简版) struct SharedPtrControlBlock{ int shared_count; }; template<class T> class shared_ptr{ T* ptr; SharedPtrControlBlock* count; };
每次复制,多一个共享同处资源的shared_ptr时,计数+1;每次释放shared_ptr时,计数-1。
当shared计数为0时,则证明所有指向同一处资源的shared_ptr们全都释放了,则随即释放该资源(哦,还会释放new出来的SharedPtrControlBlock)。
初始化方式如下:
shared_ptr<T> ptr(new T(), deleter); // deleter 是自定义释放器,可以不传
auto ptr = std::make_shared<T>()
一个例子:
void test_shared_ptr() { shared_ptr<Movie> p1(new Movie("ne za")); // count = 1 do {shared_ptr<Movie> p2 = p1;} while(0); // count先加后减 shared_ptr<Movie> p3 = p1; // count = 2 p1.reset(); // count = 1 //p3 = nullptr; // count = 0 cout << "test_shared_ptr " << endl; }
如果把上面p3=nullptr这行的注释去掉,则会在这里导致计数器变0,从而引发对象的释放。
shared_ptr类提供的成员方法:
- get(),返回所指向的对象(指针)
- reset(),放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少
- use_count(),返回引用计数的个数
- unique(),返回是否是独占所有权( use_count 为 1)
- swap(),交换两个 shared_ptr 对象
enable_shared_from_this
假如一个对象被shared_ptr托管,那么如何在类内部获取自身的shared_ptr呢?看个例子:
class Movie { public: Movie(string name) { this->name = name; cout << "let's watch moive " << name << endl; } ~Movie() {cout << "movie " << name << " end" << endl;} void getName() {cout<< "name=" << name << endl;} shared_ptr<Movie> getptr() { return shared_ptr<Movie>(this); } private: string name; }; int main() { shared_ptr<Movie> p1(new Movie("movie1")); shared_ptr<Movie> p2 = p1->getptr(); cout << p1.use_count() << endl; // 1 cout << p2.use_count() << endl; // 1 }
通过this指针生成一个新的shared_ptr,但这个新的shared_ptr并非从已有的其它shared_ptr复制而来,所以它认为自己独占了所指向的对象,
所以例子中,p1和p2的引用计数都为1,当它们析构时,会造成对象多次释放。
为了解决在需要把this指针当成shared_ptr传递到其它函数中去的问题(比如一些异步调用需要扩展对象生命周期),C++11引入了enable_shared_from_this。
std::enable_shared_from_this 能让一个对象(假设其名为 t ,且已被一个 std::shared_ptr 对象 pt 管理)安全地生成其他额外的 std::shared_ptr 实例(假设名为 pt1, pt2, ... ) ,它们与 pt 共享对象 t 的所有权。
template< class T > class enable_shared_from_this;
若一个类 T 继承 std::enable_shared_from_this<T> ,则会为该类 T 提供成员函数: shared_from_this 。 当 T 类型对象 t 被一个为名为 pt 的 std::shared_ptr<T> 类对象管理时,调用 T::shared_from_this 成员函数,将会返回一个新的 std::shared_ptr<T> 对象,它与 pt 共享 t 的所有权。
enable_shared_from_this 例子:
class Movie : public std::enable_shared_from_this<Movie> { public: Movie(string name) { this->name = name; cout << "let's watch moive " << name << endl; } ~Movie() {cout << "movie " << name << " end" << endl;} void getName() {cout<< "name=" << name << endl;} private: string name; }; int main() { shared_ptr<Movie> p1(new Movie("movie1")); shared_ptr<Movie> p2 = p1->shared_from_this(); cout << p1.use_count() << endl; // 2 cout << p2.use_count() << endl; // 2 }
线程安全
shared_ptr 的引用计数本身是安全且无锁的,但对象的读写则不是。
shared_ptr 有两个数据成员,读写操作不能原子化。(其实即使普通的int类型变量读写操作也不是原子的)
比如执行 shared_ptr<Foo> x (new Foo); 对应的数据结构如下:
再执行 shared_ptr<Foo> y = x; 那么对应的数据结构如下:
但是 y=x 涉及两个成员的复制,这两步拷贝不会同时(原子)发生。
- 中间步骤 1,复制 ptr 指针:
- 中间步骤 2,复制 ref_count 指针,导致引用计数加 1:
步骤1和步骤2的先后顺序跟实现相关,既然 y=x 有两个步骤,如果没有 mutex 保护,那么在多线程里就有 race condition。
考虑一个简单的场景,有 3 个 shared_ptr<Foo> 对象 x、g、n:
shared_ptr<Foo> g(new Foo); // 线程之间共享的 shared_ptr shared_ptr<Foo> x; // 线程 A 的局部变量 shared_ptr<Foo> n(new Foo); // 线程 B 的局部变量
假设初始状态如下:
线程 A 执行 x = g;(对g读操作) 此时完成步骤1,但未完成步骤2,这时切换到线程B
线程 B 执行 g = n; (对g写操作)假设其两个步骤一起完成了
此时 Foo1 对象已经销毁(引用计数=0),x.ptr 成了空悬指针!
最后回到线程 A,完成步骤 2:
多线程无保护地读写 g,造成了“x 是空悬指针”的后果。所以多线程读写同一个 shared_ptr 必须加锁。
最后还有一个问题:在多个线程上通过shared_ptr共享同一个对象,会不会因为shared_ptr的并发导致被分享对象被并发析构、多次析构。
在实现上,比如对cnt--操作是否需要程序员来同步呢?C++标准在实现上对cnt是原子类型,所以对同一个对象分享所有权的shared_ptr在多个线程上的析构不需要外部加锁保护。
循环引用
一个例子
class Movie { public: Movie(string name) { this->name = name; cout << "let's watch moive " << name << endl; } ~Movie() {cout << "movie " << name << " end" << endl;} void getName() {cout<< "name=" << name << endl;} void setRelative(shared_ptr<Movie>& other) { relative_movie = other; } private: string name; shared_ptr<Movie> relative_movie; }; void test_shared_ptr2() { shared_ptr<Movie> p1(new Movie("ne za")); shared_ptr<Movie> p2(new Movie("the lord of the ring")); p1->setRelative(p2); p2->setRelative(p1); cout<<p1.use_count()<<endl; // 2 cout<<p2.use_count()<<endl; // 2 }
上面的 test_shared_ptr2 函数执行完毕后,两个Movie对象析构函数都没有打印,也就是没有被释放。
模拟下代码流程:
- 开始,两个Movie对象的计数都为2;
- p2指针退出栈,p2指向的Movie("the lord of the ring")计数器减为1;
- p1指针退出栈,p1指向的Movie("ne ze")计数器减为1;
- 函数退出,两个对象的计数都为1,都不会释放;
为了解决这个问题,引入了weak_ptr指针。
weak_ptr
weak_ptr是一种不控制所指向对象生命周期的智能指针,它指向一个shared_ptr管理的对象。
大概长这个样子:
struct SharedPtrControlBlock{ int shared_count; int weak_count; }; template<class T> class weak_ptr{ T* ptr; SharedPtrControlBlock* count; };
特点
- weak_ptr可以由一个shared_ptr,或者另一个weak_ptr构造;
- weak_ptr的构造和析构不会引起shared_count的增加或减少,只会引起weak_count的增加或减少;
- 当shared计数为0,会释放被管理资源,也就是说weak_ptr不控制资源的生命周期;
- 计数区域的释放却取决于shared计数和weak计数,当两者均为0时,才会释放计数区域。
例子
class Movie { public: Movie(string name) { this->name = name; cout << "let's watch moive " << name << endl; } ~Movie() {cout << "movie " << name << " end" << endl;} void getName() {cout<< "name=" << name << endl;} void setRelative(shared_ptr<Movie>& other) { relative_movie = other; } private: string name; weak_ptr<Movie> relative_movie; }; void test_weak_ptr() { shared_ptr<Movie> p1(new Movie("ne za")); shared_ptr<Movie> p2(new Movie("the lord of the ring")); p1->setRelative(p2); p2->setRelative(p1); cout<<p1.use_count()<<endl; // 1 cout<<p2.use_count()<<endl; // 1 }
这个例子,两个对象都能够被释放;
同样也模拟下代码流程:
- 开始,两个Movie对象的shared计数都为1,weak计数也为1;
- p2指针退出栈,p2指向的Movie("the lord of the ring")的shared计数器减为0,weak计数仍为1,资源释放,同时由于p2->relation_movie指向的Movie("ne ze")也被释放引起该资源的weak计数减为0;
- p1指针退出栈,p1指向的Movie("ne za")的shared计数器减为0,weak计数仍为1,资源释放,同时由于p1->relation_movie指向的Movie("the lord of the ring")也被释放引起该资源的weak计数减为0;
- 函数退出,两个对象的weak计数都为0,释放计数区域;
另外,weak指针没有重载 * 和 -> ,所以并不能直接使用资源,但可以对weak指针调用lock方法,会返回一个shared_ptr指针。
void test_weak_ptr2() { shared_ptr<Movie> p1(new Movie("ne za")); weak_ptr<Movie> p2 = p1; //p2->getName(); // Erorr! shared_ptr<Movie> p3 = p2.lock(); p3->getName(); }
也就是说,弱引用特性,不拥有对象,只有延迟到尝试调用lock()时才会有可能临时拥有对象。