45.C++-构造析构.md 8.6 KB

C++ 构造/析构(含 RAII)

判别规则(核心,唯一要记的)

  1. 看"谁在管资源":对象放栈上靠自动析构 → RAII 安全new/delete 手动管理 → 中途 return/异常会泄漏
  2. 看"对象是不是临时":临时对象(用完即弃)→ 移动;还要继续用的已存在对象 → 拷贝。移动 = 偷指针 + 原对象置空,O(1);拷贝 = 复制数据,深拷贝独立
  3. 看"有没有显式禁用":只要没显式删除(=delete),编译器默认生成拷贝构造(声明了析构则移动构造不生成)→ 能编译;显式 =delete 才不能
  4. 看"容器重定位元素"(移动优先原则):标准库(vector/string)需要移动元素时默认走移动,只有移动不可用时(被 =delete / 未定义 / 非 noexcept)才退回拷贝 → 这就是"只能移动不能拷贝"的对象(unique_ptr)也能存 vector 的原因

一句话:"构造=创建时自动初始化,析构=销毁时自动清理;RAII 是栈对象自动析构,不是 new/delete;移动给临时对象、拷贝给存活对象;有指针成员必须深拷贝;容器默认走移动,移动不可用才走拷贝。"

抽象描述(一句话本质)

构造函数 = 对象创建时自动执行、负责初始化/获取资源;析构函数 = 对象销毁时自动执行、负责清理/释放资源。构造可重载多个(默认/带参/拷贝/移动),析构只能一个。

构造/析构 vs 拷贝/移动

构造函数 析构函数
调用时机 对象创建时 对象销毁时
个数 可重载多个 只能一个
命名 与类名相同 类名前加 ~
能否重载
用途 初始化成员、获取资源 清理资源、释放内存
默认构造 拷贝构造 移动构造
ClassName() ClassName(const ClassName&) ClassName(ClassName&&)
无参,编译器默认生成 用已存在对象建新对象(深拷贝) 移动临时对象资源(性能优化)
数据初始化 数据复制 资源转移,原对象置空

RAII(资源获取即初始化)

  • 核心:资源在构造函数获取,在析构函数释放
  • 保证:无论正常 return 还是异常(栈展开 stack unwinding),栈对象析构都会执行 → 不会泄漏
  • 正确写法:对象放栈上(FileGuard guard(fp);),不用 new/delete
  • 错误写法new/delete 手动管理,中间 return → delete 不执行 → 泄漏

    class FileGuard {
    public:
    FileGuard(FILE* f) : fp(f) {}
    ~FileGuard() { fclose(fp); }
    private:
    FILE* fp;
    };
    void func() {
    FILE* fp = fopen("a.txt", "w");
    FileGuard guard(fp);   // 栈对象,作用域结束自动 fclose
    }
    

深拷贝 vs 浅拷贝 vs 移动

浅拷贝 深拷贝 移动
指针成员 复制指针值(共享内存) 新分配内存并复制数据 偷走指针,原对象置空
两个对象 指向同一块内存 完全独立 一个持有资源,一个空
后果 双重释放崩溃 安全 安全且 O(1)
何时用 禁用 存活的已存在对象 临时对象

移动构造三要素

  1. 参数是右值引用 ClassName(ClassName&&)
  2. 转移资源:直接搬走原对象的指针/句柄,不复制数据
  3. 原对象置空other.m_data = nullptr;——防止原对象析构时 delete 同一块内存 → 双重释放崩溃

移动口诀拷贝 = 复制一份,移动 = 把东西搬走并给原对象留个空盒子。

移动构造可编译示例

class Buffer {
public:
    Buffer(size_t size) : m_size(size) {
        m_data = new char[m_size];
    }
    // 拷贝构造:深拷贝
    Buffer(const Buffer& other) : m_size(other.m_size) {
        m_data = new char[m_size];
        memcpy(m_data, other.m_data, m_size);
    }
    // 移动构造:资源转移,原对象置空防双重释放
    Buffer(Buffer&& other) noexcept : m_data(other.m_data), m_size(other.m_size) {
        other.m_data = nullptr;
        other.m_size = 0;
    }
    ~Buffer() { delete[] m_data; }   // nullptr 时 delete 安全
private:
    char* m_data;
    size_t m_size;
};

移动优先原则(标准库行为,核心考点)

