Bootstrap

拷贝构造函数详解

拷贝构造函数详解

拷贝构造函数
int main()
{
	int a;
	int b = 10;
	int c(b); 
	return 0
}
//结果就是b和c的结果都是10
int main()
{
	int a;
	int b = 10;
	int c(b);

	string s1("hello");    
	string s2(s1);     //用s1去构造s2,然后s1和s2的内容就一模一样了
	return 0
}
//但是通过监视可以看出,s1和s2的地址是一样的,也就是说s1和s2是同一块空间,那么在进行析构的时候就会出现问题,也就是说,同一块空间析构了两次

在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎.那在创建对象时,可否创建一个与一个对象一某一样的新对象呢?

概念
  • 拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用
  • 为什么要给const进行修饰,因为我们在进行拷贝构造的时候,只是希望吧d1中的内容给d2,我们并不希望试图去改变d1中的内容,但是或许可能会不小心修改了d1中的内容,所以我们为了方便,给出const进行修饰,这样的话,就算我们不小心操作失误,d1中的内容也不会被改变的
#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()
	{
		//对于日期类来说,这里面没有什么资源是需要去释放的
		//所以对于日期类来说,给不给析构函数其实都没有什么影响
		cout << "~Date():" << this << endl;
		//看析构的是哪一个对象
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2020, 5, 5);
	Date d2(d1);

	return 0;
}
拷贝构造函数的特征
  • 拷贝构造函数是构造函数的一个重载形式。
  • 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用(原因在于,如果是利用值得方式来进行传递的话,就会开辟一段临时的空间,那么这个临时的空间也是需要构造的,那么这个临时的空间就会去调用构造函数,然后移植重复进行下去,就会造成无穷的递归调用)
    在这里插入图片描述
#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函数结束程序时,才调用析构函数。
  • 局部变量存在栈中,当函数退出时要一个个销毁,先进后出的原理
若未显示定义,系统生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝或者值拷贝

在这里插入图片描述

#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()
	{
		//对于日期类来说,这里面没有什么资源是需要去释放的
		//所以对于日期类来说,给不给析构函数其实都没有什么影响
		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对象仍然创建成功了,d2的内容和d1的内容是一摸一样的,同时,代码并没有什么问题,也没有崩溃,但是有些时候,编译器自动声明的拷贝构造函数是有问题的,比如针对string类来说
那么编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?

在这里插入图片描述

  • 也就是说s1和s2其实指向的是同一块堆内存空间,但是这么看来,是存在着很大的问题的,问题在于:s1和s2是栈上面的两个对象,这两个对象指向的是同一块地址空间,那么当在进行资源释放的时候,会先去释放s2,然后再去释放s1,那么在释放s2的时候,这一块空间肯定就会被释放掉,但是,在s2释放掉资源的时候,s1是不直到这块空间已经被释放掉了,s1仍然会去释放这块空间,那么这个时候,就会引起代码的崩溃
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
#include<iostream>
#include<stdlib.h>
#include<string.h>
using namespace std;
class String
{
public:
	String(const char* str = "")
	{
		cout << "String(const char* ):" << this << endl;
		if (nullptr == str)
			str = "";

		_str = (char*)malloc(strlen(str) + 1);
		strcpy(_str, str);
	}

	// 拷贝构造函数必须显式提供

	~String()
	{
		cout << "~String():" << this << endl;
		free(_str);
		_str = nullptr;
	}

private:
	char* _str;
};

void TestString()
{
    String s1("hello");
    String s2(s1);
}

int main()
{
	TestString();
	return 0;
}
  • 上面写的这个代码是有问题的代码,因为用s1去拷贝构造s2的话,通过监视看,s1和s2公用的是用一块内存空间,也就是说两个变量的地址是一样的,又因为先构造的后析构,后构造的析构,所以在释放空间的时候,是需要先去析构s2的,那么当s2析构完成了之后,那么s1就相当于是野指针了,再去析构s1的话,就会出现问题,从而代码崩溃(因为编译器所提供的默认的拷贝构造函数,是把s1中的内容原封不动的拷贝到s2中去的,当然包括s1中指针的地址,所以两个变量公用同一块堆上的内容)
    在这里插入图片描述
;