您的位置:首页 > 编程语言 > C语言/C++

[置顶] C++智能指针的分析与使用

2018-01-11 23:39 896 查看

手动管理的弊端

在简单的程序中,我们不大可能忘记释放 new 出来的指针,但是随着程序规模的增大,我们忘了 delete 的概率也随之增大。在 C++ 中 new 出来的指针,赋值意味着引用的传递,当赋值运算符同时展现出“值拷贝”和“引用传递”两种截然不同的语义时,就很容易导致“内存泄漏”。

手动管理内存带来的更严重的问题是,内存究竟要由谁来分配和释放呢?指针的赋值将同一对象的引用散播到程序各处,但是该对象的释放却只能发生一次。当在代码中用完了一个资源指针,该不该释放 delete 掉它?这个资源极有可能同时被多个对象拥有着,而这些对象中的任何一个都有可能在之后使用该资源,其余指向这个对象的指针就变成了“野指针”;那如果不 delete 呢?也许你就是这个资源指针的唯一使用者,如果你用完不 delete,内存就泄漏了。

资源的拥有者是系统,当我们需要时便向系统申请资源,当我们不需要时就让系统自己收回去(Garbage Collection)。当我们自己处理的时候,就容易出现各种各样的问题。

C++ 中的智能指针

为了让用户免去手动 delete 资源的烦恼,不少类库采用了 RAII 风格,即 Resource Acquisition Is Initialization(资源获取即初始化)。这种风格采用类来封装资源,在类的构造函数中获取资源,在类的析构中释放资源,这个资源可以是内存,可以是一个网络连接。

智能指针就是 C++ RAII 的一种应用,是存储指向动态分配对象指针的类。智能指针在面对异常的时候格外有用,因为它们能在适当的时间删除所指向的对象(可以了解一下异常安全)。C++ 中的智能指针首先出现在 boost 中,随着使用的人越来越多,C++11 也已经引入了智能指针来管理动态对象。C++11 主要提供了 shared_ptr、unique_ptr、weak_ptr 三种不同类型的智能指针。

shared_ptr

智能指针是(几乎总是)模板类,shared_ptr 同样是模板类,所以在创建 shared_ptr 时需要指定其指向的类型。shared_ptr 负责在不使用实例时释放由它管理的对象,同时它可以自由的共享它指向的对象。

shared_ptr 使用经典的“引用计数”的方法来管理对象资源。引用计数指的是,所有管理同一个裸指针( raw pointer )的 shared_ptr,都共享一个引用计数器,每当一个 shared_ptr 被赋值(或拷贝构造)给其它 shared_ptr 时,这个共享的引用计数器就加1,当一个 shared_ptr 析构或者被用于管理其它裸指针时,这个引用计数器就减1,如果此时发现引用计数器为0,那么说明它是管理这个指针的最后一个 shared_ptr 了,于是我们释放指针指向的资源。

在底层实现中,这个引用计数器保存在某个内部类型里(这个类型中还包含了 deleter,它控制了指针的释放策略,默认情况下就是普通的delete操作),而这个内部类型对象在 shared_ptr 第一次构造时以指针的形式保存在 shared_ptr 中(所以一个智能指针的析构会影响到其他指向同一位置的智能指针)。shared_ptr 重载了赋值运算符,在赋值和拷贝构造另一个 shared_ptr 时,这个指针被另一个 shared_ptr 共享。在引用计数归零时,这个内部类型指针与 shared_ptr 管理的资源一起被释放。此外,为了保证线程安全性,引用计数器的加1,减1操作都是原子操作,它保证 shared_ptr 由多个线程共享时不会爆掉。

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

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

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

下面看一个常见用法,包括:

1. 创建 shared_ptr 实例 2. 访问所指对象 3. 拷贝和赋值操作 4. 检查引用计数。更多的用法请自行查阅。

/*
* @filename:    shared_ptr.cpp
* @author:      Tanswer
* @date:        2018年01月10日 19:40:45
* @description:
*/

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

class Test{
public:
Test(string name){
name_ = name;
cout << this->name_ << "  constructor" << endl;
}
~Test(){
cout << this->name_ << "  destructor" << endl;
}

string name_;
};

