news 2026/10/2 2:59:10

C++类的默认成员函数详解:构造、析构与拷贝构造

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++类的默认成员函数详解:构造、析构与拷贝构造

前言

「默认成员函数」是 C++ 类机制里最容易被跳过、又最容易出事的一块。很多人写完一个带new的类,只补了一个析构函数就以为万事大吉,结果程序一跑就是双重释放;也有人听说过「三法则」「五法则」,却说不清到底哪个函数在什么条件下会被删除。

先把标题里的三个概念对齐一下。默认成员函数(defaulted member function)指的是编译器在满足条件时隐式声明(implicitly declared)的那几个特殊成员函数:C++98 时代常说的是四个(默认构造、拷贝构造、拷贝赋值、析构),C++11 加入移动语义后,常被一起讨论的是六个(再加上移动构造、移动赋值)。它们和「用= default显式要求的函数」不是一回事——后者是用户声明的,只是让编译器生成定义。

三个最常见的误解:


  1. 「只要我写了析构函数,编译器就不会再生成拷贝构造」——错。写了析构函数只是让拷贝构造的隐式声明被弃用(deprecated),它仍然会被生成,行为是逐成员拷贝。真正会让拷贝构造被删除的是「声明了移动构造或移动赋值」。

  2. 「std::move会把对象搬走」——std::move只是一个到右值引用的类型转换,它本身不做任何事;搬不搬由被调用的函数决定。

  3. 「自定义析构函数不影响性能」——析构函数的存在会抑制移动构造的隐式声明,导致容器扩容时退化成拷贝。


本文按「隐式声明的规则 → 三/五/零法则 → 拷贝与移动的行为差异 → 完整可编译示例」的顺序讲清这些规则,最后汇总 8 个真会踩的坑。代码基准C++17。

一、隐式声明规则:什么时候生成,什么时候删除

这是全篇最需要记住的一张表。表中的「用户声明」包括= default和= delete,也包括手写定义。

隐式声明的函数生成条件被定义为删除的情况
默认构造类中没有用户声明的任何构造函数(不属于「删除」机制;不能生成时不会隐式声明)
析构函数总是隐式声明基类或成员的析构不可访问或被删除
拷贝构造没有用户声明的拷贝构造、移动构造、移动赋值声明了移动构造或移动赋值;某基类/成员的拷贝构造不可用
拷贝赋值没有用户声明的拷贝赋值、移动构造、移动赋值声明了移动构造或移动赋值;某基类/成员不可赋值(如const成员、引用成员)
移动构造没有用户声明的拷贝构造、拷贝赋值、移动构造、移动赋值、析构某基类/成员的移动构造不可用
移动赋值没有用户声明的拷贝构造、拷贝赋值、移动赋值、移动构造、析构某基类/成员不可移动赋值

从这张表能读出三条「反直觉」的结论:


  • 写析构函数,抑制的是移动,不是拷贝。这是最经典的陷阱:一个管理资源的类写了析构函数,移动构造就不再被隐式声明,于是std::vector扩容时只能调用拷贝构造(因为移动构造不存在,重载决议会退回到拷贝构造)。

  • 写移动,杀掉拷贝。一旦显式写了移动构造或移动赋值,拷贝构造和拷贝赋值都被定义为删除(deleted),此时Buffer b = a;会直接编译报错——编译器不想在你明确表达了「要移动」之后,还悄悄替你拷贝。

  • const成员和引用成员会让拷贝赋值被删除,因为赋值运算符无法给它们重新绑定/赋值。


二、三法则、五法则与零法则

三法则(Rule of Three):如果一个类需要自定义析构函数、拷贝构造函数、拷贝赋值运算符中的任何一个,那它多半三个都需要。

理由很直接:需要自定义析构,说明类持有需要手工释放的资源(裸指针、文件句柄、锁)。而编译器生成的拷贝是逐成员拷贝,会把那个指针按位复制一份——于是两个对象指向同一块内存,析构两次就是双重释放。

五法则(Rule of Five):C++11 之后补上移动构造和移动赋值,道理与三法则相同——需要拷贝语义的类通常也需要移动语义。

