智能指针 (Smart Pointer)

1. 介绍

当多个指针指向同一个对象的时候,为了确保“指针的寿命”和“其所指向的对象的寿命”一致,是一件比较复杂的事情。
智能指针的出现就是为了解决这种场的,智能指针内部会维护一个对指针指向对象的引用计数,在对象析构的时候,会去对该对象的引用计数减减,当应用计数为0的时候,就会去释放对象。
但是尽管智能指针是很方便,但是也要抱有敬畏心,若误用可能会出现资源使用无法被释放的大问题。

自 C++11 起, C++ 标准库提供两大类型的智能指针:

  1. std::shared_ptr 实现共享式拥有的概念,多个智能指针可以指向相同的对象,该对象和其相关资源会在“指向该对象的最后一个引用被销毁”时被释放。为了满足复杂情况,标准库还提供了 std::weak_ptr、std::bad_weak_ptr 和 enable_share_from_this 等辅助类。
  2. std::unique_ptr 实现独占式拥有或严格拥有的概念,保证同一个事件内只有一个智能指针可以指向该对象。你可以移交拥有权。

2. std::shared_ptr

通常我们都会需要“在相同时间的多处地点处理或使用对象”的能力,在程序的多个地方引用同一个对象 。这就需要我们在多个地方使用完该对象,在“指向该对象的最后一个引用被销毁”时来删除该对象本身,执行该对象的析构函数,来释放内存或归还资源等。 std::shared_ptr 提供了这样共享式拥有的语义。也就是说多个 shared_ptr 可以共享(或说拥有)同一对象,对象的最末一个拥有着有责任销毁对象,并清理与该对象相关的资源。
如果对象以 new 产生,默认情况下清理工作就是由 delete 完成。但是你也可以 (并且往往必须)定义其他清理办法。举个例子,如果你的对象是以 new[] 分配的 array,你必须定义自己的 delete[] 加以清理。

2.1 使用 std::shared_ptr

你可以像使用任何其他指针一样地使用 shared_ptr。你可以赋值、拷贝、比较它们,也可以使用操作符 * 和 -> 访问其所指向的对象的成员或方法。见下面这个例子:

#include <memory>
#include <string>

int main() {
    // 创建智能指针指向的 std::string 对象

    // 方式1
    std::shared_ptr<std::string> piStr1(new std::string("str1"));

    // 方式2 --- 更推荐,这种方式比较快,也比较安全
    auto piStr2 = std::make_shared<std::string>("str2");

    // 方式3
    std::shared_ptr<std::string> piStr3{ new std::string("str3") };

    // 方式4
    std::shared_ptr<std::string> piStr4;
    //piStr4 = new std::string("str4"); // ERROR 不允许直接赋值
    piStr4.reset(new std::string("str4"));

    // 错误方式,由于其构造函数是 explicit 所以这里是不可以直接赋值的,直接赋值操作会进行隐式转换
    // std::shared_ptr<std::string> piStr3 = new std::string("str3"); // ERROE

    // 使用 -> 或 * 直接访问 std::string 对象的方法
    std::cout << "piStr1 is \" " << piStr1->c_str() << "\"" << std::endl;
    std::cout << "piStr2 is \" " << (*piStr2).c_str() << "\"" << std::endl;
    std::cout << "piStr2 after ->replace(0, 1, \"S\") ---->" << piStr2->replace(0, 1, "S") << std::endl;

    // 使用 vector 存储 string 对象,use_count() 方法获取“当前拥有者”数量
    std::vector<std::shared_ptr<std::string>> vecStr;
    vecStr.push_back(piStr1);
    std::cout << "piStr1.use_count()--> " << piStr1.use_count() << endl; // count is 2
    vecStr.push_back(piStr1);
    std::cout << "piStr1.use_count()--> " << piStr1.use_count() << endl; // count is 3

    // string 对象销毁时机
    // 1. 程序终点处,当 string 的最后一个拥有者被销毁,shared_ptr 会对其所指向的对象调用 delete
    // 2. 将 piStr1 = nullptr; 也会触发对 piStr1 拥有的 string 对象引用计数减减
    // 3. 调用 shared_ptr<>.reset() ,也会触发对 string 对象引用计数减减
    // 4. 将 piStr1 = std::shared_ptr<int>(new int);  也会触发对 piStr1 拥有的 string 对象引用计数减减,然后在将 piStr1 指向新的对象

    return 0;
}

