C++学习之路-拷贝构造函数

拷贝构造函数

  • 什么是拷贝构造函数
  • 拷贝对象,拷贝了什么?
  • 拷贝构造函数的功能
  • 为什么要写拷贝构造函数?
  • 调用父类的拷贝构造函数
  • 拷贝对象不一定调用拷贝构造函数
  • 浅拷贝和深拷贝
    • 浅拷贝的特点
    • 深拷贝的特点

什么是拷贝构造函数

拷贝构造函数也是构造函数的一种。普通的构造函数可以是无参的,也可以是有参的。但是拷贝构造函数必须是有参的,如下所示,参数就是const类型的对象引用(格式固定,别问为什么)

class Car
{
public:
	Car() // 普通的构造函数
	{
		cout << "Car()" << endl;
	}

	Car(const Car &car)	 // 拷贝构造函数,参数是const类型的对象引用
	{
		cout << "Car():(const Car &car)" << endl;
	}

};

我们知道,当创建对象的时候会自动调用构造函数。那什么时候会调用拷贝构造函数呢?
答案是:当利用已存在的对象创建一个新对象时(拷贝对象),就会调用新对象的拷贝构造函数进行初始化新对象的成员变量。

int main()
{
	//创建对象,调用普通的构造函数 
	Car car1; //cout << "Car()" << endl;
	
	// 利用已经存在的对象,创建新对象,叫做拷贝对象,拷贝对象创建,就会调用拷贝构造函数
	Car car2(car1);	 // cout << "Car():(const Car &car)" << endl;

	getchar();
	return 0;
}

拷贝对象,拷贝了什么?

利用已经存在的对象去创建新的对象,叫作拷贝操作。拷贝对象,也就是拷贝了对象内存里的东西,也就是拷贝了原有对象的成员。用一段代码感受一下:

创建一个Car类,里面声明了两个成员变量。创建了一个带有默认参数的构造函数,并通过初始化列表的方式对成员变量进行初始化。声明了一个display函数,用于打印成员变量的值。

class Car
{
	int m_price;
	int m_length;
public:
	Car(int price = 0 ,int length = 0): m_price(price),m_length(length)	// 普通的构造函数
	{
		cout << "Car():m_price(price),m_length(length)" << endl;
	}

	void display()
	{
		cout << "m_price = " << m_price << " m_length = " << m_length << endl;
	}

};

我创建一个普通的对象,会默认的调用构造函数。由于我们构造函数是带有默认参数的,因此可以有三种初始化方式,同时也会得到不同的成员变量初始值。有疑问的可以去看以前的博客。

int main()
{
  // 对于我这样定义构造函数,那就可以有三种创建对象的方法
	Car car1;
	Car car2(100);
	Car car3(100, 20);
	car1.display(); // m_price = 0 m_length =0
	car2.display(); // m_price = 100 m_length =0
	car3.display(); // m_price = 100 m_length =20
	
	return 0;
}

接下来,我们要拷贝car3对象,拷贝给car4对象(注意:我在类中没有声明拷贝构造函数)。可以看出,我们确实把car3里成员变量拷贝给了car4,连成员变量的值都拷贝过来了。

int main()
{

	Car car3(100, 20);
	car3.display();

	// 利用已经存在的对象,创建新对象,叫做拷贝对象
	Car car4(car3);	
	car4.display(); // m_price = 100 m_length =20

	return 0;
}

此时car4对象内存空间里,也有这两个成员变量,且都有值。这就叫对象的拷贝操作,把对象里的成员变量都拷贝过来,成员变量的值也拷贝过来。注意:此时,我在类中并没有声明拷贝构造函数

car4依然可以把car3里的成员变量拷贝过来。就类似于下面的拷贝方式,直接把car3对象的值赋值给car4。

car4.m_price = car3.m_price;
car4.m_length  = car3.m_length ;

如果我在类中声明了拷贝构造函数

class Car
{
	int m_price;
	int m_length;
public:
	Car(int price = 0 ,int length = 0): m_price(price),m_length(length)	// 普通的构造函数
	{
		cout << "Car():m_price(price),m_length(length)" << endl;
	}

	Car(const Car &car)	 // 拷贝构造函数
	{
		cout << "Car():(const Car &car)" << endl;
	}

	void display()
	{
		cout << "m_price = " << m_price << " m_length = " << m_length << endl;
	}

};

当我再次拷贝car3对象时,car4打印的成员变量值是乱码,意味着并没有讲car3的值拷贝过来

Car car4(car3);	
car4.display(); // m_price = -827381223 m_length =-827381223

也就意味着,并没有执行下方操作。

