C++ 智能指针详解

C++ 智能指针详解

文章目录

  • C++ 智能指针详解
    • 1. 智能指针能解决什么问题?
    • 2. 智能指针的发展
    • 3. 智能指针 shared_ptr
      • 3.1 创建shared_ptr实例
      • 3.2 访问所指对象
      • 3.3 拷贝和赋值操作
      • 3.4 检查引用计数
      • 3.5 reset 函数
    • 4. 智能指针 weak_ptr
      • 4.1 weak_ptr 的基本用法
      • 4.2 weak_ptr 的相关函数
      • 4.3 weak_ptr 总结
    • 5. 智能指针 unique_ptr
    • 6. 智能指针注意事项

1. 智能指针能解决什么问题?

在C++中,动态内存的管理是通过一对运算符来完成的:

new,在动态内存中为对象分配空间并返回一个指向该对象的指针,可以选择对对象进行初始化;
delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。

动态内存的使用很容易出问题,因为确保在正确的时间释放内存是极其困难的。有时会忘记释放内存,在这种情况下会产生 内存泄露;有时在尚有指针引用内存的情况下就释放了它,在这种情况下就会产生引用非法内存的指针。

为了更容易(同时也更安全)地使用动态内存,C++11标准库提供智能指针(smart pointer)类型来管理动态对象。

智能指针的行为类似常规指针,重要的区别是它负责 自动释放所指的对象

智能指针是模板类而不是指针。类似vector,当创建一个智能指针时,必须提供额外的信息即指针可以指向的类型。默认初始化的智能指针中保存着一个空指针。智能指针的使用方式与普通指针类似。解引用一个智能指针返回它指向的对象。如果在一个条件判断中使用智能指针,效果就是检测它是否为空。

2. 智能指针的发展

1. auto_ptr (已被C++11舍弃)

最早的智能指针 auto_ptr 出现在 C++ 98 里面,目前已经被 C++11 标准舍弃

  • auto_ptr,对于拷贝构造和赋值运算符重载,该智能指针采用 管理权转移 的方式

当一个指针拷贝构造另一个指针时,当前指针就将对空间的管理权交给拷贝的那个指针,当前指针就指向空);

  • 但是这种方式不符合指针的要求(可以允许多个指针指向同一块空间,将一个指针赋值给另一个指针的时候,就是需要让两个指针指向同一块空间,而 auto_ptr只允许一块空间上只能有一个指针指向它),并且当管理权转移之后要想再访问之前的指针,就会出错,因为之前的指针已经为NULL,就会出现解引用空指针的问题。

2. Boost 的 scoped_ptr、shared_ptr、weak_ptr

因为 auto_ptr 有缺陷,但是 C++ 标准里面从 C++98 到 C++11 之间没有出现新的智能指针能解决这个缺陷,所以在这段时间内,boost 这个官方组织 就增加了智能指针(scoped_ptr,shared_ptr,weak_ptr等)

scoped_ptr 采用防拷贝的方式(防拷贝就是不允许拷贝,拷贝就会出错;防拷贝的实现:将拷贝构造和的赋值运算符重载只声明不实现,并且声明为私有)。

shared_ptr 为共享指针,里面采用引用计数,当有其他的 shared_ptr 指向同一块空间的时候就增加引用计数,当引用计数减为 0 的时候才释放该智能指针管理的那块空间。

weak_ptr 是位解决 shared_ptr 循环引用问题而出现的。

3. C++11 的 Unique_ptr、shared_ptr、Weak_ptr

C++11 在 Boost 库的基础上设计出三种新的智能指针。

C++11 里面的 unique_ptr 就是 boost 库里面的 scoped_ptr(防拷贝,独占);

C++11 的 shared_ptr 被广泛使用,在下面详细介绍。

3. 智能指针 shared_ptr

shared_ptr 是一个引用计数智能指针,用于共享对象的所有权,也就是说它允许多个指针指向同一个对象。这一点与原始指针一致。

对于shared_ptr在拷贝和赋值时的行为,《C++Primer第五版》中有详细的描述:

每个shared_ptr都有一个关联的计数值,通常称为引用计数。无论何时我们拷贝一个shared_ptr,计数器都会递增。

