C++11 智能指针

开篇

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

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

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

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

智能指针初识

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

  1. 明确指向的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

  1. 拷贝构造函数和移动构造函数
//调用拷贝构造函数
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 则变成了空智能指针。

  1. 自定义所指堆内存的释放规则

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);
  1. 明确指向的unique_ptr指针
std::unique_ptr<T> p3(new T);

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

  1. 移动构造函数
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模板类没有提供拷贝构造函数,只提供了移动构造函数。

  1. 自定义所指堆内存的释放规则
// 空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 表示一个普通指针,如果 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;
  1. 拷贝构造函数
std::weak_ptr<T> wp2(wp1);
  1. 通过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

参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容