拷贝(复制)构造函数
用一个已经存在的对象初始化另一个新对象时,编译器会自动调用拷贝构造函数。
1、拷贝构造函数是构造函数的一种重载形式
2、拷贝构造函数的参数:单个形参,传递const类类型的引用
1)如果传值引用,会引发无穷调用
2)如果不加const,可能会因为我们不小心写错代码,改变已经存在的对象
例1:拷贝构造函数参数不加const
class Date
{
public:
Date(int year = 2000, int month = 4, int day = 21)
{
_year = year;
_month = month;
_day = day;
}
// 拷贝构造函数
Date(Date& d) // 不加const
{
_year = d._year;
_month = d._month;
_day = d._day++; // 假如这我们不小心写错了
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2021, 4, 19);
Date d2(d1);
system("pause");
return 0;
}
创建d1调用构造函数之后,初始化好的d1对象中的成员变量的值如下图:
当我们调用拷贝构造函数初始化完d2后,d1的值发生了改变,我们只是想用d1的值初始化d2,却把d1改了,这岂不是很离谱
当我们加上const,它就不允许我们修改了,这就防止了我们手滑写错
3、编译器自动调用拷贝构造函数的三种情况
1) 用一个对象初始化同类的另一个对象
2)当函数的参数为类的对象时。在调用函数时需要将实参对象完整的传递给形参,形参实例化时会对实参进行拷贝,按实参复制一个形参,系统通过调用拷贝构造函数来实现的,这样能保证形参具有和实参完全相同的值
3)函数的返回值是类的对象。在函数调用完毕将返回值带回函数调用处时。此时需要将函数中的对象复制一个临时对象并传给该函数的调用处。
未显式定义拷贝构造函数,编译器自动生成的默认拷贝构造函数的拷贝方式是浅拷贝
4、必须显式定义拷贝构造函数的场景
1)当类中不涉及资源管理时,我们自己显式定义的拷贝构造函数和编译器自动生成的,最终效果是一样的
例2:Date类中不涉及资源管理
① 显式定义拷贝构造函数
class Date
{
public:
Date(int year = 2000, int month = 4, int day = 21)
{
_year = year;
_month = month;
_day = day;
}
// 拷贝构造函数
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2021, 4, 19); // 调用构造函数
Date d2(d1); // 调用拷贝构造函数
system("pause");
return 0;
}
② 还是Date类,未显式定义拷贝构造函数,编译器自动生成默认拷贝构造函数
class Date
{
public:
Date(int year = 2000, int month = 4, int day = 21)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2021, 4, 19);
Date d2(d1);
system("pause");
return 0;
}
和上边的监视窗口中的内容一模一样
2)当类中涉及到资源管理
例3:String类中,拷贝构造函数中使用malloc在堆上申请空间
① 未显式定义拷贝构造函数
class String
{
public:
// 构造函数
String(const char* str = "jia")
{
_str = (char*)malloc(strlen(str) + 1);
strcpy(_str, str);
}
// 析构函数
~String()
{
free(_str);
_str = nullptr;
}
private:
char* _str;
};
// 当TestString函数执行完,s1、s2就要被销毁,这样可以看到销毁时调用析构函数的结果
// 如果直接写到main函数中,要return之后,对象才会被销毁,看不到调用析构函数的结果,需要将输出定向到文件中
void TestString()
{
String s1("hello");
String s2(s1);
}
int main()
{
TestString();
system("pause");
return 0;
}
s1和s2中_str指向同一块内存空间
s1和s2中的_str变量的地址相同,这两个对象指向同一块内存空间。
调用完TestString()函数后,s1和s2对象销毁时,会调用析构函数,先释放s2中的资源,可以看到s2指向的地址已经变成了空,而s1中访问的空间已经变成了无效访问
也就是说,此时堆上申请的空间已经被释放了,当s1调用析构函数再次free这段空间时,就会出错
② 显式定义拷贝构造函数
class String
{
public:
// 构造函数
String(const char* str = "jia")
{
// 从堆上开辟能保存字符串大小的一段空间,并将这段空间的首地址传给_str
_str = (char*)malloc(strlen(str) + 1);
// 将str字符串中的字符一个一个拷贝到_str指向的空间中
strcpy(_str, str);
}
// 拷贝构造函数
String(const String& s)
{
_str = (char*)malloc(strlen(s._str)+1);
strcpy(_str, s._str);
}
// 析构函数
~String()
{
cout << "~String()" << endl;
free(_str);
_str = nullptr;
}
private:
char* _str;
};
void TestString()
{
String s1("hello");
String s2(s1);
}
int main()
{
TestString();
system("pause");
return 0;
}
s1中_str和s2中_str指向各自的内存空间
这次s2初始化时调用了显式定义的拷贝构造函数,我们在拷贝构造函数中给s2对象的_str在堆上新申请了一块空间,s1和s2各自拥有一段内存空间,所以s1和s2中_str中的地址不同
销毁对象时,调用析构函数free的时候,各自释放自己的那块空间,就不会出错了
结论:类中涉及到资源管理时一定要显式定义拷贝构造函数!