int main()
{
/* 类对象 原生指针构造 */
shared_ptr<Test> pStr1(new Test("object"));
cout << (*pStr1).name_ << endl;
/* use_count()检查引用计数 */
cout << "pStr1 引用计数:" << pStr1.use_count() << endl;

shared_ptr<Test> pStr2 = pStr1;
cout << (*pStr2).name_ << endl;
cout << "pStr1 引用计数:" << pStr1.use_count() << endl;
cout << "pStr2 引用计数:" << pStr2.use_count() << endl;

/* 最安全高效的方法,make_shared 库函数 */
shared_ptr<Test> p = make_shared<Test>("pppppp");
shared_ptr<Test> q = make_shared<Test>("qqqqqq");
cout << "p = q 语句执行前 p: "<< (*p).name_ << " q: "<< (*q).name_ << endl;
/* 执行 p = q 这样的赋值操作,会递减p的引用计数值,递增q的引用计数值 */
p = q;  //此后p 和 p 指向相同对象
cout << "p = q 语句执行后 p: "<< (*p).name_ << " q: "<< (*q).name_ << endl;
cout << "p 引用计数:" << p.use_count() << endl;
cout << "p 引用计数:" << p.use_count() << endl;

/* 先new 一个对象,把原始指针传递给shared_ptr的构造函数 */
int *pInt1 = new int(11);
shared_ptr<int> pInt2(pInt1);

/* unique()来检查某个shared_ptr 是否是原始指针唯一拥有者 */
cout << pInt2.unique() << endl; //true 1
/* 用一个shared_ptr对象来初始化另一个shared_ptr实例 */
shared_ptr<int> pInt3(pInt2);

cout << pInt2.unique() << endl; //false 0

return 0;
}


错误用法一:循环引用

循环引用可以说是引用计数策略最大的缺点,“循环引用”简单来说就是:两个对象互相使用一个 shared_ptr 成员变量指向对方(你中有我,我中有你)。突然想到一个问题:垃圾回收器是如何处理循环引用的? 下面看一个例子:

/*
* @filename:    循环引用.cpp
* @author:      Tanswer
* @date:        2018年01月10日 22:43:45
* @description:
*/

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

class Children;
class Parent{
public:
~Parent(){
cout << "Parent    destructor" << endl;
}

shared_ptr<Children> children;
};

class Children{
public:
~Children(){
cout << "Children  destructor" << endl;
}
shared_ptr<Parent> parent;
};

void Test()
{
shared_ptr<Parent> pParent(new Parent());
shared_ptr<Children> pChildren(new Children());
if(pParent && pChildren){
pParent -> children = pChildren;
pChildren -> parent = pParent;
}

cout << "pParent use_count: " << pParent.use_count() << endl;
cout << "pChildren use_count: " << pChildren.use_count() << endl;
}

int main()
{
Test();
return 0;
}


输出结果如下:



退出之前,它们的 use_count() 都为2,退出了 Test() 后,由于 pParent 和 pChildren 对象互相引用,它们的引用计数都是 1,不能自动释放(可以看到没有调用析构函数),并且此时这两个对象再无法访问到。这就引起了c++中那臭名昭著的“内存泄漏”。

那么如何解除循环引用呢?

weak_ptr

使用weak_ptr 来打破循环引用,它与一个 shared_ptr 绑定,但却不参与引用计数的计算,不论是否有 weak_ptr 指向,一旦最后一个指向对象的 shared_ptr 被销毁,对象就会被释放。weak_ptr 像是 shared_ptr 的一个助手。同时,在需要时,它还能摇身一变,生成一个与它绑定的 shared_ptr 共享引用计数的新 shared_ptr。总而言之,weak_ptr 的作用就是:在需要时变出一个 shared_ptr,在其他时候不干扰 shared_ptr 的引用计数。

它没有重载 * 和 -> 运算符,因此不可以直接通过 weak_ptr 访问对象,典型的用法是通过 lock() 成员函数来获得 shared_ptr,进而使用对象。下面是 weak_ptr 的一般用法:

std::shared_ptr<int> sh = std::make_shared<int>();
// 用一个shared_ptr初始化
std::weak_ptr<int> w(sh);
// 变出 shared_ptr
std::shared_ptr<int> another = w.lock();
// 判断weak_ptr所观察的shared_ptr的资源是否已经释放
bool isDeleted = w.expired();


我们看看它如何来解决上面的循环引用。

