C++ 智能指针

在介绍智能指针之前,先来看原始指针的一些不便之处:

  • 它的声明不能指示所指到底是单个对象还是数组。

  • 它的声明没有告诉你用完后是否应该销毁它,即指针是否拥有所指之物。

  • 如果你决定你应该销毁指针所指对象,没人告诉你该用delete还是其他析构机制(比如将指针传给专门的销毁函数)。

  • 如果你发现该用delete。第一点说了可能不知道该用单个对象形式(“delete”)还是数组形式(“delete[]”)。如果用错了结果是未定义的。

  • 假设你确定了指针所指,知道销毁机制,也很难确定你在所有执行路径上都执行了恰为一次销毁操作(包括异常产生后的路径)。少一条路径就会产生资源泄漏,销毁多次还会导致未定义行为。

  • 一般来说没有办法告诉你指针是否变成了悬空指针(dangling pointers),即内存中不再存在指针所指之物。在对象销毁后指针仍指向它们就会产生悬空指针。

Item 18: Use std::unique_ptr for exclusive-ownership resource management(对于独占资源使用std::unique_ptr)

1. 性能

可以合理假设,默认情况下,std::unique_ptr 大小等同于原始指针,而且对于大多数操作(包括取消引用),他们执行的指令完全相同。这意味着你甚至可以在内存和时间都比较紧张的情况下使用它。如果原始指针够小够快,那么std::unique_ptr一样可以。

2. 用法

2.1 拷贝与移动

std::unique_ptr 体现了专有所有权(exclusive ownership)语义。

  • 移动一个std::unique_ptr将所有权从源指针转移到目的指针。(源指针被设为null。)

  • 拷贝一个std::unique_ptr是不允许的,因为如果你能拷贝一个std::unique_ptr,你会得到指向相同内容的两个std::unique_ptr,每个都认为自己拥有(并且应当最后销毁)资源,销毁时就会出现重复销毁。
    因此,std::unique_ptr 是一种只可移动类型(move-only type)

2.2 构造方式

// 1. 
std::unique_ptr<int> sp1(new int(123));
// 2.
std::unique_ptr<int> sp2;
sp2.reset(new int(123));
// 3. 
std::unique_ptr<int> sp3 = std::make_unique<int>(123);
/* 尝试将原始指针(比如new创建)赋值给std::unique_ptr通不过编译,因为是一种从原始指针到智能指针的隐式转换。
 这种隐式转换会出问题,所以 C++11的智能指针禁止这个行为。这就是通过reset来让up接管通过new创建的对象的所有权的原因。*/
unique_ptr<int> up = nullptr;
int* ip = new int();
// up = ip; // 报错: no operator "=" matches these operands
up.reset(ip);

C++ 14 加入了 std::make_unique,而C++ 11没有

2.3 释放

默认情况下,资源析构通过对std::unique_ptr里原始指针调用delete来实现。

但是在构造过程中,std::unique_ptr对象可以被设置为使用(对资源的)自定义删除器:当资源需要销毁时可调用的任意函数(或者函数对象,包括lambda表达式): std::unique_ptr<T, DeletorFuncPtr>

#include <iostream>
#include <memory>
 
class Socket
{
public:
    Socket() {}
    ~Socket() {}

    //关闭资源句柄
    void close()
    {
	...
    }
};
 
int main()
{
    auto deletor = [](Socket* pSocket) {
        //关闭句柄
        pSocket->close();
        delete pSocket;
    };
    std::unique_ptr<Socket, decltype(deletor)> spSocket(new Socket(), deletor); // 返回类型大小是Socket*的大小
    return 0;
}

注意:
上面谈到,当使用默认删除器时(如delete),你可以合理假设std::unique_ptr对象和原始指针大小相同。当自定义删除器时,情况可能不再如此。函数指针形式的删除器,通常会使std::unique_ptr的从一个字(word)大小增加到两个。对于函数对象形式的删除器来说,变化的大小取决于函数对象中存储的状态多少,无状态函数(stateless function)对象(比如不捕获变量的lambda表达式)对大小没有影响,这意味当自定义删除器可以实现为函数或者lambda时,尽量使用lambda:

#include <iostream>
#include <memory>

class Socket
{
public:
    Socket() {}//std::cout << "gouzao" << std::endl;}
    ~Socket() {}//std::cout << "xigou" << std::endl;}

    //关闭资源句柄
    void close()
    {
    }
};

void deletor2(Socket* pSocket) {
    //关闭句柄
    pSocket->close();
    delete pSocket;
}

