C++构造函数、析构函数与拷贝控制深度解析
一、核心知识点详解
1. 构造函数
- 定义:对象创建时自动调用的特殊成员函数
- 特点 :
- 函数名与类名完全相同
- 可以重载(提供多个构造函数版本)
- 无返回类型,包括void
- 可以有参数,支持默认参数
- 分类 :
- 默认构造函数:无参或所有参数都有默认值
- 拷贝构造函数:用于对象初始化
- 移动构造函数(C++11):高效资源转移
- 委托构造函数(C++11):一个构造函数调用同类其他构造函数
- 转换构造函数:单参数构造函数,可隐式转换
2. 析构函数
- 定义:对象销毁时自动调用的特殊成员函数
- 特点 :
- 函数名为类名前加
~ - 不能重载(一个类只有一个析构函数)
- 无返回类型,包括void
- 无参数
- 函数名为类名前加
- 调用时机 :
- 栈对象离开作用域
- 堆对象被delete
- 容器中的元素被清除
- 临时对象生命周期结束
3. 深拷贝 vs 浅拷贝
- 浅拷贝 :
- 仅复制指针值,不复制指针指向的数据
- 多个对象共享同一块内存
- 可能导致:①双重释放 ②悬挂指针
- 深拷贝 :
- 复制指针指向的完整数据
- 每个对象拥有独立的数据副本
- 安全但可能效率较低
4. 移动语义(C++11)
- 核心思想:"窃取"临时对象的资源,避免不必要拷贝
- 右值引用 :
T&&表示临时对象的引用 - 移动构造函数:从临时对象"移动"资源而非复制
- 移动赋值运算符:类似移动构造的赋值版本
二、完整教学示例代码
cpp
#include <iostream>
#include <cstring>
#include <utility> // for std::move
class String {
private:
char* m_data;
size_t m_size;
public:
// ==================== 构造函数系列 ====================
// 1. 默认构造函数
String() : m_data(nullptr), m_size(0) {
std::cout << "默认构造函数" << std::endl;
}
// 2. 参数化构造函数
String(const char* str) {
std::cout << "参数化构造函数: " << (str ? str : "null") << std::endl;
if (str) {
m_size = strlen(str);
m_data = new char[m_size + 1];
strcpy(m_data, str);
} else {
m_size = 0;
m_data = nullptr;
}
}
// 3. 拷贝构造函数 - 深拷贝实现
String(const String& other) {
std::cout << "拷贝构造函数(深拷贝)" << std::endl;
m_size = other.m_size;
if (other.m_data) {
m_data = new char[m_size + 1];
strcpy(m_data, other.m_data);
} else {
m_data = nullptr;
}
}
// 4. 拷贝构造函数 - 浅拷贝实现(危险!仅用于演示)
String(const String& other, bool shallow) {
std::cout << "拷贝构造函数(浅拷贝)" << std::endl;
m_size = other.m_size;
m_data = other.m_data; // 危险:共享内存!
}
// 5. 移动构造函数(C++11)
String(String&& other) noexcept {
std::cout << "移动构造函数" << std::endl;
// "窃取"资源
m_data = other.m_data;
m_size = other.m_size;
// 置空原对象
other.m_data = nullptr;
other.m_size = 0;
}
// ==================== 赋值运算符系列 ====================
// 6. 拷贝赋值运算符
String& operator=(const String& other) {
std::cout << "拷贝赋值运算符" << std::endl;
if (this != &other) { // 防止自赋值
// 释放原有资源
delete[] m_data;
// 深拷贝
m_size = other.m_size;
if (other.m_data) {
m_data = new char[m_size + 1];
strcpy(m_data, other.m_data);
} else {
m_data = nullptr;
}
}
return *this;
}
// 7. 移动赋值运算符(C++11)
String& operator=(String&& other) noexcept {
std::cout << "移动赋值运算符" << std::endl;
if (this != &other) {
// 释放原有资源
delete[] m_data;
// 移动资源
m_data = other.m_data;
m_size = other.m_size;
// 置空原对象
other.m_data = nullptr;
other.m_size = 0;
}
return *this;
}
// ==================== 析构函数 ====================
~String() {
std::cout << "析构函数";
if (m_data) {
std::cout << ": 释放 " << m_data;
}
std::cout << std::endl;
delete[] m_data; // delete nullptr 是安全的
}
// ==================== 工具函数 ====================
const char* c_str() const { return m_data ? m_data : ""; }
size_t size() const { return m_size; }
void print() const {
std::cout << "String(\"" << (m_data ? m_data : "nullptr")
<< "\", size=" << m_size << ")" << std::endl;
}
};
// ==================== 演示函数 ====================
void demonstrate_constructors() {
std::cout << "\n=== 构造函数演示 ===" << std::endl;
// 1. 默认构造
String s1;
s1.print();
// 2. 参数化构造
String s2("Hello");
s2.print();
// 3. 拷贝构造(深拷贝)
String s3 = s2; // 等价于 String s3(s2)
s3.print();
std::cout << "s2地址: " << (void*)s2.c_str() << std::endl;
std::cout << "s3地址: " << (void*)s3.c_str() << std::endl;
std::cout << "注意:深拷贝时地址不同" << std::endl;
}
void demonstrate_shallow_copy_problem() {
std::cout << "\n=== 浅拷贝问题演示 ===" << std::endl;
String* original = new String("Test");
// 危险:创建浅拷贝
{
String shallowCopy(*original, true); // 使用浅拷贝构造函数
std::cout << "原始: ";
original->print();
std::cout << "浅拷贝: ";
shallowCopy.print();
} // shallowCopy离开作用域,调用析构函数,释放了共享的内存!
// 此时original的m_data已经是悬空指针!
// 下一行代码会导致未定义行为(可能崩溃)
// std::cout << "原始对象(已无效): " << original->c_str() << std::endl;
delete original; // 双重释放!运行时错误
}
void demonstrate_deep_copy_safety() {
std::cout << "\n=== 深拷贝安全性演示 ===" << std::endl;
String original("Safe String");
String deepCopy = original; // 使用深拷贝构造函数
// 修改拷贝,不影响原始对象
std::cout << "修改前:" << std::endl;
std::cout << "原始: ";
original.print();
std::cout << "拷贝: ";
deepCopy.print();
// 注意:这里不能直接修改,因为String没有提供修改接口
// 实际中应该提供修改方法,这里仅演示拷贝的独立性
std::cout << "原始地址: " << (void*)original.c_str() << std::endl;
std::cout << "拷贝地址: " << (void*)deepCopy.c_str() << std::endl;
std::cout << "地址不同,内存独立" << std::endl;
}
void demonstrate_move_semantics() {
std::cout << "\n=== 移动语义演示 ===" << std::endl;
// 创建临时对象
String temp("Temporary String");
std::cout << "\n1. 移动构造演示:" << std::endl;
String moved1(std::move(temp)); // 移动构造
std::cout << "移动后temp: ";
temp.print(); // temp现在为空
std::cout << "移动后moved1: ";
moved1.print();
std::cout << "\n2. 移动赋值演示:" << std::endl;
String moved2;
moved2 = std::move(moved1); // 移动赋值
std::cout << "移动后moved1: ";
moved1.print(); // moved1现在为空
std::cout << "移动后moved2: ";
moved2.print();
}
void demonstrate_anonymous_objects() {
std::cout << "\n=== 匿名对象演示 ===" << std::endl;
// 匿名对象:没有名字的临时对象
std::cout << "创建匿名对象: ";
String("Anonymous").print(); // 立即析构
// 匿名对象在表达式结束后立即析构
std::cout << "匿名对象已销毁" << std::endl;
// 匿名对象作为函数参数
std::cout << "\n匿名对象作为拷贝构造参数:" << std::endl;
String s = String("Parameter"); // 可能被编译器优化(RVO/NRVO)
s.print();
}
void demonstrate_rule_of_five() {
std::cout << "\n=== 三五法则演示 ===" << std::endl;
// 三五法则:如果需要定义以下任何一个,通常需要定义所有五个
// 1. 析构函数
// 2. 拷贝构造函数
// 3. 拷贝赋值运算符
// 4. 移动构造函数(C++11)
// 5. 移动赋值运算符(C++11)
std::cout << "String类遵循三五法则,正确定义了所有五个特殊成员函数" << std::endl;
}
int main() {
std::cout << "C++构造函数、析构函数与拷贝控制深度教学" << std::endl;
std::cout << "========================================" << std::endl;
demonstrate_constructors();
// 警告:浅拷贝演示会崩溃,注释掉以供学习
// demonstrate_shallow_copy_problem();
demonstrate_deep_copy_safety();
demonstrate_move_semantics();
demonstrate_anonymous_objects();
demonstrate_rule_of_five();
std::cout << "\n程序结束,所有栈对象将按创建相反顺序析构" << std::endl;
return 0;
}
三、关键要点总结
1. 构造函数要点
- 构造函数可以重载,提供多种初始化方式
- 使用初始化列表初始化成员变量(更高效)
- 委托构造函数可减少代码重复
2. 拷贝控制要点
- 浅拷贝问题 :
- 双重释放:两个析构函数释放同一内存
- 悬挂指针:一个对象删除数据,另一个对象指针失效
- 深拷贝实现 :
- 在拷贝构造函数和拷贝赋值运算符中分配新内存
- 注意处理自赋值情况(
if (this != &other))
3. 移动语义要点
- 移动操作"窃取"资源,不分配新内存
- 被移动的对象应处于有效但未指定的状态
- 使用
std::move将左值转换为右值引用 - 标记移动操作为
noexcept有助于标准库优化
4. 三五法则
如果一个类需要显式定义以下任何一个,则通常需要定义所有五个:
- 析构函数
- 拷贝构造函数
- 拷贝赋值运算符
- 移动构造函数
- 移动赋值运算符
5. 最佳实践
- 使用
= default让编译器生成默认版本 - 使用
= delete禁用不需要的操作 - 优先使用移动语义提高性能
- 对于资源管理类,总是实现深拷贝或禁用拷贝
- 考虑使用智能指针避免手动内存管理