reference:https://www.cyhone.com/articles/right-way-to-use-cpp-smart-pointer/
智能指针是你在堆栈上声明的类模板,并可通过使用指向某个堆分配的对象的原始指针进行初始化。 在初始化智能指针后,它将拥有原始的指针。 这意味着智能指针负责删除原始指针指定的内存。 智能指针析构函数包括要删除的调用,并且由于在堆栈上声明了智能指针,当智能指针超出范围时将调用其析构函数,尽管堆栈上的某处将进一步引发异常。
使用时引用头文件#include
使用例程
class LargeObject
{
public:
void DoSomething(){}
};
void ProcessLargeObject(const LargeObject& lo){}
void SmartPointerDemo()
{
// Create the object and pass it to a smart pointer
std::unique_ptr<LargeObject> pLarge(new LargeObject());
//Call a method on the object
pLarge->DoSomething();
//get the origin pointer
pLarge.get();//= pLarge->p;
// Pass a reference to a method.
ProcessLargeObject(*pLarge);
} //pLarge is deleted automatically when function block goes out of scope.
我们大多数场景下用到的应该都是 unique_ptr。unique_ptr 代表的是专属所有权,即由 unique_ptr 管理的内存,只能被一个对象持有。所以,unique_ptr 不支持复制和赋值,如下:
auto w = std::make_unique<Widget>();
auto w2 = w; // 编译错误
如果想要把 w 复制给 w2, 是不可以的。因为复制从语义上来说,两个对象将共享同一块内存。
因此,unique_ptr 只支持移动, 即如下:
auto w = std::make_unique<Widget>();
auto w2 = std::move(w); // w2 获得内存所有权,w 此时等于 nullptr
unique_ptr 代表的是专属所有权,如果想要把一个 unique_ptr 的内存交给另外一个 unique_ptr 对象管理。只能使用 std::move 转移当前对象的所有权。转移之后,当前对象不再持有此内存,新的对象将获得专属所有权。
如上代码中,将 w 对象的所有权转移给 w2 后,w 此时等于 nullptr,而 w2 获得了专属所有权。
因为 C++ 的 zero cost abstraction 的特点,unique_ptr 在默认情况下和裸指针的大小是一样的。所以 内存上没有任何的额外消耗,性能是最优的
在使用 shared_ptr 之前应该考虑,是否真的需要使用 shared_ptr, 而非 unique_ptr。
shared_ptr 代表的是共享所有权,即多个 shared_ptr 可以共享同一块内存。因此,从语义上来看,shared_ptr 是支持复制的。如下:
auto w = std::make_shared<Widget>();
{
auto w2 = w;
cout << w.use_count() << endl; // 2
}
cout << w.use_count() << endl; // 1
shared_ptr 内部是利用引用计数来实现内存的自动管理,每当复制一个 shared_ptr,引用计数会 + 1。当一个 shared_ptr 离开作用域时,引用计数会 - 1。当引用计数为 0 的时候,则 delete 内存。
同时,shared_ptr 也支持移动。从语义上来看,移动指的是所有权的传递。如下:
auto w = std::make_shared<Widget>();
auto w2 = std::move(w); // 此时 w 等于 nullptr,w2.use_count() 等于 1
我们将 w 对象 move 给 w2,意味着 w 放弃了对内存的所有权和管理,此时 w 对象等于 nullptr。
而 w2 获得了对象所有权,但因为此时 w 已不再持有对象,因此 w2 的引用计数为 1。
内存占用高
shared_ptr 的内存占用是裸指针的两倍。因为除了要管理一个裸指针外,还要维护一个引用计数。
因此相比于 unique_ptr, shared_ptr 的内存占用更高
原子操作性能低
考虑到线程安全问题,引用计数的增减必须是原子操作。而原子操作一般情况下都比非原子操作慢。
使用移动优化性能
shared_ptr 在性能上固然是低于 unique_ptr。而通常情况,我们也可以尽量避免 shared_ptr 复制。
如果,一个 shared_ptr 需要将所有权共享给另外一个新的 shared_ptr,而我们确定在之后的代码中都不再使用这个 shared_ptr,那么这是一个非常鲜明的移动语义。
对于此种场景,我们尽量使用 std::move,将 shared_ptr 转移给新的对象。因为移动不用增加引用计数,性能比复制更好。
BlockingQueue>
将对象转移到另外一个线程中释放,从而解放关键线程。shared_ptr例程
#include
#include
using namespace std;
int main()
{
//构建 2 个智能指针
std::shared_ptr<int> p1(new int(10));
std::shared_ptr<int> p2(p1);
//输出 p2 指向的数据
cout << *p2 << endl;
p1.reset();//引用计数减 1,p1为空指针
if (p1) {
cout << "p1 不为空" << endl;
}
else {
cout << "p1 为空" << endl;
}
//以上操作,并不会影响 p2
cout << *p2 << endl;
//判断当前和 p2 同指向的智能指针有多少个
cout << p2.use_count() << endl;
return 0;
}
weak_ptr 是为了解决 shared_ptr 双向引用的问题。即
class B;
struct A{
shared_ptr<B> b;
};
struct B{
shared_ptr<A> a;
};
auto pa = make_shared<A>();
auto pb = make_shared<B>();
pa->b = pb;
pb->a = pa;
pa 和 pb 存在着循环引用,根据 shared_ptr 引用计数的原理,pa 和 pb 都无法被正常的释放。
对于这种情况, 我们可以使用 weak_ptr:
class B;
struct A{
shared_ptr<B> b;
};
struct B{
weak_ptr<A> a;
};
auto pa = make_shared<A>();
auto pb = make_shared<B>();
pa->b = pb;
pb->a = pa;
weak_ptr 不会增加引用计数,因此可以打破 shared_ptr 的循环引用。
通常做法是 parent 类持有 child 的 shared_ptr, child 持有指向 parent 的 weak_ptr。这样也更符合语义。
很多时候,函数的参数是个指针。这个时候就会面临选择困难症,这个参数应该怎么传,应该是 shared_ptr
,还是 const shared_ptr&
,还是直接 raw pointer
更合适。
只在函数使用指针,但并不保存对象内容
假如我们只需要在函数中,用这个对象处理一些事情,但不打算涉及其生命周期的管理,也不打算通过函数传参延长 shared_ptr 的生命周期。
对于这种情况,可以使用 raw pointer 或者 const shared_ptr&。
即:
void func(Widget*);
void func(const shared_ptr&)
实际上第一种裸指针的方式可能更好,从语义上更加清楚,函数也不用关心智能指针的类型。
在函数中保存智能指针
假如我们需要在函数中把这个智能指针保存起来,这个时候建议直接传值。
void func(std::shared_ptr ptr);
这样的话,外部传过来值的时候,可以选择 move 或者赋值。函数内部直接把这个对象通过 move 的方式保存起来。
这样性能更好,而且外部调用也有多种选择。
对于智能指针的使用,实际上是对所有权和生命周期的思考,一旦想明白了这两点,那对智能指针的使用也就得心应手了。
同时理解了每种智能指针背后的性能消耗、使用场景,那智能指针也不再是黑盒子和洪水猛兽。