int main()
{
    Socket* pSocket = new Socket();
    auto deletor = [](Socket* pSocket) {
        //关闭句柄
        pSocket->close();
        delete pSocket;
    };
    std::unique_ptr<Socket, decltype(deletor)> spSocket1(new Socket(), deletor); // 返回类型大小是Socket*的大小
    std::unique_ptr<Socket, void(*)(Socket*)> spSocket2(new Socket(), deletor); // 返回类型大小是Socket*的指针加至少一个函数指针大小
    auto spSocket3 = std::make_shared<Socket>();
    std::shared_ptr<Socket> spSocket4(new Socket(),deletor);
    std::weak_ptr<Socket> sp5(spSocket3);
    std::cout << "原始指针大小: " << sizeof(pSocket) << std::endl;
    std::cout << "unique_ptr with lambda 大小:" << sizeof(spSocket1) << std::endl;
    std::cout << "unique_ptr with funcp 大小: " << sizeof(spSocket2) << std::endl;
    std::cout << "shared_ptr 大小:" << sizeof(spSocket3) << std::endl;
    std::cout << "shared_ptr 大小:" << sizeof(spSocket4) << std::endl;
    std::cout << "weak_ptr 大小:" << sizeof(sp5) << std::endl;
}
}
mingyu@ndsl84:~/cudalearn$ ./a.out 
原始指针大小: 8
unique_ptr with lambda 大小:8
unique_ptr with funcp 大小: 16
shared_ptr 大小:16
shared_ptr 大小:16
weak_ptr 大小:16

2.4 两种形式