标准库容器(vector/string 等)在重定位元素(扩容搬移)时:

  1. 默认走移动:优先调用移动构造,性能高(O(1) 搬指针)
  2. 移动不可用才走拷贝:当移动构造被 =delete、未定义、或noexcept 时,退回拷贝构造(O(n) 复制)
  3. 为什么 noexcept 很关键:vector 扩容必须保证强异常安全(失败则回滚原状)。如果移动可能抛异常,vector 无法回滚(元素已被搬走),所以会退回用拷贝;移动构造声明 noexcept 后,vector 才放心用移动
  4. 推论std::unique_ptr(只移动、不能拷贝)能安全存进 vector——扩容时用移动构造,原指针置空

C++17 保证复制省略(Guaranteed Copy Elision)

  • 规则return T();T t = T(); 等场景,C++17 保证直接在目标位置构造,不调用任何拷贝/移动构造
  • 推论:即使拷贝构造被 =delete、移动构造也不存在,这种代码在 C++17 下依然能编译
  • 注意:C++11/14 下 return T() 仍需要移动或拷贝构造(会走移动,移动不可用才拷贝)——嵌入式面试按 C++11 讲,C++17 是加分项

易混对比

自动管理(RAII) 手动管理(new/delete)
资源释放 析构自动执行,绝不遗漏 必须手动 delete,return/异常就漏
安全 ⚠️ 易泄漏
典型 栈对象、unique_ptr、vector 裸指针、new[]
拷贝 移动
给"还要继续用的已存在对象" 给"用完即弃的临时对象"
深拷贝,数据独立 偷资源,原对象置空
O(n) 复制数据 O(1) 搬指针
移动不可用时的兜底 容器默认优先使用
C++11 C++17
return T() 需要移动或拷贝构造 保证复制省略,无需任何构造
移动可用走移动,不可用走拷贝 直接构造,零拷贝

变体验证(3 题,全过=学会)

  1. class FileGuard { ... ~FileGuard() { fclose(fp); } }; 栈对象在 func 中间 return → 文件会正常关闭(栈对象自动析构,无论什么路径退出)
  2. std::string s2 = s1;拷贝(s1 是左值要复用);std::string s3 = std::string("world");移动(临时右值用完即弃)
  3. 陷阱题:class Y { Y(const Y&) = delete; };(声明了析构)→ Y a = makeY(); 在 C++11/14 下 编译失败(拷贝被 delete,移动因声明析构不生成);C++17 下能编译(保证复制省略,直接构造不调用拷贝/移动)

口述要点(面试怎么讲)

  • 结论先行:构造=创建时初始化,析构=销毁时清理;RAII = 资源构造获取/析构释放,靠栈对象自动析构保证不泄漏
  • 为什么 RAII 防泄漏:正常 return 和异常栈展开(stack unwinding)都会依次调用栈对象析构 → 无路径遗漏
  • 深拷贝 vs 移动:深拷贝给存活对象(数据独立);移动给临时对象(偷指针+置空,O(1))。浅拷贝双重释放崩溃
  • vector 扩容走什么默认走移动,移动不可用才走拷贝;移动必须 noexcept,vector 才敢用它(保证强异常安全);unique_ptr 只移动不拷贝也能存 vector
  • C++17 保证复制省略return T() 直接构造,拷贝被 =delete 也能编译(加分项)
  • 易错点:把 new/delete 当 RAII;以为移动后资源不独立;以为声明析构后拷贝构造仍生成移动构造;以为"没有拷贝就不能存 vector"

关系网络

相邻概念 关系 孤立理解会犯的错
RAII vs 资源管理 RAII 是 C++ 防泄漏的根基,比手动 new/delete 安全 以为 new/delete 就是 RAII
struct vs class(主题44) 两者都有构造/析构,用法相同 以为 struct 没有构造析构
堆 vs 栈(主题11) 栈对象自动析构(RAII),堆对象需手动 delete 以为 new 出来的对象也会自动清理
内存泄漏(主题12) 手动 new 无对应 delete = 泄漏;RAII 根治 靠手动记得 delete,不靠机制
原子/锁(主题21/27) RAII 常用于锁管理(构造加锁、析构解锁) 手动 lock/unlock,中途 return 死锁

学习日期

2026-08-26


每主题一页,复习时只翻本目录。