跳过正文
P15 智能指针
  1. S/
  2. C/C++/
  3. C/
  4. C语言程序设计/

P15 智能指针

Li
作者
Li
往前走,别回头!
目录
C 语言程序设计笔记 - 这篇文章属于一个选集。

知识点
#

16.1 直接内存管理(new/delete)、创建新工程与观察内存泄漏
#

  • @ new与delete图
    • ![[白板/P48 new与delete.canvas|P48 new与delete]]

16.1.1 直接内存管理(new/delete)
#

[!tip] 动态内存分配

  • @ 动态分配: 由程序员自己用 new 为对象分配内存(new 返回该对象的指针),用 delete 释放内存
  • new 出来的对象在==堆==上分配内存,必须自己 delete 释放,否则内存泄漏
  • 局部变量在==栈==上分配,离开作用域自动释放
  • 静态变量(static)在==静态内存==上,程序结束时释放

[!tip] new 初始化方式

  • % 直接初始化: int *p = new int(100); — 值为 100
  • % 值初始化: int *p = new int(); — 值被初始化为 0
  • % 默认初始化: int *p = new int; — 值未定义(不确定)
  • % 类类型: new 会调用构造函数初始化,值初始化对自定义类无额外意义
  • % const 对象: const int *pci = new const int(200);
// new 与 auto 配合
string *mystr2 = new string(5, 'a');
auto mystr3 = new auto(mystr2); // mystr3 被推断为 string** 类型

// vector 初始化
vector<int> *pointv = new vector<int>{1, 2, 3, 4, 5};

[!tip] new/delete 使用注意事项

  • ! newdelete 必须==成对使用==,有 new 必须有 delete
  • ! 同一块内存只能 delete ==一次==,多次 delete 会报异常
  • ! delete 后的内存==不能再使用==,否则运行异常
  • ! delete 后建议将指针设为 nullptr,避免悬空指针
  • ! 两个指针指向同一块内存时,delete 一个后另一个也不能再操作
  • const 对象可以被 delete

16.1.2 创建新工程与观察内存泄漏
#

[!tip] MFC 应用检测内存泄漏

  • @ MFC 应用: 微软基础类库框架,程序退出时能报告内存泄漏
  • OnInitDialog 函数中写入测试代码
  • F5 调试运行,退出后在"输出"窗口查看内存泄漏信息
  • “Detected memory leaks!” 表示出现内存泄漏,会给出泄漏字节数

16.2 new/delete 探秘、智能指针总述与 shared_ptr 基础
#

16.2.1 new/delete 探秘
#

[!tip] new/delete 本质

  • @ new/delete: 都是==关键字(运算符)==,不是函数
  • new 做两件事: ① 分配内存 ② 调用构造函数初始化
  • delete 做两件事: ① 调用析构函数 ② 释放内存
  • new 通过调用 operator new() 分配内存
  • delete 通过调用 operator delete() 释放内存

[!tip] new/delete 与 malloc/free 的区别

  • ! 最明显区别: new/delete 会==调用构造/析构函数==,malloc/free 不会
  • new 相比 malloc 不但分配内存,还做初始化
  • delete 相比 free 不但释放内存,还做清理工作