2.2 自定义Deleter,处理 Array

我们可以声明属于自己的 deleter, 例如让它在“删除被指向对象”之前先打印一条信息:

    std::shared_ptr<std::string> piStr5(
        new std::string("str5"),
        [] (std::string* p) {
            std::cout << "delete " << p->c_str() << std::endl;
            delete p;
        });

应用场景,对付 Array。请注意,shared_ptr 提供的 default Deleter 调用的是 delete,而不是 delete[]。这就意味着只有当 shared_ptr 拥有 “由 new 建立起来的单一对象”,default Deleter 才合适,当我们为 Array 创建一个 shared_ptr 的时候,却会出错。自定义 Deleter 是为了解决这种情况的,代码如下:

    std::shared_ptr<int> pArry(
        new int[10], 
        [](int* p) {
            delete[] p;
        }
    );

2.3 注意事项

  • 自定义 Deleter 函数是不允许抛出异常的
  • shared_ptr 只提供 operator* 和 operator ->,指针运算(pointer++)和 operator [] 都未提供。因此,如果想访问内存,你必须使用 get() 获取被 shared_ptr 包括的内部指针,然后对指针进行运算操作,如:
    pArry.get()[1]; pArry.get() ++;
  • shared_ptr 是非线程安全的,所以当在多个线程中以 shared_ptr 指向同一个对象的时候,必须使用诸如 mutex 或 lock技术,防止出现由于资源竞争导致的问题

2.4 误用 std::shared_ptr

(1)错误的赋值,导致相同资源被多次释放,记得 shared_ptr 之间赋值应该赋值 shared_ptr 对象

int main() {
    // 错误的案例
    auto p = new int;
    std::shared_ptr<int> sp1(p);
    std::shared_ptr<int> sp2(p);
    std::cout << sp1.use_count() << std::endl; // 1
    std::cout << sp2.use_count() << std::endl; // 1

    // 分析
    // 问题出在 sp1 和 sp2 都会在丢失p的拥有权的时候释放相应的资源
    // 由于传给智能指针 sp1 和 sp2 都是 p 的地址,所以这相当于是两个对象在管理同一个 p 的生命周期
    // 意味着 sp1 和 sp2 析构的时候,一共会释放两次 p 从而导致程序崩溃

    // 正确的做法
    std::shared_ptr<int> sp3(new int);
    std::shared_ptr<int> sp4 = sp3;
    return 0;
}

2.5 shared_from_this

在某些情况下,需要在类成员函数中,将类对象(this)转换成 shared_ptr 传递给成员方法中使用,使用的规则如下:
(1)类需要继承 std::enable_shared_from_this,T 就是当前 class 的类
(2)在成员函数中,使用 shared_from_this() 便可以建立起一个源自 this 的正确 shared_ptr
(3)注意,不可以在构造函数中调用 shared_from_this(), 由于 shared_ptr 本身是在基类中,也就是enable_shared_from_this<>内部的一个private成员,在当前对象构造结束之前是无法建立 shared_pointer的循环引用。所以在构造函数中,这样会做导致程序运行期错误。

class CPerson : public std::enable_shared_from_this<CPerson> { // ①
public:
    CPerson(
        const std::string name,
        std::shared_ptr<CPerson> mother = nullptr,
        std::shared_ptr<CPerson> father = nullptr
    ) : m_strName(name), m_piFather(father), m_piMother(mother) {}

    ~CPerson() {
        std::cout << "delete " << m_strName << std::endl;
    }

