在C++面向对象编程中,类(class)是构建程序的基石,而对象则是类的具体实例。为了让类的使用更加自然和安全,C++编译器会自动为每个类生成六个特殊的成员函数,我们称之为"默认成员函数"。这些函数构成了类最基础的功能框架,包括:
这些函数之所以被称为"默认",是因为即使我们不显式定义它们,编译器也会自动生成对应的版本。但理解它们的运作机制对于编写健壮的C++代码至关重要。
关键提示:默认成员函数的自动生成遵循特定规则,当我们需要自定义类行为时,必须显式定义这些函数来覆盖编译器生成的版本。
构造函数是对象创建时自动调用的特殊成员函数,负责初始化对象的状态。它的几个关键特性决定了其独特行为:
cpp复制class Date {
public:
Date() { /* 无参构造 */ }
Date(int y) { /* 单参数构造 */ }
Date(int y, int m, int d) { /* 多参数构造 */ }
};
默认构造函数(不需要参数就能调用的构造函数)在C++中有特殊地位,它分为三种形式:
重要规则:一个类中只能存在一个默认构造函数。尝试定义多个会导致编译错误。
cpp复制class Date {
public:
Date() { ... } // 无参构造
// Date(int y=1) { ... } // 错误:与上面的无参构造冲突
};
现代C++推荐使用成员初始化列表来初始化类成员,这比在构造函数体内赋值更高效:
cpp复制class Date {
int year, month, day;
public:
Date(int y, int m, int d)
: year(y), month(m), day(d) // 初始化列表
{
// 构造函数体
}
};
经验之谈:对于const成员和引用成员,必须使用初始化列表,因为它们不能在构造函数体内被赋值。
析构函数在对象生命周期结束时自动调用,负责释放对象占用的资源。它的特点包括:
~Date())cpp复制class FileHandler {
FILE* file;
public:
FileHandler(const char* name) { file = fopen(name, "r"); }
~FileHandler() {
if(file) fclose(file);
}
};
对于管理资源的类(如动态内存、文件句柄、网络连接等),必须遵循RAII(Resource Acquisition Is Initialization)原则:
cpp复制class MemoryBlock {
int* data;
size_t size;
public:
MemoryBlock(size_t sz) : size(sz), data(new int[sz]) {}
~MemoryBlock() { delete[] data; }
};
拷贝构造函数用于用一个已存在对象初始化新对象,其标准形式为:
cpp复制class MyClass {
public:
MyClass(const MyClass& other) { /* 拷贝逻辑 */ }
};
关键注意事项:
cpp复制class String {
char* data;
size_t length;
public:
String(const String& other)
: length(other.length),
data(new char[length + 1])
{
strcpy(data, other.data);
}
};
赋值运算符(=)重载需要处理两个已存在对象间的赋值,其标准模式包括:
cpp复制class String {
// ... 其他成员 ...
public:
String& operator=(const String& rhs) {
if(this != &rhs) { // 1. 自赋值检查
delete[] data; // 2. 释放旧资源
length = rhs.length; // 3. 分配新资源
data = new char[length + 1];
strcpy(data, rhs.data); // 4. 拷贝数据
}
return *this; // 5. 返回引用
}
};
避坑指南:忘记自赋值检查是常见错误,当对象赋值给自己时会导致资源被提前释放。
C++允许为自定义类型重载大多数运算符,但需遵循以下规则:
cpp复制class Date {
// ... 其他成员 ...
public:
// 成员函数形式重载+=
Date& operator+=(int days) {
// 增加days天的逻辑
return *this;
}
// 友元函数形式重载+
friend Date operator+(Date lhs, int days) {
lhs += days;
return lhs;
}
};
cpp复制bool operator==(const Date& lhs, const Date& rhs) {
return lhs.year == rhs.year &&
lhs.month == rhs.month &&
lhs.day == rhs.day;
}
bool operator!=(const Date& lhs, const Date& rhs) {
return !(lhs == rhs);
}
cpp复制class Logger {
// ... 其他成员 ...
public:
friend std::ostream& operator<<(std::ostream& os, const Logger& log) {
return os << log.timestamp << ": " << log.message;
}
};
C++11后,编译器生成默认成员函数的规则更加明确:
现代C++提供了显式控制默认函数生成的方式:
cpp复制class DefaultExample {
public:
DefaultExample() = default; // 显式要求生成默认构造
~DefaultExample() = default; // 显式要求生成默认析构
// 禁止拷贝
DefaultExample(const DefaultExample&) = delete;
DefaultExample& operator=(const DefaultExample&) = delete;
};
对于资源管理类,遵循"三法则"(C++11前)或"五法则"(C++11后):
浅拷贝导致的双重释放:
忘记自赋值检查:
if(this == &rhs) return *this;异常安全问题:
cpp复制// copy-and-swap惯用法示例
class SafeArray {
int* data;
size_t size;
void swap(SafeArray& other) noexcept {
using std::swap;
swap(data, other.data);
swap(size, other.size);
}
public:
SafeArray& operator=(SafeArray rhs) { // 注意:按值传递
swap(rhs);
return *this;
}
};
掌握这些默认成员函数的特性和实现技巧,是成为C++高级开发者的必经之路。它们不仅影响类的正确行为,也直接关系到程序的性能和安全性。在实际开发中,应该根据类的具体需求,合理选择哪些函数需要自定义实现,哪些可以依赖编译器自动生成。