动态内存

掌握C++动态内存管理——shared_ptr/unique_ptr/weak_ptr三种智能指针、new/delete、动态数组,告别裸指针和内存泄漏

学习目标

  • 能实现用 shared_ptr 管理共享所有权的动态对象——用 make_shared 创建、解释强引用计数的增减,并判断何时调用删除器
  • 能比较并选择所有权模型——共享用 shared_ptr、独占用 unique_ptr、观察用 weak_ptr,并写出不带手动 delete 的异常安全代码
  • 能回答:下面这段代码结束时,A 和 B 两个对象是否都会被释放?如果不会,怎么修?
    struct B;
    struct A { std::shared_ptr<B> b; };
    struct B { std::shared_ptr<A> a; };
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    a->b = b; b->a = a;

机制总览

动态内存:机制路径

  1. 1

    直觉:为什么你需要操心“谁负责清理”

    在之前的章节里,你写的所有变量——int、string、vector——在离开大括号时都会自动消失。这就是"栈"上的变量:创建和销毁都由编译器管,你不用操心。

  2. 2

    智能指针:自动帮你打扫的三个兄弟

    C++11 引入了三种 智能指针(smart pointer) ——它们把清理动作绑定到所有者对象的生命周期,减少手写释放路径,但不会自动替你判断所有权图是否正确。

  3. 3

    官方 Chapter 12 的完整所有权地图

    原书从智能指针继续向下追到直接管理内存、动态数组、 allocator ,最后用文本查询程序证明共享所有权的价值。关键问题始终相同:谁拥有资源、谁只观察、何时对象被构造、何时删除器可安全运行。

先按顺序建立机制,再进入实验切换阶段并检查失效证据。

章级决策实验

动态内存:机制与证据

切换《动态内存》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。

选择推理阶段

当前阶段 · 直觉:为什么你需要操心“谁负责清理”

在之前的章节里,你写的所有变量——int、string、vector——在离开大括号时都会自动消失。这就是"栈"上的变量:创建和销毁都由编译器管,你不用操心。

可核验证据

保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「直觉:为什么你需要操心“谁负责清理”」的契约。

学完《动态内存》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。

失效—证据矩阵

动态内存:失效与核验

直觉:为什么你需要操心“谁负责清理”

典型失效

若把「直觉:为什么你需要操心“谁负责清理”」当作孤立语法点,忽略类型约束、对象生命周期或库契约,代码即使通过编译也可能破坏不变量。

核验证据

保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「直觉:为什么你需要操心“谁负责清理”」的契约。

智能指针:自动帮你打扫的三个兄弟

典型失效

若把「智能指针:自动帮你打扫的三个兄弟」当作孤立语法点,忽略类型约束、对象生命周期或库契约,代码即使通过编译也可能破坏不变量。

核验证据

保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「智能指针:自动帮你打扫的三个兄弟」的契约。

官方 Chapter 12 的完整所有权地图

典型失效

若把「官方 Chapter 12 的完整所有权地图」当作孤立语法点,忽略类型约束、对象生命周期或库契约,代码即使通过编译也可能破坏不变量。

核验证据

保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「官方 Chapter 12 的完整所有权地图」的契约。

每个判断都必须能落到观测、测试或产物,不能只凭代码表面推测。

直觉:为什么你需要操心“谁负责清理”

在之前的章节里,你写的所有变量——int、string、vector——在离开大括号时都会自动消失。这就是"栈"上的变量:创建和销毁都由编译器管,你不用操心。

自动对象的生命周期通常受作用域控制,而有些数据大小到运行时才知道,或需要被多个对象按独立于当前调用栈的生命周期共享。返回值、移动和容器已经能解决很多场景;确实需要动态分配时,关键不是“堆比栈活得久”,而是明确谁拥有、谁观察、何时释放

这一章解决什么问题? 给你三样"自动保洁"工具——一个专门共享的、一个坚决独享的、一个只看着不干的——让你用规则代替记忆,从此不必手写 delete。没有它们?漏一次 delete 就漏一块内存,程序跑久了就吃光你的内存。