    void SetParentsAndTheirKids(
        std::shared_ptr<CPerson> m = nullptr,
        std::shared_ptr<CPerson> f = nullptr
    ) {
        m_piMother = m;
        m_piFather = f;

        if (m != nullptr) {
            m->m_vecKids.push_back(shared_from_this()); // ②
        }

        if (f != nullptr) {
            f->m_vecKids.push_back(shared_from_this());
        }
    }

    std::string m_strName;
    std::shared_ptr<CPerson> m_piMother;
    std::shared_ptr<CPerson> m_piFather;
    std::vector<std::weak_ptr<CPerson>> m_vecKids;
};

2.6 shared_ptr 的各项操作

点击查看详情 shared_ptr 各项操作,第一部分 shared_ptr 各项操作,第二部分

3. std::weak_ptr

使用 shared_ptr 主要是为了避免操心指向的资源,然而在某些场景下,无法使用 shared_ptr 或者说是无法满足:

  • 环向指向:两个对象使用 shared_ptr 互相指向对方,而一旦不存在其他的引用指向它们时,你想释放它们和其相应资源。这种情况下 shared_ptr 不会释放数据,因为互相指向导致每个对象的 use_count() 仍是1。此时你或许会想使用寻常的指针,但这么做又得自行管理“相应资源的释放”。
  • “明确想共享但不拥有”某对象的情况下,即共享的对象的生命周期明确是最长的, shared_ptr 绝不会提前释放对象。若使用寻常的指针,可能不会注意到它们指向的对象已经不再有效,导致“访问已被释放的数据”的风险

于是标准库提供了 weak_ptr,允许“共享但不拥有”某对像,它会建立起一个 shared_ptr,一旦最后一个拥有该对象的 shared_ptr 失去拥有权,任何 weak_ptr 都会自动成空。你不可以直接使用 operator * 和 -> 访问 weak_ptr 执向的对象,而是必须另外建立起一个 shared_ptr 去访问。 weak_ptr 只提供小量操作,只能够用来创建、复制、赋值 weak_ptr 以及转换为一个 shared_ptr 或检查自己是否指向某个对象。

3.1 shared_ptr 环向指向 导致资源无法释放

//////////////////////////////////////////////////////////////////////////

#include <memory>
#include <string>

class CPerson {
public:
    CPerson(
        const std::string name,
        std::shared_ptr<CPerson> mother = nullptr,
        std::shared_ptr<CPerson> father = nullptr
    ) : m_strName(name), m_piFather(father), m_piMother(mother) {}

    ~CPerson() {
        std::cout << "delete " << m_strName << std::endl;
    }

    std::string m_strName;
    std::shared_ptr<CPerson> m_piMother;
    std::shared_ptr<CPerson> m_piFather;
    std::vector<std::shared_ptr<CPerson>> m_vecKids;
};

//////////////////////////////////////////////////////////////////////////

std::shared_ptr<CPerson> initFamily(const std::string& name) {
    auto mom = std::make_shared<CPerson>(name + "'s Mom");
    auto dad = std::make_shared<CPerson>(name + "'s Dad");
    auto kid = std::make_shared<CPerson>(name, mom, dad);
    mom->m_vecKids.push_back(kid);
    dad->m_vecKids.push_back(kid);
    return kid;
}

//////////////////////////////////////////////////////////////////////////

int main() {
    auto Sam = initFamily("Sam");
    std::cout << "Sam's family Info: " << std::endl;
    std::cout << "- Sam is shared " << Sam.use_count() << " times" << endl;
    std::cout << "- Sam's mom is shared " << Sam->m_piMother.use_count() << " times" << endl;
    std::cout << "- Sam's dad is shared " << Sam->m_piFather.use_count() << " times" << endl;
    // 输出:
    // Sam's family Info:
    //    - Sam is shared 3 times
    //    - Sam's mom is shared 1 times
    //    - Sam's dad is shared 1 times

    // 手动释放 Sam 对 CPerson 的引用
    Sam = nullptr;

    // 此时:
    // Sam's family Info:
    //    - Sam is shared 2 times
    //    - Sam's mom is shared 1 times
    //    - Sam's dad is shared 1 times
    // 由于 mom 和 dad 的 vector 还持有 kid 对象的引用 kid 对象无法释放
    // kid 对象持有 mom 和 dad 的引用,mom 和 dad 无法释放,产生 环向指向 的现象

    return 0;
}

