1 变量中如何拷贝
int main()
{
int a;
int b = 10;//10
int c = b; //10
int d(c); //10
}
b、c、d的结果都是10
结论:d© —> d = c
int main()
{
//调用无参构造函数
Date d0;
//调用全缺省参数的构造函数
Date d1(2020, 10, 12);
//复制d1对象中的值
Date d2(d1);
return 0;
}
同样,d1和d2是相同的
2 拷贝构造函数
2.1 引入
在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎.那在创建对象时,可否创建一个与一个对象一某一样的新对象呢?
拷贝构造函数:
----->只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
为什么要给const进行修饰?
----->因为我们在进行拷贝构造的时候,只是希望吧d1中的内容给d2,我们并不希望试图去改变d1中的内容,但是或许可能会不小心修改了d1中的内容,所以我们为了方便,给出const进行修饰,这样的话,就算我们不小心操作失误,d1中的内容也不会被改变的.
举个例子:
加const后:编译报错。–不能修改
class Date
{
public:
// 成员函数在类内实现,编译器可能会将其当成内联函数来处理
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 拷贝构造函数
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
cout << "Date(Date&):" << this << endl;
}
void PrintDate()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//调用无参构造函数
Date d0;
//调用全缺省参数的构造函数
Date d1(2020, 10, 12);
//复制d1对象中的值
Date d2(d1);
return 0;
}
#endif
2.2 拷贝构造函数标准格式
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
2.3 特征
2.3.1 拷贝构造函数是构造函数的一个重载形式。
2.3.2 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用
原因:如果是利用值得方式来进行传递的话,就会开辟一段临时的空间,那么这个临时的空间也是需要构造的,那么这个临时的空间就会去调用构造函数,然后移植重复进行下去,就会造成无穷的递归调用
2.3.3 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
// 这里d2调用的默认拷贝构造完成拷贝,d2和d1的值也是一样的。
Date d2(d1);
return 0;
}
- 如果用户没有显式实现拷贝构造函数,编译器会自动生成一份默认的拷贝构造函数。默认拷贝构造函数的拷贝方式:将参数对象中的内容原封不动的拷贝到新对象中
结论:这个代码我们没有显示的声明拷贝构造函数,但是d2对象仍然创建成功了,d2的内容和d1的内容是一摸一样的,同时,代码并没有什么问题,也没有崩溃。对于时期类,可以写拷贝构造函数,也可以系统默认,不存在资源管理,所以系统不会崩溃。
2.3.4 那么编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?当然像日期类这样的类是没必要的。对string类来说,系统就会崩溃。
#if 0
// 注意:如果类中涉及到资源管理时,该类必须要显式提供析构函数,
// 在析构函数中将对象的资源释放掉
// 否则:资源泄漏了
// 注意:编译器生成的默认拷贝构造函数是按照浅拷贝方式实现的
// 浅拷贝:将一个对象中的内容原封不动的拷贝到另一个对象中
// 后果:多个对象共享同一份资源,最终在对象销毁时该份资源被释放多次而引起代码崩溃
// Date类的拷贝构造函数是否实现没有任何影响
// 但是String的拷贝构造函数不实现不可以---因为编译器生成的默认拷贝构造函数是按照浅拷贝的方式实现的
// 注意:一个类中如果涉及到资源管理时,拷贝构造函数是必须要实现的
class String
{
public:
String(const char* str = "")
{
_str = (char*)malloc(strlen(str) + 1);
strcpy(_str, str);
}
// String类没有实现自己的拷贝构造函数,则编译器会生成一份
~String()
{
if (_str)
{
free(_str);
_str = nullptr;
}
}
private:
char* _str;
};
void TestString()
{
String s1("hello world");
String s2(s1);
}
int main()
{
TestString();
return 0;
}
#endif
上面写的这个代码是有问题的代码,因为用s1去拷贝构造s2的话,通过监视看,s1和s2公用的是用一块内存空间,也就是说两个变量的地址是一样的,又因为先构造的后析构,后构造的析构,所以在释放空间的时候,是需要先去析构s2的,那么当s2析构完成了之后,那么s1就相当于是野指针了,再去析构s1的话,就会出现问题,从而代码崩溃(因为编译器所提供的默认的拷贝构造函数,是把s1中的内容原封不动的拷贝到s2中去的,当然包括s1中指针的地址,所以两个变量公用同一块堆上的内容)
原因:也就是说s1和s2其实指向的是同一块堆内存空间,但是这么看来,是存在着很大的问题的,问题在于:s1和s2是栈上面的两个对象,这两个对象指向的是同一块地址空间,那么当在进行资源释放的时候,会先去释放s2,然后再去释放s1,那么在释放s2的时候,这一块空间肯定就会被释放掉,但是,在s2释放掉资源的时候,s1是不知道这块空间已经被释放掉了,s1仍然会去释放这块空间,那么这个时候,就会引起代码的崩溃。
例1:
例2:
结论:浅拷贝后果:导致两个对象会使用同一份资源,在对象销毁时候会导致一份资源释放多次而引起代码崩溃
2.3.5 什么类必须要给出拷贝构造函数?
- Date:类中没有涉及到资源管理,因此该类的拷贝构造函数可以不用给出,使用编译器生成的默认拷贝构造,如果需要做其他事 情,用户显式给出即可
- String:类中涉及到资源管理,该类必须显式提供拷贝构造函数,否则就会造成浅拷贝,让多个对象使用同一份资源,而在对象
析构时同一份资源释放多次而导致代码崩溃
2.4 拷贝构造函数调用场景:
Date d1;
Date d2(d1);
- 以类类型对象作为函数的参数或者返回值。
- 如果用户没有显式定义,编译器会生成一个默认的拷贝构造函数–默认拷贝构造函数是按照浅拷贝的方式实现
2.5 底层原理
#include<iostream>
using namespace std;
class Date
{
public:
//构造函数
Date(int year = 2020, int month = 5, int day = 5)
{
_year = year;
_month = month;
_day = day;
cout << "Date(int,int ,int):" << this << endl;
//看构造的是哪一个对象
}
//拷贝构造函数
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
cout << "Date(const Date&d):" << this << endl;
}
//析构函数
~Date()
{
//对于日期类来说,这里面没有什么资源是需要去释放的
//所以对于日期类来说,给不给析构函数其实都没有什么影响
cout << "~Date():" << this << endl;
//看析构的是哪一个对象
}
private:
int _year;
int _month;
int _day;
};
void TestDate()
{
Date d1(2020, 5, 5);
Date d2(d1);
}
int main()
{
TestDate();
return 0;
}
由析构的结果可以看出,先析构的是d2的内容,再去析构d1的内容,也就是说,先构造的后析构,后构造的先析构,原因在于:
- 在使用构造函数和析构函数时,需要特别注意对它们的调用时间和调用顺序。
- 在一般情况下,调用析构函数的次序正好与调用构造函数的次序相反:最先被调用的构造函数,其对应的(同一对象中的)析构函数最后被调用,而最后被调用的构造函数,其对应的析构函数最先被调用。
- 可以简记为:先构造的后析构,后构造的先析构,它相当于一个栈,先进后出。
- 什么时候调用构造函数和析构函数:
- 在全局范围中定义的对象(即在所有函数之外定义的对象),它的构造函数在文件中的所有函数(包括main函数)执行之前调用。但如果一个程序中有多个文件,而不同的文件中都定义了全局对象,则这些对象的构造函数的执行顺序是不确定的。当main函数执行完毕或调用exit函数时(此时程序终止),调用析构函数。
- 如果定义的是局部自动对象(例如在函数中定义对象),则在建立对象时调用其构造函数。如果函数被多次调用,则在每次建立对象时都要调用构造函数。在函数调用结束、对象释放时先调用析构函数。
- 如果在函数中定义静态(static)局部对象,则只在程序第一次调用此函数建立对象时调用构造函数一次,在调用结束时对象并不释放,因此也不调用析构函数,只在main函数结束或调用exit函数结束程序时,才调用析构函数。
- 局部变量存在栈中,当函数退出时要一个个销毁,先进后出的原理
往期链接:
cpp_9.1 类和对象中(七)—构造函数中,无参对象如何调用?底层如何实现?
cpp_8.2类和对象中(六) — 析构函数
cpp_8.1类和对象中(五) — 构造函数链接描述
cpp_8 类和对象上(四) — this遗留
cpp_7.1 类和对象上(三) — this
cpp_7类与对象上(二) — 类对象的存储方式
cpp_6.1类与对象上(一)— 类的引入