cpp

C++ 知识点 | C11 智能指针

Posted by Aiden on January 26, 2020

由于 C++ 语言没有自动内存回收机制,程序员每次new出来的内存都要手动delete。程序员忘记delete,流程太复杂,最终导致没有delete,异常导致程序过早退出,没有执行delete的情况并不罕见。

对于编译器来说,智能指针实际上是一个栈对象,并非指针类型,在栈对象生命期即将结束时,智能指针通过析构函数释放有它管理的堆内存。所有智能指针都重载了operator->操作符,直接返回对象的引用,用以操作对象。访问智能指针原来的方法则使用.操作符。

访问智能指针包含的裸指针则可以用 get() 函数。由于智能指针是一个对象,所以if (my_smart_object)永远为真,要判断智能指针的裸指针是否为空,需要这样判断:if (my_smart_object.get())

智能指针包含了 reset() 方法,如果不传递参数(或者传递 NULL),则智能指针会释放当前管理的内存。如果传递一个对象,则智能指针会释放当前对象,来管理新传入的对象。

auto_ptr

std::auto_ptr 属于 STL,当然在 namespace std 中,包含头文件#include<memory>便可以使用。 std::auto_ptr 能够方便的管理单个堆内存对象。

我们从代码开始分析:

{
  std::auto_ptr<A> a_ptr(new A());  // 创建对象

  if(a_ptr.get())                   // 判断智能指针是否为空
  {
      a_ptr->show_message();        // 使用 operator-> 调用智能指针对象中的函数
      a_ptr.get()->show_message();  // 使用 get() 返回裸指针
  }
}                                   // a_ptr 生命周期结束,析构堆对象 A

上述为正常使用 std::auto_ptr 的代码,一切似乎都良好,无论如何不用我们显示使用该死的delete 了。

问题 1

void TestAutoPtr2() {
  std::auto_ptr<A> my_memory(new A(1));

  if (my_memory.get()) {
    std::auto_ptr<A> my_memory2;      // 创建一个新的 my_memory2 对象
    my_memory2 = my_memory;           // 复制旧的 my_memory 给 my_memory2
    my_memory2->show_message();       // 输出信息,复制成功
    my_memory->show_message();        // 崩溃
  }
}

最终如上代码导致崩溃,如上代码时绝对符合 C++ 编程思想的,居然崩溃了,跟进std::auto_ptr的源码后,我们看到,罪魁祸首是my_memory2 = my_memory,这行代码,my_memory2 完全夺取了 my_memory 的内存管理所有权,导致 my_memory 悬空,最后使用时导致崩溃。

所以,使用 std::auto_ptr 时,绝对不能使用“operator=”操作符。作为一个库,不允许用户使用,确没有明确拒绝,多少会觉得有点出乎预料。

问题 2

void TestAutoPtr3() {
  std::auto_ptr<A> my_memory(new A(1));

  if (my_memory.get()) {
    my_memory.release();
  }
}                // 没有析构

看到什么异常了吗?我们创建出来的对象没有被析构,导致内存泄露。当我们不想让my_memory继续生存下去,我们调用 release() 函数释放内存,结果却导致内存泄露(在内存受限系统中,如果my_memory占用太多内存,我们会考虑在使用完成后,立刻归还,而不是等到my_memory结束生命期后才归还)。

void TestAutoPtr3() {
  std::auto_ptr<A> my_memory(new A(1));
  if (my_memory.get()) {
    A* temp_memory = my_memory.release();
    delete temp_memory;
  }
}

void TestAutoPtr3() {
  std::auto_ptr<A> my_memory(new A(1));
  if (my_memory.get()) {
    my_memory.reset();  // 释放 my_memory 内部管理的内存
  }
}

unique_ptr

unique_ptrC++11 引入,旨在替代不安全的 auto_ptrunique_ptr 是一种定义在头文件<memory>中的智能指针。

它持有对对象的独有权——两个unique_ptr不能指向一个对象,即 unique_ptr 不共享它所管理的对象。

它无法复制到其他unique_ptr,无法通过值传递到函数,也无法用于需要副本的任何标准模板库 (STL)算法。只能移动 unique_ptr,即对资源管理权限可以实现转移。这意味着,内存资源所有权可以转移到另一个unique_ptr,并且原始 unique_ptr 不再拥有此资源。

实际使用中,建议将对象限制为由一个所有者所有,因为多个所有权会使程序逻辑变得复杂。因此,当需要智能指针用于存 C++ 对象时,可使用 unique_ptr,构造 unique_ptr 时,可使用 make_unique Helper 函数。

下图演示了两个 unique_ptr 实例之间的所有权转换。

image.png