零法则(Rule of Zero):能用现成的 RAII 类型(std::string、std::vector、std::unique_ptr、std::shared_ptr)就不要手写裸指针成员。这样编译器生成的六个默认函数全是对的,一个都不用写。这是现代 C++ 最推荐的形态。

法则适用场景需要手写几个函数
零法则成员全是 RAII 类型0
三法则自己管理资源,且不需要移动语义3(析构、拷贝构造、拷贝赋值)
五法则自己管理资源,且需要移动语义5(再加移动构造、移动赋值)

= default和= delete是两个关键工具:


  • = default放在类内,函数是「用户声明但非用户提供(not user-provided)」,可以保持类为平凡类型(trivial);放在类外定义,行为略有不同。

  • = delete用来主动禁用——比如拷贝构造= delete表示这个类型只能移动。写= delete比「声明为 private 且不定义」的老写法好,因为报错发生在编译期而非链接期,而且错误信息清楚得多。


三、拷贝与移动:行为差异与 noexcept

隐式定义的拷贝构造和拷贝赋值是逐成员(memberwise)操作:对每个基类和每个非静态数据成员,调用它自己的拷贝构造/拷贝赋值。

隐式定义的移动构造和移动赋值也是逐成员的,但每个成员会被当成右值去构造/赋值。这一点对内置类型(如int*)没有区别——拷贝指针和移动指针都是复制地址;区别在于它不会把源指针置空。所以如果你自己管理裸指针,只依赖隐式移动构造是不够的:源对象仍然指向那块内存,之后两个对象析构同一个指针,同样是双重释放。手写移动构造时必须显式把源对象置为可析构的空状态。

关于noexcept:移动构造函数应当标记noexcept。原因不是「更快」,而是容器的选择策略。标准规定std::vector在重新分配时使用std::move_if_noexcept语义:当元素的移动构造可能抛异常、而拷贝构造可用时,实现会选择拷贝而不是移动,以获得强异常安全保证。libstdc++、libc++、MSVC STL 都遵循这个策略。所以:

static_assert(std::is_nothrow_move_constructible<Buffer>::value, "移动构造必须是 noexcept,否则 vector 扩容会退化成拷贝");

维护后对象的约定:移动之后的源对象处于「有效但未指定(valid but unspecified)」状态——它必须能被安全地析构和赋值,但你不应该读它的具体值。标准只保证这么少,所以「std::move之后还是原值」这种假设不可移植。

实战:五法则的完整示例

下面这个Buffer自己管理一块int数组,把五法则写全,包含自我赋值保护、异常安全与noexcept移动。

#include <algorithm> #include <cstddef> #include <iostream> #include <type_traits> #include <utility> #include <vector> class Buffer { public: Buffer() noexcept = default; explicit Buffer(std::size_t n) : data_(n ? new int[n]() : nullptr), size_(n) {} ~Buffer() { delete[] data_; } // 1) 析构 // 2) 拷贝构造:先分配,再复制,保证异常安全 Buffer(const Buffer& other) : data_(nullptr), size_(0) { if (other.size_ > 0) { data_ = new int[other.size_]; // 若抛异常,data_ 仍是 nullptr std::copy(other.data_, other.data_ + other.size_, data_); size_ = other.size_; } } // 3) 拷贝赋值:拷贝并交换(copy-and-swap),天然自我赋值安全 Buffer& operator=(const Buffer& other) { if (this != &other) { Buffer tmp(other); // 先做可能抛异常的部分 swap(tmp); // 再交换,不抛异常 } return *this; } // 4) 移动构造:必须 noexcept,并且要把源对象置空 Buffer(Buffer&& other) noexcept : data_(other.data_), size_(other.size_) { other.data_ = nullptr; other.size_ = 0; } // 5) 移动赋值:先释放自己的资源,再接管 Buffer& operator=(Buffer&& other) noexcept { if (this != &other) { delete[] data_; data_ = other.data_; size_ = other.size_; other.data_ = nullptr; other.size_ = 0; } return *this; } void swap(Buffer& other) noexcept { std::swap(data_, other.data_); std::swap(size_, other.size_); } std::size_t size() const noexcept { return size_; } int& operator[](std::size_t i) { return data_[i]; } const int& operator[](std::size_t i) const { return data_[i]; } private: int* data_ = nullptr; std::size_t size_ = 0; }; static_assert(std::is_nothrow_move_constructible<Buffer>::value, "Buffer 的移动构造必须是 noexcept"); int main() { Buffer a(3); a[0] = 10; a[1] = 20; a[2] = 30; Buffer b = a; // 拷贝构造 Buffer c = std::move(a); // 移动构造:a 此后处于有效但未指定状态 std::cout << "b.size=" << b.size() << " c.size=" << c.size() << '\n'; b = b; // 自我赋值,必须安全 std::cout << "self-assign ok, b.size=" << b.size() << '\n'; std::vector<Buffer> v; v.reserve(1); v.push_back(Buffer(2)); // 触发一次扩容,走 noexcept 的移动构造 v.push_back(Buffer(2)); std::cout << "v.size=" << v.size() << '\n'; return 0; }