例如,当用一个 shared_ptr初始化另一个 shred_ptr,或将它当做参数传递给一个函数以及作为函数的返回值时,它所关联的计数器就会递增。当我们给 shared_ptr 赋予一个新值或是 shared_ptr 被销毁(例如一个局部的shared_ptr离开其作用域)时,计数器就会递减。一旦一个 shared_ptr 的计数器变为0,它就会自动释放自己所管理的对象。

3.1 创建shared_ptr实例

最安全和高效的方法是调用make_shared库函数, 该函数会在堆中分配一个对象并初始化,最后返回指向此对象的share_ptr实例。如果你不想使用make_ptr,也可以先明确new出一个对象,然后把其原始指针传递给share_ptr的构造函数。

int main() 
{

    // 传递给make_shared函数的参数必须和shared_ptr所指向类型的某个构造函数相匹配
    shared_ptr<string> pStr = make_shared<string>(10, 'a');
    cout << *pStr << endl;  //  aaaaaaaaaa

    int *p = new int(5);
    shared_ptr<int> pInt(p);
    cout << *pInt << endl;  // 5
}

3.2 访问所指对象

shared_ptr 的使用方式与普通指针的使用方式类似, 既可以使用解引用操作符 * 获得原始对象进而访问其各个成员,也可以使用指针访问符 -> 来访问原始对象的各个成员。

3.3 拷贝和赋值操作

我们可以用一个shared_ptr对象来初始化另一个share_ptr实例,该操作会增加其引用计数值


int main() 
{
    shared_ptr<string> pStr = make_shared<string>(10, 'a');
    cout << pStr.use_count() << endl;  //  1

    shared_ptr<string> pStr2(pStr);
    cout << pStr.use_count() << endl;  //  2
    cout << pStr2.use_count() << endl;  //  2
}

如果 shared_ptr 实例 p 和另一个 shared_ptr 实例 q 所指向的 类型相同或者可以相互转换,我们还可以进行诸如 p = q 这样赋值操作。

该操作会递减 p 的引用计数值,递增 q 的引用计数值。
p不再指向原来的对象,赋值操作后 p 指向 q 指向的对象。

class Example;
shared_ptr<Example> pStr = make_shared<Example>("a object");
shared_ptr<Example> pStr2 = make_shared<Example>("b object");

pStr = pStr2;   // 此后pStr和pStr指向相同对象

3.4 检查引用计数

shared_ptr提供了两个函数来检查其共享的引用计数值,分别是 unique() 和 use_count()。

在前面,我们已经多次使用过use_count()函数,该函数返回当前指针的引用计数值。值得注意的是 **use_count() 函数可能效率很低 **,应该只把它用于测试或调试。

unique() 函数用来测试该 shared_ptr 是否是原始指针 唯一拥有者 ,也就是 use_count() 的返回值为 1 时返回 true,否则返回 false。

int main() 
{
    shared_ptr<string> pStr = make_shared<string>(10, 'a');
    cout << pStr.unique() << endl;  // true

    shared_ptr<string> pStr2(pStr);
    cout << pStr2.unique() << endl; // false;
}

3.5 reset 函数

reset 的作用和赋值操作相似,调用者会指向新的对象

class Example;
shared_ptr<Example> pStr = make_shared<Example>("a object");
shared_ptr<Example> pStr2 = make_shared<Example>("b object");

pStr.reset(pStr2);   // 此后pStr和pStr指向相同对象

参考 https://blog.csdn.net/Xiejingfa/article/details/50750037

4. 智能指针 weak_ptr

4.1 weak_ptr 的基本用法

shared_ptr 相对于 auto_ptr 来说是近乎完美的,但是通过引用计数实现的它,虽然解决了指针独占的问题,但也引来了 引用成环 的问题,这种问题靠它自己是没办法解决的,所以在 C++11 的时候将 shared_ptrweak_ptr 一起引入了标准库,用来解决循环引用的问题。

weak_ptr 本身也是一个模板类,但是不能直接用它来定义一个智能指针的对象,只能指向shared_ptr对象,同时也不能将weak_ptr对象直接赋值给shared_ptr类型的变量,并且这样并不会改变引用计数的值。