std::unique_ptr有两种形式:

  • 一种用于单个对象(std::unique_ptr
  • 一种用于数组(std::unique_ptr<T[]>)
    结果就是,指向哪种形式没有歧义。std::unique_ptr的API设计会自动匹配你的用法,比如operator[]就是数组对象,解引用操作符(operator*和operator->)就是单个对象专有。

你应该对数组的std::unique_ptr的存在兴趣泛泛,因为std::array,std::vector,std::string这些更好用的数据容器应该取代原始数组。std::unique_ptr<T[]>有用的唯一情况是你使用类似C的API返回一个指向堆数组的原始指针,而你想接管这个数组的所有权。

2.5 转换

std::unique_ptr是C++11中表示专有所有权的方法,但是其最吸引人的功能之一是它可以轻松高效的转换为std::shared_ptr

// std::unique_ptr 禁止复制语义,但存在特例,即可以通过一个函数返回一个std::unique_ptr
#include <memory>
 
std::unique_ptr<int> func(int val)
{
    std::unique_ptr<int> up(new int(val));
    return up;
}
 
int main()
{
    std::shared_ptr<int> sp1 = func(123); // 将返回的unique_ptr 转换成 shared_ptr, 这种可以
    // 但是下面这种转换不允许的
    std::unique_ptr<int> a (new int());
    std::shared_ptr<int> b = a; // no suitable user-defined conversion from "std::unique_ptr<int, std::default_delete<int>>" to "std::shared_ptr<int>" exists
    return 0;
}

由下文的介绍可以了解到: 类似std::unique_ptr,std::shared_ptr使用delete作为资源的默认销毁机制,但是它也支持自定义的删除器。这种支持有别于std::unique_ptr。对于std::unique_ptr来说,删除器类型是智能指针类型的一部分。对于std::shared_ptr则不是:

auto loggingDel = [](Widget *pw)        //自定义删除器
                  {                     //(和条款18一样)
                      makeLogEntry(pw);
                      delete pw;
                  };

std::unique_ptr<Widget, decltype(loggingDel)> upw(new Widget, loggingDel); //删除器类型是指针类型的一部分
std::shared_ptr<Widget> spw(new Widget, loggingDel);        //删除器类型不是指针类型的一部分

3. unique_ptr 的总结

  • std::unique_ptr是轻量级、快速的、只可移动(move-only)的管理专有所有权语义资源的智能指针
  • 默认情况,资源销毁通过delete实现,但是支持自定义删除器。有状态的删除器和函数指针会增加std::unique_ptr对象的大小
  • std::unique_ptr转化为std::shared_ptr非常简单

Item 19: Use std::shared_ptr for shared-ownership resource management(对于共享资源使用std::shared_ptr)

1. 性能

std::shared_ptr通过引用计数(reference count)来确保它是否是最后一个指向某种资源的指针,引用计数关联资源并跟踪有多少std::shared_ptr指向该资源。

std::shared_ptr构造函数递增引用计数值(注意是通常——因为支持移动构造函数,不需要修改引用计数值),析构函数递减值,拷贝赋值运算符做前面这两个工作。(如果sp1和sp2是std::shared_ptr并且指向不同对象,赋值“sp1 = sp2;”会使sp1指向sp2指向的对象。直接效果就是sp1引用计数减一,sp2引用计数加一。)如果std::shared_ptr在计数值递减后发现引用计数值为零,没有其他std::shared_ptr指向该资源,它就会销毁资源。

引用计数暗示着性能问题:

  • std::shared_ptr大小是原始指针的两倍(可见Item18中的2.3的例子),因为它内部包含一个指向资源的原始指针,还包含一个指向资源的引用计数值的原始指针。(这种实现法并不是标准要求的,但是我(指原书作者Scott Meyers)熟悉的所有标准库都这样实现。)

  • 引用计数的内存必须动态分配。 概念上,引用计数与所指对象关联起来,但是实际上被指向的对象不知道这件事情(译注:不知道有一个关联到自己的计数值)。因此它们没有办法存放一个引用计数值。(一个好消息是任何对象——甚至是内置类型的——都可以由std::shared_ptr管理。)Item21会解释使std::make_shared创建std::shared_ptr可以避免引用计数的动态分配(因此,直接使用new需要为目标对象进行一次内分配,为控制块再进行一次内分配;而使用std::make_shared只有一次分配,因为std::make_shared分配一块内存,同时容纳了目标对象和控制块。),但是还存在一些std::make_shared不能使用的场景,这时候引用计数就会动态分配。

  • 递增递减引用计数必须是原子性的,因为多个reader、writer可能在不同的线程。比如,指向某种资源的std::shared_ptr可能在一个线程执行析构(于是递减指向的对象的引用计数),在另一个不同的线程,std::shared_ptr指向相同的对象,但是执行的却是拷贝操作(因此递增了同一个引用计数)。原子操作通常比非原子操作要慢,所以即使引用计数通常只有一个word大小,你也应该假定读写它们是存在开销的。

2. 用法

2.1 拷贝与移动

std::shared_ptr 持有的资源可以在多个 std::shared_ptr 之间共享

  • std::shared_ptr构造函数递增引用计数值(注意是通常——因为支持移动构造函数,不需要修改引用计数值),析构函数递减值,拷贝赋值运算符做前面这两个工作。

  • 从一个std::shared_ptr移动构造新std::shared_ptr会将原来的std::shared_ptr设置为null,那意味着老的std::shared_ptr不再指向资源,同时新的std::shared_ptr指向资源。这样的结果就是不需要修改引用计数值。因此移动std::shared_ptr会比拷贝它要快:拷贝要求递增引用计数值,移动不需要。移动赋值运算符同理,所以移动构造比拷贝构造快,移动赋值运算符也比拷贝赋值运算符快。

2.2 构造方式

std::unique_ptr类似

2.3 析构

类似std::unique_ptr(参见Item18),std::shared_ptr使用delete作为资源的默认销毁机制,但是它也支持自定义的删除器。这种支持有别于std::unique_ptr对于std::unique_ptr来说,删除器类型是智能指针类型的一部分。对于std::shared_ptr则不是

auto loggingDel = [](Widget *pw)        //自定义删除器
                  {                     //(和条款18一样)
                      makeLogEntry(pw);
                      delete pw;
                  };

std::unique_ptr<Widget, decltype(loggingDel)> upw(new Widget, loggingDel); //删除器类型是指针类型的一部分
std::shared_ptr<Widget> spw(new Widget, loggingDel);        //删除器类型不是指针类型的一部分

因此,std::shared_ptr的设计更为灵活。考虑有两个std::shared_ptr<Widget>,每个自带不同的删除器(比如通过lambda表达式自定义删除器):

auto customDeleter1 = [](Widget *pw) { … };     //自定义删除器,
auto customDeleter2 = [](Widget *pw) { … };     //每种类型不同
std::shared_ptr<Widget> pw1(new Widget, customDeleter1);
std::shared_ptr<Widget> pw2(new Widget, customDeleter2);

因为pw1和pw2有相同的类型(虽然他们的删除其类型不同,但对于shared_ptr而言,删除器类型并不是指针类型的一部分),所以它们都可以放到存放那个类型的对象的容器中:

std::vector<std::shared_ptr<Widget>> vpw{ pw1, pw2 };

它们也能相互赋值,也可以传入一个形参为std::shared_ptr<Widget>的函数。但是自定义删除器类型不同的std::unique_ptr就不行,因为std::unique_ptr把删除器视作类型的一部分。

另一个不同于std::unique_ptr的地方是,指定自定义删除器不会改变std::shared_ptr对象的大小。不管删除器是什么,一个std::shared_ptr对象都是两个指针大小。这是个好消息,但是它应该让你隐隐约约不安。自定义删除器可以是函数对象,函数对象可以包含任意多的数据。它意味着函数对象是任意大的。std::shared_ptr怎么能引用一个任意大的删除器而不使用更多的内存?

它不能。它必须使用更多的内存。然而,那部分内存不是std::shared_ptr对象的一部分。那部分在堆上面,或者std::shared_ptr创建者利用std::shared_ptr对自定义分配器的支持能力,那部分内存随便在哪都行。

2.4 内部实现

前面提到了std::shared_ptr对象包含了所指对象的引用计数的指针。没错,但是有点误导人。因为引用计数是另一个更大的数据结构的一部分,那个数据结构通常叫做控制块(control block)

每个std::shared_ptr管理的对象都有个相应的控制块。控制块除了包含引用计数值外还有一个自定义删除器的拷贝,当然前提是存在自定义删除器。如果用户还指定了自定义分配器,控制块也会包含一个分配器的拷贝。控制块可能还包含一些额外的数据,正如Item21提到的,一个次级引用计数weak count,但是目前我们先忽略它。我们可以想象std::shared_ptr对象在内存中是这样:
image

当指向对象的std::shared_ptr一创建,对象的控制块就建立了。至少我们期望是如此。通常,对于一个创建指向对象的std::shared_ptr的函数来说不可能知道是否有其他std::shared_ptr早已指向那个对象,所以控制块的创建会遵循下面几条规则:

  • std::make_shared(参见Item21)总是创建一个控制块。 它创建一个要指向的新对象,所以可以肯定std::make_shared调用时对象不存在其他控制块。

  • 当从独占指针(即std::unique_ptr)上构造出std::shared_ptr时会创建控制块。独占指针没有使用控制块,所以指针指向的对象没有关联控制块。(作为构造的一部分,std::shared_ptr侵占独占指针所指向的对象的独占权,所以独占指针被设置为null)

  • 当从原始指针上构造出std::shared_ptr时会创建控制块。如果你想从一个早已存在控制块的对象上创建std::shared_ptr,你将假定传递一个std::shared_ptr或者std::weak_ptr(参见Item20)作为构造函数实参,而不是原始指针。用std::shared_ptr或者std::weak_ptr作为构造函数实参创建std::shared_ptr不会创建新控制块,因为它可以依赖传递来的智能指针指向控制块。

从原始指针上构造超过一个std::shared_ptr就会让你走上未定义行为的快车道,因为指向的对象有多个控制块关联(但是所有的shared_ptr都指向同一个T Object)。多个控制块意味着多个引用计数值,多个引用计数值意味着对象将会被销毁多次(每个引用计数一次)。

一个尤其令人意外的地方是使用this指针作为std::shared_ptr构造函数实参的时候可能导致创建多个控制块。https://github.com/CnTransGroup/EffectiveModernCppChinese/blob/master/src/4.SmartPointers/item19.md

3. shared_ptr的总结

  • std::shared_ptr为有共享所有权的任意资源提供一种自动垃圾回收的便捷方式。

  • 较之于std::unique_ptrstd::shared_ptr对象通常大两倍,控制块会产生开销,需要原子性的引用计数修改操作。

  • 默认资源销毁是通过delete,但是也支持自定义删除器。删除器的类型是什么对于std::shared_ptr的类型没有影响。

  • 避免从原始指针变量上创建std::shared_ptr, 通常替代方案是使用std::make_shared,不过当我们要使用自定义删除器,用std::make_shared就没办法做到

Item 20: Use std::weak_ptr for std::shared_ptr-like pointers that can dangle(当std::shared_ptr可能悬空时使用std::weak_ptr)

https://github.com/CnTransGroup/EffectiveModernCppChinese/blob/master/src/4.SmartPointers/item20.md

自相矛盾的是,如果有一个像std::shared_ptr(见Item19)的但是不参与资源所有权共享的指针是很方便的。换句话说,是一个类似std::shared_ptr但不影响对象引用计数的指针。这种类型的智能指针必须要解决一个在std::shared_ptr中存在的问题:即std::shared_ptr可能指向已经销毁的对象。一个真正的智能指针应该跟踪所指对象,在悬空时知晓,悬空(dangle)就是指针指向的对象不再存在。这就是对std::weak_ptr最精确的描述。
请记住:

  • 用std::weak_ptr替代可能会悬空的std::shared_ptr。

  • std::weak_ptr的潜在使用场景包括:缓存、观察者列表、打破std::shared_ptr环状结构。

std::enable_shared_from_this 作用

下面记录自己写的一些demo,详细的回答可以看 https://www.zhihu.com/question/30957800 中@孙梓轩的回答

  1. 下面情况如果不使用shared_from_this(),会导致对同一个对象创建了两个控制块,且每个控制块的引用计数都为1,会造成对象管理不正确
#include <memory>
#include <iostream>
#include <assert.h>

using namespace std;

class T   : public enable_shared_from_this<T> {
public:
    T() { cout << "T()" << endl;}
    ~T() {cout << "~T()" << endl;}
    typedef std::shared_ptr<T> ptr;
    T::ptr GetThis() {
        return std::shared_ptr<T>(this); // 从裸指针创建shared_ptr也会创建一个控制块
        // return this->shared_from_this();
    }
private:

};


int main()
{
    // std::shared_ptr<T> t = std::make_shared<T>();
    std::shared_ptr<T> t(new T()); // 不论是make_shared 还是 从裸指针方式创建shared_ptr 都会创建一个控制块
    std::shared_ptr<T> ptr = t->GetThis();
    cout << "count = " << ptr.use_count() << endl;
    cout << "count = " << t.use_count() << endl;
    t.reset();
    cout << "count = " << ptr.use_count() << endl;
    cout << "count = " << t.use_count() << endl;
    ptr.reset();
    cout << "count = " << ptr.use_count() << endl;
    cout << "count = " << t.use_count() << endl;
}

mingyu@host-H3C-UniServer-R4900-G5:~/cpp-projects/demo$ ./a.out 
T()
count = 1
count = 1
~T()
count = 1
count = 0
~T()
munmap_chunk(): invalid pointer
已放弃 (核心已转储)
  1. 使用shared_from_this; C++解决方案是通过继承一个类,这个类本质上会给被管理的object上加一个指向计数器的weak ptr,于是就可以正确地增加引用计数而不是搞出2个独立的计数器。因此引出的一个问题就是如果要调用shared_from_this()必须调用对象至少要被一个std::shared_ptr对象管理,否则会报错:std::bad_weak_ptr。
#include <memory>
#include <iostream>
#include <assert.h>

using namespace std;

class T   : public enable_shared_from_this<T> {
public:
    T() { cout << "T()" << endl;}
    ~T() {cout << "~T()" << endl;}
    typedef std::shared_ptr<T> ptr;
    T::ptr GetThis() {
        // return std::shared_ptr<T>(this);
        return this->shared_from_this();
    }
private:

};


int main()
{
    // std::shared_ptr<T> t = std::make_shared<T>();
    std::shared_ptr<T> t(new T());
    std::shared_ptr<T> ptr = t->GetThis();
    cout << "count = " << ptr.use_count() << endl;
    cout << "count = " << t.use_count() << endl;
    t.reset();
    cout << "count = " << ptr.use_count() << endl;
    cout << "count = " << t.use_count() << endl;
    ptr.reset();
    cout << "count = " << ptr.use_count() << endl;
    cout << "count = " << t.use_count() << endl;
}
mingyu@host-H3C-UniServer-R4900-G5:~/cpp-projects/demo$ ./test_sptr 
T()
count = 2
count = 2
count = 1
count = 0
~T()
count = 0
count = 0
  1. 使用shared_from_this, 但是对象没有至少被一个shared_ptr管理
#include <memory>
#include <iostream>

using namespace std;

class T   : public enable_shared_from_this<T> {
public:
    T() { cout << "T()" << endl;}
    ~T() {cout << "~T()" << endl;}
    typedef std::shared_ptr<T> ptr;
    T::ptr GetThis() {
        // return std::shared_ptr<T>(this);
        return this->shared_from_this();
    }
private:

};


int main()
{
    T t;
    auto sp = t.GetThis();
}
mingyu@host-H3C-UniServer-R4900-G5:~/cpp-projects/demo$ ./test_wptr 
T()
terminate called after throwing an instance of 'std::bad_weak_ptr'
  what():  bad_weak_ptr
已放弃 (核心已转储)

参考:
https://github.com/CnTransGroup/EffectiveModernCppChinese/blob/master/src/4.SmartPointers/item18.md
《Effective Modern Cpp》

posted @ 2023-05-17 15:05  人生逆旅,我亦行人  阅读(138)  评论(0编辑  收藏  举报