本文前面主要介绍了拷贝构造函数和赋值运算符函数的区别,以及在什么时候调用拷贝构造函数、什么情况下调用赋值运算符函数。最后,分析了下深拷贝和浅拷贝的问题,即拷贝构造函数和赋值运算符函数的必要性和意义。本文综合了《C++ 拷贝构造函数和赋值运算符》和《拷贝构造函数和赋值函数的必要性和意义》的内容,并加上自己的理解。
本文主要介绍了拷贝构造函数和赋值运算符函数的区别,以及在什么时候调用拷贝构造函数、什么情况下调用赋值运算符函数。最后,简单的分析了下深拷贝和浅拷贝的问题。
在默认情况下(用户没有定义,但是也没有显式的删除),编译器会自动的隐式生成一个拷贝构造函数和赋值运算符函数(缺省的)。
class Person
{
public:
...
Person(const Person& p) = delete;
Person& operator=(const Person& p) = delete;
private:
int age;
string name;
};
用户可以使用delete来指定不生成拷贝构造函数和赋值运算符,这样的对象就不能通过值传递,也不能进行赋值运算。上面的定义的类 Person 显式地删除了拷贝构造函数和赋值运算符,在需要调用拷贝构造函数或者赋值运算符的地方,会提示无法调用该函数,它是已删除的函数。
如果我们不想编写拷贝构造函数和赋值运算符函数,又不允许别人使用编译器隐式生成的缺省函数,同时也不想显式地删除拷贝构造函数和赋值运算符函数,我们还可以通过将拷贝构造函数和赋值运算符函数声明成类私有函数的方式来实现。如下所示:
class Person
{
public:
...
private:
Person(const Person& p); //以常量引用的方式传递参数
Person& operator=(const Person& p);//返回值类型为该类型的引用
int age;
string name;
};
还有两点需要注意的是:
拷贝构造函数和赋值运算符函数的行为比较相似,都是将一个对象的值复制给另一个对象;但是其结果却有些不同,拷贝构造函数使用传入对象的值生成一个新的对象的实例,而赋值运算符函数是将对象的值复制给一个已经存在的实例。这种区别从两者的名字也可以很轻易的分辨出来,拷贝构造函数也是一种构造函数,那么它的功能就是创建一个新的对象实例;赋值运算符函数是执行某种运算,将一个对象的值复制给另一个对象(已经存在的)。调用的是拷贝构造函数还是赋值运算符函数,主要是看是否有新的对象实例产生。如果产生了新的对象实例,那调用的就是拷贝构造函数;如果没有,那就是对已有的对象赋值,调用的是赋值运算符函数。
调用拷贝构造函数主要有以下场景:
class Person
{
public:
Person(){}
Person(const Person& p)
{
cout << "Copy Constructor" << endl;
}
Person& operator=(const Person& p)
{
cout << "Assign" << endl;
return *this;
}
private:
int age;
string name;
};
void f(Person p)
{
return;
}
Person f1()
{
Person p;
return p;
}
int main()
{
Person p;
Person p1 = p; // 1
Person p2;
p2 = p; // 2
f(p2); // 3
p2 = f1(); // 4
Person p3 = f1(); // 5
getchar();
return 0;
}
上面代码中定义了一个类Person,显式地定义了拷贝构造函数和赋值运算符函数。然后定义了一个f函数,以值的方式参传入Person对象;f1函数,以值的方式返回Person对象。在main中模拟了5个中场景,测试调用的是拷贝构造函数还是赋值运算符函数。执行结果如下:
分析如下:
说到拷贝构造函数,就不得不提深拷贝和浅拷贝。通常,默认生成的拷贝构造函数和赋值运算符函数,只是简单的进行值的复制。例如:上面的Person类,字段只有int和string两种类型,这在拷贝或者赋值时进行值复制创建的出来的对象和源对象也是没有任何关联,对源对象的任何操作都不会影响到拷贝出来的对象。反之,有如下一个CExample类:
class CExample
{
public:
CExample(){pBuffer=NULL; nSize=0;}
~CExample(){delete pBuffer;}
void Init(int n){ pBuffer=new char[n]; nSize=n;}
private:
char *pBuffer; //类的对象中包含指针,指向动态分配的内存资源
int nSize;
};
CExample类的特点是包含指向其他资源的指针,即有一个对象为char *,pBuffer指向堆中分配的一段内存空间。
int main(int argc, char* argv[])
{
CExample theObjone;
theObjone.Init40);
//现在需要另一个对对象2:theObjtwo,需要将他初始化成对象1:theObjone的状态
CExample theObjtwo=theObjone;
...
}
由上面的分析可知,例1是使用一个对象给另一个对象初始化,所以要调用拷贝构造函数,由于没有显式地定义拷贝构造函数,故调用编译器隐式生成的缺省的拷贝构造函数,对类对象进行简单的值复制。其完成方式是内存拷贝,复制所有成员的值(包括指针的值,即地址)。完成后,theObjtwo.pBuffer==theObjone.pBuffer(地址相同)。即它们将指向同样的地方,指针虽然复制了,但所指向的空间并没有复制,而是由两个对象共用了。这样不符合要求,对象之间不独立了,并为空间的删除带来隐患。任何一个对象对该值的修改都会影响到另一个对象,这种情况就是浅拷贝。
为了解决这类问题,我们可以显示地在拷贝构造函数中解决指针成员的问题。
增加了显式定义拷贝构造函数后的CExample类定义为:
class CExample
{
public:
CExample(){pBuffer=NULL; nSize=0;}
~CExample(){delete pBuffer;}
CExample(const CExample&); //拷贝构造函数
void Init(int n){ pBuffer=new char[n]; nSize=n;}
private:
char *pBuffer; //类的对象中包含指针,指向动态分配的内存资源
int nSize;
};
CExample::CExample(const CExample& RightSides) //拷贝构造函数的定义
{
nSize=RightSides.nSize; //复制常规成员
pBuffer=new char[nSize]; //复制指针指向的内容
memcpy(pBuffer,RightSides.pBuffer,nSize*sizeof(char));
}
在显式定义了CExample类的拷贝构造函数后,在例1中就不会调用缺省的拷贝构造函数了,而是调用显式的拷贝构造函数,就避免了上述浅拷贝的问题了,这就是所谓的深拷贝。
深拷贝和浅拷贝主要是针对类中的指针和动态分配的空间来说的,因为对于指针只是简单的值复制并不能分割开两个对象的关联(只是复制了指针的值,及地址,指针指向是同一个内存空间),任何一个对象对该指针的操作都会影响到另一个对象。这时候就需要提供自定义的深拷贝的拷贝构造函数,消除这种影响。通常的原则是:
int main(int argc, char* argv[])
{
CExample theObjone;
theObjone.Init(40);
CExample theObjthree;
theObjthree.Init(60);
theObjthree=theObjone; //对一个已存在的对象赋值
return 0;
}
注:”=”号的两种不同使用,这里的”=”号是赋值符,使用默认赋值符”=”,是把被赋值对象的原内容被清除,并用右边对象的内容填充。而例1中的”=”号是在对象声明语句中,表示初始化;更多时候,这种初始化也可用括号()表示。
由上面的分析可知,这里是对一个已存在的对象实例(theObjthree)赋值,故要调用赋值运算符函数,由于没有显式地定义赋值运算符函数,故调用编译器隐式生成的缺省的赋值运算符函数。但”=”的缺省操作只是将成员变量的值相应复制。旧的值被自然丢弃。
由于对象内包含指针,将造成不良后果:指针的值被丢弃了,但指针指向的内容并未释放(delete)。指针的值被复制了,但指针所指内容并未复制。 即:这里的theObjthree.pBuffer 原有的内存没被释放,造成内存泄露;theObjthree.pBuffer 和theObjone.pBuffer 指向同一块内存,和theObjone 或theObjthree任何一方变动都会影响另一方;在对象被析构时,pBuffer 被释放了两次。
因此,包含动态分配成员的类除提供拷贝构造函数外,还应该考虑重载”=”赋值操作符号。
增加了显式定义赋值运算符函数后的CExample类定义为:
class CExample
{
...
CExample(const CExample&); //拷贝构造函数
CExample& operator = (const CExample&); //赋值符重载
...
};
CExample & CExample::operator = (const CExample& RightSides) //赋值运算符函数定义
{
nSize=RightSides.nSize; //复制常规成员
char *temp=new char[nSize]; //复制指针指向的内容
memcpy(temp,RightSides.pBuffer,nSize*sizeof(char));
delete []pBuffer; //删除原指针指向内容 (将删除操作放在后面,避免X=X特殊情况下,内容的丢失)
pBuffer=temp; //建立新指向
return *this
}
对于拷贝构造函数的实现要确保以下几点: