C++中的复制构造函数详解

普通变量的复制

有时我们会在定义一个变量的同时使用另一个变量来初始化它。

int a_variable=12;
int new_variable(a_variable);

通过已有的同类型变量来初始化自身很有用。
对自定义类型的对象是否可以通过一个存在的对象方便的复制呢?

复制构造函数

复制构造函数又叫做拷贝构造函数,它只有一个参数(既然需要复制,一个就够了,若传入两个相同对象则没有意义,若传入两个不同的对象,就没必要叫做复制构造函数了),参数类型为本类的引用。

如果程序员没有编写复制构造函数,编译器会自动生成复制构造函数,在复制构造函数中按照成员变量进行逐字节复制(初学可以这样理解,实际上,个别编译器并不总是会自动生成复制构造函数,它们可能采用直接将源对象的各个值复制到目标对象对应的成员变量上,后面会介绍这种情况)。

class MyDate
{
	int day_;
	int year_;
public:
	MyDate(int day, int year)
	{
		day_ = day;
		year_ = year;
	}
	MyDate(const MyDate& date)
	{
		day_ = date.day_;
		year_ = date.year_;
		cout << "Date类的复制构造函数执行了!" << endl;
	}
	~MyDate() {}
};
void test()
{
	MyDate date1(12, 2021);
	MyDate date2(date1);
}
int main()
{
	test();
	system("pause");
	return 0;
}

执行为:

在这里插入图片描述

对于MyDate(const MyDate& date)参数列表中的const,因为复制构造函数参数另一个对象引用,如果不加const修饰,在此复制构造函数中可能会改变原对象的内容,为了安全起见,应加尽加。

如果程序员编写了复制构造函数,则编译器就不会生成默认复制构造函数了(所以编写了复制构造函数之后就尽量在函数体内实现复制操作,不要定义了复制构造函数却不完全或不实现复制操作)。

另一方面,所有的构造函数(包括复制构造函数)、析构函数都无法从父类继承,只能自己实现。

构造函数如果只有一个参数且这个参数为本类对象,就会与复制构造函数起冲突。如图:

在这里插入图片描述

复制构造函数的三种调用

复制构造函数在以下3种情况下会被调用:

1.当使用一个A类型对象去初始化另一个A类型的对象时(刚创建好的,已创建好的不算),会调用复制构造函数。注意观察以下代码:

Date date1(12,2012);//创建一个date1对象
Date date2(date1);//调用复制构造函数
Date date3=date1;//也会调用复制构造 函数
date2=date1;//date2已存在,不会调用复制构造函数,会调用赋值=操作函数

在这里插入图片描述

可以看到复制构造函数只调用了两次。

2.我们都知道C++传参有传值和传引用(指针本质上是传值,传的是实参的地址)。如果函数参数是一个自定义对象,那么会调用该自定义对象的复制构造函数。

在传值的时候,编译器会开辟一个空间(创建了一个临时对象)存储实参的值(这个过程会将实参的各个值分配复制给临时对象),再将该值压入栈中。

//类声明略
void TestFunction(MyDate date)
{
	cout << "TestFunction()执行了!" << endl;
}
void test()
{
	MyDate date1(12, 2021);
	TestFunction(date1);
}
//main函数略

结果如下:

在这里插入图片描述

3.如果函数的返回值是类MyDate的对象,则函数返回时,会调用该对象的复制构造函数。

//类声明略
MyDate TestFunction2()
{
	MyDate date1(12, 2021);
	cout << &date1 << endl;
	return date1;
}
void test()
{
	cout << &TestFunction2() << endl;
}
//main函数省略

从复制构造函数内的输出被两个地址输出夹住即可看出在哪里调用了复制构造函数。

在这里插入图片描述

复制构造函数的禁用

如果不希望自定义类型的复制构造函数被调用。

仅仅不编写复制构造函数是不行的,编译器可能会自动生成默认的复制构造函数。应该使用private修饰复制构造函数(这时编译器就不会生成自动复制构造函数),此时不要实现这个复制构造函数,如下:

在这里插入图片描述

这样便既禁止了用户调用此复制构造函数,又禁止了用户通过其他成员函数或友元函数间接地调用它(如果我们仅仅把复制构造函数声明为private,声明并实现了复制构造函数,虽然避免了用户直接调用,但成员函数和友元函数还是可以调用,只有不实现它才能永绝后患)。

Bjarne Stroustrup认为如果你希望禁止某些操作,就把它定义为一个私有的成员函数即可。

深拷贝与浅拷贝

如果成员变量含有指针类型,默认复制构造函数并不会将指针指向的内存中的值进行赋值,仅仅将指针存储的值(也就是一个地址)复制了一次(与我们所希望的不一致)。这时两个指针指向了同一块内存空间,一旦一种一个指针所属的对象声明周期结束,会调用它自己的析构函数回收指针指向的内存空间。这时另一个指针遍指向了一个垃圾值,这个指针也变为了空悬指针。以上就是我们常提到的浅拷贝。

实际开发当中要竭力避免以上清情况的发生(当成员变量含有指针或动态分配内存等情况)。

深拷贝如下:

class MyDate
{
private:
	char* buffer_;
public:
	MyDate(const char *init);//实现略
	MyDate(const MyDate &date)
	{
		if(date.buffer_!=nullptr)
		{
			buffer_=new char[strlen(date.buffer_)+1];
			strcpy(buffer_,date.buffer_);
		}
		else
		{
			buffer=nullptr;
		}
	}
}

复制构造函数先检查date中的buffer_的字符串大小,然后分配此大小+1的内存给新创建的对象的buffer_(strlen函数不会计算""字符),最后使用strcpy函数将date的buffer_指向的内存中的内容复制到新创建的对象的buffer_所指向的空间(strcpy函数会吧""字符一并复制)。最后实现了两个指针指向了不同的存储空间(两个空间的内容相同)。

在这里插入图片描述

如果我们要编写需要字符的成员时,尽量使用string。它会像其他成员变量一样进行复制,因为string有自己的复制构造函数。

一定会生成默认复制构造函数吗?

我们前面提到如果程序员没有定义自己的复制构造函数,编译器会为我们生成一个默认复制构造函数。

实际上以下4中情况编译器会为我们生成默认复制构造函数。

1.没有为类编写复制构造函数,但该类含有自定义类型或string等类型作为成员变量时。

2.没有为类编写复制构造函数,但该类继承了一个含有复制构造函数的类时,编译器会生成默认复制构造函数,在该函数中调用父类的复制构造函数。

3.没有为类编写复制构造函数,但是该类定义了虚函数或者该类的父类定义了虚函数。,

4.没有为类编写复制构造函数,但是该类有虚基类。

参考

The C++ Programming Language (美) Bjarne Stroustrup

Thinking in C++ Volume One:Introduction toStandard C++ (美)Bruce Eckel

C++新经典 对象模型 王建伟

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注云海天教程的更多内容!