1. 课程内容概述
CS106L Lecture 13主要讲解C++中的特殊成员函数(Special Member Functions),这是每个C++开发者必须掌握的核心概念。特殊成员函数包括构造函数、析构函数、拷贝构造函数、拷贝赋值运算符、移动构造函数和移动赋值运算符,它们共同构成了C++对象生命周期的管理基础。
在实际开发中,理解这些函数的调用时机和行为差异,能够帮助我们写出更高效、更安全的代码。特别是在资源管理(如内存、文件句柄等)场景下,正确实现这些函数可以避免内存泄漏和资源竞争等问题。
2. 特殊成员函数详解
2.1 构造函数与析构函数
构造函数在对象创建时自动调用,负责初始化对象状态。C++支持多种构造函数形式:
cpp复制class MyClass {
public:
MyClass(); // 默认构造函数
MyClass(int x); // 带参数构造函数
MyClass(const MyClass& other); // 拷贝构造函数
MyClass(MyClass&& other); // 移动构造函数
};
析构函数在对象销毁时调用,负责清理资源。一个常见的RAII(Resource Acquisition Is Initialization)模式示例:
cpp复制class FileHandler {
public:
FileHandler(const std::string& filename)
: file_(fopen(filename.c_str(), "r")) {
if (!file_) throw std::runtime_error("File open failed");
}
~FileHandler() {
if (file_) fclose(file_);
}
private:
FILE* file_;
};
注意:析构函数应该总是声明为virtual,特别是在基类中,否则通过基类指针删除派生类对象会导致派生类的析构函数不被调用,产生资源泄漏。
2.2 拷贝控制成员
拷贝构造函数和拷贝赋值运算符定义了对象如何被复制。现代C++中,我们通常遵循"Rule of Five"原则:
cpp复制class ResourceHolder {
public:
// 构造函数
ResourceHolder(size_t size) : data_(new int[size]), size_(size) {}
// 1. 析构函数
~ResourceHolder() { delete[] data_; }
// 2. 拷贝构造函数
ResourceHolder(const ResourceHolder& other)
: data_(new int[other.size_]), size_(other.size_) {
std::copy(other.data_, other.data_ + other.size_, data_);
}
// 3. 拷贝赋值运算符
ResourceHolder& operator=(const ResourceHolder& other) {
if (this != &other) {
delete[] data_;
data_ = new int[other.size_];
size_ = other.size_;
std::copy(other.data_, other.data_ + other.size_, data_);
}
return *this;
}
// 4. 移动构造函数
ResourceHolder(ResourceHolder&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr;
other.size_ = 0;
}
// 5. 移动赋值运算符
ResourceHolder& operator=(ResourceHolder&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = other.data_;
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
}
return *this;
}
private:
int* data_;
size_t size_;
};
2.3 移动语义
移动语义是C++11引入的重要特性,它允许资源所有权的转移而非复制,可以显著提升性能。理解移动语义的关键点:
- 右值引用(&&)标识可移动的资源
- std::move将左值转换为右值引用
- 移动后源对象应处于有效但未定义的状态
一个典型的移动操作实现:
cpp复制class Buffer {
public:
Buffer(size_t size) : size_(size), data_(new int[size]) {}
// 移动构造函数
Buffer(Buffer&& other) noexcept
: size_(other.size_), data_(other.data_) {
other.size_ = 0;
other.data_ = nullptr;
}
// 移动赋值运算符
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = other.data_;
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
}
return *this;
}
private:
size_t size_;
int* data_;
};
3. 特殊成员函数的生成规则
3.1 默认生成的函数
编译器会在需要时自动生成某些特殊成员函数,但规则有些复杂:
- 默认构造函数:如果用户没有定义任何构造函数
- 析构函数:总是生成,除非用户定义
- 拷贝构造函数:如果用户没有定义移动操作或拷贝操作
- 拷贝赋值运算符:同上
- 移动构造函数和移动赋值运算符:仅当用户没有定义任何拷贝操作、移动操作或析构函数时
3.2 显式控制函数生成
C++11允许显式要求或禁止编译器生成特殊成员函数:
cpp复制class Example {
public:
Example() = default; // 显式要求生成默认构造函数
~Example() = default;
// 禁止拷贝
Example(const Example&) = delete;
Example& operator=(const Example&) = delete;
// 允许移动
Example(Example&&) = default;
Example& operator=(Example&&) = default;
};
4. 实际应用中的注意事项
4.1 资源管理类
对于管理资源的类(如内存、文件、锁等),必须仔细考虑所有特殊成员函数:
- 如果类管理独占资源,应该禁用拷贝,只允许移动
- 如果资源可以共享,需要实现引用计数等机制
- 析构函数必须正确释放资源
4.2 性能优化
- 对于小型、平凡的类型,使用编译器生成的默认函数通常足够
- 对于大型对象,实现移动语义可以避免不必要的拷贝
- 使用std::move提示编译器可以使用移动操作
4.3 异常安全
- 移动操作通常应标记为noexcept,以便标准库容器等能优化其行为
- 构造函数中如果可能抛出异常,应该确保已经分配的资源被正确释放
- 拷贝赋值运算符通常采用"copy-and-swap"惯用法保证异常安全
5. 常见问题与解决方案
5.1 对象切片问题
当派生类对象通过值传递给基类参数时,会发生对象切片:
cpp复制class Base { /*...*/ };
class Derived : public Base { /*...*/ };
void func(Base b) { /*...*/ }
Derived d;
func(d); // 切片发生,Derived特有部分丢失
解决方案:使用引用或指针传递多态对象。
5.2 自赋值问题
拷贝赋值运算符必须处理自赋值情况:
cpp复制class Widget {
public:
Widget& operator=(const Widget& other) {
if (this == &other) return *this; // 自赋值检查
// 赋值操作...
return *this;
}
};
更好的模式是使用"copy-and-swap"惯用法:
cpp复制class Widget {
public:
Widget& operator=(Widget other) { // 注意:参数是按值传递
swap(*this, other);
return *this;
}
friend void swap(Widget& a, Widget& b) noexcept {
using std::swap;
swap(a.data_, b.data_);
// 交换其他成员...
}
};
5.3 移动后对象状态
移动操作后,源对象应处于有效但未定义的状态:
cpp复制std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1);
// v1现在为空,但仍然是有效状态
assert(v1.empty());
6. 现代C++的最佳实践
- Rule of Zero:理想情况下,类不应该自定义任何特殊成员函数,而是依赖成员对象的相应函数
- Rule of Five:如果必须自定义其中一个特殊成员函数,通常需要自定义全部五个
- 优先使用移动语义:对于可移动的资源,实现移动操作可以显著提升性能
- 使用智能指针:std::unique_ptr和std::shared_ptr可以自���处理资源管理,减少手动实现特殊成员函数的需要
一个遵循Rule of Zero的示例:
cpp复制class ResourceWrapper {
private:
std::unique_ptr<Resource> resource_;
std::shared_ptr<SharedResource> sharedResource_;
// 不需要自定义任何特殊成员函数
};
在实际项目中,理解这些特殊成员函数的行为和交互方式,可以帮助我们设计出更健壮、更高效的类。特别是在实现资源管理类时,正确的拷贝和移动语义实现是避免资源泄漏和保证异常安全的关键。
