类与对象:深入理解默认成员函数
- 引言
- 1、默认成员函数概述
- 2、构造函数与析构函数
- 2.1 默认构造函数
- 2.2 析构函数
- 3、拷贝控制成员
- 3.1 拷贝构造函数
- 3.2 赋值运算符重载
- 4、移动语义(C++11)
- 4.1 移动构造函数
- 4.2 移动赋值运算符
- 5、三五法则与最佳实践
- 5.1 三五法则(Rule of Three/Five)
- 5.2 显式控制
- 5.3 建议
- 6、总结
引言
C语言是面向过程的语言,强调函数和结构化编程,而C++在兼容C语言的基础上,增加了面向对象编程、泛型编程等特性。
- 典型对比:
// C:过程式
typedef struct Stack
{
int* a;
int top;
int capacity;
} Stack;
void STInit(Stack* pst);
void STPush(Stack* pst, STDataType x);
// C++:面向对象
class Stack {
public:
Stack() {}
void push(int x){}
private:
int* a_;
int size_;
int capcity_;
};
1、默认成员函数概述
- 在C++中,当定义一个类时,编译器会自动生成6个默认成员函数(C++11起):
- 默认构造函数
- 析构函数
- 拷贝构造函数
- 赋值运算符重载
- 移动构造函数(C++11)
- 移动赋值运算符(C++11)
- 这些函数在特定场景下会被隐式调用,理解它们的特性和行为对编写健壮的类至关重要。
2、构造函数与析构函数
2.1 默认构造函数
- 作用:初始化对象成员。
- 生成条件:当类中没有显示定义任何构造函数时。
- 特点:
- 对内置类型不做处理(不进行初始化)。
- 对自定义类型调用其默认构造函数。
- 示例:
class A {
public:
int x; // 随机值
std::string s; // 调用std::string类的默认构造函数
};
int main() {
A a;
return 0;
}
[!WARNING]
当显示创建构造函数,编译器就不会生成默认构造函数。
class A {
public:
A(int x, std::string s) {
std::cout << "A" << std::endl;
}
int x;
std::string s;
};
int main() {
A a;
return 0;
}
2.2 析构函数
- 作用:释放对象资源。
- 生成条件:没有显示定义析构函数。
- 特点:
- 内置类型不做处理。
- 自定义类型调用其析构函数。
class A {
public:
A() {
std::cout << "A()" << std::endl;
}
~A() {
std::cout << "~A()" << std::endl;
}
private:
int x;
std::string s;
};
int main() {
A a;
// A()
// ~A()
return 0;
}
[!CAUTION]
当类管理资源(动态内存、文件句柄等)时,必须手动定义。
class Stack { public: Stack(int n = 4) { a_ = (int*)malloc(sizeof(int) * n); top_ = 0; capacity_ = n; } ~Stack() { free(a_); a_ = nullptr; top_ = capacity_ = 0; } private: int* a_; int capacity_; int top_; };
3、拷贝控制成员
3.1 拷贝构造函数
- 形式:
T(const T& val)
- 特点:
- 内置类型不做处理,直接进行值拷贝。
- 自定义类型调用其拷贝构造函数。
[!IMPORTANT]
如果不显示定义拷贝构造函数,编译器默认进行值拷贝。
class Stack { public: Stack(int n = 4) { a_ = (int*)malloc(sizeof(int) * n); top_ = 0; capacity_ = n; } ~Stack() { free(a_); a_ = nullptr; top_ = capacity_ = 0; } private: int* a_; int capacity_; int top_; }; int main() { Stack s1; Stack s2(s1); return 0; }
- 调试可观察到:
s1
对象和s2
对象内的数据数组具有相同的地址,表明是同一个。
3.2 赋值运算符重载
- 形式:
T& operator=(const T& val)
- 示例:
class Stack {
public:
Stack(int n = 4) {
a_ = (int*)malloc(sizeof(int) * n);
top_ = 0;
capacity_ = n;
}
Stack(const Stack &s) {
a_ = (int*)malloc(sizeof(int) * capacity_);
top_ = s.top_;
capacity_ = s.capacity_;
for (int i = 0; i < top_; ++i) {
a_[i] = s.a_[i];
}
}
Stack& operator=(const Stack &s) { // 赋值运算符重载
if (this != &s) { // 杜绝自己赋值给自己
free(a_); // 将原数组释放,防止内存泄漏
a_ = (int*)malloc(sizeof(int) * capacity_);
top_ = s.top_;
capacity_ = s.capacity_;
for (int i = 0; i < top_; ++i) {
a_[i] = s.a_[i];
}
}
return *this;
}
~Stack() {
free(a_);
a_ = nullptr;
top_ = capacity_ = 0;
}
private:
int* a_;
int capacity_;
int top_;
};
4、移动语义(C++11)
4.1 移动构造函数
- 形式:
T(T&&)
。 - 关键:转移资源所有权而非拷贝。
- 标记:使用
noexcept
保证异常安全。
class Vector {
public:
Vector(int n = 4) {
data_ = new int[n];
size_ = 0;
capacity_ = n;
}
Vector(Vector&& v) noexcept
: data_(v.data_), size_(v.size_), capacity_(v.capacity_) {
v.data_ = nullptr;
v.size_ = 0;
v.capacity_ = 0;
}
~Vector() {
delete[] data_;
size_ = capacity_ = 0;
}
private:
int* data_;
int size_;
int capacity_;
};
4.2 移动赋值运算符
- 形式:
T& operator=(T&&)
。
Vector& operator=(Vector&& v) noexcept {
if (this != &v) {
data_ = v.data_;
size_ = v.size_;
capacity_ = v.capacity_;
v.data_ = nullptr;
v.size_ = 0;
v.capacity_ = 0;
}
return *this;
}
- 生成规则:
- 没有自定义拷贝控制成员时。
- 类未声明移动操作。
- 析构函数未显示定义。
5、三五法则与最佳实践
5.1 三五法则(Rule of Three/Five)
- 需要自定义析构函数 ⇒ \Rightarrow ⇒必须处理拷贝(三法则)。
- C++11后扩展为五法则(包含移动操作)。
5.2 显式控制
=default
:显示要求编译器生成默认版本。=delete
:禁用特定成员函数。
5.3 建议
- 优先使用移动语义。
- 使用智能指针管理资源。
- 默认使用
=default
保持代码简洁。
6、总结
成员函数 | 默认行为 | 自定义场景 |
---|---|---|
默认构造函数 | 内置类型不做处理,自定义类型调用其默认构造函数 | 需要特定初始化逻辑 |
析构函数 | 内置类型不做处理,自定义类型调用其默认析构函数 | 管理资源释放 |
拷贝构造函数 | 浅拷贝 | 需要深拷贝或禁止拷贝 |
赋值运算符重载 | 浅拷贝 | 同拷贝构造函数 |
移动构造函数 | 转移资源(C++11) | 优化资源转移 |
移动赋值运算符 | 转移资源(C++11) | 同移动构造函数 |