只针对六大特殊成员函数:默认构造、析构、拷贝构造、拷贝赋值、移动构造、移动赋值。

它们可以在函数声明末尾加 = default; 或 = delete; 来接管编译器的行为。


一、= default:显式默认

告诉编译器:请你帮我生成这个函数的默认版本,和编译器隐式自动生成的一模一样。

1.1 什么时候用

① 你写了别的构造函数,编译器本来不再生成默认构造,可以手动把它召唤回来:

1
2
3
4
struct A {
A(int a) {}
A() = default; // 强制让编译器生成无参默认构造
};

② 把函数定义放到类外,依然要求编译器生成默认实现:

1
2
3
4
struct A {
A();
};
A::A() = default; // 定义写在类外,实现仍由编译器生成

③ 控制特殊成员函数的属性(constexpr、noexcept)。

1.2 特点

  • 函数存在、可调用
  • 函数体由编译器自动生成,是空默认逻辑
  • 默认版本写在类内声明处时,还是隐式的 inline

二、= delete:删除函数

告诉编译器:不要生成这个函数,调用它直接编译报错。

旧方案是把函数只声明不实现(放在 private),= delete 是 C++11 起的推荐现代写法,更清晰。

例子:禁用拷贝,让对象不可复制

1
2
3
4
5
struct A {
A() = default;
A(const A&) = delete; // 删除拷贝构造
A& operator=(const A&) = delete; // 删除拷贝赋值
};
1
2
A a;
A b = a; // 编译错误!拷贝构造被 delete

⚠️ = delete 并不限于六大特殊成员函数,普通成员函数、全局函数也能 delete:

1
2
3
4
5
void foo(int) = delete;
void foo(double) {}

foo(10); // 报错,foo(int) 被删除
foo(3.14); // OK

常用于:阻止隐式类型转换。


三、对比表

写法 作用 调用后果
= default 强制编译器生成默认版本 正常调用,执行默认逻辑
= delete 删除这个函数,让它不存在 调用 → 编译期报错

四、高频坑

  1. = delete 是编译期检查,不是运行时。
  2. 如果你自己手写了拷贝构造,编译器不会自动生成移动构造;想用默认移动构造,必须显式写 A(A&&) = default;。
  3. = default 只能用于编译器本来就能自动生成的特殊函数,普通自定义函数不能随便写。
  4. 析构函数也可以 = default / = delete;析构被 delete 后,这个类不能实例化对象。
1
2
3
4
struct Bad {
~Bad() = delete;
};
Bad b; // 编译报错,析构函数被删除,无法销毁对象

经典例子:不可复制的单例简易写法

1
2
3
4
5
struct Singleton {
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};

五、trivially copyable(平凡可复制)

可以直接用内存拷贝(memcpy)安全复制对象,不需要调用构造 / 析构 / 拷贝函数。

普通拷贝构造走的是 C++ 对象模型;memcpy 只是按字节暴力复制内存。只有满足 trivially_copyable,这么干才是标准合法、不产生未定义行为。

5.1 判定规则(必须全部满足)

  1. 没有用户自定义的拷贝构造、移动构造、拷贝赋值、移动赋值,全部由编译器隐式生成(或显式 = default)
  2. 没有用户自定义析构函数,析构也必须是默认生成(~T() = default 可以)
  3. 所有非静态成员(成员变量、基类)全部都是平凡可复制类型
  4. 没有虚函数、没有虚基类(虚表指针会破坏平凡性)

只要你手写任意一个拷贝 / 移动 / 析构,就直接不再是平凡可复制。= default 是允许的,但必须写在类定义内。

5.2 trivial class 与 trivially copyable 的区别

  • trivially copyable(平凡可复制):重点是 memcpy 能安全拷贝内存。
  • trivial class(平凡类):额外还要求默认构造函数也是平凡的。

trivial 类一定是 trivially copyable;但 trivially copyable 不一定是 trivial(可以没有默认构造)。

5.3 例子

✅ 平凡可复制:

1
2
3
4
5
struct Point {
int x;
int y;
// 没有手写任何特殊成员,编译器全部默认生成
};

✅ 用 = default 依然保持平凡可复制:

1
2
3
4
5
6
7
struct Point2 {
int x;
Point2() = default;
Point2(const Point2&) = default;
Point2& operator=(const Point2&) = default;
~Point2() = default;
};

❌ 不再平凡可复制(手写了拷贝构造):

1
2
3
4
struct Bad1 {
int x;
Bad1(const Bad1& other) { x = other.x; } // 用户手写,直接失去平凡性
};

❌ 有虚函数,直接失效:

1
2
3
4
struct Bad2 {
virtual void f() {} // 存在虚表指针,不是平凡可复制
int a;
};

5.4 memcpy 的安全边界

1
2
3
Point a{1, 2};
Point b;
std::memcpy(&b, &a, sizeof(Point)); // ✅ 合法,Point 平凡可复制

如果类不是 trivially_copyable,强行 memcpy,标准直接判定为未定义行为。

哪怕跑起来看起来正常,也是 UB,不要这么写。

5.5 用 <type_traits> 判断

1
2
3
4
#include <type_traits>

std::is_trivially_copyable_v<Point>; // true
std::is_trivial_v<Point>; // true

5.6 和六大特殊成员的联动

  • 平凡可复制类型的拷贝构造、拷贝赋值都是平凡拷贝:只逐字节复制成员,不做额外逻辑。
  • 移动构造 / 移动赋值如果是平凡的,移动就等价于内存拷贝。

六、standard-layout(标准布局)

标准布局类型:内存布局和 C 语言 struct 的规则完全一致,成员严格按声明顺序排列,编译器不会偷偷插入额外隐藏成员(对齐 padding 除外),可以直接和 C 语言 API 互通。

头文件 <type_traits>:std::is_standard_layout_v<T> 判断是否是标准布局。

6.1 必须满足的规则(缺一不可)

  1. 所有非静态数据成员都放在同一个类 / 基类里。
    • 不能一部分成员在基类、一部分在派生类(高频坑)
  2. 所有非静态成员的访问控制权限相同(全 public / 全 protected / 全 private,不能混着来)
  3. 没有虚函数、没有虚基类(虚函数会引入 vptr,直接破坏标准布局)
  4. 所有基类、所有非静态成员本身,也都必须是标准布局类型
  5. 基类不能和派生类有同类型的非静态成员(防止内存歧义)
  6. 只有一个基类可以有非静态成员(不能多层继承叠加成员)

简单理解:C 语言的 struct 天然就是标准布局类型。

6.2 合法示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 标准布局,和 C 结构体内存布局一模一样
struct S {
int x;
double y;
};
static_assert(std::is_standard_layout_v<S>); // true

struct Base { int a; };

// Derived 没有新增非静态成员,允许
struct Derived : Base {
// 这里不能写新的成员变量
};
static_assert(std::is_standard_layout_v<Derived>); // true