一、C++中调用拷贝构造函数的情形有:
1、类对象的按值初始化,即:用已有的类对象本身去初始化新的类对象。(注意,只有在用已有类对象本身去初始化新的类对象时,才会调用拷贝构造函数。当对象A已被创建,再用对象B给对象A赋值,就不会调用拷贝构造函数,因为这个过程已不是初始化的过程,而是普通的赋值过程。);
2、类对象的按值传参,即函数的形参为类对象(而不是指针或引用);
3、类对象的按值返回,即函数的返回值直接为类对象(而不是指针或引用)。
代码示例(类和函数的定义):
#include<iostream>
using namespace std;
class Scientist
{
public:
int age;
int* soul;
public:
Scientist(int hisAge, int* hisSoul)
{
cout << "The constructor is called !" << endl;
soul = (int*)malloc(sizeof(hisSoul) + 1);
*soul = *hisSoul;
age = hisAge;
}
//自定义一个拷贝构造函数
Scientist(const Scientist& myScientist)
{
cout << "The deep-copy constructor is called !" << endl;
soul = (int*)malloc(sizeof(myScientist.soul) + 1);
*soul = *(myScientist.soul);
age = myScientist.age;
}
~Scientist()
{
cout << "The destructor is called !" << endl;
if (soul != NULL)
{
free(soul);
soul = NULL;
}
}
};
void displaySoul(const Scientist myScientist)
{
cout << "His soul is : " << myScientist.soul << endl;
}
const Scientist getScientist_1(const Scientist& myScientist)
{
return myScientist;
}
const Scientist getScientist_2(const Scientist myScientist)
{
return myScientist;
}
情形1:类对象的按值初始化
int main(int argc, char* argv[])
{
int hisSoul = 10000;
Scientist scientistA(22,&hisSoul);
Scientist scientistB(scientistA);
}
/*
打印结果:
The constructor is called !
The deep-copy constructor is called !
The destructor is called !
The destructor is called !
*/
情形1的main函数中,语句Scientist scientistB(const Scientist& )表明,scientistB是用scientistA按值初始化的。打印结果表明,按值初始化类对象会导致拷贝构造函数的调用。
情形2:类对象的按值传参
int main(int argc, char* argv[])
{
int hisSoul = 10000;
Scientist scientistA(22,&hisSoul);
displaySoul(scientistA);
}
/*
打印结果:
The constructor is called !
The deep-copy constructor is called !
His soul is : 012D12F8
The destructor is called !
The destructor is called !
*/
情形2的main函数调用了void displaySoul(const Scientist )函数,该函数的形参列表为Scientist类型,表明为按值传递类对象参数。打印结果表明,类对象的按值传参会导致拷贝构造函数的调用。
情形3:类对象的按值返回
int main(int argc, char* argv[])
{
int hisSoul = 10000;
Scientist scientistA(22,&hisSoul);
getScientist_1(scientistA);
}
/*
打印结果:
The constructor is called !
The deep-copy constructor is called !
The destructor is called !
The destructor is called !
*/
情形3的main函数调用了const Scientist getScientist_1(const Scientist& )函数,该函数按值返回类对象scientistA。打印结果表明,类对象的按值返回会导致拷贝构造函数的调用。
补充示例:
int main(int argc, char* argv[])
{
int hisSoul = 10000;
Scientist scientistA(22,&hisSoul);
getScientist_2(scientistA);
}
/*
打印结果:
The constructor is called !
The deep-copy constructor is called !
The deep-copy constructor is called !
The destructor is called !
The destructor is called !
The destructor is called !
*/
补充示例中的main函数调用了const Scientist getScientist_1(const Scientist)函数,该函数既按值传递Scientist类对象,又按值返回Scientist类对象,因此对它的调用会引起拷贝构造函数的2次调用。打印结果正是如此。
二、浅拷贝与深拷贝
上述示例已经展示了拷贝构造函数被调用的三种情形。然而,拷贝构造又有浅拷贝和深拷贝之分,上述示例使用的为深拷贝。简单而言,拷贝者若与被拷贝者指向同一内存地址,则为浅拷贝,否则为深拷贝。在对象属性中包含指向堆区内存的数据成员时(即用new关键字或malloc函数创建的数据成员),浅拷贝会导致堆区内存的重复释放,以致程序运行崩溃。
1、错误代码示例(此段代码运行会导致程序崩溃!!!)
#define _CRT_SECURE_NO_WARNINGS//防止遇到strcpy函数而报错
#include<iostream>
using namespace std;
class Person
{
private:
char* name;
public:
Person(char* hisName)
{
cout << "The constructor is called !" << endl;
//注意数组对象的new创建方法
name = new char[strlen(hisName)+1];
strcpy(name, hisName);//将hisName复制给name
}
~Person()
{
cout << "The destructor is called !" << endl;
if (name != NULL)
{
//注意数组的delete销毁方法
delete[] name;
name = NULL;//防止野指针
}
}
inline const char* getName()
{
return name;
}
};
int main(int argc, char* argv[])
{
Person personA((char*)("PhaseLee"));
cout << personA.getName()<<endl;
/*
注意:personA为临时变量。进入此条代码之前,personA生命周期已结束,
因此已被释放,导致personA.name已被释放!!!
此语句发生了浅拷贝:即调用默认拷贝构造函数,也就是简单的按值拷贝,并未
对name属性开辟新的堆区内存,personB.name和personA.name指向相同的内存地址。
*/
Person personB(personA);
cout << personB.getName() << endl;
/*
此步执行完后,临时变量personB被释放,会导致personB.name指向的堆区内
存被释放。而personB是用personA按值初始化的,调用了默认拷贝构造函数,
导致personB.name与personA.name指向同一片堆区。此前,personA.name已被释
放,则personB的释放会导致该堆区被重复释放,从而导致程序崩溃。
*/
return 0;
}
在上述的基础上,注释掉“delete[] name;”语句,程序会正常运行,只不过name属性的内存不会被释放。
2、注释掉“delete[] name;”,并打印出personA.name和personB.name的地址为:
#define _CRT_SECURE_NO_WARNINGS//防止遇到strcpy函数而报错
#include<iostream>
#include<cstdio>
using namespace std;
class Person
{
private:
char* name;
public:
Person(char* hisName)
{
cout << "The constructor is called !" << endl;
name = new char[strlen(hisName)+1];
strcpy(name, hisName);
}
~Person()
{
cout << "The destructor is called !" << endl;
if (name != NULL)
{
//delete[] name;
name = NULL;//防止野指针
}
}
inline const char* getName()
{
return name;
}
};
int main(int argc, char* argv[])
{
Person personA((char*)("PhaseLee"));
cout << personA.getName()<<endl;
Person personB(personA);
cout << personB.getName() << endl;
//这里使用printf打印是因为cout对[]运算符进行了重载
printf("The address of personA.name is %d\n", &(personA.getName()[0]));
printf("The address of personB.name is %d\n", &(personB.getName()[0]));
return 0;
}
/*
输出结果:
The constructor is called !
PhaseLee
PhaseLee
The address of personA.name is 19862144
The address of personB.name is 19862144
The destructor is called !
The destructor is called !
*/
上述打印结果表明,通过浅拷贝,personA.name与personB.name确实指向相同的内存地址。
3、解决浅拷贝的方法就是深拷贝,即避免默认拷贝构造函数,需要自己提供拷贝构造函数。自己提供了拷贝构造函数的代码示例如下:
#define _CRT_SECURE_NO_WARNINGS//防止遇到strcpy函数而报错
#include<iostream>
#include<cstdio>
using namespace std;
class Person
{
private:
char* name;
public:
Person(char* hisName)
{
cout << "The constructor is called !" << endl;
//注意数组对象的new创建方法
name = new char[strlen(hisName)+1];
strcpy(name, hisName);//将hisName复制给name
}
//自定义拷贝构造函数
Person(const Person& myPerson)
{
//开辟新的堆区空间
cout << "The deep-copy constructor is called !" << endl;
name = new char[strlen(myPerson.name) + 1];
strcpy(name, myPerson.name);
}
~Person()
{
cout << "The destructor is called !" << endl;
if (name != NULL)
{
//注意数组的delete销毁方法
delete[] name;
name = NULL;//防止野指针
}
}
inline const char* getName()
{
return name;
}
};
int main(int argc, char* argv[])
{
Person personA((char*)("PhaseLee"));
Person personB(personA);
printf("The address of personA.name is %d\n", &(personA.getName()[0]));
printf("The address of personB.name is %d\n", &(personB.getName()[0]));
return 0;
}
/*
打印结果:
The constructor is called !
The deep-copy constructor is called !
The address of personA.name is 21172424
The address of personB.name is 21173768
The destructor is called !
The destructor is called !
*/
自定义拷贝构造函数需要按引用传参。在上述代码中的自定义拷贝构造函数中,为name属性分配了新的堆区空间,因此personA.name和personB.name不再指向同一片堆区内存,释放personA和释放personB对应的name属性的释放是对不同的堆区内存进行释放,不会导致堆区内存的重复释放。
结论:默认拷贝构造函数往往是被隐式调用的,而且默认拷贝构造函数的调用为浅拷贝。为了避免浅拷贝带来的问题,最好自定义拷贝构造函数,使某些情形下发生深拷贝,以避免堆区内存被重复释放。以C++为开发语言时,多数难以发现的隐晦bug往往就出现在构造函数/拷贝构造函数的隐式调用上。而导致这些函数的隐式调用往往就与类对象的按值初始化、按值传参、按值返回,以及类对象副本和临时类对象的隐式创建有关。因此,在编写C++程序时,要重视引用变量或指针变量的意义!