类与对象三大核心函数:构造、析构、拷贝构造详解
一、引言
在C++面向对象编程中,构造函数、析构函数和拷贝构造函数被称为"三大件"(Rule of Three)。它们是类设计的基石,决定了对象的创建、拷贝和销毁行为。本文将通过多个实际案例,深入剖析这三大核心函数的作用、原理及使用技巧。
二、构造函数:对象诞生的第一步
2.1 构造函数的多种形态
构造函数是特殊的成员函数,在创建对象时自动调用。让我们看一个日期类示例:
class Date { public: // 1. 全缺省构造函数(最常用) Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } // 2. 无参构造函数 Date() { _year = 1; _month = 1; _day = 1; } // 3. 带参构造函数 Date(int year, int month, int day) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; };⚠️重要注意事项:
- 不要同时提供无参构造函数和全缺省构造函数,会导致调用不明确
- 创建无参对象时,不要加括号:
Date d1;✅Date d2();❌(会被解析为函数声明)
2.2 编译器自动生成的构造函数
如果没有显式定义任何构造函数,编译器会自动生成一个默认构造函数。但需要注意:
- 对内置类型(
int、指针等)不做初始化 - 对自定义类型成员,会调用其自身的默认构造函数
class MyQueue { public: // 编译器默认生成构造函数会自动调用Stack的构造函数 private: Stack pushst; // 会调用Stack的构造函数 Stack popst; // 会调用Stack的构造函数 };三、析构函数:对象生命的终结者
3.1 析构函数的作用
析构函数在对象生命周期结束时自动调用,用于清理资源。对于管理动态内存的类,必须自定义析构函数。
class Stack { public: // 构造函数 Stack(int n = 4) { _a = (STDataType*)malloc(sizeof(STDataType) * n); if (nullptr == _a) { perror("malloc申请空间失败"); return; } _capacity = n; _top = 0; } // 析构函数 ~Stack() { cout << "~Stack()" << endl; free(_a); // 释放动态内存 _a = nullptr; // 防止野指针 _top = _capacity = 0; } private: STDataType* _a; // 动态数组指针 size_t _capacity; size_t _top; };3.2 析构函数的自动调用
当类包含自定义类型成员时,其析构函数会自动调用成员的析构函数:
class MyQueue { public: // 即使不写析构函数,编译器生成的析构函数也会: // 1. 先执行函数体(如果有) // 2. 再自动调用成员的析构函数(先popst,后pushst) ~MyQueue() { // 可以在这里添加MyQueue特有的清理工作 } // 函数体结束后会自动调用popst.~Stack()和pushst.~Stack() private: Stack pushst; Stack popst; };四、拷贝构造函数:深度复制的重要性
4.1 拷贝构造函数的语法
拷贝构造函数用于用一个已存在的对象初始化同类型的新对象:
class Date { public: // ✅ 正确的拷贝构造函数声明 Date(const Date& d) // 必须传引用,否则无限递归 { _year = d._year; _month = d._month; _day = d._day; } // ❌ 错误的声明:参数不是引用 // Date(Date d) // 会无限递归调用自身 };📌必须传引用的原因:
如果传值,需要先拷贝参数对象,而拷贝参数又需要调用拷贝构造函数,形成无限递归。
4.2 深拷贝 vs 浅拷贝
这是理解拷贝构造函数的关键!让我们通过栈类的例子来说明:
情况一:使用默认拷贝构造(浅拷贝)
Stack st1; st1.Push(1); st1.Push(2); // 使用编译器自动生成的拷贝构造函数 Stack st2 = st1; // 浅拷贝:st2._a 和 st1._a 指向同一块内存 // 问题:st1和st2析构时都会释放同一块内存,导致双重释放,程序崩溃!情况二:实现自定义拷贝构造(深拷贝)
class Stack { public: // 深拷贝构造函数 Stack(const Stack& st) { // 1. 申请新内存 _a = (STDataType*)malloc(sizeof(STDataType) * st._capacity); if (nullptr == _a) { perror("malloc申请空间失败!!!"); return; } // 2. 拷贝数据 memcpy(_a, st._a, sizeof(STDataType) * st._top); // 3. 拷贝其他成员 _top = st._top; _capacity = st._capacity; } };深拷贝的效果:
Stack st1; // st1._a → [内存块A] st1.Push(1); st1.Push(2); Stack st2 = st1; // st2._a → [内存块B](新申请的内存) // 内存块B的内容和内存块A相同 // 现在st1和st2有各自独立的内存,可以安全析构4.3 何时需要自定义拷贝构造函数?
遵循"Rule of Three"原则:如果你需要自定义析构函数,那么很可能也需要自定义拷贝构造函数和拷贝赋值运算符。
需要自定义拷贝构造的典型场景:
- 类包含动态分配的内存
- 类包含文件句柄、网络连接等需要特殊管理的资源
- 类包含指针成员,且不希望共享指针指向的资源
五、拷贝构造函数的调用场景
理解拷贝构造函数何时被调用非常重要:
class Date { public: Date(int year = 1, int month = 1, int day = 1) : _year(year), _month(month), _day(day) {} Date(const Date& d) { cout << "调用拷贝构造函数" << endl; _year = d._year; _month = d._month; _day = d._day; } private: int _year, _month, _day; };场景1:用同类型对象初始化
Date d1(2024, 7, 5); Date d2(d1); // ✅ 拷贝构造 Date d3 = d1; // ✅ 拷贝构造(注意:这是初始化,不是赋值!)场景2:函数传值参数
void Func1(Date d) // 传值参数,会调用拷贝构造 { d.Print(); } Date d1(2024, 7, 5); Func1(d1); // 调用拷贝构造函数创建形参d场景3:函数返回局部对象(有坑!)
// ❌ 危险:返回局部对象的引用 Date& Func2() { Date tmp(2024, 7, 5); return tmp; // tmp是局部变量,函数结束就销毁 } // 返回的是野引用! // ✅ 正确:返回对象(会调用拷贝构造) Date Func3() { Date tmp(2024, 7, 5); return tmp; // 编译器可能会优化(RVO/NRVO),但逻辑上应该调用拷贝构造 }六、组合类的拷贝构造
当类包含其他自定义类型成员时,拷贝构造函数会自动调用成员的拷贝构造函数:
class MyQueue { private: Stack pushst; Stack popst; }; MyQueue mq1; // 编译器自动生成的拷贝构造函数会: // 1. 调用pushst的拷贝构造函数 // 2. 调用popst的拷贝构造函数 MyQueue mq2 = mq1;关键点:
- 如果
Stack实现了深拷贝,那么MyQueue的拷贝就是安全的 - 如果
Stack没有实现深拷贝,那么MyQueue的拷贝也是浅拷贝
七、综合示例:完整的Stack类
class Stack { public: // 构造函数 Stack(int n = 4) : _a(nullptr) , _capacity(0) , _top(0) { _a = (STDataType*)malloc(sizeof(STDataType) * n); if (_a) { _capacity = n; _top = 0; } } // 拷贝构造函数(深拷贝) Stack(const Stack& other) : _a(nullptr) , _capacity(0) , _top(0) { if (other._a && other._capacity > 0) { _a = (STDataType*)malloc(sizeof(STDataType) * other._capacity); if (_a) { memcpy(_a, other._a, sizeof(STDataType) * other._top); _capacity = other._capacity; _top = other._top; } } } // 析构函数 ~Stack() { if (_a) { free(_a); _a = nullptr; } _capacity = _top = 0; } // 入栈操作 void Push(STDataType x) { if (_top == _capacity) { int newCapacity = _capacity == 0 ? 4 : _capacity * 2; STDataType* tmp = (STDataType*)realloc(_a, sizeof(STDataType) * newCapacity); if (tmp) { _a = tmp; _capacity = newCapacity; } } _a[_top++] = x; } private: STDataType* _a; size_t _capacity; size_t _top; };八、总结与最佳实践
1. 构造函数
- 尽量使用初始化列表进行成员初始化
- 优先使用全缺省构造函数替代无参和多个带参构造函数
- 对于内置类型,编译器生成的默认构造函数不会初始化
2. 析构函数
- 有动态资源就必须自定义析构函数
- 遵循"谁申请,谁释放"原则
- 释放后将指针置为
nullptr防止野指针
3. 拷贝构造函数
- 参数必须是const引用
- 有动态资源必须实现深拷贝
- 遵循"Rule of Three":定义了析构函数,通常也需要定义拷贝构造和赋值运算符
4. 现代C++建议
- 考虑使用智能指针管理动态内存
- 使用
std::vector等标准容器替代手动内存管理 - 对于不可拷贝的类型,使用
= delete明确删除拷贝操作
class NonCopyable { public: NonCopyable() = default; ~NonCopyable() = default; // 禁止拷贝 NonCopyable(const NonCopyable&) = delete; NonCopyable& operator=(const NonCopyable&) = delete; };九、思考题
- 为什么拷贝构造函数的参数必须是引用?如果传值会发生什么?
- 什么时候编译器会自动生成拷贝构造函数?
- 深拷贝和浅拷贝的区别是什么?各自适用什么场景?
- 如何避免拷贝构造带来的性能开销?
通过本文的学习,相信你已经掌握了C++类设计中三大核心函数的关键要点。合理使用它们,可以避免许多常见的内存错误和逻辑错误,写出更安全、更健壮的代码。
深入剖析C++拷贝构造的四大核心问题
一、为什么拷贝构造函数的参数必须是引用?如果传值会发生什么?
1.1 问题的本质
这是拷贝构造函数设计中最重要的理解点。让我们先看错误示例:
class Date { public: // ❌ 错误的拷贝构造函数声明 Date(Date d) // 参数是值传递 { _year = d._year; _month = d._month; _day = d._day; } private: int _year, _month, _day; };1.2 无限递归的发生过程
当调用这个拷贝构造函数时:
Date d1(2024, 7, 5); Date d2(d1); // 尝试用d1构造d2调用链分析:
- 编译器要创建
d2,需要调用Date::Date(Date d) - 参数
d是传值的,这意味着需要复制实参d1来创建形参d - 复制
d1到d需要调用拷贝构造函数 - 调用拷贝构造函数又需要传值参数…
- 无限递归开始!
这个过程可以用流程图表示:
Date d2(d1) → 调用Date(Date d) → 需要复制d1到d → 调用Date(Date d) → 需要复制d1到d → ... ↑ ↓ └──────────────────────────────────────────────────────────────┘1.3 引用传递的解决方案
class Date { public: // ✅ 正确的拷贝构造函数 Date(const Date& d) // 传引用,不产生副本 { _year = d._year; _month = d._month; _day = d._day; } };引用传递的优势:
- 避免无限递归:引用只是别名,不需要拷贝
- 提高效率:避免不必要的数据复制
- 支持
const:防止意外修改原对象
1.4 编译器如何阻止错误
如果你不小心写成传值形式,编译器会报错:
// error: invalid constructor; you probably meant 'Date (const Date&)' Date(Date d); // 编译错误现代编译器能识别这个常见错误并给出提示。
二、什么时候编译器会自动生成拷贝构造函数?
2.1 自动生成的时机
编译器在以下所有条件都满足时会自动生成拷贝构造函数:
- 没有显式定义拷贝构造函数
- 没有定义移动构造函数(C++11及以后)
- 没有定义移动赋值运算符(C++11及以后)
示例:
class SimpleClass { int x; double y; char z; public: SimpleClass() = default; // 编译器会自动生成: // SimpleClass(const SimpleClass& other) : x(other.x), y(other.y), z(other.z) {} };2.2 自动生成的拷贝构造函数做了什么?
自动生成的拷贝构造函数执行成员级别的拷贝:
class Example { private: int a; double b; std::string c; // 自定义类型 int* ptr; // 指针 public: // 编译器生成的拷贝构造函数相当于: // Example(const Example& other) // : a(other.a), // 值拷贝 // b(other.b), // 值拷贝 // c(other.c), // 调用string的拷贝构造函数 // ptr(other.ptr) {} // ❌ 危险:浅拷贝! };重要特点:
- 内置类型:值拷贝(包括指针,只拷贝地址,不拷贝指向的内容)
- 自定义类型:调用该类型的拷贝构造函数
2.3 需要手动定义拷贝构造的场景
| 场景 | 原因 | 示例 |
|---|---|---|
| 动态内存管理 | 避免浅拷贝导致双重释放 | vector、string等容器 |
| 文件/资源句柄 | 避免重复关闭资源 | 文件流、数据库连接 |
| 唯一资源所有权 | 确保资源不被共享 | 独占指针、网络连接 |
| 复杂对象图 | 需要深拷贝整个结构 | 树、图等数据结构 |
2.4 C++11后的变化
class ModernClass { public: // 如果定义其中任何一个,编译器不会自动生成拷贝构造 ~ModernClass(); // 析构函数 ModernClass(ModernClass&&); // 移动构造 ModernClass& operator=(ModernClass&&); // 移动赋值 // 但可以显式要求编译器生成 ModernClass(const ModernClass&) = default; // 显式默认 ModernClass& operator=(const ModernClass&) = delete; // 显式删除 };三、深拷贝和浅拷贝的区别是什么?各自适用什么场景?
3.1 直观对比
// 浅拷贝:只拷贝指针,不拷贝指向的内存 int* p1 = new int[100]; int* p2 = p1; // 浅拷贝:p1和p2指向同一内存 // 深拷贝:既拷贝指针,也拷贝指向的内存 int* p3 = new int[100]; int* p4 = new int[100]; // 新申请内存 memcpy(p4, p3, 100 * sizeof(int)); // 拷贝内容3.2 类级别的对比示例
class ShallowCopy { private: int* data; int size; public: // 编译器生成的拷贝构造执行浅拷贝 // 相当于:data = other.data; size = other.size; }; class DeepCopy { private: int* data; int size; public: // 手动实现深拷贝 DeepCopy(const DeepCopy& other) { size = other.size; data = new int[size]; // 申请新内存 for(int i = 0; i < size; ++i) data[i] = other.data[i]; // 拷贝内容 } };3.3 内存布局对比
浅拷贝的内存布局:
原对象: [指针:0x1000] → [数据区] 副本对象: [指针:0x1000] → 指向同一数据区 问题:两次delete会崩溃!深拷贝的内存布局:
原对象: [指针:0x1000] → [数据区] 副本对象: [指针:0x2000] → [新数据区,内容与原数据区相同] 安全:各自独立,可单独释放3.4 适用场景对比
浅拷贝适用场景:
| 场景 | 原因 | 示例 |
|---|---|---|
| 只读共享 | 多个对象读取同一数据 | 配置信息、常量数据 |
| 无资源管理 | 不涉及动态内存 | 简单的值类、POD类型 |
| 引用计数 | 配合智能指针使用 | shared_ptr管理的对象 |
| 性能优先 | 避免拷贝开销 | 大对象的临时引用 |
// 适合浅拷贝的类示例 class Point // 简单的值类型 { double x, y, z; public: // 可以用浅拷贝(默认生成的即可) // 没有动态资源,只有基本类型 };深拷贝适用场景:
| 场景 | 原因 | 示例 |
|---|---|---|
| 动态内存 | 避免双重释放 | 自定义容器、字符串 |
| 文件句柄 | 需要独立副本 | 文件流、数据库连接 |
| 网络资源 | 避免冲突 | socket、连接池 |
| 线程安全 | 需要独立状态 | 线程局部存储 |
// 需要深拷贝的类示例 class String { char* str; size_t length; public: String(const String& other) { length = other.length; str = new char[length + 1]; // 深拷贝 strcpy(str, other.str); } ~String() { delete[] str; } };3.5 混合策略
实际开发中,可以根据需要采用混合策略:
class SmartArray { private: int* data; size_t size; mutable int* cache; // 可共享的缓存 public: // 对data深拷贝,对cache浅拷贝 SmartArray(const SmartArray& other) { size = other.size; data = new int[size]; // 深拷贝:核心数据 for(size_t i = 0; i < size; ++i) data[i] = other.data[i]; cache = other.cache; // 浅拷贝:可共享的缓存 } };四、如何避免拷贝构造带来的性能开销?
4.1 避免不必要的拷贝
技巧1:使用引用传递
// ❌ 传值:会有拷贝开销 void processData(Data data); // ✅ 传const引用:无拷贝开销 void processData(const Data& data); // 如果不需要修改原对象,优先使用const引用技巧2:使用移动语义(C++11+)
class BigData { int* hugeArray; size_t size; public: // 移动构造函数 BigData(BigData&& other) noexcept : hugeArray(other.hugeArray) // 转移资源 , size(other.size) { other.hugeArray = nullptr; // 置空原对象 other.size = 0; } // 移动赋值 BigData& operator=(BigData&& other) noexcept { if(this != &other) { delete[] hugeArray; // 释放已有资源 hugeArray = other.hugeArray; // 转移资源 size = other.size; other.hugeArray = nullptr; other.size = 0; } return *this; } };4.2 编译器优化技术
返回值优化(RVO)
Data createData() { Data d; // 直接在返回位置构造 // ... 初始化d return d; // 可能被优化,不调用拷贝构造 // 编译器优化为: // 直接在调用者的栈帧上构造对象 }命名返回值优化(NRVO)
Data createData(bool flag) { Data d1, d2; if(flag) return d1; // 可能被优化 else return d2; // 可能被优化 }4.3 延迟拷贝(写时复制 - Copy on Write)
class COWString { private: struct Data { char* buffer; size_t refcount; // 引用计数 size_t length; }; Data* data; // 真正的拷贝只在需要修改时发生 void detach() { if(data &&>4.4 使用智能指针共享资源class SharedResource { private: class Impl { // 实际的资源数据 std::vector<int> data; }; std::shared_ptr<Impl> pImpl; // 共享实现 public: // 拷贝构造:只增加引用计数 SharedResource(const SharedResource& other) : pImpl(other.pImpl) // 引用计数+1 {} // 修改时如果需要独立副本 void modify() { if(pImpl.use_count() > 1) // 有多个引用 { pImpl = std::make_shared<Impl>(*pImpl); // 深拷贝 } // 现在可以安全修改 } };
4.5 设计模式应用
原型模式
class Prototype { public: virtual ~Prototype() = default; virtual std::unique_ptr<Prototype> clone() const = 0; }; class ConcretePrototype : public Prototype { HeavyData* data; // 昂贵的数据 public: // 延迟拷贝:只有调用clone时才真正拷贝 std::unique_ptr<Prototype> clone() const override { auto copy = std::make_unique<ConcretePrototype>(); if(data) copy->data =>4.6 实际性能对比// 测试不同策略的性能 class TestObject { std::vector<int> data; // 大量数据 public: // 传统深拷贝 TestObject(const TestObject& other) : data(other.data) // 立即拷贝所有数据 {} // 移动构造 TestObject(TestObject&& other) noexcept : data(std::move(other.data)) // 只转移所有权 {} // 惰性拷贝 void lazyCopyFrom(const TestObject& other) { if(data.empty()) data = other.data; // 需要时才拷贝 } };
五、总结与最佳实践
5.1 拷贝构造的核心要点
- 参数必须是const引用:避免无限递归
- 编译器自动生成浅拷贝:只适合无动态资源的简单类
- 有资源必须深拷贝:遵循Rule of Three/Five
- 考虑性能优化:引用传递、移动语义、延迟拷贝
5.2 现代C++最佳实践
class ModernResource { private: std::unique_ptr<Data> data; // 使用智能指针 std::shared_ptr<Cache> cache; // 共享资源 public: // 默认行为 ModernResource() = default; // 禁止拷贝 ModernResource(const ModernResource&) = delete; ModernResource& operator=(const ModernResource&) = delete; // 允许移动 ModernResource(ModernResource&&) = default; ModernResource& operator=(ModernResource&&) = default; // 显式拷贝接口 ModernResource clone() const { ModernResource copy; if(data) copy.data = std::make_unique<Data>(*data); // 按需深拷贝 copy.cache = cache; // 共享缓存 return copy; } };
5.3 决策流程图
需要拷贝功能吗? ├── 是 → 有动态资源吗? │ ├── 是 → 实现深拷贝 │ │ ├── 性能敏感? → 考虑移动语义/COW │ │ └── 共享资源? → 使用智能指针 │ └── 否 → 使用默认拷贝 │ └── 否 → 删除拷贝构造 ├── 只移动? → 实现移动语义 └── 单例模式 → 私有化拷贝构造
5.4 性能优化检查清单
- ✅ 函数参数使用
const T&而不是T - ✅ 返回局部对象时依赖编译器优化(RVO/NRVO)
- ✅ 对大对象实现移动语义
- ✅ 考虑写时复制(COW)模式
- ✅ 使用智能指针管理共享资源
- ✅ 避免不必要的拷贝操作
掌握这些技巧,你就能在保证正确性的同时,编写出高性能的C++代码。