写在最前面:
除非是有必要,否则不要自己手动书写拷贝构造函数。网上大佬们提出在以下情况需要自己写构造函数:
1、这里有个简单的规则:如果你需要定义一个非空的析构函数,那么通常情况下你也需要定义一个拷贝构造函数。
2、有一个原则:一般来说你在类中进行了new操作(有new,就有delete),你就需要析构函数(执行堆内存释放delete),在你需要析构函数的类中,一般需要加上挎贝构造函数和赋值函数。
在第10课中讲过类对象的复制,类对象的复制就是每个成员变量逐个复制,下面再讲解一下类对象的复制。
在.h头文件这样声明:
#ifndef __MAIN_H__
#define __MAIN_H__
class student
{
public:
int age;
int hight;
int weight;
int num;
student();
};
#endif // !__MAIN_H__
在.cpp文件这样定义无参构造函数和对象复制:
#include#include #include #include"main.h" using namespace std; student::student() { age = 20; hight = 185; weight = 80; num = 1002; cout << "age = " << age << endl; cout << "hight = " << hight << endl; cout << "weight= " << weight << endl; cout << "num = " << num << endl; }; int main() { student stu1; //调用默认构造函数(不带形参) student stu2 = stu1; //没有调用构造函数,但stu2的成员变量值也等于stu1的成员变量了 student stu3 (stu1); //没有调用构造函数,但stu2的成员变量值也等于stu1的成员变量了 student stu4 { stu1 };//没有调用构造函数,但stu2的成员变量值也等于stu1的成员变量了 student stu5={ stu1 };//没有调用构造函数,但stu2的成员变量值也等于stu1的成员变量了 return 0; }
运行结果信息如下:
下面引出拷贝构造函数的概念
如果一个类的构造函数的第一个参数是属于类类型引用(建议在第一个参数前面加constX修饰),若有额外的参数,那么这些额外的参数都有默认值。该构造函数的默认参数必须放在函数声明中,除非该构造函数没有函数声明,那么这个构造函数就叫拷贝构造函数。这个拷贝函数会在一定的时机被系统自动调用。下面对拷贝构造函数进行举例子。
.h头文件如下:
#ifndef __MAIN_H__
#define __MAIN_H__
class student
{
public:
int age;
int hight;
int weight;
int num;
student();//无参构造函数
student(const student &stu,int stuAge=20,int stuHight=185,int stuWeight=80,int stuNum=1002);//带参数的构造函数
};
#endif // !__MAIN_H__
在.cpp文件定义无参构造函数、有参构造函数和创建类对象:
#include#include #include #include"main.h" using namespace std; student::student() { age = 22; hight = 170; weight = 80; num = 1001; cout << "调用无参构造函数" << endl; } student::student(const student& stu, int stuAge, int stuHight, int stuWeight, int stuNum):age(stuAge),hight(stuHight),weight(stuWeight),num(stuNum) { cout << "age = " << age << endl; cout << "hight = " << hight << endl; cout << "weight= " << weight << endl; cout << "num = " << num << endl; }; int main() { student stu1; //调用默认构造函数(不带形参) cout << endl; student stu2 = stu1; //调用拷贝构造函数 cout << "stu2.age = " << stu2.age << endl; cout << "stu2.hight = " << stu2.hight << endl; cout << "stu2.weight= " << stu2.weight << endl; cout << "stu2.num = " << stu2.num << endl; cout << endl; student stu3 (stu1); //调用拷贝构造函数 cout << "stu3.age = " << stu3.age << endl; cout << "stu3.hight = " << stu3.hight << endl; cout << "stu3.weight= " << stu3.weight << endl; cout << "stu3.num = " << stu3.num << endl; cout << endl; student stu4 { stu1 };//调用拷贝构造函数 cout << "stu4.age = " << stu4.age << endl; cout << "stu4.hight = " << stu4.hight << endl; cout << "stu4.weight= " << stu4.weight << endl; cout << "stu4.num = " << stu4.num << endl; cout << endl; student stu5={ stu1 };//调用拷贝构造函数 cout << "stu5.age = " << stu5.age << endl; cout << "stu5.hight = " << stu5.hight << endl; cout << "stu5.weight= " << stu5.weight << endl; cout << "stu5.num = " << stu5.num << endl; cout << endl; return 0; }
编译运行后结果如下所示:
需要注意的问题:
(1)const问题。这个拷贝构造函数的第一个参数是带了const的。拷贝构造函数的第一个参数都带const修饰的(推荐使用这种写法),第五课讲到,带const修饰可以避免无形修改了形参值,导致实参值被无意修改掉,并且实参可以更加灵活。
(2)explicit问题。在第十课中讲过,只要在构造函数声明前面增加explicit,那么在定义对象时一些进行了隐式转换的构造对象方法就会失败,这里可以测试一下在拷贝构造函数声明之前增加explicit后,上面的复制拷贝构造函数哪些会失败,如果失败了则说明都进行了隐式转换。
在“student(const student &stu,int stuAge=20,int stuHight=185,int stuWeight=80,int stuNum=1002);”前面增加explicit,然后编译程序,发现“student stu2 = stu1;//调用拷贝构造函数”和“student stu5={ stu1 };//调用拷贝构造函数”报错。
编者老师建议:
(1)单参数的构造函数,一般声明为explicit,以防止出现代码模糊不清的问题。
(2)拷贝构造函数,一般都不声明为explicit。所以这里可以把拷贝构造函数声明前面的explicit去掉。
在没有创建拷贝构造函数时(即取消类声明时带参构造函数的参数默认赋值),这种“student stu1;student stu2 = stu1;”代码是能够把stu1里的各种成员变量的值逐个复制到stu2里面去的,如下所示:
但是在类声明中带参构造函数的参数增加默认赋值(也称复制拷贝构造函数),这种“student stu2 = stu1;”代码的执行结果却发生了变化,也就是说,stu1里面的成员变量的值没有被复制到对象stu2里面的成员变量中去,stu2(stu3、stu4、stu5同stu2)里面的成员变量的值都等于在类声明时带参构造函数的默认赋的值。
所以,“成员变量逐个复制”的功能因为在类中声明了自己的拷贝构造函数而失去了作用。
在这个过程中,系统可能会做以下事情:
(1)如果一个类没有自己的拷贝构造函数,编译器可能会合成一个“拷贝构造函数”,也可能不会合成一个“拷贝构造函数”,是否合成取决于具体需要。但是上面我们的代码如果不写自己的拷贝构造函数,编译器并不会合成“拷贝构造函数”,编译器内部只需要简单按对象成员变量的值复制到新对象对应的成员变量中去就行。
(2)如果是编译器合成的构造函数,大概是这个样子,看上去和自己书写的拷贝构造函数非常相似:
student::student(const student &stu, ......){......}
(3)每个类成员变量的类型决定了该成员如何复制,例如成员变量如果是整型这种简单数据类型,那么直接就进行值复制(利用赋值语句),如果成员变量是类类型,那就会调用这个类的拷贝构造函数进行复制。
在上面的.h头文件中,在student类声明文件中,创建一个新类action,然后把student这个类的带参拷贝构造函数屏蔽掉如下代码:
#ifndef __MAIN_H__
#define __MAIN_H__
class action
{
public:
action();
action(const action& act);
};
class student
{
public:
int age;
int hight;
int weight;
int num;
action act1;
student();
//student(const student& stu, int stuAge = 20, int stuHight = 185, int stuWeight = 80, int stuNum = 1002);
};
#endif // !__MAIN_H__
在.cpp文件中这样定义:
#include#include #include #include"main.h" using namespace std; action::action() { cout << "调用了action::action()构造函数" << endl; } action::action(const action& act) { cout << "调用了action::action(const act)拷贝构造函数" << endl; } student::student() { age = 22; hight = 170; weight = 80; num = 1001; cout << "调用无参构造函数" << endl; } int main() { student stu1; //调用默认构造函数(不带形参) cout << endl; student stu2 = stu1; //调用拷贝构造函数 cout << "stu2.age = " << stu2.age << endl; cout << "stu2.hight = " << stu2.hight << endl; cout << "stu2.weight= " << stu2.weight << endl; cout << "stu2.num = " << stu2.num << endl; cout << endl; student stu3 (stu1); //调用拷贝构造函数 cout << "stu3.age = " << stu3.age << endl; cout << "stu3.hight = " << stu3.hight << endl; cout << "stu3.weight= " << stu3.weight << endl; cout << "stu3.num = " << stu3.num << endl; cout << endl; student stu4 { stu1 };//调用拷贝构造函数 cout << "stu4.age = " << stu4.age << endl; cout << "stu4.hight = " << stu4.hight << endl; cout << "stu4.weight= " << stu4.weight << endl; cout << "stu4.num = " << stu4.num << endl; cout << endl; student stu5={ stu1 };//调用拷贝构造函数 cout << "stu5.age = " << stu5.age << endl; cout << "stu5.hight = " << stu5.hight << endl; cout << "stu5.weight= " << stu5.weight << endl; cout << "stu5.num = " << stu5.num << endl; cout << endl; return 0; }
编程程序运行后如下所示:
因为类student的拷贝构造函数被注释掉,又因为类类型对象act1所在的类action存在拷贝构造函数,所以,此时编译器会合成一个Time类的拷贝构造函数,编译器合成这个拷贝构造函数的目的就是向其中插入能够调用action中拷贝构造函数的代码。
(3)如果定义了自己的拷贝构造函数,那么就取代编译器合成的拷贝构造函数(假如编译器合成了拷贝构造函数),这时就必须在自己的拷贝构造函数中给类中成员逐个赋值,以免出现类成员没有被赋值就拿来使用的情况。
另外,如果定义了自己的拷贝构造函数,并希望在其中执行action类的拷贝构造函数以正确构造act1对象,那么在.cpp的拷贝构造函数实现中的代码应写如下:
student::student(const student& stu,int stuAge, int stuHight, int stuWeight, int stuNum) :act1(stu.act1),age(stuAge), hight(stuHight), weight(stuWeight), num(stuNum)
{
cout << "age = " << age << endl;
cout << "hight = " << hight << endl;
cout << "weight= " << weight << endl;
cout << "num = " << num << endl;
};
运行结果如如下所示:
笔者老师建议:如果定义对象时搞不太清楚是调用构造函数还是拷贝构造函数,可以向自己定义的构造函数或者拷贝构造函数中增加一些打印信息来确认是否调用。
上面介绍的一些调用拷贝构造函数的情形,还有一些也会调用拷贝构造函数:
(1)将一个对象作为实参传递给一个非引用类型的形参(因为要复制构造,因此这种写法效率低,不提倡使用)。
void func(student stu)
{
return;
}
(2)从一个函数中返回一个对象。
student func()
{
student stu;
return stu;
}
(3)创建一个类指针对象。
student stu1; //调用默认构造函数(不带形参) student *stu2 = new student(stu1);
下面例子就是需要自己手动写拷贝构造函数:
例子:把一个类对象的字符串拷贝到另外一个类的字符串。
.h头文件实现:
#ifndef __MAIN_H__ #define __MAIN_H__ #include#include #include struct NAME { char* buff; int buffSize; }; class student { public: NAME name; int num; student(const char*stuName,int stuNum);//带参普通构造函数 student(const student& stu);//拷贝构造函数 ~student();//析构函数 const char* GetName(); }; #endif // !__MAIN_H__
.cpp文件中实现:
#include"main.h"
using namespace std;
student::student(const char* stuName,int stuNum)//带参普通构造函数
{
name.buffSize = strlen(stuName) + 1;
name.buff = new char[name.buffSize];
strcpy(name.buff,stuName);
num = stuNum;
};
student::student(const student& stu)//拷贝构造函数
{
name.buffSize = stu.name.buffSize;
name.buff = new char[name.buffSize];
strcpy(name.buff, stu.name.buff);
}
student::~student()//析构函数
{
delete[] name.buff;
}
const char* student::GetName()
{
return name.buff;
}
int main()
{
student stu1 = student("xiaoming",1001);
cout << stu1.GetName() << endl;
cout << stu1.num << endl;
student stu2 = stu1;
cout << stu1.GetName() << endl;
cout << stu1.num << endl;
return 0;
}
思考:如果不用拷贝构造函数会发生什么?
(1)在第三课里面讲到,new和delete需要成对出现,如果不用拷贝构造函数,类对象stu1和stu2的buff指向的是同一个堆内存块地址,stu1运行完以后,会把自己的buff堆内存块释放了,而此时stu2的buff还指向stu1的buff所指的内存块地址(stu2的buff指向一块已经被释放的内存块地址),此时会报错。
(2)手动定义一个构造拷贝函数,把stu1的buff内容内容拷贝到stu2的buff中。
2022.07.06结。