unique_ptr 与原始指针一样有效,并可用于 STL 容器。将 unique_ptr 实例添加到 STL 容器运行效率很高,因为通过 unique_ptr 的移动构造函数,不再需要进行复制操作。

unique_ptr 指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过 reset 方法重新指定、通过 release 方法释放所有权、通过移动语义转移所有权,unique_ptr 还可能没有对象,这种情况被称为 empty

//智能指针的创建  
unique_ptr<int> u_i; 	//创建空智能指针
u_i.reset(new int(3)); 	//绑定动态对象  
unique_ptr<int> u_i2(new int(4));//创建时指定动态对象
unique_ptr<T,D> u(d);	//创建空 unique_ptr,执行类型为 T 的对象,用类型为 D 的对象 d 来替代默认的删除器 delete

//所有权的变化  
int *p_i = u_i2.release();	//释放所有权  
unique_ptr<string> u_s(new string("abc"));  
unique_ptr<string> u_s2 = std::move(u_s); //所有权转移(通过移动语义),u_s所有权转移后,变成“空指针” 
u_s2.reset(u_s.release());	//所有权转移
u_s2=nullptr;//显式销毁所指对象,同时智能指针变为空指针。与u_s2.reset()等价

shared_ptr

shared_ptr是一个标准的共享所有权的智能指针,允许多个指针指向同一个对象,定义在memory文件中,命名空间为stdshared_ptr最初实现于Boost库中,后由 C++11 引入到 C++ STL

shared_ptr 利用引用计数的方式实现了对所管理的对象的所有权的分享,即允许多个 shared_ptr 共同管理同一个对象。 像 shared_ptr 这种智能指针,《Effective C++》称之为“引用计数型智能指针”(reference-counting smart pointer,RCSP)。

shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的),在使用引用计数的机制上提供了可以共享所有权的智能指针,当然这需要额外的开销:

  1. shared_ptr 对象除了包括一个所拥有对象的指针外,还必须包括一个引用计数代理对象的指针;
  2. 时间上的开销主要在初始化和拷贝操作上, *-> 操作符重载的开销跟 auto_ptr 是一样;
  3. 开销并不是我们不使用 shared_ptr 的理由,永远不要进行不成熟的优化,直到性能分析器告诉你这一点。
#include <iostream>
#include <memory>
using namespace std;
class A
{
public:
    int i;
    A(int n):i(n) { };
    ~A() { cout << i << " " << "destructed" << endl; }
};
int main()
{
    shared_ptr<A> sp1(new A(2)); //A(2)由sp1托管,
    shared_ptr<A> sp2(sp1);       //A(2)同时交由sp2托管
    shared_ptr<A> sp3;
    sp3 = sp2;   //A(2)同时交由sp3托管
    cout << sp1->i << "," << sp2->i <<"," << sp3->i << endl;
    A * p = sp3.get();      // get返回托管的指针,p 指向 A(2)
    cout << p->i << endl;  //输出 2
    sp1.reset(new A(3));    // reset导致托管新的指针, 此时sp1托管A(3)
    sp2.reset(new A(4));    // sp2托管A(4)
    cout << sp1->i << endl; //输出 3
    sp3.reset(new A(5));    // sp3托管A(5),A(2)无人托管,被delete
    cout << "end" << endl;
    return 0;
}

weak_ptr

weak_ptr 被设计为与 shared_ptr 共同工作,可以从一个 shared_ptr 或者另一个 weak_ptr 对象构造而来。

weak_ptr 是为了配合 shared_ptr 而引入的一种智能指针,它更像是 shared_ptr 的一个助手而不是智能指针,因为它不具有普通指针的行为,没有重载 operator*operator-> ,因此取名为 weak,表明其是功能较弱的智能指针。

它的最大作用在于协助 shared_ptr 工作,可获得资源的观测权,像旁观者那样观测资源的使用情况。观察者意味着 weak_ptr 只对 shared_ptr 进行引用,而不改变其引用计数,当被观察的 shared_ptr 失效后,相应的 weak_ptr 也相应失效。

使用 weak_ptr 的成员函数 use_count() 可以观测资源的引用计数,另一个成员函数 expired() 的功能等价于 use_count()==0,但更快,表示被观测的资源(也就是shared_ptr管理的资源)已经不复存在。

weak_ptr可以使用一个非常重要的成员函数lock()从被观测的 shared_ptr 获得一个可用的 shared_ptr 管理的对象, 从而操作资源。但当 expired()==true 的时候,lock() 函数将返回一个存储空指针的 shared_ptr

weak_ptr的基本用法总结如下:

weak_ptr<T> w;	 	     // 创建空 weak_ptr,可以指向类型为 T 的对象。
weak_ptr<T> w(sp);	   // 与 shared_ptr 指向相同的对象,shared_ptr 引用计数不变。T必须能转换为 sp 指向的类型。
w=p;				           // p 可以是 shared_ptr 或 weak_ptr,赋值后 w 与 p 共享对象。
w.reset();			       // 将 w 置空。
w.use_count();		     // 返回与 w 共享对象的 shared_ptr 的数量。
w.expired();		       // 若 w.use_count() 为 0,返回 true,否则返回 false。
w.lock();			         // 如果 expired() 为 true,返回一个空 shared_ptr,否则返回非空 shared_ptr。

下面是一个简单的使用示例:

#include < assert.h>

#include <iostream>
#include <memory>
#include <string>
using namespace std;

int main()
{
	shared_ptr<int> sp(new int(10));
	assert(sp.use_count() == 1);
	weak_ptr<int> wp(sp); 	//从shared_ptr创建weak_ptr
	assert(wp.use_count() == 1);
	if (!wp.expired())		//判断weak_ptr观察的对象是否失效
	{
		shared_ptr<int> sp2 = wp.lock();//获得一个shared_ptr
		*sp2 = 100;
		assert(wp.use_count() == 2);
	}
	assert(wp.use_count() == 1);
	cout << "int:" << *sp << endl;
    return 0;
}

如何选择智能指针

文简单地介绍了 C++ 标准模板库 STL 中四种智能指针,当然,除了STL 中的智能指针,C++ 准标准库 Boost 中的智能指针,比如boost::scoped_ptr、boost::shared_array、boost:: intrusive_ptr 也可以在实际编程实践中拿来使用.

  1. 如果程序要使用多个指向同一个对象的指针,应选择shared_ptr
两个对象都包含指向第三个对象的指针
STL容器包含指针。很多STL算法都支持复制和赋值操作,这些操作可用于shared_ptr,但不能用于unique_ptr(编译器发出warning)和auto_ptr(行为不确定)。如果你的编译器没有提供shared_ptr,可使用Boost库提供的shared_ptr。
  1. 如果程序不需要多个指向同一个对象的指针,则可使用unique_ptr。如果函数使用new分配内存,并返还指向该内存的指针,将其返回类型声明为unique_ptr是不错的选择。这样,所有权转让给接受返回值的 unique_ptr,而该智能指针将负责调用 delete。可将 unique_ptr 存储到 STL 容器中,只要不调用将一个 unique_ptr 复制或赋值给另一个的算法(如 sort())。例如,可在程序中使用类似于下面的代码段。
unique_ptr<int> make_int(int n)
{
    return unique_ptr<int>(new int(n));
}

void show(unique_ptr<int>& p1)
{
    cout << *p1 << ' ';
}

int main()
{
    ...
    vector<unique_ptr<int> > vp(size);
    for(int i = 0; i < vp.size(); i++)
		vp[i] = make_int(rand() % 1000);           // copy temporary unique_ptr
    vp.push_back(make_int(rand() % 1000));     // ok because arg is temporary
    for_each(vp.begin(), vp.end(), show);      // use for_each()
    ...
}

其中 push_back 调用没有问题,因为它返回一个临时 unique_ptr,该 unique_ptr 被赋给 vp 中的一个 unique_ptr

另外,如果按值而不是按引用给 show() 传递对象,for_each() 将非法,因为这将导致使用一个来自 vp 的非临时 unique_ptr 初始化 pi,而这是不允许的。前面说过,编译器将发现错误使用 unique_ptr 的企图。

unique_ptr 为右值时,可将其赋给 shared_ptr,这与将一个 unique_ptr 赋给另一个 unique_ptr 需要满足的条件相同,即 unique_ptr 必须是一个临时对象。与前面一样,在下面的代码中,make_int() 的返回类型为 unique_ptr<int>

unique_ptr<int> pup(make_int(rand() % 1000));   	// ok
shared_ptr<int> spp(pup);                    	    // not allowed, pup as lvalue
shared_ptr<int> spr(make_int(rand() % 1000));    	// ok

模板 shared_ptr 包含一个显式构造函数,可用于将右值 unique_ptr 转换为 shared_ptrshared_ptr 将接管原来归 unique_ptr 所有的对象。

在满足 unique_ptr 要求的条件时,也可使用 auto_ptr,但 unique_ptr 是更好的选择。如果你的编译器没有 unique_ptr,可考虑使用 Boost 库提供的 scoped_ptr,它与 unique_ptr 类似。


转载内容:

C++ STL 四种智能指针_Dablelv的博客专栏-CSDN博客

C++ 智能指针详解 - 大气象 - 博客园