[!tip] 数组的 new[]/delete[]

  • ! new[]delete[] 必须==配套使用==
  • new[] 分配类对象数组时,会==多分配 4 字节==保存数组大小,供 delete[] 知道调用几次析构函数
  • 内置类型(如 intnew[] 不会多分配 4 字节
  • ! 有自定义析构函数的类,用 delete(而非 delete[])释放数组会==报异常==
  • 无自定义析构函数的类或内置类型,deletedelete[] 效果一样(但不推荐混用)
// 正确用法
int *p = new int[2];
delete[] p;           // 规范写法

A *pA = new A[2]();
delete[] pA;          // 必须用 delete[],调用 2 次析构函数

// 错误用法
A *pA = new A[2]();
delete pA;            // 异常!只调用 1 次析构,且释放地址错乱

16.2.2 智能指针总述
#

[!tip] 智能指针概念

  • @ 智能指针: 对==裸指针==进行包装,能够==自动释放所指向的对象==
  • 智能指针让程序员不用再担心 delete,程序更健壮
  • 优先选择智能指针,裸指针能做的智能指针都能做

[!tip] 四种智能指针

  • % std::shared_ptr: 共享式指针,多个指针指向同一对象,最后一个指针销毁时释放对象
  • % std::unique_ptr: 独占式指针,同一时刻只有一个指针指向该对象(所有权可移交)
  • % std::weak_ptr: 辅助 shared_ptr 工作,不控制对象生存期
  • % std::auto_ptr: C++98 已有,已被 unique_ptr 取代,==不要再使用==

16.2.3 shared_ptr 基础
#

[!tip] shared_ptr 初始化

  • @ shared_ptr: 共享式智能指针,采用==引用计数==管理对象生存期
  • 多个 shared_ptr 指向同一对象,最后一个不需要时才释放
  • shared_ptrexplicit 的,==不能隐式转换==
// 常规初始化
shared_ptr<int> pi(new int(100));       // 正确:直接初始化
// shared_ptr<int> pi2 = new int(100);  // 错误:不能隐式转换

// make_shared(推荐)
shared_ptr<int> p2 = make_shared<int>(100);      // 指向值为 100 的 int
shared_ptr<string> p3 = make_shared<string>(5,'a'); // 5 个 'a'
auto p5 = make_shared<string>(5, 'a');            // auto 简写

// 裸指针初始化(不推荐)
int *pi = new int;
shared_ptr<int> p1(pi);  // 不推荐
shared_ptr<int> p1(new int); // 稍好,直接传递 new

[!tip] make_shared 优势

  • @ make_shared: 标准库函数模板,==最安全和高效==的分配方式
  • 在堆中分配并初始化对象,返回 shared_ptr
  • 编译器只分配==一次内存==(对象+控制块),而 shared_ptr<T>(new T) 至少分配两次
  • ! 使用 make_shared 则==无法自定义删除器==

16.3 shared_ptr 常用操作、计数与自定义删除器等
#

  • @ shared_ptr图
    • ![[白板/P49 shared_ptr.canvas|P49 shared_ptr]]

16.3.1 shared_ptr 引用计数的增加和减少
#

[!tip] 引用计数增加的情况

  • 1 用一个 shared_ptr 初始化另一个 shared_ptr
  • 1 将 shared_ptr 作为实参传递给函数(值传递)
  • 1 函数返回 shared_ptr(有变量接收时)

[!tip] 引用计数减少的情况

  • 1 给 shared_ptr 赋新值,指向新对象
  • 1 局部 shared_ptr 离开其作用域
  • 1 当引用计数变为 ==0== 时,自动释放所管理的对象

16.3.2 shared_ptr 指针常用操作
#

[!tip] 常用成员函数

函数说明
use_count()返回指向同一对象的智能指针数量(主要用于调试)
unique()是否独占指向的对象(引用计数为 1 返回 true
reset()不带参:释放对象并置空;带参:指向新对象
get()返回保存的裸指针(小心使用,不要 delete
swap()交换两个智能指针所指向的对象
*p解引用,获得指向的对象
= nullptr引用计数减 1,若为 0 则释放对象,指针置空
// reset 用法
shared_ptr<int> pi(new int(100));
pi.reset();               // 释放对象,pi 置空
pi.reset(new int(1));     // 指向新内存

// get 用法
shared_ptr<int> myp(new int(100));
int *p = myp.get();       // 获取裸指针
// delete p;              // 绝对不可以!

// 智能指针作为判断条件
if (ps1) { /* ps1 指向一个对象 */ }

[!tip] 自定义删除器

[!tip] 删除器与数组

  • @ 删除器: 默认用 delete 释放对象,可自定义删除逻辑
  • shared_ptr 指定删除器:在参数中添加删除器函数名或 lambda
  • ! shared_ptr 管理动态数组时,默认删除器==不支持数组==,需自定义或用 T[]
// 自定义删除器(函数)
void myDeleter(int *p) { delete p; }
shared_ptr<int> p(new int(12345), myDeleter);

// 自定义删除器(lambda)
shared_ptr<int> p2(new int(12345), [](int *p) { delete p; });

// 管理数组方式1:自定义删除器
shared_ptr<A> pA(new A[10], [](A *p) { delete[] p; });

// 管理数组方式2:尖括号加 [](推荐)
shared_ptr<A[]> pA(new A[10]);  // 推荐!可直接用 pA[0], pA[1] 等

// 管理数组方式3:default_delete
shared_ptr<A> pA(new A[10], std::default_delete<A[]>());

[!tip] 删除器与类型

  • ! 两个 shared_ptr 删除器不同,只要指向类型相同,就属于==同一类型==
  • 这意味着可以放入同一容器中
  • make_shared 无法自定义删除器

16.4 weak_ptr 简介、weak_ptr 常用操作与尺寸问题
#

  • @ weak_ptr图
    • ![[白板/P50 weak_ptr.canvas|P50 weak_ptr]]

16.4.1 weak_ptr 简介
#

[!tip] weak_ptr 概念

  • @ weak_ptr: 弱引用智能指针,指向 shared_ptr 管理的对象,但==不控制对象生存期==
  • 绑定到 shared_ptr 不会改变引用计数(弱引用不影响强引用计数)
  • weak_ptr 构造和析构不会增加/减少所指向对象的引用计数
  • 不能直接访问对象,必须通过 lock() 获取 shared_ptr
auto pi = make_shared<int>(100);
weak_ptr<int> piw(pi);     // piw 弱共享 pi,强引用计数不变
// *piw;                    // 错误!不能直接访问
auto pi2 = piw.lock();     // 返回 shared_ptr,强引用计数 +1
if (pi2 != nullptr)
    cout << "所指对象存在" << endl;

16.4.2 weak_ptr 常用操作
#

[!tip] weak_ptr 常用成员函数

函数说明
use_count()获取与该弱指针共享对象的 shared_ptr 数量(强引用计数)
expired()是否过期(use_count() 为 0 返回 true
reset()将弱引用指针置空,弱引用计数减 1
lock()检查对象是否存在,存在返回 shared_ptr,不存在返回空
auto pi = make_shared<int>(100);
weak_ptr<int> piw(pi);
int isc = piw.use_count();  // 1

pi.reset();
if (piw.expired())          // true,对象已释放
    cout << "piw 已过期" << endl;

auto p2 = piw.lock();       // 返回空 shared_ptr

16.4.3 尺寸问题
#

[!tip] 智能指针尺寸

  • shared_ptrweak_ptr 尺寸都是裸指针的 ==2 倍==(x86 下为 8 字节)
  • 包含两个裸指针:
    • 1 指向所管理对象的指针
    • 1 指向==控制块==的指针
  • 控制块包含:强引用计数、弱引用计数、自定义删除器指针等
  • 控制块由第一个指向某对象的 shared_ptr 创建

16.5 shared_ptr 使用场景、陷阱、性能分析与使用建议
#

16.5.1 shared_ptr 使用场景
#

shared_ptr<int> createO(int value) {
    return make_shared<int>(value);  // 返回 shared_ptr
}

void myfunc(int value) {
    shared_ptr<int> ptmp = createO(10);
    return;  // ptmp 离开作用域,内存自动释放
}

16.5.2 shared_ptr 使用陷阱分析
#

[!tip] 陷阱1:慎用裸指针

  • ! 把裸指针绑定到 shared_ptr 后,==不应再使用裸指针==访问该内存
  • ! 不要用裸指针初始化==多个== shared_ptr,否则会产生多个独立引用计数,导致重复释放
// 错误:裸指针初始化多个 shared_ptr
int *pi = new int;
shared_ptr<int> p1(pi);
shared_ptr<int> p2(pi);  // p1、p2 各自引用计数为 1,释放时重复 delete!

// 正确
shared_ptr<int> p1(new int);  // 直接传递 new

[!tip] 陷阱2:慎用 get 返回的指针

  • ! get() 返回的指针==不能 delete==
  • ! 不能用 get() 返回的指针初始化另一个 shared_ptr
shared_ptr<int> myp(new int(100));
int *p = myp.get();
// delete p;                    // 绝对不可以!
// shared_ptr<int> myp2(p);    // 绝对不可以!
shared_ptr<int> myp2(myp);      // 正确:共享引用

[!tip] 陷阱3:用 enable_shared_from_this 返回 this

  • ! 在类成员函数中直接用 this 创建 shared_ptr 会导致==独立引用计数==
  • 解决:继承 std::enable_shared_from_this<类名>,使用 shared_from_this()
class CT : public std::enable_shared_from_this<CT> {
public:
    shared_ptr<CT> getself() {
        // return shared_ptr<CT>(this);  // 错误!
        return shared_from_this();        // 正确
    }
};

[!tip] 陷阱4:避免循环引用

  • ! 两个类互相持有对方的 shared_ptr,会导致==循环引用==,引用计数永远不为 0,内存泄漏
  • 解决:将其中一个改为 weak_ptr
class CA;
class CB;

class CA {
public:
    shared_ptr<CB> m_pbs;  // 强引用 CB
    ~CA() { cout << "~A()" << endl; }
};

class CB {
public:
    weak_ptr<CA> m_pas;    // 改为弱引用!
    ~CB() { cout << "~B()" << endl; }
};

16.5.3 性能说明
#

[!tip] shared_ptr 性能

  • 尺寸为裸指针的 2 倍(8 字节 x86),包含对象指针和控制块指针
  • 控制块创建时机:
    • 1 make_shared 创建时
    • 1 用裸指针创建 shared_ptr
  • 指定删除器不影响 shared_ptr 大小
  • 移动语义: 移动不会增加引用计数,复制会增加
shared_ptr<int> p1(new int(100));
shared_ptr<int> p2(std::move(p1));  // 移动构造,p1 置空,p2 引用计数为 1

16.5.4 补充说明和使用建议
#

[!tip] 使用建议

  • 优先使用 make_shared 构造智能指针(更高效,只分配一次内存)
  • 不要 new shared_ptr<T>
  • 不要 memcpy shared_ptr<T>
  • 不确定的用法要慎重分析

16.6 unique_ptr 简介与常用操作
#

  • @ unique_ptr图
    • ![[白板/P51 unique_ptr.canvas|P51 unique_ptr]]

16.6.1 unique_ptr 简介
#

[!tip] unique_ptr 概念

  • @ unique_ptr: 独占式智能指针,同一时刻==只能有一个==指针指向该对象
  • 销毁时自动释放所指向的对象
  • ==优先选择== unique_ptr,不能满足需求时再考虑 shared_ptr
// 常规初始化
unique_ptr<int> pi;                     // 空指针
unique_ptr<int> pi2(new int(105));      // 绑定 new 返回的指针

// make_unique(C++14)
unique_ptr<int> p1 = make_unique<int>(100);
auto p2 = make_unique<int>(200);

16.6.2 unique_ptr 常用操作
#

[!tip] unique_ptr 不支持的操作

  • ! 不支持==复制==:unique_ptr<T> p2(p1) — 编译错误
  • ! 不支持==赋值==:p2 = p1 — 编译错误
  • 支持移动:unique_ptr<T> p2(std::move(p1))

[!tip] unique_ptr 常用成员函数

函数说明
release()放弃控制权,返回裸指针,智能指针置空
reset()不带参:释放对象并置空;带参:指向新对象
get()返回保存的裸指针(不要 delete
swap()交换两个智能指针所指向的对象
*p解引用(数组不支持)
= nullptr释放对象并置空
// release 用法
unique_ptr<string> ps1(new string("hello"));
unique_ptr<string> ps2(ps1.release());  // ps1 置空,ps2 接管

// 注意:release 后必须处理返回的裸指针
string *tempp = ps2.release();
delete tempp;  // 手动释放

// reset 用法
unique_ptr<string> prs(new string("hello"));
prs.reset();                        // 释放并置空
prs.reset(new string("world"));     // 指向新对象

[!tip] unique_ptr 管理数组

  • 尖括号中加 [] 即可管理数组,支持 [] 下标访问
  • ! 有析构函数的类,<> 中必须写 T[],否则报异常
unique_ptr<int[]> ptrarray(new int[10]);
ptrarray[0] = 12;   // 支持下标访问

unique_ptr<A[]> ptrarray(new A[10]);  // 正确
// unique_ptr<A> ptrarray(new A[10]); // 异常!

[!tip] 转换为 shared_ptr

  • unique_ptr 可以转换为 shared_ptr(通过移动语义)
  • 转换后 unique_ptr 置空,shared_ptr 引用计数为 1
  • 系统会为 shared_ptr 创建控制块
unique_ptr<string> ps(new string("hello"));
shared_ptr<string> ps2(std::move(ps));  // ps 置空,ps2 引用计数为 1

16.7 返回 unique_ptr、删除器与尺寸问题
#

16.7.1 返回 unique_ptr
#

[!tip] 函数返回 unique_ptr

  • unique_ptr 不支持复制,但==从函数返回==是例外
  • 返回局部 unique_ptr 时,编译器会调用移动构造函数
unique_ptr<string> tuniqp() {
    return unique_ptr<string>(new string("hello"));
    // 或者
    // unique_ptr<string> pr(new string("hello"));
    // return pr;  // 也可以
}

unique_ptr<string> ps;
ps = tuniqp();  // 可以接收

16.7.2 删除器
#

[!tip] unique_ptr 删除器

  • 删除器类型写在尖括号中:unique_ptr<对象类型, 删除器类型>
  • ! 删除器类型是 unique_ptr ==类型的一部分==,不同于 shared_ptr
  • 删除器不同则 unique_ptr 类型不同,不能放入同一容器
// 函数指针类型作为删除器
typedef void(*fp)(string*);
unique_ptr<string, fp> ps1(new string("hello"), mydeleter);

// using 定义函数指针类型
using fp2 = void(*)(string*);
unique_ptr<string, fp2> ps2(new string("hello"), mydeleter);

// decltype 推导
auto mydella = [](string *pdel) { delete pdel; };
unique_ptr<string, decltype(mydella)> ps5(new string("hello"), mydella);

16.7.3 尺寸问题
#

[!tip] unique_ptr 尺寸

  • 通常情况下,unique_ptr 尺寸与==裸指针相同==(x86 下 4 字节)
  • 如果删除器是 lambda 表达式:尺寸==不变==
  • 如果删除器是函数指针:尺寸==变大==(x86 下 8 字节)
  • shared_ptr 不同:shared_ptr 不管什么删除器,大小始终是裸指针 2 倍

16.8 智能指针总结
#

[!tip] 智能指针设计思想

  • 核心目的:==自动释放内存==,防止内存泄漏
  • 智能指针在对象离开作用域时自动调用析构函数释放内存

[!tip] auto_ptr 为何被废弃

  • auto_ptr 复制时会默默转移所有权,原指针变空,后续使用会崩溃
  • unique_ptr 复制时直接编译报错,更安全
  • unique_ptr 通过移动语义显式转移所有权

[!tip] 智能指针选择

  • 优先考虑 unique_ptr(独占式,开销小)
  • 需要多个指针指向同一对象时选择 shared_ptr
  • weak_ptr 辅助 shared_ptr 解决循环引用问题
  • 不要再使用 auto_ptr
Reply by Email
C 语言程序设计笔记 - 这篇文章属于一个选集。

相关文章