【C++11智能指针】

开篇

C/C++开发过程中,动态内存的管理通过new/delete完成。new在动态内存中为对象分配一块空间并返回一个指向该对象的指针;delete指向一个动态独享的指针,销毁对象,并释放与之关联的内存。

在日常动态内存的使用中,经常会出现以下问题:

  • 申请动态内存后忘记释放,造成内存泄漏,长时间运行会导致内存耗尽;
  • 尚有指针引用动态内存的情况下就释放了它,造成引用非法内存指针,导致程序异常coredump。

为解决上述问题,C++11引入了智能指针的概念。

智能指针初识

智能指针就是RAII(资源获取即初始化)模板类,其将基本类型指针封装为(模板)类对象指针,在离开作用域时调用析构函数,delete指向的内存空间。C++11在头文件,提供了shared_ptr、unique_ptr、weak_ptr。

「注」 auto_ptr也是一种智能指针,不过已经被unique_ptr取代,本篇不讨论此指针。

shared_ptr

shared_ptr采用引用计数的智能指针。如果需要将一个原始指针分配给多个所有者(例如,从容器返回了指针副本又想保留原始指针时),可以使用该指针。 直至所有shared_ptr所有者结束生命周期或放弃所有权,才会delete原始指针。

创建方式

第1种 创建空shared_ptr指针

std::shared_ptr<T> p1;             //不传入任何实参
std::shared_ptr<T> p2(nullptr);    //传入空指针 nullptr

空的 shared_ptr 指针,其初始引用计数为 0,而不是 1。

第2种 创建明确指向的shared_ptr指针

std::shared_ptr<T> p3(new T());  // new方式
std::shared_ptr<T> p3 = std::make_shared<T>(); // make_shared方式

此两种方式创建的p3完全相同,《Effective Modren C++》第21条款推荐优先使用make_shared而非new

第3种 通过拷贝构造函数或移动构造函数创建

//调用拷贝构造函数
std::shared_ptr<T> p4(p3);//或者 std::shared_ptr<T> p4 = p3;

//调用移动构造函数
std::shared_ptr<T> p5(std::move(p4)); //或者 std::shared_ptr<T> p5 = std::move(p4);

p3 和 p4 都是shared_ptr 类型的智能指针,因此可以用 p3 来初始化 p4,由于 p3 是左值,因此会调用拷贝构造函数。需要注意的是,如果 p3 为空智能指针,则 p4 也为空智能指针,其引用计数初始值为 0;反之,则表明 p4 和 p3 指向同一块堆内存,同时该堆空间的引用计数会加 1。

而对于 std::move(p4) 来说,该函数会强制将 p4 转换成对应的右值,因此初始化 p5 调用的是移动构造函数。另外和调用拷贝构造函数不同,用 std::move(p4) 初始化 p5,会使得 p5 拥有了 p4 的堆内存,而 p4 则变成了空智能指针。

第4种 创建自定义删除器的shared_ptr指针

shared_ptr支持自定义释放规则,即在智能指针生命结束时调用自定义函数。

// 空智能指针p,在删除共享指针时调用删除函数d
shared_ptr<T> p(d);
// 非空智能指针p, 管理原始指针q,在删除共享指针时调用删除函数d
shared_ptr<T> p(q, d);
// E.g,可配合lambda表达式
shared_ptr<FILE> fp(fopen("./tmp.txt","r"), fclose);

在某些场景中,自定义释放规则很有必要。比如,对于申请的动态数组来说,shared_ptr 指针默认的释放规则是不支持释放数组的,只能自定义对应的释放规则,才能正确地释放申请的堆内存。

shared_ptr常用函数

成员函数名功 能
operator=()重载赋值号,使得同一类型的 shared_ptr 智能指针可以相互赋值。
operator*()重载 * 号,获取当前 shared_ptr 智能指针对象指向的数据。
operator->()重载 -> 号,当智能指针指向的数据类型为自定义的结构体时,通过 -> 运算符可以获取其内部的指定成员。
swap()交换 2 个相同类型 shared_ptr 智能指针的内容。
reset()当函数没有实参时,该函数会使当前 shared_ptr 所指堆内存的引用计数减 1,同时将当前对象重置为一个空指针;当为函数传递一个新申请的堆内存时,则调用该函数的 shared_ptr 对象会获得该存储空间的所有权,并且引用计数的初始值为 1。
get()获得 shared_ptr 对象内部包含的普通指针。
use_count()返回同当前 shared_ptr 对象(包括它)指向相同的所有 shared_ptr 对象的数量。
unique()判断当前 shared_ptr 对象指向的堆内存,是否不再有其它 shared_ptr 对象再指向它。
operator bool()判断当前 shared_ptr 对象是否为空智能指针,如果是空指针,返回 false;反之,返回 true。