编译与运行:

g++ -std=c++17 -Wall -Wextra -O2 buffer.cpp -o buffer clang++ -std=c++17 -Wall -Wextra -O2 buffer.cpp -o buffer

一个值得知道的标准细节:C++17 起,返回纯右值时有保证的拷贝省略(guaranteed copy elision)。例如函数体里写return Buffer(2);或return {};时,不会调用任何拷贝/移动构造,对象直接在调用方的存储上构造。这意味着即使把拷贝构造和移动构造都= delete,这种写法依然能编译。C++17 之前这不是保证,只是编译器可以做的优化(copy elision / RVO),标准并不强制。

常见坑点

1. 只写析构,不写拷贝构造

❌

class Buffer { public: explicit Buffer(std::size_t n) : p_(new int[n]) {} ~Buffer() { delete[] p_; } // 没有拷贝构造:隐式生成的是逐成员拷贝 private: int* p_; }; // Buffer a(4); Buffer b = a; -> b.p_ 与 a.p_ 指向同一块内存,析构两次

这是UB,标准不保证任何行为,典型症状是double free or corruption。

✅ 按三法则补齐拷贝构造与拷贝赋值,或者改用std::vector<int>成员(零法则)。

2. 移动构造忘了把源对象置空

❌

Buffer(Buffer&& other) noexcept : data_(other.data_), size_(other.size_) {} // 没有 other.data_ = nullptr; 两个对象都指向同一块内存

✅ 置空:other.data_ = nullptr; other.size_ = 0;。

3. 移动构造没标 noexcept

❌Buffer(Buffer&& other) : data_(other.data_), size_(other.size_) { ... }

✅Buffer(Buffer&& other) noexcept : ...。没有noexcept时,std::vector扩容会退回到拷贝(标准规定的std::move_if_noexcept策略),且这个退化是静默的,不会报错。

4. 拷贝赋值漏掉自我赋值检查

❌

Buffer& operator=(const Buffer& o) { delete[] data_; // 如果 &o == this,数据已经没了 data_ = new int[o.size_]; // 读的是已释放内存,UB return *this; }

✅ 用拷贝并交换(copy-and-swap),或者先判断if (this != &other)。

5. 拷贝构造参数按值传递

❌Buffer(Buffer other) { ... }—— 编译错误(GCC 报 "copy constructor must pass its first argument by reference")。按值传参本身就要调用拷贝构造,会无限递归。

✅Buffer(const Buffer& other)。要不要const取决于你是否需要窃取源对象的内容;绝大多数情况都该加const。

6. 多态基类没有虚析构函数

❌

struct Base { ~Base() {} }; struct Derived : Base { std::vector<int> big; }; Base* p = new Derived(); delete p; // UB:只调用了 Base 的析构,Derived 的成员没被析构

这是UB,标准不保证任何行为,症状是内存泄漏或堆结构损坏。

✅virtual ~Base() = default;。反过来也一样:如果析构函数不是虚的,就不要通过基类指针delete。

7. 值传递造成对象切片

❌

void handle(Base b) { } // 按值传参:Derived 的部分被切掉 Derived d; handle(d); // 只拷贝了 Base 子对象

✅ 形参用引用或指针:void handle(const Base& b)。如果确实需要拷贝,用虚的clone()之类的手段显式要一份多态副本。

8. 用 memset / memcpy 处理非平凡类型

❌

Buffer b(4); Buffer c; std::memcpy(&c, &b, sizeof b); // 含非平凡成员时是 UB

对含有非平凡拷贝构造/析构的类型(标准里称为非平凡可复制类型),用memcpy/memset复制或清零是UB,标准不保证任何行为。

✅ 用拷贝构造或赋值运算符。只有在类型满足「平凡可复制(trivially copyable)」时,memcpy才是合法的。

总结

要点结论
隐式声明默认构造、析构、拷贝构造、拷贝赋值、移动构造、移动赋值共六个
关键规则声明移动 → 拷贝被删除;声明析构 → 移动不再隐式声明
拷贝行为逐成员拷贝;若成员是不可赋值的const/引用,拷贝赋值被删除
移动行为逐成员移动,但不会自动把源指针置空,必须自己写
noexcept移动构造标noexcept,否则vector扩容按标准退化为拷贝
三/五/零法则管理资源就写五法则,不管理资源就写零法则
移动后状态「有效但未指定」,只能析构或重新赋值,不要读值
C++17 变化返回纯右值时有保证的拷贝省略,连= delete的拷贝构造也能通过


判断一个类该写哪些默认成员函数,只需要问一句话:这个类持有需要手工释放的资源吗?如果答案是「不」,那就是零法则,什么都别写;如果是「是」,就按五法则一次写全,并且把移动构造标上noexcept、把源对象置空。至于「哪个函数会被隐式删除」,上面那张表可以当速查表用——真正的坑几乎都出在「写了析构却期待移动还在」这一条上。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 2:58:16

杭电OJ2011-2025刷题复盘:十五道入门题避坑与基础能力拆解

我最近把杭电oj的2011到2025这十五道题重新过了一遍&#xff0c;顺手把踩过的坑、总结的思路都整理了出来。这批题目属于典型的入门巩固区间&#xff0c;难度不大但考察点很杂&#xff0c;有浮点数精度处理、有递推思维、有数组下标陷阱、也有字符串边界问题。如果你是刚接触OJ…

作者头像 李华
网站建设 2026/10/2 2:57:22

工业腐蚀检测数据集预处理六步法:从解压到可训练

简介&#xff1a;本资源是面向工业智能检测领域的腐蚀目标检测与实例分割专用数据集&#xff0c;适用于从事设备健康监测、基建安全评估及材料耐久性研究的算法工程师与科研人员。数据集共386张工业场景图片&#xff08;含训练/验证/测试集&#xff09;&#xff0c;配套386个YO…

作者头像 李华
网站建设 2026/10/2 2:56:42

缓存刷新实战:双缓存加版本号,高并发下最稳的方案

1. 一次线上事故引发的思考&#xff1a;缓存刷新到底难在哪事情是这样的&#xff0c;某个周五下午&#xff0c;我正在处理一个看似"人畜无害"的需求&#xff1a;给后台管理系统加一个按钮&#xff0c;点击之后刷新某个业务模块的缓存。当时我的第一反应很简单——不就…

作者头像 李华
网站建设 2026/10/2 2:56:29

分数阶微积分在细胞膜电学特性建模中的应用与实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 2:56:20

Windows内存占用高怎么办?开源工具WinMemoryCleaner帮你精准清理

你是不是也遇到过这样的场景&#xff1a;Windows 11 任务管理器一打开&#xff0c;16GB 内存直接显示占用 50% 以上&#xff1b;浏览器标签页开多了&#xff0c;微信、QQ、办公软件一起挂在后台&#xff0c;右下角就冒出来“系统内存不足”&#xff1b;有时候跑个稍大点的程序&…

作者头像 李华
网站建设 2026/10/2 2:55:45

SpringBoot+Vue+MySQL企业车辆管理系统毕业设计全攻略

毕业设计选了个企业车辆管理系统&#xff0c;SpringBootVueMySQL这套组合&#xff0c;说难不难&#xff0c;说简单也真有不少坑。我帮你把这套系统的完整设计思路、数据库表结构、核心代码实现和部署流程全拆开讲清楚&#xff0c;从项目骨架到答辩要点&#xff0c;一篇讲透。先…

作者头像 李华