要解这个,就要用到 weak_ptr。其实不论是 kid 还是 dad、mom,只要任何一方在 CPerson 中定义为 weak_ptr 都可以解这个节。但是在这个场景下,由于在kid中还可能会使用 dad 或 mom,所以 dad 和 mom 还是得使用 shared_ptr ,vector 中的 kid 使用 weak_ptr 。

3.2 std::weak_ptr 使用

int main() {
    try {
        auto piStr = std::make_shared <std::string>("str");
        
        // 创建一个 weak_ptr 直接赋值 std::shared_ptr 
        std::weak_ptr<std::string> piWeakStr = piStr;

        // 访问对象
        {
            auto piStrShare = piWeakStr.lock();
            if (piStrShare != nullptr) { // piStrShare 可能为nullptr
                std::cout << "piStrShare -> " << piStrShare->c_str() << std::endl;
                std::cout << "piStrShare shared " << piStrShare.use_count() << " times" << std::endl; // shared 2 times
            }
        }

        // 手动释放 piStr 智能指针的引用
        piStr = nullptr;

        // 三种方式检查 weak_ptr是否还可以使用
        // 方式一 -- 推荐,expired() 在 weak_ptr 不在共享对象时返回true,等同于检查 use_count() 是否为0,但速度较快
        std::cout << "piWeakStr.expired() ->" << std::boolalpha << piWeakStr.expired() << std::endl; // true
        // 方式二 -- 判断 use_count() 是否为0,这个效率较差
        std::cout << "piWeakStr.use_count() -> " << piWeakStr.use_count() << std::endl; // piWeakStr.use_count() -> 0
        // 方式三 -- 使用 lock()方法,判断返回的 shared_ptr 是否等于 nullptr,若是也认为不在共享对象
        auto piStrShare = piWeakStr.lock();
        if (piStrShare == nullptr) { // piStrShare 可能为nullptr
            std::cout << "piStrShare is a nullptr"  << std::endl;
        }
        // 方式四 -- 使用相应的 shared_ptr 构造函数明确将 weak_ptr 转换为一个 shared_ptr,如果对象已经不存在,该构造
        // 函数会抛出 bad_weak_ptr 的异常 (e.what() 输出 bad_weak_ptr)
        std::shared_ptr<std::string> sharedConvert(piWeakStr);
    }
    catch (const std::exception& e) {
        std::cout << "exception: " << e.what() << std::endl;
    }

    return 0;
}

3.3 weak_ptr 的各项操作

点击查看详情 weak_ptr 各项操作

4. std::unique_ptr

unique_ptr 是 C++ 标准库自 C++11 起开始提供的类型。它是一种在异常发生的时候可以帮助避免资源泄露的智能指针。一般而言,这个智能指针实现了独占式拥有概念,意味着它可确保一个对象和其他相应的资源同一时间只能被一个 unique_ptr 拥有。一旦拥有着被销毁或变成nullptr,或开始拥有另一个对象,先前拥有的那个对象就会被销毁,其任何相应的资源也会被释放。

unique_ptr 继承自 auto_ptr,后者由 C++98 引入但已不再被认可。

4.1 std::unique_ptr 使用