查看 weak_ptr 的代码时发现,它主要有 lock、swap、reset、expired、operator=、use_count 几个函数,与 shared_ptr 相比多了 lock、expired 函数,但是却少了 get 函数,甚至连 operator* 和 operator-> 都没有,可用的函数数量少的可怜,下面通过一些例子来了解一下weak_ptr的具体用法。

#include 
#include 

using namespace std;

class CB;

class CA
{
public:
    CA(){}
    ~CA() { cout << "~CA() called! " << endl; }
    void set_ptr(shared_ptr<CB>& ptr) { m_ptr_b = ptr; }
private:
    shared_ptr<CB> m_ptr_b;
};

class CB
{
public:
    CB(){}
    ~CB() { cout << "~CB() called! " << endl; }
    void set_ptr(shared_ptr<CA>& ptr) { m_ptr_a = ptr; }
private:
    shared_ptr<CA> m_ptr_a;
};

int main()
{
	shared_ptr<CA> ptr_a(new CA());
    shared_ptr<CB> ptr_b(new CB());

    cout << "a use count : " << ptr_a.use_count() << endl;
    cout << "b use count : " << ptr_b.use_count() << endl;

    ptr_a->set_ptr(ptr_b);
    ptr_b->set_ptr(ptr_a);

    cout << "a use count : " << ptr_a.use_count() << endl;
    cout << "b use count : " << ptr_b.use_count() << endl;
	return 0;
}

C++ 智能指针详解_第1张图片

通过结果可以看到,最后 CA 和 CB 的对象并没有被析构,其中的引用效果如下图所示:

起初定义完 ptr_a 和 ptr_b 时,只有①③两条引用,然后调用函数 set_ptr 后又增加了②④两条引用,当主函数结束时, ptr_a 和 ptr_b被销毁,也就是①③两条引用会被断开,但是②④两条引用依然存在,每一个的引用计数都不为0,结果就导致其指向的内部对象无法析构,造成内存泄漏。

C++ 智能指针详解_第2张图片

解决这种状况的办法就是将两个类中的一个成员变量改为weak_ptr对象,因为weak_ptr不会增加引用计数,使得引用形不成环,最后就可以正常的释放内部的对象,不会造成内存泄漏,比如将CB中的成员变量改为weak_ptr对象。

将class CB 中 shared_ptr m_ptr_a; 修改为
weak_ptr m_ptr_a;

C++ 智能指针详解_第3张图片

4.2 weak_ptr 的相关函数

weak_ptr 中只有 函数 lock 和 expired 两个函数比较重要,因为它本身不会增加引用计数,所以它指向的对象可能在它用的时候已经被释放了,所以在用之前需要使用expired 函数来检测是否过期,lock 是为了保证在多线程环境下能安全使用,

class tester ;

void fun(shared_ptr<tester> sp)
{
  // !!!在这大量使用sp指针.
  shared_ptr<tester> tmp = sp;
}

int main()
{
  shared_ptr<tester> sp1(new tester);
  // 开启两个线程,并将智能指针传入使用。
  thread t1(bind(&fun, sp1));
  thread t2(bind(&fun, sp1));
  t1.join();
  t2.join();
  return 0;
}

这个代码带来的问题很显然,由于多线程同时访问智能指针,并将其赋值到其它同类智能指针时,很可能发生两个线程同时在操作引用计数(但并不一定绝对发生),而导致计数失败或无效等情况,从而导致程序崩溃,如若不知根源,就无法查找这个bug,那就只能向上帝祈祷程序能正常运行。

引入weak_ptr可以解决这个问题,将 fun 函数修改如下:

void fun(weak_ptr<tester> wp)
{
	if (!wk_ptr_a.expired())
	{……}
}

此时这个方案只 **解决了多线程对引用计数同时访问的读写问题,**并没有解决对 share_ptr 指向的数据的多线程安全问题,因此 weak_ptr 只是安全的获得 share_ptr 的一种方式,因为可以确保在获得share_ptr的时候的多线程安全。

为了保证数据安全,我们还需要 lock 加锁。

void fun(weak_ptr<tester> wp)
{
  shared_ptr<tester> sp = wp.lock;
  if (sp)
  {
    // 在这里可以安全的使用sp指针.
  }
  else
  {
    std::cout << “指针已被释放!<< std::endl;
  }
} 

