C++智能指针
C++提供了四个智能指针模版类,分别为:auto_ptr
,unique_ptr
,shared_ptr
与weak_ptr
(其中auto_ptr为C++98提供的解决方案,在C++11中已废除,并提供另外三种)。这三者均定义了类似指针的对象,可以将new获得(直接或间接)的地址赋给这种对象。当智能指针过期时,其析构函数将使用delete来释放内存。(创建智能指针对象需要头文件<memory>
)
三种指针的区别?
std::unique_ptr<T>
:独占资源所有权的指针。当进行赋值时,会将旧指针的所有权转让,使得对于特定对象,只能有一个智能指针可以拥有它。相比于auto_ptr
会执行更严格的所有权转让策略。std::shared_ptr<T>
:共享资源所有权的指针。通过引用计数(reference counting),跟踪引用特定对象的智能指针数。当发生赋值操作时,计数增1,当指针过期时,计数减1.仅当最后一个指针过期时,才调用deletestd::weak_ptr<T>
:共享资源的观察者,需要与shared_ptr一起使用,不影响资源的生命周期:它指向一个shared_ptr管理的对象,而进行内存管理的只有shared_ptr.weak_ptr
主要用来帮助解决循环引用问题,它的构造与析构函数不会引起引用计数的增加或减少。std::auto_ptr
:当进行赋值时,会将旧指针的所有权转让,使得对于特定对象,只能有一个智能指针可以拥有它。——已被废除
unique_ptr和auto_ptr的区别?
- 所有权转让机制不同:auto_ptr允许通过直接赋值进行转让,但是这样会留下危险的悬挂指针,容易使得程序在运行阶段崩溃。unique_ptr仅仅允许将临时右值进行赋值,否则会在编译阶段发生错误,更加安全。
- 相较于auto_ptr和shared_ptr,unique_ptr可以使用
new[]
分配的内存作为参数:std::unique_ptr<double[]> pda(new double(5));
使用三种指针的例子
std::unique_ptr(基于RAII思想)
- 使用裸指针时,要记得释放内存:
int* p = new int(100);
//……
delete p;//释放内存
而使用std::unique_ptr自动管理内存:
std::unique_ptr<int> uptr = std::make_unique<int>(100);
//……
//离开uptr作用域时自动释放内存
- std::unique_ptr是move_only的:
std::unique_ptr<int> uptr = std::make_unique<int>(100);
std::unique_ptr<int> uptr1 = uptr;//编译错误
//unique_ptr只有移动构造函数,因此只能移动(转移内部对象所有权:浅拷贝),不能拷贝(深拷贝)
std::unique_ptr<int> uptr2 = std::move(uptr);
//unique_ptr只有移动赋值重载函数,参数是&&,只能接右值,因此必须用std::move转换类型
assert(uptr == nullptr);
- std::unique_ptr可以指向一个数组:
std::unique_ptr<int[]> uptr = std::make_unique<int[]>(10);
for(int i = 0; i < 10; i++){
uptr[i] = i;
}
for(int i = 0; i < 10; i++){
std::cout << uptr[i] << std::endl;
}
- 自定义deleter:
struct FileCloser{
void operator()(FILE* fp) const{
if(fp != nullptr){
fclose(fp);
}
}
};
std::unique_ptr<FILE, FileCloser> uptr(fopen(“file.txt”, “w”));
使用Lambda的deleter:
std::unique_ptr<FILE, std::function<void(FILE*)>> uptr(
fopen(“file.txt”, “w”), [](FILE* fp){
fclose(fp);
});
这种写法存在一个问题就是因为std::function本来就不是lambda的原类型,std::function是通用多态函数封装器,非常强大但有代价,需要一定的内存开销。而把std::function作为类型传给了unique_ptr deleter时,等于在unique_ptr里把这个std::function也给存起来,这时开销就大了……
改进的写法就是用decltype直接获取lambda原类型,同样可以进行EBO(空基类优化:如果deleter是个空基类并且可以继承的话,就不需要保存这个deleter类型的成员,直接继承这个deleter类型):
std::unique_ptr<FILE, decltype(FileCloser)> uptr1(fopen(“file.txt”, “w”), FileCloser);
//C++17不允许uptr1只有第一个参数,也就是必须带着第二个参数FileCloser即使FileCloser没存进unique_ptr中。
//C++20允许了std::unique_ptr<FILE, decltype(FileCloser)> uptr1(fopen(“file.txt”, “w”));
std::shared_ptr
- shared_ptr对资源进行引用计数并且当引用计数为0时自动释放资源:
std::shared_ptr<int> sptr = std::make_shared<int>(500);
assert(sptr.use_count() == 1);//创建完毕后引用计数为1
{
std::shared_ptr<int> sptr1 = sptr;
assert(sptr.get() == sptr1.get());
assert(sptr.use_count() == 2);//sptr与sptr1共享资源
}
assert(sptr.use_count() == 1);//sptr1已经释放
//use_count()为0时自动释放内存
- 与unique_ptr相同,shared_ptr也可以指向数组和自定义deleter:
{
//std::shared_ptr<int[]> sptr = std::make_shared<int[]>(100);
//C++20才支持std::make_shared<int[]>
std::shared_ptr<int[]> sptr(new int[10]);
for(int i = 0; i < 10; i++){
sptr[i] = i;
}
for(int i = 0; i < 10; i++){
std::cout << sptr[i] << std::endl;
}
}
{
std::shared_ptr<FILE> sptr(
fopen(“file.txt”, “w”), [](FILE* fp){
std::cout << “close ” << fp << std::endl;
fclose(fp);
});
}
std::weak_ptr
std::weak_ptr要与std::shared_ptr一起使用。一个weak_ptr对象看错是shared_ptr对象管理管理的资源的观察者,它不影响共享资源的生命周期:
- 如果需要使用weak_ptr正在观察的资源,可以将weak_ptr提升为shared_ptr。
- 当shared_ptr管理的资源释放时,weak_ptr会自动变为nullptr。
void Observe(std::weak_ptr<int> wptr){
if(auto sptr = wptr.lock()){
std::cout << “value: ” << *sptr << std::endl;
}else{
std::cout << “wptr lock fail” << std::endl;
}
}
std::weak_ptr<int> wptr;
{
auto sptr = std::make_shared<int>(521);
wptr = sptr;
Observe(wptr);//sptr指向资源未被释放,wptr可成功提升为shared_ptr
}
Observe(wptr);//sptr指向资源已被释放,无法提升
- 当shared_ptr析构并释放共享资源的时候,只要weak_ptr对象还存在,控制块就会保留,weak_ptr可以通过控制块观察对象是否存活。
std::shared_ptr实现原理
shared_ptr的开销
一个shared_ptr对象内存开销比裸指针和无自定义deleter的unique_ptr对象略大:
using namespace std;
cout << sizeof(int*) << endl;//8
cout << sizeof(unique_ptr<int>) << endl;//8
cout << sizeof(unique_ptr<FILE, function<void(FILE*)>>)
<< endl;//40 ! ! !
cout << sizeof(shared_ptr<int>) << endl;//16
shared_ptr<FILE> sptr(fopen("test_file.txt", "w"), [](FILE* fp) {
cout << "close " << fp << endl;
fclose(fp);
});
cout << sizeof(sptr) << endl; //16
- 其中无自定义deleter的unique_ptr只是把裸指针用RAII手法封装,无需保存其他信息,故开销与裸指针相同。
- 而shared_ptr需要维护两部分信息:
- 指向共享资源的指针。
- 引用计数等共享资源的控制信息(维护一个指向控制信息的指针)
- 另外,在使用shared_ptr时,会涉及两次内存分配:一次分配共享资源对象;一次分配控制块。
C++标准库提供std::make_shared函数来创建一个shared_ptr对象,只需一次内存分配。
shared_ptr支持aliasing constructor(别名构造函数)
template <class U>
shared_ptr(const shared_ptr<U>& x, element_type* p) noexcept;
此时构造出的shared_ptr对象并不拥有p,也不会管理p的内存;而是和x共同拥有x管理的对象,并增加x的一个计数,同时负责x指向对象的内存管理。简单来说,aliasing构造方式,构造了一个拥有x(负责x生命管理周期),但是指向p(访问p的数据)的共享指针。
有两个概念,一个是stored pointer 存储指针(访问该对象的数据),一个是owned pointer 所有者指针(负责该对象的生存周期管理);
实际应用的举例如下:
struct C{int * data};
std::shared_ptr<C> obj (new C); //obj是C类型对象的一个共享指针
std::shared_ptr<int> p9 (obj, obj->data); //p9是obj的一个共享指针,但指向的是C对象的data数据成员
cout << *p9 << endl; //访问的是obj->data
cout << p9->get() << endl; //访问的是obj->data
这里,访问p9存储的数据时,实际上是访问的obj->data,也就是p9是obj->data的一个stored pointer,不负责obj->data的生存周期管理;
而p9实际上管理的是obj的生存周期,也就是p9是obj的owned pointer;
aliasing constructor这种用法实际上是为了解决一种场景:一个智能指针有可能指向了另一个智能指针中的某一部分,但又要保证这两个智能指针销毁时,只对那个被指的对象完整地析构一次,而不是两个指针分别析构一次。
shared_ptr::owner_before()
- 在标准库的shared_ptr中,operator<,比较的是stored pointer,因此上面举例的那种情况,p9和obj两个shared_ptr是不相等的;而owner_before()是基于owner pointer的比较,因此p9和obj是相等的;
- shared_ptr作为map的key时,用的就是owner_before()而不是operator<,否则可能不满足我们实际的使用需求;
- 注意,boost库中的shared_ptr和标准库中的share_ptr实现有所不同;boost库中operator<和owner_before()都是比较的owner pointer;
enable_shared_from_this
一个类的成员函数如何获得指向自身(this)的shared_ptr?
class Foo {
public:
std::shared_ptr<Foo> GetSPtr() {
return std::shared_ptr<Foo>(this);
}
};
auto sptr1 = std::make_shared<Foo>();
assert(sptr1.use_count() == 1);
auto sptr2 = sptr1->GetSPtr();
assert(sptr1.use_count() == 1);
assert(sptr2.use_count() == 1);
这里的代码会生成两个独立的shared_ptr,它们的控制块是独立的,最终导致一个Foo对象被析构两次。
成员函数获取this的shared_ptr正确做法为继承std::enable_shared_from_this:
class Bar : public std::enable_shared_from_this<Bar> {
public:
std::shared_ptr<Bar> GetSPtr() {
return shared_from_this();
}
};
auto sptr1 = std::make_shared<Bar>();
assert(sptr1.use_count() == 1);
auto sptr2 = sptr1->GetSPtr();
assert(sptr1.use_count() == 2);
assert(sptr2.use_count() == 2);
一般情况下,继承了 std::enable_shared_from_this 的子类,成员变量中增加了一个指向 this 的 weak_ptr。这个 weak_ptr 在第一次创建 shared_ptr 的时候会被初始化,指向 this。
总结
智能指针,本质上是对资源所有权和生命周期管理的抽象:
- 当资源是被独占时,使用 std::unique_ptr 对资源进行管理。
- 当资源会被共享时,使用 std::shared_ptr 对资源进行管理。
- 使用 std::weak_ptr 作为 std::shared_ptr 管理对象的观察者。
- 通过继承 std::enable_shared_from_this 来获取 this 的 std::shared_ptr 对象。