car4.m_price = car3.m_price;
car4.m_length  = car3.m_length ;

那,这是为什么呢?????我写了拷贝构造函数,执行拷贝操作时,反而没有拷贝成功。我们又应该如何解决这一问题呢?????

拷贝构造函数的功能

拷贝构造函数既然是构造函数的一种,那必然是用来初始化成员变量的。既然是拷贝构造函数,那必然是用来初始化拷贝对象的成员变量的(也就是我们例子中的car4对象)。当我在类中自己定义拷贝构造函数,那也就意味着系统默认的拷贝操作不存在了,因为会调用自己默认的拷贝构造函数。

我们想要解决刚才的那个问题,只需要在拷贝构造函数里将传进来的对象的成员变量赋值给当前对象的成员变量即可(不写this也行)。

class Car
{
	int m_price;
	int m_length;
public:
	Car(int price = 0 ,int length = 0): m_price(price),m_length(length)	// 普通的构造函数
	{
		cout << "Car():m_price(price),m_length(length)" << endl;
	}

	Car(const Car &car)	 // 拷贝构造函数
	{
		cout << "Car():(const Car &car)" << endl;
		this->m_price = car.m_price;  // 将传进来的car的对象,传递给新创建的对象,不写this一样
		this->m_length = car.m_length;
	}

	void display()
	{
		cout << "m_price = " << m_price << " m_length = " << m_length << endl;
	}

};

那么在执行拷贝操作的时候,会默认调用自定义的拷贝构造函数,由于拷贝构造函数内部完成了成员变量的拷贝,所以自然会拷贝成功。

Car car4(car3);	//将car4的地址值,传递给拷贝构造函数的this指针
car4.display(); // m_price = 100 m_length =20

为什么要写拷贝构造函数?

如果我在类中定义了拷贝构造函数,那么拷贝对象创建的时候,就会直接调用自定义的拷贝构造函数。假如,此时拷贝构造函数里没有成员变量的拷贝,那结果就不会有成员变量的拷贝

如果我在类中没有定义拷贝构造函数,当我拷贝对象创建的时候,就会默认对成员变量进行拷贝,就不要额外写成员变量拷贝的代码

所以,如果重写拷贝构造函数,就必须自己手动的进行成员变量的拷贝。如果没有重写拷贝构造函数,那就啥事也没有,编译器会默认帮你拷贝类中所有的成员变量。

通过上面的例子发现,似乎不重写拷贝构造函数也能完成对象的拷贝。同时,写了拷贝构造函数之后,还需要手动的完成赋值操作,反而麻烦了。

但拷贝构造函数的奥义远不如此,我们这里先埋下一个伏笔,等我们了解了浅拷贝和深拷贝之后,再来回答这个问题。(如果类内成员变量都是基本数据类型:int、double…,不写拷贝构造函数完全没有问题)

导航回来,意味着我们了解了深拷贝和浅拷贝的区别。回答我们提出的问题:为什么要写拷贝构造函数?

当类中的成员变量有指针变量时,如果不重写拷贝构造函数,编译器默认的拷贝操作是浅拷贝操作。也就意味着仅仅将指针变量的地址值拷贝过去,而不会将指针指向存储空间里的内容拷贝过去。

所以,当类中成员变量有指针变量时,如果想要实现拷贝对象操作,则必须重写拷贝构造函数。

调用父类的拷贝构造函数

调用父类的构造函数,通常是用于子类对象初始化父类中的成员变量。这是因为父类中的成员变量是私有的private,子类中无法直接访问初始化,只能通过调用父类中的构造函数初始化。

class Person
{
	int m_age;
public:
	Person(int age):m_age(age){}
};


class Student : public Person
{
	int m_score;
public:
	Student(int age,int score):Person(age),m_score(score){}
};

调用父类的拷贝构造函数也是一样的道理。将子类对象传递给父类指针(父类指针指向子类对象),进而完成父类私有成员变量的拷贝

class Person
{
	int m_age;
public:
	Person(int age):m_age(age){}
	Person(const Person &person):m_age(person.m_age){} //拷贝构造函数

};

class Student : public Person
{
	int m_score;
public:
	Student(int age,int score):Person(age),m_score(score){}
	Student(const Student& student): Person(student),m_score(student.m_score){} 
	//调用父类拷贝构造函数,初始化子类继承于父类的成员变量
};

此时,stu2对象里的m_age拷贝成功,初始化为18。但是,如果子类和父类都不重写拷贝构造函数,也是可以完成拷贝操作

Student stu1(18, 100);
Student stu2(stu1);