4.3 weak_ptr 总结

  • weak_ptr 虽然是一个模板类,但是不能用来直接定义指向原始指针的对象。
  • weak_ptr 接受 shared_ptr 类型的变量赋值,但是反过来是行不通的,需要使用lock函数。
  • weak_ptr 设计之初就是为了服务于 shared_ptr 的,所以不增加引用计数就是它的核心功能。
  • 由于不知道什么之后 weak_ptr 所指向的对象就会被析构掉,所以使用之前请先使用 expired 函数检测一下。

参考 https://blog.csdn.net/albertsh/article/details/82286999
参考 https://blog.csdn.net/man_sion/article/details/77196766

5. 智能指针 unique_ptr

unique_ptr "独占"所指向的对象,基本用法和 shared_ptr 相同,两者的区别在于:

C++ 智能指针详解_第4张图片

6. 智能指针注意事项

(0) 可以认为每个 shared_ptr 都有一个关联的计数器,通常称其为引用计数 (reference count)。无论何时拷贝一个 shared_ptr ,计数器都会递增。

例如,当用一个 shared_ptr 初始化另一个 shared_ptr,或将它作为参数传递给一个函数以及作为函数的返回值时,它所关联的计数器就会递增。

当给 shared_ptr 赋予一个新值或是 shared_ptr 被销毁(例如一个局部的shared_ptr离开其作用域)时,计数器就会递减。一旦一个 shared_ptr 的计数器变为 0,它就会自动释放自己所管理的对象。

(1) shared_ptr 的类型转换不能使用一般的 static_cast,这种方式进行的转换会导致转换后的指针无法再被 shared_ptr 对象正确的管理。应该使用专门用于 shared_ptr 类型转换的

static_pointer_cast() ,
const_pointer_cast()
dynamic_pointer_cast()。

(2) 可以通过构造函数、赋值函数或者 make_shared 函数初始化智能指针。

最安全的分配和使用动态内存的方法是调用一个名为 make_shared 的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的 shared_ptr。

(3) 不要把一个 **原生指针 **给多个 shared_ptr 管理;

A* p = new A(10);
shared_ptr sp1§, sp2§;

sp1 和 sp2 并不会共享同一个对 p 的托管计数,而是各自将对 p 的托管计数都记为 1(sp2 无法知道 p 已经被 sp1 托管过)。这样,当 sp1 消亡时要析构 p,sp2 消亡时要再次析构 p,这会导致 程序崩溃

只有通过拷贝或复制时,才会增加 shared_ptr 的引用计数。
当进行拷贝或赋值操作时,每个 shared_ptr 都会记录有多少个其它 shared_ptr 指向相同的对象。

class A;
shared_ptr<A> sp1(new A(2)); 
shared_ptr<A> sp2(sp1);      //拷贝操作

shared_ptr<A> sp3;
sp3 = sp2;   				 //赋值操作

(4) 只有指向动态分配的对象的指针才能交给 shared_ptr 对象托管。将指向普通局部变量、全局变量的指针交给 shared_ptr 托管,编译时不会有问题,但程序运行时会出错,因为不能析构一个并没有指向动态分配的内存空间的指针。

(5) 在多线程环境中使用共享指针的代价非常大,这是因为你需要避免关于引用计数的数据竞争;shared_ptr并不是万能的,而且使用它们的话也是需要一定的开销的。

(6) 如果你使用智能指针管理的资源不是 new 分配的内存,记住传递给它一个删除器。
shared_ptr 的默认能力是管理动态内存,但支持自定义的Deleter以实现个性化的资源释放动作。

所谓删除器就是:你需要写一个函数来释放不是通过 new 分配的内存 (那应该就是 malloc),这个函数就是删除器,你需要在定义智能指针的时候将这个函数传递给智能指针对象。

这样智能指针就知道该如何释放内存。

class A;

void deleter(A* p)
{
	free(p);
}
int main ()
{
	A*p=(A*)malloc(sizeof(A));
	shared_ptr<A>sp(p,deleter);

参考 https://blog.csdn.net/fengbingchun/article/details/52202007
参考 https://blog.csdn.net/man_sion/article/details/77196766

你可能感兴趣的:(C++)