1. 什么是C++的默认成员函数
在C++中,类有六个特殊的成员函数,它们会在特定情况下由编译器自动生成,即使程序员没有显式定义它们。这些函数被称为"默认成员函数"(Default Member Functions),它们构成了C++类的基础骨架。
这六个默认成员函数分别是:
- 默认构造函数(Default Constructor)
- 析构函数(Destructor)
- 拷贝构造函数(Copy Constructor)
- 拷贝赋值运算符(Copy Assignment Operator)
- 移动构造函数(Move Constructor)
- 移动赋值运算符(Move Assignment Operator)
这些函数之所以特殊,是因为它们管理着对象的生命周期和资源所有权,是C++对象模型的核心组成部分。理解它们的行为对于编写正确、高效的C++代码至关重要。
2. 默认构造函数详解
2.1 默认构造函数的定义与作用
默认构造函数是在创建对象时被调用的特殊成员函数,它没有参数或者所有参数都有默认值。当程序员没有为类提供任何构造函数时,编译器会自动生成一个默认构造函数。
cpp复制class Example {
public:
Example() = default; // 显式声明使用编译器生成的默认构造函数
};
默认构造函数的主要职责是初始化对象的成员变量。对于内置类型(int, float, 指针等),它们不会被初始化;对于类类型成员,会调用它们各自的默认构造函数。
2.2 默认构造函数的调用场景
默认构造函数在以下情况下会被调用:
- 声明对象时不带任何初始化参数:
cpp复制Example obj; // 调用默认构造函数 - 动态分配对象数组时:
cpp复制Example* arr = new Example[10]; // 对每个元素调用默认构造函数 - 作为其他类的成员且没有在成员初始化列表中显式初始化时
2.3 默认构造函数的注意事项
在实际开发中,有几个关键点需要注意:
- 如果类定义了任何构造函数(包括拷贝构造函数),编译器将不再生成默认构造函数
- 对于包含引用成员或const成员的类,编译器无法生成有效的默认构造函数
- 在性能敏感的场景中,默认构造函数可能成为瓶颈,因为可能涉及不必要的初始化
提示:在C++11及以后版本中,可以使用
= default显式要求编译器生成默认构造函数,这比手动实现更安全且能获得更好的优化。
3. 析构函数详解
3.1 析构函数的作用与定义
析构函数是在对象生命周期结束时自动调用的特殊成员函数,用于释放对象占用的资源。它的名称是在类名前加波浪线(~),没有返回类型和参数。
cpp复制class Example {
public:
~Example() { // 析构函数定义
// 释放资源的代码
}
};
3.2 析构函数的调用时机
析构函数在以下情况下会被调用:
- 局部对象离开作用域时
- 通过delete删除动态分配的对象时
- 临时对象生命周期结束时
- 程序结束时全局和静态对象的析构
3.3 析构函数的实现原则
编写析构函数时,应遵循以下原则:
- 遵循RAII(Resource Acquisition Is Initialization)原则,确保所有资源都能被正确释放
- 析构函数不应该抛出异常,否则可能导致资源泄漏
- 对于基类,析构函数通常应该声明为virtual,以确保通过基类指针删除派生类对象时能正确调用派生类的析构函数
cpp复制class Base {
public:
virtual ~Base() = default; // 虚析构函数
};
class Derived : public Base {
public:
~Derived() override { // 派生类析构函数
// 派生类特定的清理代码
}
};
4. 拷贝构造函数与拷贝赋值运算符
4.1 拷贝构造函数
拷贝构造函数用于通过已有对象创建新对象,其典型签名如下:
cpp复制class Example {
public:
Example(const Example& other); // 拷贝构造函数
};
编译器生成的拷贝构造函数会逐个成员进行拷贝(浅拷贝)。对于包含指针成员的类,通常需要自定义拷贝构造函数实现深拷贝。
拷贝构造函数在以下情况被调用:
- 用一个对象初始化另一个对象时:
cpp复制Example obj1; Example obj2 = obj1; // 调用拷贝构造函数 - 函数参数按值传递时
- 函数返回对象时(可能被优化掉)
4.2 拷贝赋值运算符
拷贝赋值运算符用于将一个对象的值赋给另一个已存在的对象:
cpp复制class Example {
public:
Example& operator=(const Example& other); // 拷贝赋值运算符
};
实现拷贝赋值运算符时,需要注意以下几点:
- 处理自赋值情况(
x = x) - 遵循"拷贝并交换"惯用法可以简化实现并保证异常安全
- 通常返回
*this以支持链式赋值
cpp复制Example& Example::operator=(const Example& other) {
if (this != &other) { // 自赋值检查
// 执行拷贝操作
}
return *this;
}
5. 移动构造函数与移动赋值运算符
5.1 移动语义基础
C++11引入了移动语义,允许资源的所有权转移而非拷贝,这对提高性能特别是处理大型对象时非常有用。
5.2 移动构造函数
移动构造函数通过"窃取"临时对象的资源来构造新对象:
cpp复制class Example {
public:
Example(Example&& other) noexcept; // 移动构造函数
};
移动构造函数应该:
- 标记为noexcept以确保它能在异常安全的环境中工作
- 将源对象置于有效但可析构的状态
- 通常比拷贝构造函数更高效,因为它避免了不必要的拷贝
5.3 移动赋值运算符
移动赋值运算符允许将临时对象的资源转移到已存在的对象:
cpp复制class Example {
public:
Example& operator=(Example&& other) noexcept; // 移动赋值运算符
};
实现移动赋值运算符时,除了遵循移动构造函数的原则外,还需要处理自移动赋值的情况。
6. 默认成员函数的生成规则
6.1 编译器生成的条件
编译器在以下条件下会生成默认成员函数:
- 默认构造函数:当没有定义任何构造函数时
- 析构函数:当没有定义析构函数时
- 拷贝构造函数:当没有定义拷贝构造函数且没有定义移动操作时
- 拷贝赋值运算符:当没有定义拷贝赋值运算符且没有定义移动操作时
- 移动构造函数:当没有定义任何拷贝操作、移动操作和析构函数时
- 移动赋值运算符:当没有定义任何拷贝操作、移动操作和析构函数时
6.2 显式控制默认函数的生成
C++11允许显式控制这些函数的生成:
cpp复制class Example {
public:
Example() = default; // 显式要求生成默认构造函数
Example(const Example&) = delete; // 禁止拷贝构造函数
Example(Example&&) = default; // 显式要求生成移动构造函数
};
6.3 三/五法则
在实践中,遵循"三法则"(C++98)或"五法则"(C++11)是个好习惯:
- 三法则:如果一个类需要自定义析构函数、拷贝构造函数或拷贝赋值运算符中的任何一个,那么它很可能需要全部三个
- 五法则:在C++11中扩展到包括移动构造函数和移动赋值运算符
7. 实际应用中的注意事项
7.1 资源管理类
对于管理资源的类(如文件句柄、内存等),通常需要:
- 自定义析构函数释放资源
- 禁用或自定义拷贝操作(或实现深拷贝)
- 实现移动操作以提高效率
cpp复制class FileHandle {
public:
FileHandle(const char* filename); // 获取资源
~FileHandle(); // 释放资源
FileHandle(const FileHandle&) = delete; // 禁止拷贝
FileHandle& operator=(const FileHandle&) = delete;
FileHandle(FileHandle&& other) noexcept; // 实现移动
FileHandle& operator=(FileHandle&& other) noexcept;
};
7.2 性能优化
默认成员函数对性能有重要影响:
- 移动操作可以显著减少不必要的拷贝
- 默认生成的函数通常是内联的,效率较高
- 对于小型、简单的类,使用编译器生成的默认函数通常是最佳选择
7.3 现代C++实践
在现代C++中:
- 优先使用
= default而非手动实现简单的默认函数 - 使用
= delete明确禁止不希望使用的操作 - 考虑使用智能指针等RAII包装器来简化资源管理
- 对于不可拷贝的类型,明确禁用拷贝操作
理解并正确使用这六大默认成员函数是成为高效C++程序员的关键。它们构成了C++对象模型的基础,直接影响代码的正确性、安全性和性能。在实际开发中,应该根据类的具体需求仔细考虑哪些函数需要自定义,哪些可以使用编译器生成的版本。
