C++中的智能指针包括unique_ptr
,shared_ptr
和weak_ptr
。 它们主要作用是让用户不需要自己来维护new和delete。当智能指针变量在作用域内失效的时候,它们自己会释放内存(RAII)。它们都包含在头文件#include <memory>
中。
首先,要明白unique_ptr
,shared_ptr
和weak_ptr
都是c++包装好了的类。所以,它们需要调用构造函数来构造。
unique_ptr
对于同一个object只能有一个指针指向它,原因是在unique_ptr
只能通过构造函数(而且构造函数是 explicit,不能进行隐式转换)
给它传递参数,不能通过赋值。也不能把它复制传递给别的指针(赋值构造函数是delete的)。
std::unique_ptr<int> newPtr(new int(47));
// std::unique_ptr<int> newPtr = new int(47);//不行的
// otherPtr = newPtr; //这样也是不行的
但是,如果说想把它传递给别人也就是说把对当前对象的所有权(ownership)交出去的话,需要调用move constructor,由于move constructor 写的时候一般是
this.ptr = other.ptr;
other.ptr = nullptr;
所以,如果调用
std::unique_ptr<Obj> newPtr(new Obj(parameters));
std::unique_ptr<Obj> otherPtr(std::move(newPtr));
就相当于是newPtr把自己对于内存的控制权交给了otherPtr。
同样的类型的unique_ptr可以swap,可以用p = std::move(q)
交接所有权(即调用unique_ptr的move assignment operator)。参考如下代码:
#include <iostream>
#include <algorithm>
#include <memory>
template <typename T>
void my_swap(T& a, T& b) {
T temp = std::move(a);
/* 全部调用的是move assignment operator, https://en.cppreference.com/w/cpp/memory/unique_ptr/operator%3D
* 对于unique_ptr的move assignment operator会将other的ownership交出去,other会成为nullptr
*/
if (a == nullptr) puts("Guess right");
a = std::move(b);
if (b == nullptr) puts("Guess right");
b = std::move(temp);
if (temp == nullptr) puts("Guess right");
}
int main(int argc, char const *argv[])
{
// {
// std::unique_ptr<int> p1 = std::make_unique<int>(5);
// std::unique_ptr<int> p2;
// p2 = std::move(p1);
// //std::cout <<*p1 << std::endl;
// if (p1 == nullptr)
// puts("Guess right");
// std::cout <<*p2 << std::endl;
// }
{
std::unique_ptr<int> p1 = std::make_unique<int>(5);
std::unique_ptr<int> p2 = std::make_unique<int>(10);
//p2 = std::move(p1);
::my_swap(p1, p2);
std::cout <<*p1 << std::endl;
// if (p1 == nullptr)
// puts("Guess right");
std::cout <<*p2 << std::endl;
}
return 0;
}
make_unique
在c++14 以后加入了make_unique,所以可以这么写:
std::unique_ptr<double> dPtr = std::make_unique<double>(1234.0 / 4);
std::unique_ptr<double> dPtr2(std::move(dPtr));
//std::cout << *dPtr << std::endl;
// printf("%.2lf\n",*dPtr);
assert(dPtr == nullptr);
printf("%.2lf\n",*dPtr2);
make_unique的好处是,由于更好的封装性,所以能很好的处理exception。
但是,要注意的是,也可以:
double a = 123.0 / 4;
std::unique_ptr<double> dPtr = std::make_unique<double>(a);
std::unique_ptr<double> dPtr2 = std::make_unique<double>(a);
但是,不要这么写,这就失去了unique
的意义。我感觉这是一个bug。
上边的代码的理解错了。当你执行 std::make_unique[HTML_REMOVED](a),这个操作会在堆(heap)上动态分配一个新的 double 对象,并将 a 的值复制到这个新对象。然后,std::unique_ptr 被设置为指向这个新创建的 double 对象。
这就是为什么即使 dPtr
所管理的 double 对象的值与 a 相同,但 dPtr.get()
(即 dPtr 所指向的 double 对象的地址)和 &a
(即 a 的地址)是不同的。
下面是一个简单的程序来验证:
#include <iostream>
#include <memory>
int main() {
double a = 123.0 / 4;
std::unique_ptr<double> dPtr = std::make_unique<double>(a);
std::unique_ptr<double> dPtr2 = std::make_unique<double>(a);
std::cout << "Address of a: " << &a << std::endl;
std::cout << "Address pointed by dPtr: " << dPtr.get() << std::endl;
std::cout << "Address pointed by dPtr2: " << dPtr2.get() << std::endl;
return 0;
}
运行结果为:
Address of a: 0x7fff888571f8
Address pointed by dPtr: 0x137fc20
Address pointed by dPtr2: 0x137fc40
显然是不一样的。
shared_ptr
shared_ptr的原理就是在unique_ptr的基础上加入了reference counting
机制。比如,对于Obj,如果有一个指针指向它,那reference counting的值为1,如果有两个指针指向它,那reference counting 的值为2。反之,如果一个指针释放了,那rc的值减1,但此时内存并没有释放,因为当前的rc为1仍然有其他的指针指向。只到rc减为0的时候,内存才会真正的释放。
#include <iostream>
using namespace std;
#include <memory>
class Rectangle {
int length;
int breadth;
public:
Rectangle(int l, int b)
{
length = l;
breadth = b;
}
int area()
{
return length * breadth;
}
};
int main()
{
shared_ptr<Rectangle> P1(new Rectangle(10, 5));
// This'll print 50
cout << P1->area() << endl;
shared_ptr<Rectangle> P2;
P2 = P1;
// This'll print 50
cout << P2->area() << endl;
// This'll now not give an error,
cout << P1->area() << endl;
// This'll also print 50 now
// This'll print 2 as Reference Counter is 2
cout << P1.use_count() << endl;
return 0;
}
make_shared
shared_pointer当然也可以用类似unique_ptr构造函数的方式来构造,但是也可以用make_shared来构造。用std::make_shared
来构造的好处除了避免exception以为,由于引入了reference counting,所以需要开辟额外的空间,如果是用构造函数来构造的话就需要开辟两段不同的空间,这样效率不高。而如果是用了make_shared以后除了Obj需要的空间,reference count的空间也会一并开辟,这样的话效率会大大提高。
{
std::shared_ptr<Entity> e0;
{
std::shared_ptr<Entity> shared_Entity = std::make_shared<Entity>();
e0 = shared_Entity;
}
}//这里才会释放Entity,即调用Entity的destructor,原因是e0也指向了同一段内存,e0在这里才释放
shared_ptr的thread safety即线程安全性
参考: shared_ptr的线程安全性
shared_ptr中的引用计数本身是线程安全的,即它是atomic的操作。但是,倘若多个shared_ptr对同一个Obj进行操作的时候,需要加锁。下面的图是shared_ptr架构图,由ptr和ref_count两部分组成。Foo表示所指向的object, 而ref_count就是引用计数。这里需要注意的是make_shared只保证了图中对象Foo的空间和引用计数vptr的空间,在内存中是连续的。但是,shared_ptr的结构还是没有变,依旧是ptr指向一段空间,ref_count指向一段空间。
引用计数自己在加或者减的时候是atomic的。但是,考虑如下的场景。
它表示的是shared_ptr<Foo> x(new Foo),对应的内存数据结构。(为了简单,只画了reference count的值)。
然后,执行shared_ptr<Foo> y = x; 那么对应的数据结构如下:
但是 y=x 涉及两个成员的复制,这两步拷贝不会同时(原子)发生。中间步骤 1,复制 ptr 指针:
中间步骤 2,复制 ref_count 指针,导致引用计数加 1:
步骤1和步骤2的先后顺序跟实现相关(因此步骤 2 里没有画出 y.ptr 的指向),一般都是先1后2。
既然 y=x 有两个步骤,如果没有 mutex 保护,那么在多线程里就有 race condition。
题外话,c++中atomic操作一次只能做一个atomic操作,可以用tagged pointer来“做”两次。但是,一般来说tagged pointer能“偷”到的指针位数有限,比如int*可以在x86-64平台上偷最后2位(因为是int是4字节对齐,所以最后两位一定为0),但是偷2位最多区分0-4这4个数,非常有限,所以不建议这么做。
weak_ptr
weak_ptr也可以share同一段内存,但是它没有refernce count,也就是没有对该段内存的实际控制权。
{
std::weak_ptr<Entity> e0;
{
std::shared_ptr<Entity> shared_Entity = std::make_shared<Entity>();
e0 = shared_Entity; //e0拥有了对Entity的控制权,但e0并没有将reference count加1
} // 这里Entity就被释放了(因为reference count 变为了0),e0也失去了对Entity的控制权
}
Referrece
SMART POINTERS in C++ (std::unique_ptr, std::shared_ptr, std::weak_ptr)
Smart Pointers in C++ and How to Use Them
好文: 智能指针-使用、避坑和实现