/*
* @filename:    打破循环引用.cpp
* @author:      Tanswer
* @date:        2018年01月10日 22:43:45
* @description:
*/

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

class Children;

class Parent{
public:
~Parent(){
cout << "Parent    destructor" << endl;
}

weak_ptr<Children> children; //注意这里
};

class Children{
public:
~Children(){
cout << "Children  destructor" << endl;
}
weak_ptr<Parent> parent;    //注意这里
};

void Test()
{
shared_ptr<Parent> pParent(new Parent());
shared_ptr<Children> pChildren(new Children());
if(pParent && pChildren){
pParent -> children = pChildren;
pChildren -> parent = pParent;
}

// 看一下各自的引用计数
cout << "pParent use_count: " << pParent.use_count() << endl;
cout << "pChildren use_count: " << pChildren.use_count() << endl;
}

int main()
{
Test();
return 0;
}


结果如下:



可以看到各自的引用计数分别为1,而且函数执行完毕后,各自指向的对象得到析构,这样来解除了上面的循环引用。

当然解决循环引用的方法不止这一种,我们还可以当只剩下最后一个引用的时候手动打破循环引用释放对象;或者当 Parent 的生命期超过 Children 的生命期时,Children 改用一个普通指针指向 Parent。这两种手段都需要程序员去手动控制,比较麻烦而且容易出错,所以一般就使用 weak_ptr 。

错误用法二:多个无关的shared_ptr管理同一裸指针

int *a = new int;
shared_ptr<int> p1(a);
shared_ptr<int> p2(a);

cout << "p1 的引用计数: "<< p1.use_count() << endl;
cout << "p2 的引用计数: "<< p2.use_count() << endl;




p1 和 p2 同时管理同一裸指针 a,这样操作的话,p1 和 p2 拥有两个独立的引用计数器(初始化 p2 时,直接用的裸指针,没有办法获取 p1 的引用计数),于是会导致 a 被 delete 两次,分别由 p1 和 p2 的析构导致。

为了避免这种情况,永远不要将 new 用在 shared_ptr 构造函数参数列表以外的地方,或者干脆不用 new,改用 make_shared。

但是下面的情况会绕过上面这种说法,请看:

class A {
public:
std::shared_ptr<A> getShared_ptr() {
return std::shared_ptr<A>(this);
}
};
int main() {
std::shared_ptr<A> pa = std::make_shared<A>();
std::shared_ptr<A> pbad = pa->getShared_ptr();
return 0;
}


程序直接崩掉,pa 和 pbad 也拥有各自独立的引用计数器,和上面一样。

总而言之,管理同一资源的 shared_ptr 只能由同一个初始化 shared_ptr 通过一系列赋值或者拷贝构造途径得来。更抽象地说,管理同一资源的 shared_ptr 的构造顺序必须是一个无环有向的连通图,无环能够保证没有循环引用,连通性能够保证每个 shard_ptr 都来自相同的源。

enable_shared_from_this

那如何解决“生成 this 指针的 shared_ptr” 的问题?使用 enable_shared_from_this,这是一个基类:

template<typename T>
class enable_shared_from_this {
public:
shared_ptr<T> shared_from_this();
}


如果想要一个由 shared_ptr 管理的类 A 对象能够在方法内部得到 this 指针的 shared_ptr,且返回的 shared_ptr 和管理这个类的 shared_ptr 共享引用计数,只需让这个类派生自 enable_shared_from_this < A> 即可,之后调用 shared_from_this() 即可获得正确的 shared_ptr。

muduo 中的 TcpConnection Class 就是继承 enable_shared_from_this。

class TcpConnection : boost::noncopyable,
public boost::enable_shared_from_this<TcpConnection>


那么上面的例子就可以改成这样:

/*
* @filename:    shared_ptr_test1.cpp
* @author:      Tanswer
* @date:        2018年01月12日 00:04:29
* @description:
*/

#include <iostream>
#include <memory>

using namespace std;

class A : public enable_shared_from_this<A> {
public:
A()
{
cout << "constructor..." << endl;
}
~A()
{
cout << "destructor..." << endl;
}
};
int main()
{
shared_ptr<A> pa = make_shared<A>();
shared_ptr<A> pgood = pa -> shared_from_this();
return 0;
}




参考链接:

shared_ptr 原理及事故

c++ 智能指针及 循环引用问题

enable_shared_from_this模板类使用完全解析
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: