C++(day 7)什么时候需要自己定义拷贝构造函数?

拷贝构造函数的定义

拷贝其实可以理解为复制,例如我们常见的整型数据之间的复制:

int a = 5;
int b = a;		//用a的值拷贝给新创建的b

上述代码中,a的值是由一个常数5赋值的,而b的值是由一个和它相同类型的a赋值的。
类比我们的类对象,类对象的创建首先要调用构造函数来初始化,这个构造函数可以是默认构造函数的也可以是我们自己定义的构造函数。那么类对象能不能不通过构造函数,而由另一个类“复制”给新的类对象呢?
答案是可以的。
例如下面的代码,声明了一个Student类:

class Student
{
public:
	Student(const char* pName = "no name", int ssId = 0)
	{
		id = ssId;
		strcpy(name, pName);
		cout << "Constructing new student " << pName << endl;
	}
	Student(Student& s)
	{
		cout << "Constructing copy of " << s.name << endl;
		strcpy(name, "copy of");
		strcat(name, s.name);
		id = s.id;
	}
	~Student()
	{
		cout << "Destructing " << name << endl;
	}
protected:
	char name[40];
	int id;
};

int Student::i = 0;

void fn(Student s)
{
	cout << "In function fn()\n";
}

int main()
{
	Student randy("Randy", 1234);
	cout << "Calling fn()\n";
	fn(randy);
	cout << "Returned from fn()\n";
}

在main函数中,我们先用Student的带参构造函数构造了Student类对象randy,由于调用了构造函数,所以构造函数被执行了一次,输出"Constructing new student Randy";执行完main函数的创建对象语句后,执行cout语句,输出了第二句话"Calling fn()" ;紧接着调用了fun函数,我们知道调用函数时实参要拷贝给形参(值传递),所以这里发生了一次对象的拷贝,对象拷贝需要调用拷贝构造函数Student(Student& s),输出的第三条语句是"Constructing copy of Randy",在拷贝构造函数内部strcat()之后新建立的对象的name为"copy of randy",紧接着进入fn()函数体,输出第四条语句"In function fn()",在函数返回时,需要释放临时变量对象s,s调用了一次析构函数~Student(),输出第五条语句"Destructing copy of Randy",回到main函数,输出第六条语句"Returned from fn()",在main()函数结束时调用析构函数析构了对象randy,于是输出第七条语句"Destructing Randy"。
我们把上述代码运行一下,看看对不对:
拷贝构造函数
应当注意到: 由于我们在拷贝给新的对象时不是用构造函数初始化的,而是基于旧的对象,因此需要调用的函数的参数是Student&。也就是: Student(Student& s);

如果不自行写拷贝构造函数会怎样?

为什么C++要用上面的拷贝构造函数,而它自己不会做像整型变量复制那样呢?
答:因为对象的类型多种多样,不像基本数据类型那么简单,有些对象还申请了系统资源。如图所示,
不同对象指向同一资源
如果仅仅是二进制内存空间上的s拷贝,那意味着t也拥有了这个资源。
举个例子,s申请到了一台打印机的打印单号,表示s可以在该台打印机打印了,而拷贝给对象t时使用的默认拷贝构造函数是将类成员数据直接赋值给t,也就是说把对象s的打印单号复印一份给了对象t,那么如果是对象t先去打印了,当它打印完自然得调用析构函数将自己的打印单号给失效(表示当前单号的服务已结束,该单号失效),而当对象s拿着同样的单号去要求打印服务时,由于相同单号已被对象t使用过了,打印机就会说对象s给的单号是无效的。
总之,因为默认拷贝构造函数只是把类成员数据给复制了一份,不自行写拷贝构造函数的话,如果对象中有对资源的使用权限时,由于当对象消灭时都会在析构函数里将申请到的资源释放掉,这就会导致另一个对象无法使用该资源。
上述举例中申请到的资源就是打印机单号。
下面我们用一段代码来看看不自行拷贝构造函数的结果(就是在前面的代码加了静态数据成员count,并在构造函数和析构函数对其进行操作,并把拷贝构造删掉了)

#include <iostream>
#include<string.h>
using namespace std;

class Student
{
public:
	Student(const char* pName = "no name", int ssId = 0)
	{
		id = ssId;
		strcpy(name, pName);
		cout << "Constructing new student " << pName << endl;
		count++;
		cout << "静态变量的值为:" << count <<endl;
	}
	~Student()
	{
		cout << "Destructing " << name << endl;
		count--;
		cout << "静态变量的值为  " << count << endl;
	}
protected:
	char name[40];
	int id;
	static int count;
};

int Student::count = 0;

void fn(Student s)
{
	cout << "In function fn()\n";
}

int main()
{
	Student randy("Randy", 1234);
	cout << "Calling fn()\n";
	fn(randy);
	cout << "Returned from fn()\n";
}