子类构造函数调用父类构造函数是为了初始化父类中的成员变量。子类拷贝构造函数调用父类拷贝构造函数是为了拷贝父类中的成员变量。

拷贝对象不一定调用拷贝构造函数

当利用已存在的对象创建一个新对象时(拷贝对象),就会调用新对象的拷贝构造函数进行初始化新对象的成员变量。

上面这句话是前面给出的定义。请看下面代码,当我执行“car5 = car3;”时,也会完成拷贝操作,将car3对象的成员变量拷贝给car5。但是,却没有调用拷贝构造函数,这是怎么回事呢?

Car car3(100, 20);
Car car5;
car5 = car3;

仔细看定义,利用已存在的对象创建一个新对象时,会调用重写的拷贝构造函数。已存在的对象也就是car3对象,但是car5在拷贝的时候已经不是新对象了,也是已经存在的对象了。

下面代码,叫做创建新的对象(创建对象的同时,即刻拷贝操作)

Car car5 = car3;

浅拷贝和深拷贝

编译器默认提供的拷贝是浅拷贝

在叙述浅拷贝和深拷贝操作之前,我们先举一个例子,定义一个Car类,声明两个成员变量:注意有一个指针变量

class Car
{
	int m_price;
	char *m_name;
public:
	Car(int price = 0, char *name = NULL):m_price(price),m_name(name){ }

	void display()
	{
		cout << "m_price = " << m_price << " m_name = " << m_name << endl;
	}
};

然后,我们创建一个Car对象,并调用有参构造函数初始化成员变量。


int main()
{
	// 回忆C语言声明字符串
	 
	//第一种:常量字符串
	const char *const_name = "benz";
	//第二种: 字符数组,因为字符串的本质就是字符数据。必须以\0为结束标志位,后面即使出现字符,也不算
	char name[] = { 'b','e','n','z','\0','w','s'};
	cout << name << endl; // benz,没有后面的'\0','w','s'
	cout << strlen(name) << endl;	 //4, 字符串的长度不算‘\0’
	cout << sizeof(name) << endl;	 //7, 因为有7个字符

	// 创建对象,并初始化成员变量
	Car *car = new Car(100, name);	 
	
	getchar();
	return 0;
}

创建对象之后,我们将内存布局绘制出来,如下所示:

栈空间的对象指针car指向堆空间的Car对象内存区域,堆空间中的一个指针m_name又指向栈空间内存。

C++学习之路-拷贝构造函数_第1张图片

这种做法很危险(堆空间指向栈空间)。因为栈空间的内存无法掌控,随时可能被回收。假如栈空间的内存被回收掉,堆空间的指针仍然指向这块被回收掉的内存,这就是野指针了。(因为被回收的内存很可能会被另外一块程序使用,然而我堆空间的指针还指向这块内存,指向了我不想指向的内存,就容易出事)

解决方法是什么呢?将栈空间的东西拷贝到堆空间,让堆空间的指针指向堆空间,就不会出现上述问题。

C++学习之路-拷贝构造函数_第2张图片

所以,我们要完成拷贝操作,下面展示如何实现:

之前声明的构造函数指定是不能那么用了,因为还是会将字符数组的地址值直接传递给堆空间里的m_name

class Car
{
	int m_price;
	char *m_name;
public:
	Car(int price = 0, char *name = NULL):m_price(price)
	{ 
		// 申请新的堆空间,用于存放拷贝的栈空间内容
		m_name = new char[sizeof(name)]();	//加个括号,将内存空间全部初始化为0
		// 拷贝字符串的数据到新的内存空间,专门的API
		strcpy(m_name, name);	//将name地址指向的内存空间拷贝给m_name地址指向的内存空间
	}

	~Car()
	{
		delete[] m_name;
		m_name = NULL;
	}

	void display()
	{
		cout << "m_price = " << m_price << " m_name = " << m_name << endl;
	}

};

我们调用display函数

Car *car = new Car(100, name2);	
car->display();

debug模式下,查看m_name指向的内存空间存储的也是"benz",但是地址已经不跟name2相同了,因为m_name已经是指向堆空间内存了,通过car对象的地址也能验证这一点。这说明我们完成了栈空间到堆空间的拷贝操作

在这里插入图片描述

拷贝的过程中遇到一个bug:

strcpy在C++中不安全,然后然我们用别的,否则就发出警告。我们如何避免这种警告呢?

在这里插入图片描述
找到:项目 -> 属性 -> C/C++ -> 命令行
C++学习之路-拷贝构造函数_第3张图片
然后输入-D “_CRT_SECURE_NO_WARNINGS”,即可解决警告的问题。

浅拷贝的特点