使用示例

static void help_info()
{
    LOG("usage: \n"
        "a. Regular test.\n"
        "b. Custom class test.\n"
        "c. Custorm delete test.\n"
        "q. exit.\n"
    );
}

static void regular_use(shared_ptr<string> &str)
{
    shared_ptr<string> pTmpStr(str);
    LOG("pTmpStr: %s. memory count: %ld.\n", pTmpStr->c_str(), pTmpStr.use_count());
}

class CTestSharedPtr
{
public:
    CTestSharedPtr(string desc) : mDescription(desc) {
        LOG("Enter %s.\n", __FUNCTION__);
    }

    ~CTestSharedPtr() {
        LOG("Enter %s.\n", __FUNCTION__);
    }

    string GetDesc() {
        return mDescription;
    }

private:
    string mDescription;
};

int main(int argc, char *argv[])
{
    char a;
    help_info();

    do
    {
        scanf("%c", &a);
        switch (a)
        {
            case 'a':   // shared_ptr 标准类型
            {
                shared_ptr<string> pStr1(new string("hello world"));
                LOG("pStr1: %s. memory count: %ld.\n", pStr1->c_str(), pStr1.use_count());
                regular_use(pStr1);
                shared_ptr<string> pStr2(pStr1);
                LOG("pStr2: %s. memory count: %ld.\n", pStr2->c_str(), pStr2.use_count());
            }
            break;

            case 'b':   // shared_ptr 自定义类型
            {
                shared_ptr<CTestSharedPtr> pCTest1 = make_shared<CTestSharedPtr>("ClassTest");
                LOG("pCTest1: %s. memory count: %ld.\n", pCTest1->GetDesc().c_str(), pCTest1.use_count());
                shared_ptr<CTestSharedPtr> pCTest2(pCTest1);
                LOG("pCTest2: %s. memory count: %ld.\n", pCTest2->GetDesc().c_str(), pCTest2.use_count());
            }
            break;

            case 'c':   // 自定义shared_ptr删除器
            {
                char *pArry = nullptr;

                auto fStart = [](char *p) {
                    p = (char *)malloc(sizeof(char) * 6);
                    strncpy(p, "hello", 6);
                    cout << "Enter fStart(). malloc p:" << p << endl;
                    return p;
                };

                auto fStop = [](char *p) {
                    cout << "Enter fStop(). p:" << p;
                    free(p);
                    p = nullptr;
                    cout << " free." << endl;
                };

                // 自定义删除器,当pDTest1生命周期结束时,通过delete_test(pDTest1)释放内存,不再调用delete
                shared_ptr<char> pDTest1(fStart(pArry), fStop);
                LOG("pDTest1.use_count: %ld. %s\n", pDTest1.use_count(), pDTest1.get());
            }
            break;

            default:
            break;
        }
    } while(a != 'q');

    return 0;
}

「执行输出」

$ ./exe 
usage: 
a. Regular test.
b. Custom class test.
c. Custorm delete test.
q. exit.
a
pStr1: hello world. memory count: 1.
pTmpStr: hello world. memory count: 2.
pStr2: hello world. memory count: 2.
b
Enter CTestSharedPtr.
pCTest1: ClassTest. memory count: 1.
pCTest2: ClassTest. memory count: 2.
Enter ~CTestSharedPtr.
c
Enter fStart(). malloc p:hello
pDTest1.use_count: 1. hello
Enter fStop(). p:hello free.

unique_ptr

只允许基础指针的一个所有者。可以移到新所有者,但不会复制或共享。替换已弃用的auto_ptr。必要情况下,可以转化为shared_ptr

创建方式

第1种 创建空unique_ptr指针

std::unique_ptr<T> p1();
std::unique_ptr<T> p2(nullptr);

第2种 创建明确指向的unique_ptr指针

std::unique_ptr<T> p3(new T);

C++11 标准中并没有为unique_ptr类型指针添加类似的模板函数。C++14提供了make_unique<T>()模板函数用于初始化unique_ptr指针。

第3种 通过移动构造函数创建

std::unique_ptr<T> p4(new T);
// std::unique_ptr<T> p5(p4);//编译错误,堆内存不共享
std::unique_ptr<T> p5(std::move(p4));//正确,调用移动构造函数

unique_ptr指针不共享拥有的堆内存,因此C++11标准中的 unique_ptr模板类没有提供拷贝构造函数,只提供了移动构造函数。