在上述代码中,count表示当前已有的类对象,当有对象被构造时count加1,当有对象被析构时count减1.
我们只看main函数,首先构造了randy对象,此时对象个数为1,接着调用fn函数时拷贝了一个对象,此时对象个数为2,fn结束时对象被析构,此时对象个数为1个,最后main函数结束时对象randy被析构,此时对象个数为0个。这是我们预期的,那么我们来看看实际运行结果:
不自己创建拷贝构造函数
最后输出的count居然为-1!看第五个输出结果,拷贝出来的对象中name和被拷贝对象的name一样,这是因为默认构造函数只负责把类数据成员复制了,我们必须自己写一个能够将静态数据成员count加1的拷贝构造函数。
通过打印机的例子和代码的例子,相信你已经知道了自行写拷贝构造函数的原因和重要性了。

默认拷贝构造函数

C++提供的默认拷贝构造函数工作的方法是,完成一个成员一个成员的拷贝。如果成员是类对象,则调用成员对象的拷贝构造函数或者默认拷贝构造函数。
代码如下:

#include <iostream>
#include <string.h>
using namespace std;

class Student
{
public:
	Student(const char* pName = "No name")
	{
		cout << "Constructing new student  " << pName << endl;
		strcpy(name, pName);
		name[sizeof(name)-1]='\0';
	}

	Student(Student& s)
	{
		cout << "Constructing copy of " << s.name << endl;
		strcpy(name, "copy of ");
		strcat(name, s.name);
	}

	~Student()
	{
		cout << "Destructing " << name << endl;
	}
protected:
	char name[40];
};

class Tutor
{
public:
	Tutor(Student& s) :student(s)
	{
		cout << "Constructing tutor\n";
	}
protected:
	Student student;
};

void fn(Tutor tutor)
{
	cout << "In function fn()\n";
}

int main()
{
	Student randy("Randy");
	Tutor tutor(randy);
	cout << "Calling fn()\n";
	fn(tutor);
	cout << "Returnde from fn()\n";
}

运行结果如下:
默认拷贝构造函数
第一条语句创建了Student的对象randy,对象的成员数据name为Randy,输出了第一条语句。紧接着创建了Tutor的对象tutor,其成员数据student用对象randy初始化,在实参randy传给新参s的过程中调用了Student类的拷贝构造函数,因此输出了第二条语句。进入Tutor的构造函数体后输出了第三条语句。创建好tutor对象后来到main函数
输出了第四条语句。调用fn()函数,参数类型是Tutor,在传值过程中又是要调用类的拷贝构造函数,不过Tutor类并没有自定义的拷贝构造函数,调用的是默认拷贝构造函数,由于它的数据成员是一个Student对象,在复制Student对象时调用了Student类的自定义拷贝构造函数,因此输出了第五条语句。拷贝完Tutor类对象后进入fun()函数体,输出第六条语句,结束fun()时,将形参tutor析构,析构顺序是先析构tutor中的student类成员,所以调用了student的析构函数,输出了第七条语句。完成析构形参tutor后回到主函数main,输出第八条语句。函数结束时先析构了对象tutor,同样的在析构对象tutor中student时输出了第九条语句。最后析构randy对象,输出了第十条语句。

总结:在对象复制给对象的过程中就要用到拷贝构造函数,到底是自己定义拷贝构造函数还是使用默认拷贝构造函数,这就要看看这个被拷贝的对象是否占有资源,如果占有资源,我们要把资源也复制一份给新对象,而不是把使用被拷贝对象使用资源的“权限”复制给新对象。(此时这就是浅拷贝和深拷贝的区别了)

C++中,我们可以创建一个名为`Date`的类来表示日期,并包含必要的成员变量如年份、月份和日期。同时,我们会定义构造函数、拷贝构造函数以及析构函数。以下是相关的代码示例: ```cpp #include <iostream> class Date { public: // 构造函数 Date(int year = 0, int month = 1, int day = 1) : year(year), month(month), day(day) { if (month < 1 || month > 12) { std::cerr << "Invalid month!" << std::endl; } } // 拷贝构造函数 Date(const Date& other) : year(other.year), month(other.month), day(other.day) {} // 判断是否为闰年 bool isLeapYear() const { if (year % 4 != 0) return false; // 年份不是4的倍数直接返回false else if (year % 100 != 0) return true; // 如果不是世纪年,能被4整除就是闰年 else if (year % 400 != 0) return false; // 否则是世纪年,还要看能否被400整除 else return true; } // 析构函数(默认的无操作) ~Date() {} private: int year; int month; int day; }; int main() { // 创建一个Date对象 Date myDate(2024, 2, 29); // 假设是闰年的2月29日 // 输出是否为闰年 std::cout << "Is " << myDate.year << " a leap year? " << (myDate.isLeapYear() ? "Yes" : "No") << std::endl; // 拷贝一个对象 Date copyDate(myDate); std::cout << "Copy of the date: " << copyDate.year << "/" << copyDate.month << "/" << copyDate.day << std::endl; return 0; } ``` 在这个例子中,我们首先定义了`Date`类,包含了构造函数用于初始化年、月、日,拷贝构造函数用于复制已有对象,以及一个`isLeapYear`方法来检查给定的年份是否为闰年。在主函数中,我们创建了一个实例并判断其是否为闰年,同时也展示了如何通过拷贝构造函数创建一个对象。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值