现在我们要进行拷贝对象操作了,利用已存在的car对象,创建新的对象car2。car2调用display函数,打印没有问题。

Car *car1 = new Car(100, name2);
Car *car2 = car;
car2->display();  // m_price = 100 m_name = benz

首先我没有在类中重写拷贝构造函数,编译器会进行默认的拷贝行为:将car对象成员变量的值拷贝给car2对象。也就是完成默认完成下面两行操作:

car2.m_price = car1.m_price;
car2.m_name = car1.m_name;

但是这样有一个bug。那就是car1对象和car2对象内存空间中的m_name地址值是一样的(因为默认的拷贝操作是浅拷贝,因此直接把值拷贝过去就完事了),这也就意味着两个不同对象中有一个变量指向的是同一块内存

C++学习之路-拷贝构造函数_第4张图片

这有什么问题呢?

  • 如果我更改了car1的m_name,那car2中的m_name也跟着变了
  • 析构car1对象和car2对象,使得这块堆空间内存double free,也就是析构两次,释放两次。在有些平台一旦重复释放同一块内存,整个程序立马崩掉。

问题出在哪?问题就出现在默认的拷贝操作上,因为默认的拷贝操作是浅拷贝,也就是简单的将值拷贝。这种拷贝叫作:浅拷贝

浅拷贝一个非常明显的特点就是对于指针的拷贝:只会将指针里面存储的地址值拷贝过去,而不会将地址值指向的内存空间的内容拷贝过去。总结一句话就是:指针类型的变量只会拷贝地址值

深拷贝的特点

那怎么解决上述拷贝对象和源对象的指针成员变量指向同一块内存的问题呢?那就得在拷贝的过程中,采用深拷贝策略进行指针变量的拷贝,也就是下图所示:car2拷贝car1的时候,将m_name指向的内存空间也拷贝一份,让car2对象中的m_name指向,就能实现即拷贝了内容,又不会出现问题。

C++学习之路-拷贝构造函数_第5张图片
深拷贝的特点:产生新的内存空间,也可以理解为内容拷贝,而不是地址拷贝。怎么实现对象间的深拷贝呢?那就得自己重写拷贝构造函数了:将传进来的对象(car1)的m_name的地址值指向的内存空间拷贝给新对象(car2)的m_name的地址值指向的内存空间。这就完成了深拷贝。

class Car
{
	int m_price;
	char *m_name;
public:
	Car(int price = 0, char *name = NULL):m_price(price)
	{ 
		if (car.m_name == NULL) return;
		// 申请新的堆空间,用于存放拷贝的栈空间内容
		m_name = new char[sizeof(name)]();	//加个括号,将内存空间全部初始化为0
		// 拷贝字符串的数据到新的内存空间,专门的API
		strcpy(m_name, name);	//将name地址指向的内存空间拷贝给m_name地址指向的内存空间
	}

	Car(const Car &car)	:m_price(car.m_price)	//不是指针变量,正常写即可
	{
		if (car.m_name == NULL) return;
		// 申请新的堆空间,内存多大取决于传进来对象的car.m_name占用多大
		m_name = new char[sizeof(car.m_name)]();//加个括号,将内存空间全部初始化为0
		// 拷贝字符串的数据到新的内存空间,专门的API
		strcpy(m_name, car.m_name);	//将name地址指向的内存空间拷贝给m_name地址指向的内存空间
	}
	
	~Car()
	{
		delete[] m_name;
		m_name = NULL;
	}

	void display()
	{
		cout << "m_price = " << m_price << " m_name = " << m_name << endl;
	}

};

了解完深拷贝和浅拷贝之后,就可以回到开始的那个问题了。拷贝构造函数的作用?为什么要写拷贝构造函数?

补充一个tips,上面的类其实可以写成这样。由于深拷贝操作只在类内使用,我们可以将其封装成一个private函数,供构造函数和析构函数调用,可增强程序的可读性。

class Car
{
	int m_price;
	char *m_name;
	void DeepCopy(const char *name = NULL)
	{
		if (name == NULL) return;
		m_name = new char[sizeof(name)]();
		strcpy(m_name, name);
	}
public:
	Car(int price = 0, char *name = NULL):m_price(price)
	{ 
		DeepCopy(name);
	}

	Car(const Car &car)	:m_price(car.m_price)	//不是指针变量,正常写即可
	{
		DeepCopy(car.m_name);
	}

	~Car()
	{
		delete[] m_name;
		m_name = NULL;
	}

	void display()
	{
		cout << "m_price = " << m_price << " m_name = " << m_name << endl;
	}
};

你可能感兴趣的:(C++本质,C++面向对象,学习之路,c++)