前言
在 C++ 中,当我们定义一个空类时,它真的 "空" 吗?答案是否定的。编译器会默默为我们生成多个默认成员函数,它们掌管着对象从创建、复制到销毁的完整生命周期。理解这些默认函数的行为,是掌握 C++ 面向对象编程的关键一步。
本文将深入讲解类的默认成员函数体系,并重点剖析构造函数、析构函数和拷贝构造函数的底层原理与使用细节。
一、类的默认成员函数概览
1.1 什么是默认成员函数
如果我们在类中不显式定义某些函数,编译器会自动生成对应的默认版本。这些由编译器自动生成的函数,就叫做默认成员函数。
C++ 类有 6 个经典的默认成员函数:
- 构造函数:对象创建时初始化
- 析构函数:对象销毁时清理资源
- 拷贝构造函数:用已有对象创建新对象
- 赋值运算符重载:对象之间的赋值
- 取地址运算符重载:取普通对象地址
- const 取地址运算符重载:取 const 对象地址
其中前四个是重中之重,也是面试高频考点。后两个几乎不需要我们手动实现,编译器默认生成的版本完全够用。
1.2 一个 "空类" 的真实大小
空类大小为 1 字节,这是因为编译器需要给每个对象分配唯一的内存地址来区分不同对象。虽然没有成员变量,但 6 个默认成员函数已经隐含其中。
二、构造函数
2.1 概念与作用
构造函数是一个特殊的成员函数,名字与类名相同,没有返回值(连 void 都不写),在对象创建时由编译器自动调用,且在对象整个生命周期内只调用一次。
它的核心任务是初始化对象的成员变量,而不是开空间创建对象(对象的空间在构造函数调用前已经分配好了)。
2.2 特性
- 函数名与类名相同
- 无返回值
- 对象实例化时编译器自动调用
- 可以重载(一个类可以有多个构造函数)
- 如果类中没有显式定义构造函数,编译器会自动生成一个无参的默认构造函数;一旦显式定义,编译器便不再生成
2.3 代码示例
2.4 默认构造函数
默认构造函数指的是不用传参就可以调用的构造函数,有三种:
- 编译器自动生成的无参构造
- 我们显式写的无参构造
- 全缺省的构造函数
⚠️ 注意:默认构造函数只能有一个。无参构造和全缺省构造不能同时存在,否则会产生二义性。
2.5 编译器生成的默认构造做了什么?
很多人以为默认构造会把成员变量初始化为 0,这是错误的。C++ 把类型分为两类:
- 内置类型(基本类型):int、char、double、指针等
- 自定义类型:class、struct 定义的类型
编译器生成的默认构造函数规则:
- 对内置类型成员不做处理(值是随机的)
- 对自定义类型成员,会去调用它的默认构造函数
这也是 C++ 经常被吐槽的点 —— 内置类型成员不初始化。所以一般建议我们自己写构造函数,确保成员变量都有合理初值。
三、析构函数
3.1 概念与作用
析构函数与构造函数功能相反,名字是类名前加~,同样没有返回值和参数。它在对象销毁时由编译器自动调用,负责完成对象中资源的清理工作。
3.2 特性
- 析构函数名是在类名前加
~ - 无参数、无返回值
- 一个类有且只有一个析构函数,不能重载
- 对象生命周期结束时,编译器自动调用
- 若未显式定义,编译器会生成默认析构函数
3.3 代码示例
3.4 什么时候需要自己写析构?
- 如果类中没有申请资源(堆内存、文件句柄等),析构函数可以不写,用编译器生成的默认即可
- 如果类中有动态申请的资源,必须显式写析构函数释放资源,否则会造成内存泄漏
3.5 编译器生成的默认析构做了什么?
和构造函数规则类似:
- 对内置类型成员不做处理
- 对自定义类型成员,会去调用它的析构函数
四、拷贝构造函数
4.1 概念
拷贝构造函数是一个特殊的构造函数,它只有一个形参,且形参是本类对象的 const 引用。在用已有对象创建新对象时,编译器会自动调用拷贝构造函数。
4.2 为什么必须是引用?
这是初学者最容易问的问题。拷贝构造函数的参数必须是引用,否则会引发无穷递归调用。
如果用传值方式,实参传给形参时又需要调用拷贝构造函数来创建形参 d,这样就会无限递归下去。所以必须用引用传参,一般加上 const 保证不修改原对象。
4.3 正确写法
4.4 编译器生成的默认拷贝构造
如果我们不写,编译器会自动生成默认拷贝构造函数,它会进行浅拷贝(按内存字节序直接拷贝)。
对于像 Date 这样没有动态资源的类,默认拷贝构造完全够用。但对于有动态资源的类(比如 Stack),浅拷贝会出大问题。
4.5 浅拷贝的问题
这段代码会崩溃!因为 s1 和 s2 的_data指向同一块内存,两个对象销毁时都会调用析构函数释放同一块空间,造成二次释放。
4.6 深拷贝解决方案
当类中涉及资源申请时,必须自己实现拷贝构造,做深拷贝:
这样每个对象都有自己独立的内存空间,析构时互不影响。
4.7 拷贝构造的调用场景
- 使用已有对象创建新对象
- 函数参数为类类型的传值调用
- 函数返回值为类类型的传值返回
五、总结
| 函数 | 调用时机 | 是否有参 | 是否可重载 | 不写时编译器行为 |
|---|---|---|---|---|
| 构造函数 | 对象创建时 | 可有参可无参 | 是 | 生成默认构造,内置类型不处理,自定义类型调用其构造 |
| 析构函数 | 对象销毁时 | 无参 | 否 | 生成默认析构,内置类型不处理,自定义类型调用其析构 |
| 拷贝构造 | 用同类型对象初始化新对象时 | 一个:const 引用 | 否 | 生成默认拷贝,执行浅拷贝(逐字节复制) |
核心记忆点
1. **构造函数**管出生,负责初始化;**析构函数**管死亡,负责清理资源
2. **拷贝构造**是特殊的构造函数,参数必须是引用,否则无限递归
3. 没有资源的类可以用编译器默认函数;涉及动态内存、文件句柄等资源时,必须自己实现深拷贝和析构
4. 编译器生成的默认函数对内置类型 "摆烂" 不处理,对自定义类型会递归调用对应函数
掌握了这三大默认成员函数,你就理解了 C++ 对象生命周期管理的核心。在此基础上再去学习赋值运算符重载和移动语义,就能形成完整的知识体系。