第4种 创建自定义删除器的unique_ptr指针

// 空unique_ptr指针, 删除智能指针时,执行d而非delete
unique_ptr<T, D> u1(d);
// 非空unique_ptr指针, 管理指针p; 删除智能指针时,执行d而非delete
unique_ptr<T, D> u2(p, d);

unique_ptr常用函数

成员函数名功 能
成员函数名功 能
operator*()获取当前 unique_ptr 指针指向的数据。
operator->()重载 -> 号,当智能指针指向的数据类型为自定义的结构体时,通过 -> 运算符可以获取其内部的指定成员。
operator =()重载了 = 赋值号,从而可以将 nullptr 或者一个右值 unique_ptr 指针直接赋值给当前同类型的 unique_ptr 指针。
operator重载了 [] 运算符,当 unique_ptr 指针指向一个数组时,可以直接通过 [] 获取指定下标位置处的数据。
get()获取当前 unique_ptr 指针内部包含的普通指针。
get_deleter()获取当前 unique_ptr 指针释放堆内存空间所用的规则。
operator bool()unique_ptr 指针可直接作为 if 语句的判断条件,以判断该指针是否为空,如果为空,则为 false;反之为 true。
release()释放当前 unique_ptr 指针对所指堆内存的所有权,但该存储空间并不会被销毁。
reset§其中 p 表示一个普通指针,如果 p 为 nullptr,则当前 unique_ptr 也变成空指针;反之,则该函数会释放当前 unique_ptr 指针指向的堆内存(如果有),然后获取 p 所指堆内存的所有权(p 为 nullptr)。
swap(x)交换当前 unique_ptr 指针和同类型的 x 指针。

使用示例

static void help_info()
{
    LOG("usage: \n"
        "a. reset() test.\n"
        "b. Custom class test.\n"
        "c. Custorm delete test.\n"
    );
}

class CTestUniquePtr
{
public:
    CTestUniquePtr(string desc) : mDescription(desc) {
        LOG("Enter %s.\n", __FUNCTION__);
    }

    ~CTestUniquePtr() {
        LOG("Enter %s.\n", __FUNCTION__);
    }

    string GetDesc() {
        return mDescription;
    }

private:
    string mDescription;
};

int main(int argc, char *argv[])
{
    char a;
    help_info();

    do {
        scanf("%c", &a);

        switch(a)
        {
            case 'a':
            {
                unique_ptr<CTestUniquePtr> pUnPtr1(new CTestUniquePtr("unique_ptr"));
                pUnPtr1.reset();
                LOG("pUnPtr1 is %d.\n", pUnPtr1 ? 1 : 0);
            }
            break;

            case 'b':
            {
                unique_ptr<CTestUniquePtr> pUnPtr1(new CTestUniquePtr("unique_ptr"));
                unique_ptr<CTestUniquePtr> pUnPtr2(pUnPtr1.release());
                LOG("pUnPtr1 is %s. pUnPtr2 is %s.\n", pUnPtr1 ? pUnPtr2->GetDesc().c_str() : "nullptr",
                        pUnPtr2 ? pUnPtr2->GetDesc().c_str() : "nullptr");
            }
            break;

            case 'c':
            {
             std::unique_ptr< int, function<void(int*)> > ptr1(new int[100],
              [](int*p)->void {
               cout << "Delete int[]" << endl;
               delete []p;
              }
             );
             
             std::unique_ptr< FILE, function<void(FILE*)> > ptr2(fopen("data.txt","w"),
              [](FILE*p)->void {
               cout << "Delet FILE" << endl;
               fclose(p);
              }
          );
            }
            break;

            default:
            break;
        }
    } while (a != 'q');

    return 0;
}

「执行输出」

$./exe 
usage: 
a. reset() test.
b. Custom class test.
c. Custorm delete test.
a
Enter CTestUniquePtr.
Enter ~CTestUniquePtr.
pUnPtr1 is 0.
b
Enter CTestUniquePtr.
pUnPtr1 is nullptr. pUnPtr2 is unique_ptr.
Enter ~CTestUniquePtr.
c
Delet FILE
Delete int[]

weak_ptr

结合shared_ptr使用的弱智能指针。weak_ptr提供对一个或多个shared_ptr实例拥有的对象的访问,但不参与引用计数。 如果需要观察某个对象但不需要其保持活动状态,可使用该实例。可解决shared_ptr实例间的循环引用导致的内存泄漏问题。