int main() {
    // 创建智能指针指向的 std::string 对象

    // 方式1
    std::unique_ptr<std::string> piStr(new std::string("str"));

    // 方式2 -- 更推荐
    auto piStr1 = std::make_unique<std::string>("str1");

    // 方式3 
    std::unique_ptr<std::string> piStr3{ new std::string("str3") };

    // 方式4
    std::unique_ptr<std::string> piStr4;
    //piStr4 = new std::string("str4"); // ERROR 不允许直接赋值
    piStr4.reset(new std::string("str4"));

    // 错误方式,由于其构造函数是 explicit 所以这里是不可以直接赋值的,直接赋值操作会进行隐式转换
    // std::unique_ptr<std::string> piStr3 = new std::string("str3"); // ERROE

    // 使用 -> 或 * 直接访问 std::string 对象的方法
    (*piStr).replace(0, 1, "S");
    piStr->append("_test");
    std::cout << piStr->c_str() << std::endl;

    // 赋值 
    // std::unique_ptr<std::string> piStr5(piStr); // ERROR 不允许
    std::unique_ptr<std::string> piStr5(std::move(piStr)); // okk

    // 与 shared_ptr 不同的是,可以调用 release 方法,放弃对对象的拥有权
    std::string* strNew = piStr.release();
    std::cout << "after piStr.release() ---> piStr is ---> " << piStr << std::endl; // piStr is 0
    std::unique_ptr<std::string> newStr(strNew);
    std::cout << "newStr is ---> " << newStr->c_str() << std::endl; // newStr is ---> Str_test

    // string 对象销毁时机
    // 1. 程序终点处,当 string 的最后一个拥有者被销毁,unique_ptr 会对其所指向的对象调用 delete
    // 2. 将 piStr1 = nullptr; 也会触发对 piStr1 拥有的 string 对象引用计数减减
    // 3. 调用 unique_ptr<>.reset() ,也会触发对 string 对象引用计数减减
    // 4. 将 piStr1 = std::unique_ptr<int>(new int);  也会触发对 piStr1 拥有的 string 对象引用计数减减,然后在将 piStr1 指向新的对象

    return 0;
}

4.2 std::unique_ptr 处理 Array

int main() {
    // 使用智能指针,创建对象数组
    // 如果使用 shared_ptr 需要自己定义 deleter 才能处理 array
    std::shared_ptr<std::string> sp1(new std::string[10], [](std::string* p) { delete[] p; });

    // unique_ptr 提供了一个偏特化版本用来处理array
    // 在 sp2 对象销毁时候,会使用 delete[] 来释放其所拥有的 string 对象
    std::unique_ptr<std::string[]> sp2(new std::string[10]);

    // sp2 不支持使用 operator * 和 -> ,改而提供 operator [],用于访问其所指向的 array 中某一个对象
    // 一如既往,确保 [] 索引合法是程序员的责任,不合法的索引会导致不确定的行为
    // std::cout << *sp2 << std::endl; // ERROR 编译错误
    sp2[0].append("str2");
    std::cout << "sp2[0] is " << sp2[0].c_str() << std::endl;
    return 0;
}

4.3 自定义Deleter

unique_ptr的 Deleter 定义方式与 shared_ptr略不相同,必须知名 deleter 的类型T2作为 unique_ptr<T1, T2> 实参,改类型可以是类、函数、函数指针或者是函数对象。如果是函数对象,其 function call 操作() 应该接受一个“指向对象”的指针。

// 类
class ADeleter
{
public:
    void operator () (ClassA* p) {
        delete p;
    }
};
std::unique_ptr<ClassA, ADeleter> up(new ClassA());

// 如果定义的 Deleter 是个函数或 lambda,你必须声明 Deleter 的类型为 void(*)(T*) 或 std::function<void(T*)>,在或者就使用 decltype
std::unique_ptr<int[], void(*)(int* p)> up1(new int[10], [](int* p) {delete[] p; });

#include <functional>
std::unique_ptr<int[], std::function<void(int*)>> up2(new int[10], [](int* p) {delete[] p; });

4.4 注意事项

  • unique_ptr 只提供 operator* 和 operator -> 或 Array 提供 operator [],指针运算(pointer++)未提供。因此,如果想访问内存,你必须使用 get() 获取被 unique_ptr 包括的内部指针,然后对指针进行运算操作,如:piStr.get() ++;

4.5 unique_ptr 的各项操作

点击查看详情 weak_ptr 各项操作
posted @ 2021-11-21 19:04  Microm  阅读(277)  评论(0编辑  收藏  举报