C++11及C++14标准的智能指针

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/haolexiao/article/details/56773039

智能指针这个概念经常会碰见,而且面试的时候太经常会被问到了,特来总结一下。

C++11之前的智能指针

auto_ptr出现的背景

C++11之前的智能指针是auto_ptr,一开始它的出现是为了解决指针没有释放导致的内存泄漏。比如忘了释放或者在释放之前,程序throw出错误,导致没有释放。所以auto_ptr在这个对象声明周期结束之后,自动调用其析构函数释放掉内存。

int t = 3, m =4;
auto_ptr<int> p1(&t);
auto_ptr<const int> p2(&m);
auto_ptr<int> p3(new int[5]);   //注意这里一定是[5]而不是(5),因为(5)表示申请了一个里面存着数字5的地址,不要记混了

注意这里只是阐述了怎么用,p1,p2一般不能那么定义,因为一般不用智能指针去指向非堆内存中的地址,因为自行释放非堆地址很有可能出现问题。所以上述程序运行会报错。
相当于如下操作

int t = 3;
int *p = &t;
delete p;

这样是不行的,运行时候会报错。
所以千万不要用一块非new分配的动态内存去初始化一个智能指针。

auto_ptr被弃用的原因

先说结论就是,

1.避免潜在的内存崩溃

智能指针auto_ptr在被赋值操作的时候,被赋值的取得其所有权,去赋值的丢失其所有权。如【2】中举的例子:

auto_ptr< string> ps (new string ("I reigned lonely as a cloud.");
auto_ptr<string> vocation; 
vocaticn = ps;

执行完上面这步之后,ps就不再指向原来的string串了,变成了空串,vocation指向了原来的string串。
但是会出下如下的错误:

 auto_ptr<string> films[5] =
 {
  auto_ptr<string> (new string("Fowl Balls")),
  auto_ptr<string> (new string("Duck Walks")),
  auto_ptr<string> (new string("Chicken Runs")),
  auto_ptr<string> (new string("Turkey Errors")),
  auto_ptr<string> (new string("Goose Eggs"))
 };
 auto_ptr<string> pwin;
 pwin = films[2]; // films[2] loses ownership. 将所有权从films[2]转让给pwin,此时films[2]不再引用该字符串从而变成空指针
 for(int i = 0; i < 5; ++i)
     cout << *films[i] << endl;

以上的程序编译正常,但是运行到输出环节的时候就会出现错误。因为films[2]此时已经丢掉了控制权。
而如果用unique_ptr的时候就会在编译期间发现这个错误,因为unique_ptr是不允许直接赋值的。

2.auto_ptr不够方便——没有移动语义的后果

比如auto_ptr不能够作为函数的返回值和函数的参数,也不能在容器中保存autp_ptr。
而这些unique_ptr都可以做到。因为C++11之后有了移动语义的存在,这里调用的是移动构造函数。因为移动语义它可以接管原来对象的资源,同时让原来对象的资源置为空。

C++11之后的智能指针

C++11之后智能指针分为了三种:shared_ptr, unique_ptr, weak_ptr
而weak_ptr相当于shared_ptr的一个辅助指针, 所以正式的智能指针只有shared_ptr和unique_ptr

explict关键字

C++11之后的智能指针的构造函数都有explict关键词修饰,表明它不能被隐式的类型转换。
即如下p1的形式是不行的:

shared_ptr<int> p1 = new int(1024);  //这种是不行的,因为等号右边是一个int*的指针,因为有explict修饰,所以它不能被隐式的转换为shared_ptr<int>的类型
shared_ptr<int> p2(new int(1024));   //这种是直接采用了初始化的形式

shared_ptr和unique_ptr的共性

以下操作是以上两个智能指针都支持的操作

//声明,可以用一个指针显示的初始化,或者声明成一个空指针,可以指向一个类型为T的对象
shared_ptr<T> sp;
unique_ptr<T> up;
//赋值,返回相对应类型的智能指针,指向一个动态分配的T类型对象,并且用args来初始化这个对象
make_shared<T>(args);
make_unique<T>(args);     //注意make_unique是C++14之后才有的
//用来做条件判断,如果其指向一个对象,则返回true否则返回false
p;
//解引用
*p;
//获得其保存的指针,一般不要用
p.get();
//交换指针
swap(p,q);
p.swap(q); 

举个例子

unique_ptr<int> p1 = make_unique<int>(3);
unique_ptr<int> p2(new int(4));

看上面一定要注意make的相当于一个new了,一定不要用指针去初始化,而是用正常的值去初始化
而下面那种初始化方式需要用一个指针去初始化
不要搞混了

删除器的用法

删除器是一个函数

//比如网络资源断开连接
//对于shared_ptr
 shared_ptr<connect> p (&c,end_connect);
 //对于unique_ptr,需要额外定义一个删除器的类型
 unique_ptr<objT,delT> p(new objT,fcn);
 //举个例子如下
 unique_ptr<connect,decltype(end_connect)*> p(&c,end_connect)
 //这里 用了decltype来获得函数指针类型非常的精妙

shared_ptr特性用法

//复制构造函数函数
shared_ptr<T>p(q)   //会让q的计数器+1
p = q               //会让q的计数器+1,同时p原来的计数器-1,如果减为0则自动释放
//引用计数的判断
p.unique()          //一个bool函数 如果只有一个引用计数则返回true,如果不是则返回false
p.use_count()       //返回与p共享对象的智能指针数量,这个操作可能会比较慢,一般调试的时候用,正式情况下一般不用 
//重新赋值
p.reset(new int(1024))  //将p更新为新的指针,同时原来的引用计数-1

unique_ptr特性用法——release()和reset()用法区别

//release()用法
 //release()返回原来智能指针指向的指针,只负责转移控制权,不负责释放内存,常见的用法
 unique_ptr<int> q(p.release()) // 此时p失去了原来的的控制权交由q,同时p指向nullptr  
 //所以如果单独用:
 p.release()
 //则会导致p丢了控制权的同时,原来的内存得不到释放
//reset()用法
 p.reset()     // 释放p原来的对象,并将其置为nullptr,
 p = nullptr   // 等同于上面一步
 p.reset(q)    // 注意此处q为一个内置指针,令p释放原来的内存,p新指向这个对象

主意release()只转移控制权,并不释放内存,而reset和=nullptr操作会释放原来的内存

C++14之中的make_unique

在C++11标准中,还没有make_unique呢,所以在《C++ primer》中说,「与shared_ptr不同,没有类似make_ptr的标准库函数返回一个unique_ptr」但是这个问题在C++14之中就已经解决了。C++14中已经有make_unique了。
std::make_shared是C++11的部分,但是,不幸的是,std::make_unique不是。它是在C++14中才被加入到标准库的。
unique_ptr的两种方式进行赋值,一旦其被赋值之后,不能简单的被替换。

unique_ptr所有权的转移

一般情况下

智能指针需要注意的问题

尽量用make_shared/make_unique,少用new

这个是《modern effective C++》21条里说的Item 21: 比起直接使用new优先使用std::make_unique和std::make_shared
原因如下:

1.更加快速

std::shared_ptr在实现的时候使用的refcount技术,因此内部会有一个计数器(控制块,用来管理数据)和一个指针,指向数据。因此在执行std::shared_ptr<A> p2(new A)的时候,首先会申请数据的内存,然后申请内控制块,因此是两次内存申请,而std::make_shared<A>()则是只执行一次内存申请,将数据和控制块的申请放到一起。

2.避免内存泄漏

比如一个函数传进去一个智能指针,如果采用如下形式:

fun(shared_ptr<T>(new T), compute());

这个就和编译器有关了,因为函数的参数必须在函数被调用前被估值,所以在调用fun时,会先new
所以执行如下操作:

  1. new T
  2. shared_ptr的构造函数必须被执行
  3. 计算compute()
    但是不一定会按照上面的顺序来执行,可能先执行1步,再执行3步,最后执行2步。但是如果2步此时出现了错误,就会导致第一步分配的T泄露了,永远不会被删除。而采用make_shared就能避免这种情况:
fun(make_shared<T>(), compute());

不论上面两个参数哪个先执行,都不会导致内存泄漏。make_unique同理

陷阱

  1. 不要使用相同的内置指针来初始化(或者reset)多个智能指针
  2. 不要delete get()返回的指针
  3. 不要用get()初始化/reset另一个智能指针
  4. 智能指针管理的资源它只会默认删除new分配的内存,如果不是new分配的则要传递给其一个删除器

剩下更多的陷阱详见下面这篇博客
必须要注意的 C++ 动态内存资源管理(五)——智能指针陷阱

参考资料

  1. C++11中的智能指针
  2. C++智能指针简单剖析
  3. 【C++11新特性】 C++11智能指针之weak_ptr
  4. Item 21: 比起直接使用new优先使用std::make_unique和std::make_shared
  5. 必须要注意的 C++ 动态内存资源管理(五)——智能指针陷阱

没有更多推荐了,返回首页