智能指针:自动帮你打扫的三个兄弟

C++11 引入了三种 ——它们把清理动作绑定到所有者对象的生命周期,减少手写释放路径,但不会自动替你判断所有权图是否正确。

C++ 智能指针三兄弟shared_ptr共享所有权shared_ptrptr → | ctrl →堆对象T object控制块use: 1weak: 0sp₁sp₂两个 shared_ptr 指向同一堆对象 → 共享所有权共享(多个shared_ptr同指一个对象)引用计数use_count() 记录共享者数量拷贝语义允许拷贝(计数器+1)销毁时机最后一个 shared_ptr 销毁→delete适用场景不确定唯一所有者时unique_ptr独占所有权unique_ptr →堆对象T object✕ 禁止拷贝up₁up₂?✓ 可以移动up₁up₂std::move 转移所有权所有权独占(同一时刻只有一个unique_ptr拥有)引用计数无(不需要计数,开销更低)拷贝语义禁止拷贝(已删除拷贝构造/赋值)销毁时机unique_ptr 离开作用域→delete适用场景明确唯一所有者、工厂函数返回weak_ptr弱引用 / 旁观者weak_ptr ↷spuse: 1weak: 1← wp 增的是它堆对象(可能已释放)核心作用:破除循环引用A.sp→B B.sp→A → 谁都不释放A.sp→B B.wp→A → 正常释放所有权不拥有(weak_ptr 不影响生命周期)引用计数增加 weak_count()(非 use_count)拷贝语义允许拷贝(旁观者可以多份)销毁时机不影响堆对象存活(只影响控制块)适用场景观察者、缓存、破除循环引用shared_ptr→共享借用 | unique_ptr→独占传递 | weak_ptr→旁观避循环 —— 选对指针,告别手动 delete 和内存泄漏
shared_ptr:多个指针共享一个堆对象,引用计数为零时自动释放;unique_ptr:独享堆对象,禁止拷贝只能移动;weak_ptr:不增加引用计数的弱引用,专治循环引用造成的内存泄漏。

三兄弟各有各的本事:

  • shared_ptr:允许多个指针共享同一个对象。控制块维护——强计数降到零时调用删除器;若所有权成环,计数不会自然归零。

  • unique_ptr:独占一个堆对象。不能拷贝(拷贝构造函数和赋值运算符被显式删除了),只能把所有权给另一个 unique_ptr。就像一件传家宝——同一时刻只能一个人戴着,可以传给下一个,但不可能凭空变出一份。

  • weak_ptr:不拥有堆对象,只是一个"旁观者"。它不增加引用计数,所以不阻止对象被销毁。主要用于打破两个 shared_ptr 之间的。就像你远远看着一座房子——你能知道它还在不在,但它没了也不影响你。

一句话选型口诀:共享用 shared,独占用 unique,旁观用 weak。

官方 Chapter 12 的完整所有权地图

原书从智能指针继续向下追到直接管理内存、动态数组、allocator,最后用文本查询程序证明共享所有权的价值。关键问题始终相同:谁拥有资源、谁只观察、何时对象被构造、何时删除器可安全运行。

shared_ptr:最常用的共享管家

shared_ptr 是日常用得最多的智能指针。它背后有三个部分在协作:

shared_ptr 内存布局与生命周期栈(Stack)堆(Heap)use: 1weak: 0← 控制块T objectsp₁make_shared<T>(args)一次分配控制块+对象 (use_count=1)① 创建 make_shared② 拷贝 sp₂=sp₁③ 重置 sp₁.reset()④ 析构 use_count→0
① 创建make_shared<T>() 一次分配控制块和堆对象在连续内存,引用计数初始为 1。优于先 new T 再传 shared_ptr 的两步分配。

如何创建 shared_ptr

推荐用 make_shared<T>()——实现通常把控制块和对象放进一次分配,减少分配开销,并避免在复杂表达式中让裸指针处于尚未被接管的中间态:

auto sp = std::make_shared<std::string>("hello");
// sp 现在持有堆上的 string("hello"),引用计数 = 1

make_shared 接收构造参数并转发给 T。若资源由 C API 或其他分配器创建,不能默认套用 delete,必须按 API 契约提供匹配删除器:

std::shared_ptr<int> sp(new int(42));  // new 创建的 int,可由默认 delete 管理

引用计数怎么变

shared_ptr 的引用计数是自动增减的,你可以用 use_count() 查看。下面这个例子展示了整个生命周期:

auto sp1 = std::make_shared<int>(10);   // use_count = 1
{
    auto sp2 = sp1;                     // use_count = 2 (拷贝构造)
    auto sp3 = sp1;                     // use_count = 3
    // sp3 离开作用域 → use_count = 2
}
// sp2 离开作用域 → use_count = 1
// sp1 离开调用函数时会降到 0 → 堆对象被 delete

要点use_count 只在"有几个 shared_ptr 指向它"这个意义上可靠,不要用它做任何业务逻辑——它的值只是随着别处 shared_ptr 的存活/离开而变化。你只需信任它——最后一个离开的人会关门。

拷贝 vs 重置 vs 析构

把 shared_ptr 比一块共享内存上的多个"钥匙"。每种操作对引用计数和堆对象的影响见下——你用 Stepper 一步步看:

先预测:sp1sp2 共享对象时先 sp1.reset(),对象会不会立刻析构?再让 sp2 离开作用域,强计数、对象和控制块分别发生什么?写下答案后再逐步验证。

分步1 / 4

创建

auto sp1 = std::make_shared<Widget>();
shared_ptr 内存布局与生命周期栈(Stack)堆(Heap)use: 1weak: 0← 控制块T objectsp₁make_shared<T>(args)一次分配控制块+对象 (use_count=1)① 创建 make_shared② 拷贝 sp₂=sp₁③ 重置 sp₁.reset()④ 析构 use_count→0
① 创建make_shared<T>() 一次分配控制块和堆对象在连续内存,引用计数初始为 1。优于先 new T 再传 shared_ptr 的两步分配。

unique_ptr:独享且无共享计数

unique_ptr 没有共享引用计数,空删除器常可借助空基类优化做到接近裸指针大小;但状态化删除器会增加对象大小,标准不保证固定为一个指针。代价是不能拷贝,只能移动。

创建和使用

#include <memory>
 
std::unique_ptr<int> up(new int(42));   // 本书 C++11 写法
 
std::cout << *up << '\n';                // 用 * 解引用,和裸指针一样
up.reset();                              // 显式释放,等价于 up = nullptr
up.reset(new int(100));                  // 释放旧对象并接管新对象

make_unique 到 C++14 才进入标准库。C++11 项目可把裸 new 直接放进 unique_ptr 构造表达式,或提供经过评审的 make_unique 辅助函数;不要先把裸指针存进临时变量再跨多条可能抛异常的语句传递。

release 和 reset 的区别

  • reset(p):释放当前所有对象(如果有),接管新的裸指针 p
  • release():放弃当前对象的所有权,返回裸指针——但不 delete。调用方从此负责手动 delete
std::unique_ptr<int> up(new int(42));
int* raw = up.release();    // up 现在是空,raw 指着那块内存
// 你现在必须手动 delete raw,否则内存泄漏
delete raw;

几乎永远优先用 reset——它按删除器释放旧对象。release 只在所有权确实转交给另一接口时使用,并必须确认接收方采用匹配的释放机制;C API 未必使用 C++ delete

weak_ptr:破掉循环引用这个死结

两个 shared_ptr 互相持有是动态内存管理中最经典的陷阱。看这个例子:

循环引用:shared_ptr 的死锁 vs weak_ptr 的解法✕ shared_ptr 循环引用 = 内存泄漏离开作用域后 spA/spB 的引用计数各剩 1 → 都不释放spA → A | spB → B对象 Ashared_ptr<B>对象 Bshared_ptr<A>spA/spB 离开作用域后引用计数 = 1(互相持有)→ A 和 B 永远留在堆上 → 内存泄漏spA→ptr_B; spB→ptr_A; // 循环// 离开 scope → 两个都活着✓ weak_ptr 破除循环 = 正常释放B 持有 A 的 weak_ptr → 不增加引用计数 → 正常析构spA → A | spB → B对象 Ashared_ptr<B>对象 Bweak_ptr<A>spA/spB 离开作用域后→ A 的引用计数 = 1(只有 spA)→ 先析构 A(spA 释放)→ B 的计数变 0 → 再析构 BspA→ptr_B; spB→weak_ptr<A>;// 离开 scope → 正常释放 ✓口诀:A 持有 B 用 shared,B 回看 A 用 weak —— 单向 shared + 反向 weak 是安全公式
左侧(循环引用):A 和 B 各持对方的 shared_ptr,离开作用域后引用计数各剩 1,谁都不释放 → 内存泄漏右侧(weak_ptr 解法):B 改用 weak_ptr 观察 A,不增加 A 的引用计数 → 正常析构顺序释放。

在没有 weak_ptr 的世界里(左图),A 和 B 互相持有对方的 shared_ptr:spA/spB 离开作用域后各自的 use_count 从 2 变成 1(对方手里还拿着),没有谁降到 0——两块内存永远挂在堆上。

公式:把回看的一边换成 weak_ptr。A → B 用 shared_ptr(正常父子关系),B → A 用 weak_ptr(只观察、不增加计数)。这样 spA 离开时 use_count 降到 0,A 正常析构,析构过程中 A 持有的 B 的 shared_ptr 也被析构 → B 的计数变 0 → B 也正常析构。

从 weak_ptr 获取 shared_ptr:lock()

weak_ptr 不能直接解引用——它不拥有对象。必须通过 lock() 获取一个临时的 shared_ptr:

std::shared_ptr<int> sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp;     // wp 不会增加引用计数
 
if (auto locked = wp.lock()) {  // lock() 返回 shared_ptr
    *locked = 100;              // 可以安全使用了
} else {
    // 对象已被销毁
}

lock() 返回空表示对象的最后一个 shared_ptr 已离开——你已经帮不上什么忙了,优雅地走开。用 expired() 可以提前判断,但判断后到 lock() 之间可能对象已被销毁——所以直接用 lock() + 判空才是正确姿势。

new/delete:现代 C++ 应该少用,但必须懂

在智能指针出现之前,new/delete 是程序员唯一的堆内存工具。如今你仍会在以下场景遇到它们:遗留代码、C API 返回裸指针、"placement new" 这类高级用法。了解它们才能理解智能指针在替你做什么。

new 做了什么

new 做两件事——不要混在一起理解:

  1. 分配内存:调用 operator new 在堆上申请足够放一个 T 的空间
  2. 构造对象:在那块内存上调用 T 的构造函数
int *pi = new int;           // 分配 + 默认初始化(值未定义)
int *pi2 = new int(42);      // 分配 + 值初始化
std::string *ps = new std::string("hello");  // 分配 + 调用构造
auto *ps2 = new auto(*ps);   // 用 *ps 的类型和值复制一份(C++11)

delete 则反过来:

  1. 析构对象:调用 T 的析构函数
  2. 释放内存:调用 operator delete 把空间还回堆

忘了 delete = 内存泄漏。delete 了又用 = 使用后释放(undefined behavior)。

new[] / delete[]:动态数组

分配数组用 new[],释放必须配套用 delete[]——不是 delete

int *arr = new int[100];  // 堆上分配 100 个 int
// ... 使用 arr[0..99] ...
delete[] arr;             // 必须 delete[]!写成 delete arr 是未定义行为

"忘记 delete[]"是多容易翻车? 函数里一提前 return、一抛异常——delete[] 就被跳过去了。这就是为什么 是现代 C++ 的核心思想:

动态数组:new[] 手动管理 vs vector 自动管理new[] / delete[] 手动管理① 分配int *arr = new int[n];堆上分配的数组(n 个 int)[0][1][2][3][4][5]② 使用arr[2] = 42;cout << arr[i];③ 释放delete[] arr;必须 delete[]!忘记 = 内存泄漏✕ 忘记 delete[]函数提前 return / 抛异常 …delete[] 被跳过 → 数组永远留在堆上→ 内存泄漏!vector 自动管理① 创建std::vector<int> vec(n);vector 栈对象(24 字节)begin → 堆数组 | end → 末尾+1 | capacity → 容量边界堆上连续数组(自动分配+扩容)② 使用vec[2] = 42;vec.push_back(100); // 自动扩容for (auto x : vec) ③ 自动释放离开作用域自动析构 → delete[]提前 return / 异常 → 也会调析构函数RAII:资源获取即初始化vector 构造时申请堆内存析构时自动释放 → 永远不会忘记VS优先使用 vector(或 array)代替 new[]——它能自动释放、支持范围 for、push_back 自动扩容,且异常安全
new[] 方案:手动分配,手动释放;忘记 delete[](提前 return / 抛异常)→ 内存泄漏。vector 方案:自动管理堆数组,RAII 保证离开作用域时自动 delete[];支持 push_back 自动扩容、范围 for 遍历;异常安全。

一句话总结:需要可变长度对象序列时优先 vector 它用分配器管理连续存储,自动处理元素构造、销毁与异常回滚,并提供容量和迭代器接口。new[] 仍可能出现在 ABI、底层库、定制布局或遗留接口边界,但它不是简单的“更省几个字节”,也不会自动提供长度与所有权信息。

allocator:把分配原始存储与构造对象分开

new T[n] 会同时分配存储并默认构造 n 个对象;有时程序只想先取得空间,随后按实际读入数量逐个构造。 把这两个阶段分开:

std::allocator<std::string> alloc;
const std::size_t n = 100;
std::string *const begin = alloc.allocate(n);
std::string *current = begin;
 
try {
    std::string value;
    while (current != begin + n && std::cin >> value)
        alloc.construct(current++, value);
} catch (...) {
    while (current != begin) alloc.destroy(--current);
    alloc.deallocate(begin, n);
    throw;
}
 
while (current != begin) alloc.destroy(--current);
alloc.deallocate(begin, n);

只有 [begin,current) 中的对象已构造,才能解引用或调用 destroy;未构造存储不是 T 对象数组。deallocate 前必须先销毁全部已构造对象,并传回与 allocate 相同的元素数量。实际业务优先使用 vector,这里只用 allocator 看清存储与对象生命周期是两件事。

TextQuery:共享文本而不复制结果

是本章的综合案例。TextQuery 读取文件并拥有文本;每个 QueryResult 只保存查询词、匹配行号集合和原文本的 shared_ptr,不复制整份文件:

class QueryResult;
class TextQuery {
public:
    typedef std::vector<std::string>::size_type line_no;
    explicit TextQuery(std::ifstream &);
    QueryResult query(const std::string &) const;
private:
    std::shared_ptr<std::vector<std::string> > file;
    std::map<std::string, std::shared_ptr<std::set<line_no> > > wm;
};
 
class QueryResult {
public:
    QueryResult(std::string s,
                std::shared_ptr<std::set<TextQuery::line_no> > p,
                std::shared_ptr<std::vector<std::string> > f)
        : sought(s), lines(p), file(f) {}
    friend std::ostream &print(std::ostream &, const QueryResult &);
private:
    std::string sought;
    std::shared_ptr<std::set<TextQuery::line_no> > lines;
    std::shared_ptr<std::vector<std::string> > file;
};

构造时,filemake_shared<vector<string>>() 创建;读取每行后,索引通过 wm[word] 取得对应的 shared_ptr<set<line_no>>,首次遇到单词时再创建集合。query 用 find 做纯查询,命中则把索引中的 shared_ptr 连同 file 传给结果:

QueryResult TextQuery::query(const std::string &word) const {
    static std::shared_ptr<std::set<line_no> > noData(
        new std::set<line_no>);
    std::map<std::string,
             std::shared_ptr<std::set<line_no> > >::const_iterator found =
        wm.find(word);
    if (found == wm.end())
        return QueryResult(word, noData, file);
    return QueryResult(word, found->second, file);
}

即使原 TextQuery 对象先离开作用域,只要 QueryResult 仍持有 file 与 lines,底层数据就继续存活。这个设计把“结果借用查询器内部引用”的悬空风险,转换成明确的共享所有权成本。

实战:用智能指针写一个安全的二叉树节点

下面是本章最重要的一个完整例子:一个二叉树节点类,展示 shared_ptr 和 weak_ptr 各自该在什么位置出现。

#include <memory>
#include <string>
#include <iostream>
 
struct TreeNode {
    std::string value;
    std::shared_ptr<TreeNode> left;   // 父→子:用 shared_ptr
    std::shared_ptr<TreeNode> right;
    std::weak_ptr<TreeNode> parent;   // 子→父:用 weak_ptr,避免循环引用
 
    TreeNode(const std::string& v) : value(v) {}
    ~TreeNode() { std::cout << "释放 " << value << '\n'; }
};

父子关系是天然的"所有权":父亲创建孩子(make_shared)、父亲被销毁后孩子应该跟着消失。所以 left / rightshared_ptr 没问题——父亲持有孩子的所有权。

这里选择 shared_ptr 是为了允许树外代码长期共享某个子节点。若模型保证严格树形且父节点独占孩子,unique_ptr 子节点通常表达得更准确,parent 可使用生命周期受树约束的非拥有原始指针;不要为了“看起来安全”默认把所有边都改成 shared_ptr。

但孩子回看父亲如果用 shared_ptr,就会形成循环:父亲持有孩子 → 孩子持有父亲 → 谁都不释放。孩子对父亲用 weak_ptr——只观察、不拥有。

auto root = std::make_shared<TreeNode>("根");
root->left = std::make_shared<TreeNode>("左孩子");
root->right = std::make_shared<TreeNode>("右孩子");
root->left->parent = root;   // weak_ptr = shared_ptr,合法
root->right->parent = root;

遍历时通过 lock() 临时拿到 shared_ptr 来访问父节点:

void print_value(const std::shared_ptr<TreeNode>& node) {
    std::cout << node->value;
}
 
// 从任意节点打印从根到当前节点的路径
void print_path(const std::shared_ptr<TreeNode>& node) {
    if (auto p = node->parent.lock())   // lock() 拿到临时的 shared_ptr
        print_path(p);                  // 递归向根走
    print_value(node);                  // 到达根后自上而下打印
}

这个所有权图没有强引用环,因此 root 以及所有外部强引用离开后,节点能按引用计数释放。没有手写 delete 不等于任意 shared_ptr 图都不会泄漏;正确性来自明确的强边与弱边,而不是指针类型名字本身。

删除器:让 shared_ptr 不只是 delete

shared_ptr 默认用 delete 释放堆对象。但如果你要管理一个不是 new 出来的资源(比如 fopen 打开的文件),可以指定一个

// 管理一个 FILE*,析构时自动 fclose
auto closer = [](FILE* f) { if (f) fclose(f); };
std::shared_ptr<FILE> fp(fopen("data.txt", "r"), closer);
// 离开作用域时调用 closer;打开与关闭错误仍需业务代码检查

unique_ptr 也支持删除器,但需要把它写在模板参数里(std::unique_ptr<T, Deleter>),语法稍重;shared_ptr 的删除器是运行时存下来的,语法更轻。

常见误区

小结

  • 三兄弟各司其职:shared_ptr 共享陪伴、unique_ptr 独占传承、weak_ptr 旁观破环——选对就是安全
  • make_shared 优先:实现通常合并控制块与对象分配,并避免裸指针中间态;永远不要从同一裸指针建立两个独立控制块
  • unique_ptr 不能拷贝、只能 move:这是编译期硬约束;它没有共享计数,但状态化删除器可能增加对象大小
  • 循环引用用 weak_ptr 破:A 持有 B 用 shared,B 回看 A 用 weak——单向 shared + 反向 weak = 安全公式
  • 对象与存储分开看:vector 是动态序列默认选择;allocator 用于显式控制构造区间,new[]/delete[] 必须严格配对

