【编程技巧】 C++11智能指针

本文涉及的产品
Serverless 应用引擎 SAE,800核*时 1600GiB*时
注册配置 MSE Nacos/ZooKeeper,118元/月
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: C++11引入了智能指针以自动管理内存,防止内存泄漏和悬挂指针:- `shared_ptr`:引用计数,多所有权,适用于多个对象共享资源。- `unique_ptr`:独占所有权,更轻量级,适用于单一对象所有者。- `weak_ptr`:弱引用,不增加引用计数,解决`shared_ptr`循环引用问题。## shared_ptr- 支持引用计数,所有者共同负责资源释放。- 创建方式:空指针、new操作、拷贝构造/移动构造,以及自定义删除器。- 提供`operator*`和`operator->`,以及`reset`、`swap`等方法。## unique_ptr

[TOC]

开篇

  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指针

```c++
// 空智能指针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 表示一个普通指针,如果 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

参考

相关文章
|
3天前
|
编译器 C++
【C++核心】指针和引用案例详解
这篇文章详细讲解了C++中指针和引用的概念、使用场景和操作技巧,包括指针的定义、指针与数组、指针与函数的关系,以及引用的基本使用、注意事项和作为函数参数和返回值的用法。
11 3
|
24天前
|
C++
C++(十八)Smart Pointer 智能指针简介
智能指针是C++中用于管理动态分配内存的一种机制,通过自动释放不再使用的内存来防止内存泄漏。`auto_ptr`是早期的一种实现,但已被`shared_ptr`和`weak_ptr`取代。这些智能指针基于RAII(Resource Acquisition Is Initialization)原则,即资源获取即初始化。RAII确保对象在其生命周期结束时自动释放资源。通过重载`*`和`-&gt;`运算符,可以方便地访问和操作智能指针所指向的对象。
|
24天前
|
C++
C++(九)this指针
`this`指针是系统在创建对象时默认生成的,用于指向当前对象,便于使用。其特性包括:指向当前对象,适用于所有成员函数但不适用于初始化列表;作为隐含参数传递,不影响对象大小;类型为`ClassName* const`,指向不可变。`this`的作用在于避免参数与成员变量重名,并支持多重串联调用。例如,在`Stu`类中,通过`this-&gt;name`和`this-&gt;age`明确区分局部变量与成员变量,同时支持链式调用如`s.growUp().growUp()`。
|
1月前
|
存储 安全 C++
C++:指针引用普通变量适用场景
指针和引用都是C++提供的强大工具,它们在不同的场景下发挥着不可或缺的作用。了解两者的特点及适用场景,可以帮助开发者编写出更加高效、可读性更强的代码。在实际开发中,合理选择使用指针或引用是提高编程技巧的关键。
24 1
|
1月前
|
安全 NoSQL Redis
C++新特性-智能指针
C++新特性-智能指针
|
1月前
|
编译器 C++
virtual类的使用方法问题之在C++中获取对象的vptr(虚拟表指针)如何解决
virtual类的使用方法问题之在C++中获取对象的vptr(虚拟表指针)如何解决
|
1月前
|
存储 安全 编译器
C++入门 | auto关键字、范围for、指针空值nullptr
C++入门 | auto关键字、范围for、指针空值nullptr
51 4
|
1月前
|
存储 C++
c++学习笔记06 指针
C++指针的详细学习笔记06,涵盖了指针的定义、使用、内存占用、空指针和野指针的概念,以及指针与数组、函数的关系和使用技巧。
30 0
|
1月前
|
安全 编译器 容器
C++STL容器和智能指针
C++STL容器和智能指针
|
1月前
|
C++
C++通过文件指针获取文件大小
C++通过文件指针获取文件大小
25 0