【C++类和数据抽象】复制构造函数
目录
一、对象复制的本质需求
1.1 为什么需要对象复制?
1.2 默认复制行为的问题
二、复制构造函数的基本概念
2.1 定义与作用
2.2 与拷贝赋值运算符的区别
三、复制构造函数的触发场景
3.1 对象初始化
3.2 函数参数传递
3.3 函数返回对象
3.4 容器元素操作
四、默认复制构造函数:浅拷贝的风险
4.1 编译器自动生成的默认行为
4.2 浅拷贝引发的典型问题
五、自定义复制构造函数:实现深拷贝
5.1 深拷贝的实现原则
5.2 深拷贝代码示例
六、复制构造函数的高级特性
6.1 防止对象复制:删除复制构造函数
6.2 移动语义与复制构造函数
6.3 复制构造函数与 const 关键字
七、常见陷阱与最佳实践
7.1 陷阱一:遗漏复制构造函数导致资源泄漏
7.2 陷阱二:复制构造函数中未正确初始化成员
7.3 最佳实践:遵循 Rule of Three/Five
八、总结
九、参考资料
在 C++ 的面向对象编程中,对象的复制是一个核心操作。复制构造函数作为类的特殊成员函数,负责从一个已存在的对象创建新对象,确保资源管理的正确性和数据的一致性。
一、对象复制的本质需求
1.1 为什么需要对象复制?
在C++程序设计中,对象复制是面向对象编程的核心操作之一。典型应用场景包括:
-
函数参数的值传递
-
返回局部对象
-
容器元素操作
-
对象初始化
void processObject(MyClass obj); // 值传递触发拷贝
MyClass createObject(); // 返回对象触发拷贝int main() {MyClass a;MyClass b = a; // 初始化拷贝vector<MyClass> vec;vec.push_back(a); // 容器存储拷贝
}
1.2 默认复制行为的问题
编译器生成的默认拷贝构造函数执行浅拷贝(Shallow Copy),对于包含指针成员的类会引发严重问题:
class ShallowArray {int* data;size_t size;
public:ShallowArray(size_t n) : size(n), data(new int[n]) {}~ShallowArray() { delete[] data; }
};int main() {ShallowArray arr1(5);ShallowArray arr2 = arr1; // 双重释放崩溃!
}
二、复制构造函数的基本概念
2.1 定义与作用
复制构造函数是一种特殊的构造函数,其函数名与类名相同,没有返回类型,参数为当前类的常量引用(const ClassName&
)。它的作用是:
- 从已存在的对象创建新对象,实现对象的深拷贝(Deep Copy)。
- 确保在对象复制过程中,资源(如动态内存、文件句柄等)被正确复制,避免浅拷贝(Shallow Copy)导致的悬挂指针或资源泄漏。
语法格式:
class ClassName {
public:ClassName(const ClassName& obj); // 复制构造函数声明
};// 定义示例
ClassName::ClassName(const ClassName& obj) {// 实现对象复制逻辑
}
2.2 与拷贝赋值运算符的区别
特性 | 复制构造函数 | 拷贝赋值运算符(operator=) |
---|---|---|
触发时机 | 对象创建时(初始化阶段) | 对象已存在时(赋值操作阶段) |
参数 | const ClassName& | const ClassName& |
返回值 | 无(构造函数无返回值) | ClassName& (当前对象引用) |
核心功能 | 从无到有创建对象的复制版本 | 从已有对象向当前对象赋值 |
示例对比:
ClassName obj1; // 调用默认构造函数
ClassName obj2(obj1); // 调用复制构造函数(对象创建)
obj2 = obj1; // 调用拷贝赋值运算符(对象已存在)
三、复制构造函数的触发场景
3.1 对象初始化
当使用一个已存在的对象初始化新对象时,复制构造函数被触发:
#include <iostream> // 包含输入输出流头文件
using namespace std; // 使用标准命名空间class Point {
public:int x, y;Point(int a, int b) : x(a), y(b) {}Point(const Point& p) : x(p.x), y(p.y) { // 复制构造函数cout << "Copy constructor called" << endl;}
};int main() {Point p1(1, 2);Point p2 = p1; // 等价于Point p2(p1),触发复制构造函数return 0;
}
3.2 函数参数传递
当对象以值传递方式传入函数时,会触发复制构造函数(形参是实参的副本):
#include <iostream>
using namespace std;// 定义Point类
class Point {
public:int x, y; // 公有数据成员// 构造函数Point(int a, int b) : x(a), y(b) {}// 复制构造函数Point(const Point& p) : x(p.x), y(p.y) {cout << "Copy constructor called: (" << x << "," << y << ")" << endl;}
};// 值传递函数,触发复制构造函数
void printPoint(Point p) {cout << "Point value: " << p.x << "," << p.y << endl;
}int main() {Point p(3, 4); // 创建Point对象pprintPoint(p); // 调用printPoint,触发复制构造函数return 0;
}
3.3 函数返回对象
当函数返回一个对象时,会通过复制构造函数创建临时对象作为返回值:
#include <iostream>
using namespace std;class Point {
public:int x, y;// 构造函数Point(int a = 0, int b = 0) : x(a), y(b) {cout << "Constructor called: (" << x << "," << y << ")" << endl;}// 复制构造函数Point(const Point& p) : x(p.x), y(p.y) {cout << "Copy constructor called: (" << x << "," << y << ")" << endl;}// 移动构造函数(C++11,用于验证RVO与移动语义)Point(Point&& p) noexcept : x(p.x), y(p.y) {cout << "Move constructor called: (" << x << "," << y << ")" << endl;p.x = p.y = 0; // 标记原对象已移动}
};Point getPoint() {Point temp(5, 6); // 调用构造函数创建tempcout << "Returning temp from getPoint" << endl;return temp; // 返回对象,触发复制/移动构造或RVO
}int main() {cout << "Creating point p..." << endl;Point p = getPoint(); // 接收返回值,观察构造函数调用cout << "p.x = " << p.x << ", p.y = " << p.y << endl;return 0;
}
3.4 容器元素操作
在容器(如vector
、list
)中插入对象时,可能触发复制构造函数:
#include <iostream>
#include <vector> // 引入vector容器头文件
using namespace std;// 定义Point类
class Point {
public:int x, y;// 构造函数Point(int a = 0, int b = 0) : x(a), y(b) {cout << "Constructor called: (" << x << "," << y << ")" << endl;}// 复制构造函数(关键:确保容器复制元素时正确调用)Point(const Point& p) : x(p.x), y(p.y) {cout << "Copy constructor called: (" << x << "," << y << ")" << endl;}
};int main() {vector<Point> vec; // 创建空vectorPoint p(1, 2); // 调用构造函数创建对象pcout << "Pushing p into vector..." << endl;vec.push_back(p); // 向vector添加元素,触发复制构造函数cout << "Vector size: " << vec.size() << endl;return 0;
}
四、默认复制构造函数:浅拷贝的风险
4.1 编译器自动生成的默认行为
如果类中未显式定义复制构造函数,编译器会自动生成一个默认复制构造函数,执行浅拷贝(逐字节复制):
- 对于基本数据类型(如
int
、double
),浅拷贝是安全的。 - 对于指针成员,浅拷贝会导致多个对象指向同一内存地址,引发悬挂指针或双重释放问题。
4.2 浅拷贝引发的典型问题
场景:类中包含动态分配的内存(如int* data
),使用默认复制构造函数:
class BadExample {
public:int* data;BadExample(int size) {data = new int[size]; // 分配内存}// 未定义复制构造函数,使用默认浅拷贝
};int main() {BadExample obj1(5);BadExample obj2 = obj1; // 默认复制构造函数,浅拷贝data指针obj1.data[0] = 10; // obj2.data[0]也会变为10(共享内存)delete[] obj1.data; // 释放内存后,obj2.data成为悬挂指针delete[] obj2.data; // 二次释放,程序崩溃return 0;
}
问题根源: 浅拷贝导致多个对象的指针成员指向同一内存块,释放后引发未定义行为。
五、自定义复制构造函数:实现深拷贝
5.1 深拷贝的实现原则
当类中包含资源管理成员(如指针、文件句柄等)时,必须显式定义复制构造函数,实现深拷贝:
- 为新对象分配独立的资源。
- 将原对象的资源内容复制到新资源中。
- 确保原对象与新对象的资源相互独立,互不影响。
5.2 深拷贝代码示例
#include <iostream>
#include <algorithm> // 包含copy算法
using namespace std; class GoodExample {
public:int* data;int size;GoodExample(int s) : size(s) {data = new int[size]; // 分配动态内存fill(data, data + size, 0); // 使用fill初始化数组为0(替代手动循环)}// 自定义复制构造函数(深拷贝)GoodExample(const GoodExample& obj) : size(obj.size) {data = new int[size]; // 为新对象分配独立内存copy(obj.data, obj.data + size, data); // 复制数据cout << "Deep copy constructor called" << endl;}~GoodExample() {delete[] data; // 释放动态内存}
};int main() {GoodExample obj1(3); // 创建包含3个元素的对象obj1.data[0] = 1; // 修改obj1的第一个元素为1GoodExample obj2 = obj1; // 调用深拷贝构造函数,创建obj2obj1.data[0] = 100; // 修改obj1的第一个元素为100// 输出obj2的第一个元素(深拷贝后应保持原值0,因fill初始化时设为0,且未被obj2修改)cout << "obj2.data[0] = " << obj2.data[0] << endl; return 0;
}
六、复制构造函数的高级特性
6.1 防止对象复制:删除复制构造函数
通过将复制构造函数声明为delete
,可以禁止对象的复制(C++11 及以后):
class NonCopyable {
public:NonCopyable() = default;NonCopyable(const NonCopyable&) = delete; // 禁止复制构造NonCopyable& operator=(const NonCopyable&) = delete; // 禁止拷贝赋值
};int main() {NonCopyable obj1;NonCopyable obj2 = obj1; // 编译错误:复制构造函数被删除return 0;
}
6.2 移动语义与复制构造函数
在 C++11 中,移动构造函数(Move Constructor
)与复制构造函数共同构成对象的复制 / 移动语义:
- 复制构造函数:处理左值引用(已存在的对象),执行深拷贝。
- 移动构造函数:处理右值引用(临时对象),转移资源所有权,避免深拷贝的开销。
示例:移动构造函数优化性能
#include <iostream>// 定义 Resource 类
class Resource {
public:int* data;// 构造函数,分配指定大小的内存Resource(int size) : data(new int[size]) {std::cout << "Constructor: " << data << std::endl;}// 复制构造函数(深拷贝)Resource(const Resource& rhs) : data(new int[rhs.data[0]]) {std::cout << "Copy Constructor: " << data << " from " << rhs.data << std::endl;}// 移动构造函数(资源转移)Resource(Resource&& rhs) noexcept : data(rhs.data) {rhs.data = nullptr; // 原对象置空,避免双重释放std::cout << "Move Constructor: " << data << " from " << rhs.data << std::endl;}// 析构函数,释放动态分配的内存~Resource() {delete[] data;std::cout << "Destructor: " << data << std::endl;}
};// 返回一个 Resource 对象
Resource getResource() {return Resource(10); // 返回右值,触发移动构造函数
}int main() {Resource obj = getResource(); // 优先调用移动构造函数return 0;
}
6.3 复制构造函数与 const 关键字
复制构造函数的参数必须为常量引用(const ClassName&
),原因如下:
- 允许接收临时对象:临时对象是右值,只能绑定到
const
引用。 - 避免递归调用:若参数为非
const
引用,复制构造函数内部创建临时对象时会再次触发复制构造,导致无限递归。
ClassName::ClassName(ClassName& obj) { // 错误:非const引用无法接收临时对象// 编译错误:无法从const ClassName转换为ClassName&
}
七、常见陷阱与最佳实践
7.1 陷阱一:遗漏复制构造函数导致资源泄漏
场景:类中包含动态资源(如文件句柄、网络连接),未定义复制构造函数:
class FileHandler {
public:FILE* file;FileHandler(const char* path) {file = fopen(path, "r"); // 打开文件}~FileHandler() {fclose(file); // 关闭文件}
};int main() {FileHandler f1("data.txt");FileHandler f2 = f1; // 默认浅拷贝,f1和f2的file指针相同// f1和f2析构时会两次关闭同一文件,导致程序崩溃return 0;
}
解决方案:定义复制构造函数,实现资源的深拷贝或禁止复制。
7.2 陷阱二:复制构造函数中未正确初始化成员
错误示例:
class Vector {
public:int* elements;int length;Vector(int len) : length(len) {elements = new int[len];}Vector(const Vector& vec) { // 错误:未初始化lengthcopy(vec.elements, vec.elements + vec.length, elements);}
};
正确实现:
Vector(const Vector& vec) : length(vec.length) { // 先初始化lengthelements = new int[length];copy(vec.elements, vec.elements + length, elements);
}
7.3 最佳实践:遵循 Rule of Three/Five
- Rule of Three(C++98):若类需要自定义析构函数、复制构造函数或拷贝赋值运算符中的任意一个,通常需要同时定义这三个函数。
- Rule of Five(C++11):新增移动构造函数和移动赋值运算符,若需要其中一个,通常需定义五个函数(析构 + 复制 + 移动)。
示例:完整资源管理类
#include <iostream>
#include <algorithm>class ResourceManager {
public:// 构造函数ResourceManager(int size) : data(new int[size]), length(size) {std::cout << "Constructor called with size: " << length << std::endl;}// 析构函数~ResourceManager() {std::cout << "Destructor called for size: " << length << std::endl;delete[] data;}// 复制构造函数ResourceManager(const ResourceManager& rhs) : data(new int[rhs.length]), length(rhs.length) {std::cout << "Copy constructor called for size: " << length << std::endl;std::copy(rhs.data, rhs.data + length, data);}// 拷贝赋值运算符ResourceManager& operator=(const ResourceManager& rhs) {std::cout << "Copy assignment operator called for size: " << length << std::endl;if (this != &rhs) {delete[] data;data = new int[rhs.length];length = rhs.length;std::copy(rhs.data, rhs.data + length, data);}return *this;}// 移动构造函数ResourceManager(ResourceManager&& rhs) noexcept :data(rhs.data), length(rhs.length) {std::cout << "Move constructor called for size: " << length << std::endl;rhs.data = nullptr;rhs.length = 0;}// 移动赋值运算符ResourceManager& operator=(ResourceManager&& rhs) noexcept {std::cout << "Move assignment operator called for size: " << length << std::endl;if (this != &rhs) {delete[] data;data = rhs.data;length = rhs.length;rhs.data = nullptr;rhs.length = 0;}return *this;}private:int* data;int length;
};int main() {// 测试构造函数ResourceManager rm1(5);// 测试复制构造函数ResourceManager rm2(rm1);// 测试拷贝赋值运算符ResourceManager rm3(3);rm3 = rm1;// 测试移动构造函数ResourceManager rm4(std::move(ResourceManager(7)));// 测试移动赋值运算符ResourceManager rm5(2);rm5 = std::move(ResourceManager(9));return 0;
}
八、总结
复制构造函数是 C++ 中对象复制的核心机制,其设计直接影响程序的正确性和性能。关键点总结:
- 何时需要自定义:当类中包含资源管理成员(如指针、文件句柄)时,必须定义复制构造函数实现深拷贝。
- 避免浅拷贝陷阱:默认复制构造函数的浅拷贝会导致资源共享,引发悬挂指针和双重释放。
- 结合现代 C++ 特性:利用移动语义(
std::move
)和const
引用提升性能与安全性。 - 遵循资源管理规则:严格遵守 Rule of Three/Five,确保析构函数、复制 / 移动函数的一致性。
九、参考资料
- 《C++ Primer(第 5 版)》这本书是 C++ 领域的经典之作,对 C++ 的基础语法和高级特性都有深入讲解。
- 《Effective C++(第 3 版)》书中包含了很多 C++ 编程的实用建议和最佳实践。
- 《C++ Templates: The Complete Guide(第 2 版)》该书聚焦于 C++ 模板编程,而
using
声明在模板编程中有着重要应用,如定义模板类型别名等。 - C++ 官方标准文档:C++ 标准文档是最权威的参考资料,可以查阅最新的 C++ 标准(如 C++11、C++14、C++17、C++20 等)文档。例如,ISO/IEC 14882:2020 是 C++20 标准的文档,可从相关渠道获取其详细内容。