练习

问题 1(代码填空) 下面的 Widget 类有一个子 Widget 列表和一个父 Widget。子用 shared_ptr、父用 weak_ptr——把 ??? 替换成正确的类型,并写一行创建父子关联的代码。

struct Widget {
    std::string name;
    ??? children;                     // Widget 的孩子们
    ??? parent;                       // 指向父 Widget(只观察)
    Widget(const std::string& n) : name(n) {}
};
 
int main() {
    auto root = std::make_shared<Widget>("root");
    auto child = std::make_shared<Widget>("child");
    // 写下让 root 成为 child 的父、child 成为 root 的子 的一行代码:
}

问题 2(改代码) 改写下面这段代码,让它不再手动 new/delete,而使用合适的智能指针。然后回答:改写后 get_widget() 返回的是什么所有权?

Widget* get_widget() {
    return new Widget("factory made");
}
 
void use_widget() {
    Widget* w = get_widget();
    w->do_something();
    delete w;                // 这里如果抛异常就会跳过 delete!
}

问题 3(判断与修复) 下面每一段代码,判断它是否安全释放资源;若不安全或不能编译,说明原因并给修复:

a)

auto sp = std::make_shared<int>(10);
sp.reset();

b)

int* raw = new int(10);
auto sp1 = std::shared_ptr<int>(raw);
auto sp2 = std::shared_ptr<int>(raw);

c)

std::unique_ptr<int> up1(new int(10));
auto up2 = up1;  // 编译错误!

资料与写作方式声明

本章以C++ Primer, Fifth Edition, Chapter 12: Dynamic Memory权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

智能指针(smart pointer)
封装指针与所有权规则的类模板。unique_ptr 表达独占,shared_ptr 通过控制块表达共享,weak_ptr 只观察。拥有者结束时按删除器清理,但强引用环仍需设计者用弱边打破。
引用计数(reference count / use_count)
shared_ptr 控制块中的强所有者计数。最后一个强引用消失时调用保存的删除器并销毁对象;weak_ptr 不增加强计数,控制块可能继续存在到最后一个弱引用消失。
移动(std::move)
C++11 引入的语义——不拷贝资源本身,只把所有权从一个对象转给另一个。unique_ptr 只能移动不能拷贝。移动后源指针变空(nullptr)。类比:把一件传家宝交给下一个人——你不能凭空复制一件,但可以传出去。
循环引用(circular reference)
两个对象各自持有对方的 shared_ptr:A 有 B 的 shared_ptr、B 也有 A 的 shared_ptr。各自的引用计数永远大于等于 1,谁都不会被 delete——两块内存在堆上永远挂着。解法:把回看的那一边从 shared_ptr 改成 weak_ptr(不增加引用计数)。
RAII(Resource Acquisition Is Initialization)
把资源有效期绑定到对象生命周期。构造成功后对象负责收尾,析构调用与资源匹配的释放操作;异常展开时局部拥有者也会析构。它减少遗漏路径,但不能修复错误的所有权图。
删除器(deleter)
shared_ptr 或 unique_ptr 析构时调用的自定义释放函数。默认是 delete,但你可以改成 fclosefree 或任何你需要的清理逻辑,让智能指针管理任何资源(文件、socket、GPU 纹理等)。
allocator(allocator)
C++11 中用于分开管理原始存储和对象生命周期的工具。allocate/deallocate 管存储,construct/destroy 管已构造对象;释放前必须只销毁实际构造过的区间。
文本查询共享模型(TextQuery/QueryResult)
本章综合案例。TextQuery 拥有原文与单词索引,QueryResult 通过 shared_ptr 共享文本和匹配行集合,使结果离开查询函数后仍有效且无需深拷贝整份文件。

讨论

评论区加载中…