目录
一、默认拷贝构造函数
默认的拷贝构造函数会在以下几种情况下自动被调用:
-
对象初始化时的复制:当你用一个已存在的同类型对象来初始化一个新的对象时,如
ClassA obj2 = obj1;
,这里会调用默认拷贝构造函数来创建obj2
,将其初始化为obj1
的副本。 -
函数参数传递:当你将一个对象作为参数,以值传递的方式传递给一个函数时,会调用拷贝构造函数来创建函数内的临时对象副本。例如,
void func(ClassA obj);
调用func(obj1);
时。 -
函数返回值:当一个函数以值方式返回一个对象时,为了将函数内部的对象复制给调用者,会调用拷贝构造函数创建返回对象的副本。例如,
ClassA func() { ClassA localObj; return localObj; }
。 -
容器类操作:当你将对象插入到STL容器(如
std::vector
,std::list
等)中时,如果这些容器需要复制元素(比如在扩容时),它们会使用拷贝构造函数来复制对象。 -
赋值操作的副产品:在执行某些赋值操作前,可能会先创建一个临时对象,此时会调用拷贝构造函数。尽管这不是直接的赋值操作,但在某些编译器优化或特定情境下可能会发生。
默认拷贝构造函数执行浅拷贝,即逐个成员地复制对象的数据,对于内置类型和POD(Plain Old Data)类型来说,这通常能满足需求。但如果对象中有指针成员指向动态分配的内存,或者有成员是自定义类的对象,仅执行浅拷贝会导致问题(如两对象共享同一块内存),这时就需要自定义拷贝构造函数来执行深拷贝。
默认拷贝构造函数的示例:
#include <iostream>
#include <vector>
class MyClass {
public:
MyClass(int id) : _id(id) {
std::cout << "MyClass object with ID " << _id << " is being constructed." << std::endl;
}
// 默认拷贝构造函数,由编译器自动生成
// 当以下场景发生时,此默认构造函数会被调用
// 1. 对象初始化时的复制
// 2. 函数参数传递
// 3. 函数返回值
// 4. 容器类操作等
int getId() const { return _id; }
private:
int _id;
};
// 函数以值传递方式接收对象
void printObject(MyClass obj) {
std::cout << "Printing object with ID " << obj.getId() << std::endl;
}
// 函数返回对象
MyClass createObject() {
MyClass obj(2);
std::cout << "Creating and returning an object with ID " << obj.getId() << std::endl;
return obj;
}
int main() {
MyClass obj1(1); // 显示构造调用
MyClass obj2 = obj1; // 调用默认拷贝构造函数
std::cout << "obj2's ID: " << obj2.getId() << std::endl;
printObject(obj1); // 函数参数传递,调用默认拷贝构造函数
MyClass obj3 = createObject(); // 函数返回值,调用默认拷贝构造函数
std::cout << "obj3's ID: " << obj3.getId() << std::endl;
std::vector<MyClass> vec;
vec.push_back(obj1); // 容器操作,可能调用拷贝构造函数(或移动构造函数,取决于编译器优化)
return 0;
}
在这个示例中,MyClass
是一个简单的类,包含一个整型成员_id
。当我们通过值传递方式创建对象的副本,将对象作为函数参数,从函数返回对象,或在容器中存储对象时,编译器都会自动调用默认的拷贝构造函数来完成对象的复制。
二、自定义拷贝构造函数
自定义拷贝构造函数的示例:
#include <iostream>
#include <vector>
class CustomClass {
public:
// 自定义构造函数
CustomClass(int size) : data(new int[size]), dataSize(size) {
std::cout << "CustomClass object with size " << dataSize << " created." << std::endl;
for (int i = 0; i < dataSize; ++i) {
data[i] = i;
}
}
// 自定义拷贝构造函数
CustomClass(const CustomClass &other) {
std::cout << "Custom copy constructor called." << std::endl;
dataSize = other.dataSize;
data = new int[dataSize];
for (int i = 0; i < dataSize; ++i) {
data[i] = other.data[i]; // 深拷贝数据
}
}
// 析构函数,释放动态分配的内存
~CustomClass() {
delete[] data;
std::cout << "CustomClass object destructed." << std::endl;
}
int getSize() const { return dataSize; }
int getElementAt(int index) const { return data[index]; }
private:
int *data; // 指向动态分配数组的指针
int dataSize; // 数组的大小
};
void displayObject(const CustomClass &obj) {
std::cout << "Displaying object with size " << obj.getSize() << std::endl;
for (int i = 0; i < obj.getSize(); ++i) {
std::cout << obj.getElementAt(i) << " ";
}
std::cout << std::endl;
}
int main() {
CustomClass original(5); // 创建原始对象
CustomClass copy(original); // 使用自定义拷贝构造函数创建副本
std::cout << "Copy's size: " << copy.getSize() << std::endl;
displayObject(copy); // 显示副本的内容
return 0;
}
在这个例子中,CustomClass
类有一个指向动态分配数组的指针。我们自定义了拷贝构造函数来执行深拷贝,确保当复制对象时,也复制了指向的数组内容,而不是仅仅复制指针。这样,原始对象和其副本就各自拥有独立的内存空间,修改一个对象不会影响另一个对象的数据。
三、对象赋值
定义:对象拷贝就是对象赋值。
相同点:对象赋值和使用默认(自定义)拷贝构造函数一样,如果赋值的对象需要做深拷贝,那么必须自定义赋值操作符(即需要重载=号运算符)。
不同点:和使用自定义拷贝构造函数不一样的地方是(使用场景不一样,下一小节会说明,这里简单说一下:由于对象赋值针对的是已经存在的对象,自定义拷贝构造函数针对的是新创建的对象。):自定义赋值操作符必须注意,待被赋值的对象已经存在,必须先将原来的资源释放掉(指针变量等资源),才可以进行深拷贝式的赋值。
示例:
#include <iostream>
// 继续之前的类定义
// 赋值运算符重载
MyClass& operator=(const MyClass &other) {
std::cout << "Assignment operator called." << std::endl;
if (this != &other) { // 防止自我赋值
delete data; // 释放原有资源
data = new int(*other.data); // 深拷贝数据
}
return *this;
}
int main() {
MyClass obj1(10);
MyClass obj2(20);
obj2 = obj1; // 这里会调用自定义赋值运算符
return 0;
}
注意:赋值操作必须判断对象是否在给自己本身赋值:代码中的(if (this != &other) { // 防止自我赋值);赋值操作的返回不应该是临时对象:代码中的(return *this)
四、何时使用默认(自定义)拷贝构造函数和对象赋值
在C++中,选择使用默认拷贝构造函数还是自定义拷贝构造函数,以及何时使用对象赋值,主要取决于你的类中数据成员的性质以及你希望如何管理对象的复制行为。
(一)使用默认拷贝构造函数的情况:
-
简单数据类型:如果你的类只包含基本数据类型(如int, double, char等)或POD(Plain Old Data)类型作为数据成员,且不需要在复制时执行特殊操作,那么使用默认的拷贝构造函数通常是足够的。
-
不需要深拷贝:如果你的类中没有动态分配的内存或资源,或者你确信默认的浅拷贝行为能满足你的需求(比如,复制时你希望两个对象共享某些资源),则可以依赖默认的拷贝构造函数。
(二)使用自定义拷贝构造函数的情况:
-
深拷贝需求:当你的类中包含指针或引用成员变量,指向动态分配的内存或外部资源时,需要自定义拷贝构造函数来进行深拷贝,即为新对象分配新的内存,并复制原来内存中的内容,以避免多个对象共享同一块内存导致的问题。
-
资源管理:如果你的类负责管理某种资源(如文件句柄、网络连接等),在复制对象时可能需要创建资源的新副本,或者实现资源引用计数等高级资源管理策略。
-
执行特定初始化:在复制对象时可能需要执行一些额外的初始化操作,比如更新某些计数器、记录日志等。
(三)对象赋值(赋值运算符重载)的使用:
- 当已经存在一个对象,并希望将其状态复制给另一个已经存在的对象时,就需要使用对象赋值(通常是通过重载赋值运算符
=}
来实现)。这是与拷贝构造函数不同的场景,拷贝构造函数用于创建新对象时的初始化,而赋值运算符用于已有对象状态的替换。
(四)总结:
- 默认拷贝构造函数适合于简单数据结构或无需特殊处理的复制行为。
- 自定义拷贝构造函数适用于需要深拷贝、资源管理或执行特定初始化逻辑的类。
- 对象赋值(赋值运算符重载)适用于已有对象状态的更新,与拷贝构造函数在使用场景上有所区分,但两者都需根据实际需求考虑是否需要自定义实现。
- 一般来说,赋值操作符是与拷贝构造函数和析构函数结队而行的。
参考文献
[1] 钱能. C++程序设计教程(第二版)[M]. 北京: 清华大学出版社, 2005.9.