1. 类和对象中的默认成员函数解析
在C++中,类有6个特殊的默认成员函数,它们会在用户没有显式实现时由编译器自动生成。这些函数构成了C++对象模型的基础,理解它们的行为对于编写健壮的C++代码至关重要。
1.1 默认成员函数概述
默认成员函数包括:
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值运算符
- 取地址运算符重载
- const取地址运算符重载
其中前4个最为重要,后两个在实际开发中很少需要显式实现。C++11后又新增了移动构造和移动赋值两个默认成员函数。
1.2 学习默认成员函数的两大要点
-
编译器默认生成的行为:了解当我们不写这些函数时,编译器会生成什么样的实现,这些默认实现是否能满足我们的需求。
-
自定义实现方法:当默认实现不能满足需求时,我们需要知道如何正确实现这些函数。
2. 构造函数深度解析
构造函数是类中最重要的成员函数之一,它负责对象的初始化工作。
2.1 构造函数的特点
- 命名规则:函数名与类名相同
- 无返回值:不需要写void或其他返回类型
- 自动调用:对象实例化时自动调用
- 可重载:一个类可以有多个构造函数
- 默认构造函数:如果类中没有显式定义构造函数,编译器会自动生成一个无参的默认构造函数
2.2 默认构造函数的三种形式
- 无参构造函数
- 全缺省构造函数
- 编译器自动生成的构造函数
这三种形式有且只能存在一个,因为它们都会导致不传参数就能调用构造函数的情况。
2.3 构造函数对成员变量的处理
对于内置类型成员(int、char等),编译器生成的构造函数不会进行初始化(值不确定)。对于自定义类型成员,会调用该类型的默认构造函数进行初始化。
cpp复制class Date {
public:
// 无参构造函数
Date() {
_year = 1;
_month = 1;
_day = 1;
}
// 带参构造函数
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
// 全缺省构造函数(与无参构造冲突)
/*Date(int year = 1, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}*/
};
2.4 构造函数调用注意事项
cpp复制Date d1; // 调用默认构造函数
Date d2(2025,1,1); // 调用带参构造函数
Date d3(); // 错误!这会被解析为函数声明
3. 析构函数详解
析构函数负责对象资源的清理工作,在对象生命周期结束时自动调用。
3.1 析构函数的特点
- 命名规则:类名前加~
- 无参数无返回值
- 不可重载:一个类只能有一个析构函数
- 自动调用:对象生命周期结束时自动调用
3.2 析构函数对成员的处理
对于内置类型成员,编译器生成的析构函数不做任何处理。对于自定义类型成员,会调用其析构函数。
3.3 何时需要显式定义析构函数
- 类中没有资源申请时,可以不写(如Date类)
- 类中有资源申请时,必须显式定义(如Stack类)
- 多个对象析构顺序:后定义先析构
cpp复制class Stack {
public:
Stack(int n = 4) {
_a = (int*)malloc(sizeof(int) * n);
_capacity = n;
_top = 0;
}
~Stack() {
free(_a); // 必须显式释放内存
_a = nullptr;
_top = _capacity = 0;
}
};
4. 拷贝构造函数深入理解
拷贝构造函数用于用一个已存在的对象初始化一个新对象。
4.1 拷贝构造函数的特点
- 特殊构造函数:是构造函数的一个重载形式
- 参数要求:第一个参数必须是类类型的引用
- 自动调用场景:
- 用已有对象初始化新对象
- 函数参数传递
- 函数返回值
4.2 深浅拷贝问题
- 浅拷贝:编译器默认生成的拷贝构造函数执行浅拷贝(逐字节复制)
- 深拷贝:当类中有动态资源时,必须自定义拷贝构造函数实现深拷贝
cpp复制class Stack {
public:
Stack(const Stack& st) {
// 深拷贝实现
_a = (int*)malloc(sizeof(int) * st._capacity);
memcpy(_a, st._a, sizeof(int) * st._top);
_capacity = st._capacity;
_top = st._top;
}
};
4.3 拷贝构造函数的调用场景
cpp复制Date d1(2024,7,5);
Date d2(d1); // 拷贝构造
Date d3 = d1; // 这也是拷贝构造
5. 运算符重载实战
运算符重载让自定义类型也能使用内置运算符,提高代码可读性。
5.1 运算符重载基本规则
- 不能创建新运算符
- 不能改变运算符的优先级和结合性
- 至少有一个操作数是类类型
- 部分运算符不能重载(如.、::、sizeof等)
5.2 赋值运算符重载
赋值运算符重载必须定义为成员函数,用于两个已存在对象间的赋值。
cpp复制class Date {
public:
Date& operator=(const Date& d) {
if(this != &d) { // 防止自赋值
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this; // 支持连续赋值
}
};
5.3 流运算符重载
<<和>>运算符通常重载为全局函数,因为它们左侧操作数是流对象。
cpp复制ostream& operator<<(ostream& out, const Date& d) {
out << d._year << "年" << d._month << "月" << d._day << "日";
return out;
}
istream& operator>>(istream& in, Date& d) {
in >> d._year >> d._month >> d._day;
return in;
}
6. 日期类完整实现
下面是一个完整的日期类实现,展示了各种运算符重载的实际应用。
6.1 日期比较运算符
cpp复制bool Date::operator<(const Date& d) const {
if(_year < d._year) return true;
else if(_year == d._year) {
if(_month < d._month) return true;
else if(_month == d._month) {
return _day < d._day;
}
}
return false;
}
// 其他比较运算符可以复用operator<
bool Date::operator<=(const Date& d) const {
return *this < d || *this == d;
}
6.2 日期加减运算
cpp复制// 日期+=天数
Date& Date::operator+=(int day) {
if(day < 0) return *this -= -day;
_day += day;
while(_day > GetMonthDay(_year, _month)) {
_day -= GetMonthDay(_year, _month);
++_month;
if(_month == 13) {
++_year;
_month = 1;
}
}
return *this;
}
// 日期+天数(不修改原对象)
Date Date::operator+(int day) const {
Date tmp = *this;
tmp += day;
return tmp;
}
6.3 前置和后置++/--
cpp复制// 前置++
Date& Date::operator++() {
*this += 1;
return *this;
}
// 后置++(通过int参数区分)
Date Date::operator++(int) {
Date tmp = *this;
*this += 1;
return tmp;
}
6.4 日期相减
cpp复制int Date::operator-(const Date& d) const {
Date max = *this;
Date min = d;
int flag = 1;
if(*this < d) {
max = d;
min = *this;
flag = -1;
}
int n = 0;
while(min != max) {
++min;
++n;
}
return n * flag;
}
7. 实际应用与注意事项
7.1 构造函数与析构函数的实际价值
通过对比C和C++实现的栈结构,可以看出构造函数和析构函数带来的便利:
cpp复制// C版本栈使用
ST st;
STInit(&st); // 必须手动初始化
// ...使用栈...
STDestroy(&st); // 必须手动销毁
// C++版本栈使用
Stack st; // 自动构造
// ...使用栈...
// 自动析构
7.2 深浅拷贝的实际影响
cpp复制Stack st1;
st1.Push(1);
st1.Push(2);
// 如果没有正确实现拷贝构造
Stack st2 = st1; // 浅拷贝,两个对象共享同一块内存
// 析构时会double free导致崩溃
7.3 运算符重载的最佳实践
- 保持运算符的直观语义
- 考虑运算符的返回值类型
- 处理自赋值情况
- 尽量复用已有运算符实现
8. 常见问题与解决方案
8.1 构造函数常见问题
问题1:忘记初始化成员变量
解决:使用成员初始化列表或在构造函数体内显式初始化
问题2:默认构造函数与全缺省构造函数冲突
解决:只保留其中一种形式
8.2 拷贝构造常见问题
问题1:参数不是引用导致无限递归
错误示例:
cpp复制Date(Date d); // 错误!会导致无限递归
正确写法:
cpp复制Date(const Date& d);
问题2:浅拷贝导致资源重复释放
解决:对有资源的类实现深拷贝
8.3 运算符重载常见问题
问题1:重载<<和>>作为成员函数
错误结果:使用时需要写成d << cout,不符合习惯
解决:重载为全局函数
问题2:忘记处理自赋值
解决:在赋值运算符重载中添加自赋值检查
cpp复制Date& operator=(const Date& d) {
if(this != &d) { // 自赋值检查
// 赋值操作
}
return *this;
}
9. 性能优化建议
- 尽量使用const引用传参:减少不必要的拷贝
- 复用已有运算符实现:如用<实现>、<=等
- 移动语义:C++11后可以使用移动构造和移动赋值减少拷贝
- 内联简单函数:如GetMonthDay等频繁调用的小函数
10. 测试用例设计
良好的测试用例应该覆盖各种边界情况:
cpp复制void TestDate() {
// 测试大数字加减
Date d1(2026,3,29);
Date d2 = d1 + 30000;
// 测试前后置++
Date d3 = ++d1;
Date d4 = d1++;
// 测试日期相减
Date d5(2026,3,29);
Date d6(2036,3,29);
int days = d5 - d6;
// 测试流操作
cout << d1;
cin >> d1 >> d2;
cout << d1 << d2;
// 测试const对象
const Date d7(2026,3,29);
d7.Print();
d7 + 100; // 应该能调用
// d7 += 100; // 应该不能编译
}
掌握类和对象中的这些默认成员函数和运算符重载,是成为合格C++程序员的重要一步。在实际开发中,需要根据类的具体需求合理实现这些函数,既要保证功能的正确性,也要考虑性能和易用性。