1. 简介
拷贝构造是一种特殊的构造函数,用于创建一个对象,该对象是从同一类中的另一个对象复制而来的。拷贝构造函数通常采用引用参数来接收要复制的对象,并使用该对象的副本来创建一个新对象。
2. 结构
class MyClass {
public:
MyClass(const MyClass& other) {
// 在此处实现拷贝构造函数
}
};
具体代码:
#include <iostream>
#include <string>
using namespace std;
//拷贝构造函数
class stu{
public:
string name;
int age;
stu(){
cout << "无参构造" <<endl;
}
stu(string name , int age ) : name(name) , age (age){
cout << "有参构造" <<endl;
}
//-=========================
//stu s2 = s1;
stu(stu & s){
cout << "运行拷贝构造函数" <<endl;
//把拷贝源对象的数据,拷贝到现在创建的对象身上。
name = s.name;
age = s.age ;
}
//==========================
~stu(){
cout << "析构函数" <<endl;
}
};
int main() {
stu s1("叶凡" ,24);
cout << "s1: " <<s1.name << " " << s1.age <<endl;
stu s2 = s1; //会执行拷贝构造函数
cout << "s2: " <<s2.name << " " << s2.age <<endl;
return 0;
}
运行结果:
有参构造
s1: 叶凡 24
运行拷贝构造函数
s2: 叶凡 24
析构函数
析构函数
可知 s1对象是有参构造,s2 对象利用 s1 拷贝构造的,两个是不同的对象,所以执行两次析构函数。
3.细节
拷贝构造函数:
- 由于是拷贝,所以函数的参数一定是拷贝源(源对象),如果是其他参数例如,则是有参构造
stu(int age)
- 参数一定是当前类的类型,不能是其他类型。
- 为什么拷贝构造函数的参数一定是引用的类型呢?
(1)避免无限递归调用:如果拷贝构造函数的参数不是引用,那么在执行拷贝构造函数时会一直调用自己,造成无限递归。这是因为拷贝构造函数的定义是使用一个同类对象来初始化新对象,如果参数不是引用,那么在创建参数对象时需要调用拷贝构造函数,如此就会形成无限递归,最终导致程序堆栈溢出。
stu(const stu & s){// stu s = s1; --->? stu s = s1; ---> stu s = s1;
(2)保证对象的数据正确性:拷贝构造函数的目的是创建一个与原对象完全相同的新对象。如果参数是值传递,那么在创建参数时就需要调用拷贝构造函数,这就会使得新对象与原对象不同。而使用引用传递,可以确保新对象与原对象完全一致。
- 拷贝构造函数,一般会给参数加上 const , 为什么?
为了确保在拷贝构造函数中不会对传递进来的对象进行修改,避免无意中修改原始对象。
4. 浅拷贝的问题
对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。如果数据中有属于动态成员( 在堆内存存放 ) ,那么浅拷贝只是做指向而已,不会开辟新的空间。默认情况下,编译器提供的拷贝操作即是浅拷贝。
#include <iostream>
#include <string>
using namespace std;
class stu{
public:
string * name = nullptr;
int age;
stu(){
cout << "无参构造" <<endl;
}
stu(string name , int age ) : name(new string(name)) , age (age){
cout << "有参构造" <<endl;
}
stu(stu & s){
cout << "拷贝构造" <<endl;
name = s.name;
age = s.age ;
}
~stu(){
cout << "析构函数" <<endl;
}
};
int main() {
stu s1("叶凡" ,24);
cout << "s1: " <<*s1.name << " " << s1.age <<endl;
stu s2 = s1;
*s2.name = "叶黑";
cout << "s1: " <<*s1.name << " " << s1.age <<endl;
cout << "s2: " <<*s2.name << " " << s2.age <<endl;
return 0;
}
运行结果:
有参构造
s1: 叶凡 24
拷贝构造
s1: 叶黑 24
s2: 叶黑 24
析构函数
析构函数
画图显示其中的关系:
浅拷贝的问题:
如果一个类有一个指向动态分配内存的指针成员变量
- 在拷贝对象和原始对象中,成员变量所指向的内存区域是相同的,如果在拷贝对象中修改了这些成员变量,那么原始对象中对应的成员变量也会被修改,这可能会导致意想不到的行为。
- 如果拷贝构造函数使用浅拷贝,那么拷贝对象和原始对象将共享同一块内存,如果在拷贝对象中释放了这块内存,那么原始对象会成为一个悬空指针,可能导致程序崩溃或者出现未定义的行为。
5. 深拷贝
#include <iostream>
#include <string>
using namespace std;
class stu{
public:
string * name = nullptr;
int age;
stu(){
cout << "无参构造" <<endl;
}
stu(string name , int age ) : name(new string(name)) , age (age){
cout << "有参构造" <<endl;
}
stu(stu & s){
cout << "拷贝构造" <<endl;
//name = s.name;
name = new string (*s.name);
age = s.age ;
}
~stu(){
cout << "析构函数" <<endl;
}
};
int main() {
stu s1("叶凡" ,24);
cout << "s1: " <<s1.name << " " << *s1.name << " " << s1.age <<endl;
stu s2 = s1;
cout << "s2: " <<s2.name << " " << *s1.name<< " " << s2.age <<endl;
*s2.name = "叶黑";
cout << "s1: " <<s1.name << " " << *s1.name<< " " << s1.age <<endl;
cout << "s2: " <<s2.name << " " << *s2.name<< " " << s2.age <<endl;
return 0;
}
运行结果:
有参构造
s1: 000001F37461E410 叶凡 24
拷贝构造
s2: 000001F3746235C0 叶凡 24
s1: 000001F37461E410 叶凡 24
s2: 000001F3746235C0 叶黑 24
析构函数
析构函数
图标说明如下:
深拷贝也是执行拷贝,只是在面对对象含有动态成员时,会执行新内存的开辟,仅仅是拷贝数据,而两个对象的指针成员有各自的空间。
6. 拷贝出现的场景
6.1 对象的创建依赖于其他对象
当使用一个对象初始化另一个对象时,拷贝构造函数将被调用来创建新对象的副本。
MyClass obj1;
MyClass obj2 = obj1; // 拷贝构造函数被调用来创建 obj2
6.2 函数参数(传递对象)
如果一个对象作为参数传递给一个函数,那么拷贝构造函数将被调用来创建一个新的对象,该对象是原始对象的副本。例如:
void func(MyClass obj) {
// ...
}
MyClass obj1;
func(obj1); // 拷贝构造函数被调用来创建 obj2
6.3 函数返回值 (返回对象)
如果一个函数返回一个对象,那么拷贝构造函数将被调用来创建返回值的副本。例如:
MyClass func() {
MyClass obj;
// ...
return obj; // 拷贝构造函数被调用来创建返回值的副本
}
只要存在了类型 对象 = 已有对象 ----> 就一定会执行拷贝构造函数。