1. C++六大默认成员函数概述
在C++中,每个类都有六个特殊的成员函数,它们被称为"六大默认成员函数"。这些函数在特定情况下会被编译器自动生成,但开发者也可以根据需要自定义实现。理解这些函数的特性和行为对于编写高质量的C++代码至关重要。
六大默认成员函数包括:
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值运算符
- 移动构造函数(C++11新增)
- 移动赋值运算符(C++11新增)
本文将重点讨论前四个最基础的成员函数,它们构成了C++对象生命周期管理的核心机制。
2. 构造函数详解
2.1 构造函数的基本特性
构造函数是类中特殊的成员函数,它在对象创建时自动调用,负责对象的初始化工作。构造函数有以下几个关键特点:
- 函数名与类名相同
- 没有返回值(连void都不需要写)
- 可以重载(一个类可以有多个构造函数)
- 如果用户没有显式定义,编译器会自动生成一个默认构造函数
cpp复制class Date {
public:
// 无参构造函数
Date() {
_year = 1;
_month = 1;
_day = 1;
}
// 带参构造函数
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
2.2 默认构造函数的三种形式
默认构造函数是指不需要传递参数就能调用的构造函数,它有三种形式:
- 无参构造函数:完全不接受任何参数
- 全缺省构造函数:所有参数都有默认值
- 编译器生成的构造函数:当用户没有定义任何构造函数时,编译器自动生成
这三种形式不能同时存在,因为它们会导致调用时的歧义。
2.3 编译器生成构造函数的行为
当用户不显式定义构造函数时,编译器会自动生成一个默认构造函数。这个自动生成的构造函数对不同类型成员的处理方式不同:
- 对于内置类型(int、float、指针等):不进行初始化(在VS中会赋随机值)
- 对于自定义类型:调用该类型的默认构造函数
cpp复制class Time {
public:
Time() { // Time类的默认构造函数
_hour = 1;
_minute = 1;
_second = 1;
}
private:
int _hour;
int _minute;
int _second;
};
class Date {
public:
void Print() {
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year; // 内置类型,不初始化
int _month;
int _day;
Time _t; // 自定义类型,调用Time的默认构造函数
};
2.4 C++11的补丁:成员变量声明时初始化
为了解决内置类型成员不初始化的问题,C++11允许在声明成员变量时直接给缺省值:
cpp复制class Date {
private:
int _year = 1; // C++11特性:声明时初始化
int _month = 1;
int _day = 1;
};
2.5 何时需要显式定义构造函数
建议在以下情况下显式定义构造函数:
- 类中有需要特定初始化的内置类型成员
- 类中有指针成员需要分配资源
- 需要提供多种初始化方式(重载构造函数)
只有成员全为自定义类型且这些类型都有合适的默认构造函数时,可以不显式定义构造函数。
3. 析构函数深入解析
3.1 析构函数的基本特性
析构函数与构造函数功能相反,它在对象销毁时自动调用,负责资源的清理工作。析构函数的特点包括:
- 函数名为类名前加~,如~Date()
- 没有返回值和参数
- 不能重载(一个类只能有一个析构函数)
- 如果用户不显式定义,编译器会自动生成
- 对象生命周期结束时自动调用
cpp复制class Stack {
public:
Stack(int capacity = 4) {
_array = (int*)malloc(sizeof(int) * capacity);
_capacity = capacity;
_top = 0;
}
~Stack() {
free(_array); // 释放动态分配的内存
_array = nullptr;
_capacity = _top = 0;
}
private:
int* _array;
int _capacity;
int _top;
};
3.2 编译器生成析构函数的行为
与构造函数类似,编译器生成的析构函数对不同类型成员的处理方式不同:
- 对于内置类型:不做任何处理
- 对于自定义类型:调用该类型的析构函数
需要注意的是,即使我们显式定义了析构函数,自定义类型成员仍然会自动调用其析构函数。
3.3 析构函数的调用顺序
析构函数的调用顺序遵循"后定义先析构"的原则,但需要注意static对象的影响:
- 全局对象先于局部对象构造
- 局部对象按照定义顺序构造(无论是否为static)
- static对象会延长生命周期到程序结束
- 析构顺序与构造顺序相反,static对象在局部对象之后析构
cpp复制class A {}; class B {}; class C {}; class D {};
C c; // 全局对象
int main() {
A a;
B b;
static D d;
return 0;
}
// 构造顺序:c a b d
// 析构顺序:b a d c
3.4 何时需要显式定义析构函数
在以下情况下需要显式定义析构函数:
- 类中有需要手动释放的资源(如动态内存、文件句柄等)
- 类中有指针成员指向动态分配的资源
如果类中没有资源需要清理(如Date类),可以不定义析构函数。
4. 拷贝构造函数全面剖析
4.1 拷贝构造函数的基本形式
拷贝构造函数用于用一个已存在的对象初始化一个新对象。它的标准形式是:
cpp复制class Date {
public:
Date(const Date& d) { // 拷贝构造函数
_year = d._year;
_month = d._month;
_day = d._day;
}
};
拷贝构造函数的特点:
- 是构造函数的一个重载形式
- 第一个参数必须是本类类型的引用(通常是const引用)
- 可以有多个参数,但后面的参数必须有默认值
4.2 为什么参数必须是引用
如果拷贝构造函数的参数不是引用,而是传值,会导致无限递归调用:
cpp复制// 错误的拷贝构造函数
Date(Date d) { ... } // 传值会导致无限递归
原因:传值参数需要调用拷贝构造函数来创建副本,而创建副本又需要调用拷贝构造函数,如此循环。
4.3 编译器生成的拷贝构造函数
如果用户不显式定义拷贝构造函数,编译器会自动生成一个。这个自动生成的拷贝构造函数执行的是浅拷贝(逐成员拷贝):
- 对于内置类型:按字节拷贝(值拷贝)
- 对于自定义类型:调用该类型的拷贝构造函数
cpp复制class Stack {
public:
Stack(int capacity = 4) {
_array = (int*)malloc(sizeof(int) * capacity);
_capacity = capacity;
_top = 0;
}
private:
int* _array;
int _capacity;
int _top;
};
int main() {
Stack st1;
Stack st2 = st1; // 使用编译器生成的拷贝构造函数
}
上面的代码会导致问题,因为两个Stack对象会共享同一个_array指针,析构时会双重释放。
4.4 深拷贝与浅拷贝
浅拷贝只是简单地复制指针值,导致多个对象共享同一资源。深拷贝则会创建资源的完整副本:
cpp复制// 正确的深拷贝实现
Stack(const Stack& st) {
_array = (int*)malloc(sizeof(int) * st._capacity);
memcpy(_array, st._array, sizeof(int) * st._capacity);
_capacity = st._capacity;
_top = st._top;
}
4.5 何时需要显式定义拷贝构造函数
在以下情况下需要显式定义拷贝构造函数:
- 类中有指针成员指向动态分配的资源
- 类中有需要深拷贝的其他资源(如文件句柄)
如果类中所有成员都是值语义(如Date类),可以不定义拷贝构造函数,使用编译器生成的即可。
5. 赋值运算符重载深度解析
5.1 运算符重载基础
C++允许重载大多数运算符,使其能用于自定义类型。运算符重载的形式为:
cpp复制返回类型 operator运算符(参数列表)
运算符重载的限制:
- 不能创建新运算符
- 重载的运算符至少有一个类类型参数
- 不能重载的运算符:.* . ?: :: 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; // 支持连续赋值
}
};
赋值运算符重载的特点:
- 参数通常是const引用
- 返回引用以支持连续赋值
- 需要处理自赋值情况
5.3 编译器生成的赋值运算符
如果不显式定义赋值运算符,编译器会自动生成一个,其行为与拷贝构造函数类似:
- 内置类型:值拷贝
- 自定义类型:调用该类型的赋值运算符
5.4 何时需要显式定义赋值运算符
需要显式定义赋值运算符的情况与拷贝构造函数类似:
- 类中有指针成员指向动态分配的资源
- 需要深拷贝的其他资源
6. 取地址运算符重载与const成员函数
6.1 const成员函数
const成员函数是指在函数声明后加const关键字的成员函数,它承诺不会修改对象状态:
cpp复制class Date {
public:
void Print() const { // const成员函数
cout << _year << "-" << _month << "-" << _day << endl;
}
};
const成员函数的this指针类型为const Date* const,而非普通的Date* const。
6.2 取地址运算符重载
取地址运算符重载通常不需要显式定义,编译器生成的版本就足够使用。但在特殊情况下(如不希望暴露真实地址),可以自定义:
cpp复制class Secret {
public:
Secret* operator&() {
return nullptr; // 返回假地址
}
const Secret* operator&() const {
return nullptr; // const版本
}
};
7. 实际开发中的经验与技巧
7.1 三/五法则
在C++中,如果一个类需要定义以下任何一个特殊成员函数,通常需要定义全部三个(析构函数、拷贝构造函数、赋值运算符)或五个(加上移动构造函数和移动赋值运算符):
- 析构函数
- 拷贝构造函数
- 拷贝赋值运算符
- 移动构造函数(C++11)
- 移动赋值运算符(C++11)
7.2 资源管理的最佳实践
- 使用RAII(Resource Acquisition Is Initialization)原则管理资源
- 优先使用智能指针而非裸指针
- 对于不可复制的资源,将拷贝构造函数和赋值运算符声明为delete
cpp复制class NonCopyable {
public:
NonCopyable() = default;
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
};
7.3 性能优化建议
- 尽量使用const引用传递对象参数
- 对于简单的值类型,传值可能比传引用更高效
- 利用移动语义(C++11)避免不必要的拷贝
7.4 常见错误与调试技巧
- 浅拷贝导致的双重释放:使用valgrind等工具检测内存问题
- 忘记处理自赋值:在赋值运算符中总是检查this != &rhs
- 异常安全问题:确保赋值运算符在异常发生时保持对象一致性
8. 现代C++中的改进(C++11/14/17)
8.1 移动语义
C++11引入了移动构造函数和移动赋值运算符,允许资源的高效转移:
cpp复制class Buffer {
public:
Buffer(Buffer&& other) noexcept // 移动构造函数
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr;
other.size_ = 0;
}
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:
int* data_;
size_t size_;
};
8.2 =default和=delete
C++11允许显式要求编译器生成默认实现或删除特定函数:
cpp复制class Defaulted {
public:
Defaulted() = default;
Defaulted(const Defaulted&) = default;
Defaulted& operator=(const Defaulted&) = default;
~Defaulted() = default;
};
class Deleted {
public:
Deleted(const Deleted&) = delete;
Deleted& operator=(const Deleted&) = delete;
};
8.3 委托构造函数
C++11允许构造函数调用同类中的其他构造函数:
cpp复制class Rectangle {
public:
Rectangle() : Rectangle(0, 0) {} // 委托构造函数
Rectangle(int w, int h) : width(w), height(h) {}
private:
int width;
int height;
};
理解并正确实现C++的六大默认成员函数是编写健壮、高效C++代码的基础。在实际开发中,应该根据类的具体需求决定哪些成员函数需要显式定义,哪些可以使用编译器生成的版本。遵循RAII原则和三/五法则可以避免许多常见的资源管理问题。