weak_ptr没有提供常用的指针操作,无法直接访问内存,需要先通过lock方法提升为shared_ptr强智能指针,才能访问资源。

创建方式

第1种 创建空weak_ptr指针

std::weak_ptr<T> wp1;

第2种 通过拷贝构造函数创建

std::weak_ptr<T> wp2(wp1);

第3种 通过shared_ptr构建weak_ptr

auto = make_shared<T>();
std::weak_ptr<T> wp2(sp); // 不增加sp内部引用计数

weak_ptr常用函数

成员函数名功 能
operator=()重载 = 赋值运算符,是的 weak_ptr 指针可以直接被 weak_ptr 或者 shared_ptr 类型指针赋值。
swap(x)其中 x 表示一个同类型的 weak_ptr 类型指针,该函数可以互换 2 个同类型 weak_ptr 指针的内容。
reset()将当前 weak_ptr 指针置为空指针。
use_count()查看指向和当前 weak_ptr 指针相同的 shared_ptr 指针的数量。
expired()判断当前 weak_ptr 指针为否过期(指针为空,或者指向的堆内存已经被释放)。
lock()如果当前 weak_ptr 已经过期,则该函数会返回一个空的 shared_ptr 指针;反之,该函数返回一个和当前 weak_ptr 指向相同的 shared_ptr 指针。

使用示例

{
    shared_ptr<string> pShPtr1 = make_shared<string>("ptr");    // 创建共享指针, 内存引用 +1
    LOG("1. pShPtr1.use_count: %ld.\n", pShPtr1.use_count());
    shared_ptr<string> pShPtr2(pShPtr1);                        // 共享指针拷贝,内存引用 +1
    LOG("2. pShPtr1.use_count: %ld.\n", pShPtr1.use_count());
    weak_ptr<string> pWeakPtr(pShPtr1);                         // 弱引用智能指针,内存引用不增加
    LOG("3. pWeakPtr.use_count: %ld.\n", pWeakPtr.use_count());
    shared_ptr<string> pShPtr3(pWeakPtr.lock());                // 拷贝弱指针返回的共享指针,内存引用 +1
    LOG("4. pShPtr3.use_count: %ld.\n", pShPtr3.use_count());
}

「执行输出」

1. pShPtr1.use_count: 1.
2. pShPtr1.use_count: 2.
3. pWeakPtr.use_count: 2.
4. pShPtr3.use_count: 3.

总结

  • 通过本篇对三种指针的介绍,大致梳理出三者的使用场景:
    独占内存用unique_ptr;
    内存被多个指针引用shared_ptr;
    当作为内存观察者或者解决循环引用时使用weak_ptr
  • C++智能指针的使用注意事项:
    ① 优先使用unique_ptr而非auto_ptr
    shared_ptr不支持动态数组,若默认使用delete来释放管理资源,delete只会调用第一个元素的析构函数,导致内存泄漏;(可通过自定义删除器管理) unique_ptr支持动态数组,默认detele会自动使用delete[]。
    使用unique_ptr可转化为shared_ptr,反之则不行。
    shared_ptr消耗资源比unique_ptr大,若无内存共享需求,优先考虑unique_ptr
    ⑤ 禁止使用静态分配对象指针初始化智能指针,否则智能指针生命周期结束时,会试图删除指向非动态分配对象的指针,导致未定义的行为。
    ⑥ 谨慎使用智能指针的get()release()方法。
    当使用get()方法返回裸指针时,智能指针并没有释放指向对象的所有权,因此避免裸指针的使用导致崩溃。
    unique_ptr.release()返回裸指针并让出内存控制权,需要及时接管或者delete,避免导致内存泄漏。
    ⑦ 禁止使用一个裸指针初始化多个智能指针;禁止手动delete智能指针的裸指针。
    ⑧ 不要把类对象指针(this)作为shared_ptr返回,改用enable_shared_from_this
    ⑨ 通过weak_ptr.lock()方法获取shared_ptr时,必须判断该shared_ptr是否有效。
    ⑩ 优先考虑使用std::make_uniquestd::make_shared而非new(C++11暂未提供std::make_shared)。
    shared_ptr没有保证共享对象的线程安全性。
    ⑫ 循环引用shared_ptr会导致内存泄漏,应替换为weak_ptr

参考

  • 《C++ Primer 第五版》
  • 《Effective Modren C++》
  • https://learn.microsoft.com/zh-cn/cpp/cpp/smart-pointers-modern-cpp?view=msvc-170
  • http://c.biancheng.net/view/7898.html
  • https://blog.csdn.net/LU_ZHAO/article/details/105113241
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值