C++ 六大特殊成员函数:= default / = delete、平凡可复制与标准布局
只针对六大特殊成员函数:默认构造、析构、拷贝构造、拷贝赋值、移动构造、移动赋值。
它们可以在函数声明末尾加 = default; 或 = delete; 来接管编译器的行为。
一、= default:显式默认
告诉编译器:请你帮我生成这个函数的默认版本,和编译器隐式自动生成的一模一样。
1.1 什么时候用
① 你写了别的构造函数,编译器本来不再生成默认构造,可以手动把它召唤回来:
1 | struct A { |
② 把函数定义放到类外,依然要求编译器生成默认实现:
1 | struct A { |
③ 控制特殊成员函数的属性(constexpr、noexcept)。
1.2 特点
- 函数存在、可调用
- 函数体由编译器自动生成,是空默认逻辑
- 默认版本写在类内声明处时,还是隐式的
inline
二、= delete:删除函数
告诉编译器:不要生成这个函数,调用它直接编译报错。
旧方案是把函数只声明不实现(放在 private),
= delete是 C++11 起的推荐现代写法,更清晰。
例子:禁用拷贝,让对象不可复制
1 | struct A { |
1 | A a; |
⚠️
= delete并不限于六大特殊成员函数,普通成员函数、全局函数也能 delete:
1 | void foo(int) = delete; |
常用于:阻止隐式类型转换。
三、对比表
| 写法 | 作用 | 调用后果 |
|---|---|---|
= default |
强制编译器生成默认版本 | 正常调用,执行默认逻辑 |
= delete |
删除这个函数,让它不存在 | 调用 → 编译期报错 |
四、高频坑
= delete是编译期检查,不是运行时。- 如果你自己手写了拷贝构造,编译器不会自动生成移动构造;想用默认移动构造,必须显式写
A(A&&) = default;。 = default只能用于编译器本来就能自动生成的特殊函数,普通自定义函数不能随便写。- 析构函数也可以
= default/= delete;析构被 delete 后,这个类不能实例化对象。
1 | struct Bad { |
经典例子:不可复制的单例简易写法
1 | struct Singleton { |
五、trivially copyable(平凡可复制)
可以直接用内存拷贝(memcpy)安全复制对象,不需要调用构造 / 析构 / 拷贝函数。
普通拷贝构造走的是 C++ 对象模型;
memcpy只是按字节暴力复制内存。只有满足trivially_copyable,这么干才是标准合法、不产生未定义行为。
5.1 判定规则(必须全部满足)
- 没有用户自定义的拷贝构造、移动构造、拷贝赋值、移动赋值,全部由编译器隐式生成(或显式
= default) - 没有用户自定义析构函数,析构也必须是默认生成(
~T() = default可以) - 所有非静态成员(成员变量、基类)全部都是平凡可复制类型
- 没有虚函数、没有虚基类(虚表指针会破坏平凡性)
只要你手写任意一个拷贝 / 移动 / 析构,就直接不再是平凡可复制。
= default是允许的,但必须写在类定义内。
5.2 trivial class 与 trivially copyable 的区别
- trivially copyable(平凡可复制):重点是
memcpy能安全拷贝内存。 - trivial class(平凡类):额外还要求默认构造函数也是平凡的。
trivial 类一定是 trivially copyable;但 trivially copyable 不一定是 trivial(可以没有默认构造)。
5.3 例子
✅ 平凡可复制:
1 | struct Point { |
✅ 用 = default 依然保持平凡可复制:
1 | struct Point2 { |
❌ 不再平凡可复制(手写了拷贝构造):
1 | struct Bad1 { |
❌ 有虚函数,直接失效:
1 | struct Bad2 { |
5.4 memcpy 的安全边界
1 | Point a{1, 2}; |
如果类不是 trivially_copyable,强行 memcpy,标准直接判定为未定义行为。
哪怕跑起来看起来正常,也是 UB,不要这么写。
5.5 用 <type_traits> 判断
1 |
|
5.6 和六大特殊成员的联动
- 平凡可复制类型的拷贝构造、拷贝赋值都是平凡拷贝:只逐字节复制成员,不做额外逻辑。
- 移动构造 / 移动赋值如果是平凡的,移动就等价于内存拷贝。
六、standard-layout(标准布局)
标准布局类型:内存布局和 C 语言 struct 的规则完全一致,成员严格按声明顺序排列,编译器不会偷偷插入额外隐藏成员(对齐 padding 除外),可以直接和 C 语言 API 互通。
头文件
<type_traits>:std::is_standard_layout_v<T>判断是否是标准布局。
6.1 必须满足的规则(缺一不可)
- 所有非静态数据成员都放在同一个类 / 基类里。
- 不能一部分成员在基类、一部分在派生类(高频坑)
- 所有非静态成员的访问控制权限相同(全 public / 全 protected / 全 private,不能混着来)
- 没有虚函数、没有虚基类(虚函数会引入 vptr,直接破坏标准布局)
- 所有基类、所有非静态成员本身,也都必须是标准布局类型
- 基类不能和派生类有同类型的非静态成员(防止内存歧义)
- 只有一个基类可以有非静态成员(不能多层继承叠加成员)
简单理解:C 语言的 struct 天然就是标准布局类型。
6.2 合法示例
1 | // 标准布局,和 C 结构体内存布局一模一样 |