普通构造函数,拷贝构造函数,赋值构造函数:
一、拷贝构造函数基础:
1.什么是拷贝构造函数:
CA(const CA& C)就是我们自定义的拷贝构造函数。可见,拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它的唯一的一个参数是本类型的一个引用变量,该参数是const类型,不可变的。例如:类X的拷贝构造函数的形式为X(X& x)。
当用一个已初始化过了的自定义类类型对象去初始化另一个新构造的对象的时候,拷贝构造函数就会被自动调用。也就是说,当类的对象需要拷贝时,拷贝构造函数将会被调用。以下情况都会调用拷贝构造函数:
① 程序中需要新建立一个对象,并用另一个同类的对象对它初始化,如前面介绍的那样。
② 当函数的参数为类的对象时。在调用函数时需要将实参对象完整地传递给形参,也就是需要建立一个实参的拷贝,这就是按实参复制一个形参,系统是通过调用复制构造函数来实现的,这样能保证形参具有和实参完全相同的值。
③ 函数的返回值是类的对象。在函数调用完毕将返回值带回函数调用处时。此时需要将函数中的对象复制一个临时对象并传给该函数的调用处。如
Box f( ) //函数f的类型为Box类类型
{Box box1(12,15,18);
return box1; //返回值是Box类的对象
}
int main( )
{Box box2; //定义Box类的对象box2
box2=f( ); //调用f函数,返回Box类的临时对象,并将它赋值给box2
}
如果在类中没有显式地声明一个拷贝构造函数,那么,编译器将会自动生成一个默认的拷贝构造函数,该构造函数完成对象之间的位拷贝。位拷贝又称浅拷贝,后面将进行说明。
自定义拷贝构造函数是一种良好的编程风格,它可以阻止编译器形成默认的拷贝构造函数,提高源码效率。
浅拷贝和深拷贝
在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。
深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。
2.C++拷贝构造函数的几个细节
1) 以下函数哪个是拷贝构造函数,为什么?
1.X::X( const X&);
2.X::X(X);
3.X::X(X&, int a=1);
4.X::X(X&, int a=1, b=2);
解答:1) 对于一个类X,如果一个构造函数的第一个参数是下列之一:
a) X&
b) const X&
c) volatile X&
d) const volatile X&
且没有其他参数或其他参数都有默认值,那么这个函数是拷贝构造函数.
1.X::X( const X&); //是拷贝构造函数
2.X::X(X&, int =1); //是拷贝构造函数
2) 一个类中可以存在多于一个的拷贝构造函数吗?
解答:类中可以存在超过一个拷贝构造函数,
1.class X {
2.public :
3. X( const X&);
4. X(X&); // OK
5.};
注意,如果一个类中只存在一个参数为X&的拷贝构造函数,那么就不能使用const X或volatile X的对象实行拷贝初始化.
1.class X {
2.public :
3. X();
4. X(X&);
5.};
6.
7.const X cx;
8.X x = cx; // error
如果一个类中没有定义拷贝构造函数,那么编译器会自动产生一个默认的拷贝构造函数.
这个默认的参数可能为X::X(const X&)或X::X(X&),由编译器根据上下文决定选择哪一个.
默认拷贝构造函数的行为如下:
默认的拷贝构造函数执行的顺序与其他用户定义的构造函数相同,执行先父类后子类的构造.
拷贝构造函数对类中每一个数据成员执行成员拷贝(memberwise Copy)的动作.
a)如果数据成员为某一个类的实例,那么调用此类的拷贝构造函数.
b)如果数据成员是一个数组,对数组的每一个执行按位拷贝.
c)如果数据成员是一个数量,如int,double,那么调用系统内建的赋值运算符对其进行赋值.
3) 拷贝构造函数不能由成员函数模版生成.
struct X {
template < typename T>
X( const T& ); // NOT copy ctor, T can't be X
template < typename T>
operator=( const T& ); // NOT copy ass't, T can't be X
};
原因很简单, 成员函数模版并不改变语言的规则,而语言的规则说,如果程序需要一个拷贝构造函数而你没有声明它,那么编译器会为你自动生成一个. 所以成员函数模版并不会阻止编译器生成拷贝构造函数, 赋值运算符重载也遵循同样的规则
3.拷贝构造函数与赋值函数的异同:
1) 拷贝构造,是一个的对象来初始化一片内存区域,这片内存区域就是你的新对象的内存区域赋值运算,对于一个已经被初始化的对象来进行operator=操作
class A;
A a;
A b=a; //拷贝构造函数调用
//或
A b(a); //拷贝构造函数调用
///
A a;
A b;
b =a; //赋值运算符调用
你只需要记住,在C++语言里,
String s2(s1);
String s3 = s1;
只是语法形式的不同,意义是一样的,都是定义加初始化,都调用拷贝构造函数。
2) 一般来说是在数据成员包含指针对象的时候,应付两种不同的处理需求的 一种是复制指针对象,一种是引用指针对象 copy大多数情况下是复制,=则是引用对象的
例子:
class A
{
int nLen;
char * pData;
}
显然
A a, b;
a=b的时候,对于pData数据存在两种需求
第一种copy
a.pData = new char [nLen];
memcpy(a.pData, b.pData, nLen);
另外一种(引用方式):
a.pData = b.pData
通过对比就可以看到,他们是不同的
往往把第一种用copy使用,第二种用=实现
你只要记住拷贝构造函数是用于类中指针,对象间的COPY
3) 拷贝构造函数首先是一个构造函数,它调用的时候产生一个对象,是通过参数传进来的那个对象来初始化,产生的对象。
operator=();是把一个对象赋值给一个原有的对象,所以如果原来的对象中有内存分配要先把内存释放掉,而且还要检查一下两个对象是不是同一个对象,如果是的话就不做任何操作。
还要注意的是拷贝构造函数是构造函数,不返回值
而赋值函数需要返回一个对象自身的引用,以便赋值之后的操作
4) 在形式上
类名(形参表列); //普通构造函数的声明,如Box(int h,int w,int len);
类名(类名& 对象名); //复制构造函数的声明,如Box(Box &b);
5) 在建立对象时,实参类型不同。系统会根据实参的类型决定调用普通构造函数或复制构造函数。如:
Box box1(12,15,16); //实参为整数,调用普通构造函数
Box box2(box1); //实参是对象名,调用复制构造函数
二、拷贝构造函数小结:
拷贝构造是确确实实构造一个新的对象,并给新对象的私有成员赋上参数对象的私有成员的值,新构造的对象和参数对象地址是不一样的,所以如果该类中有一个私有成员是指向堆中某一块内存,如果仅仅对该私有成员进行浅拷贝,那么会出现多个指针指向堆中同一块内存,这是会出现问题,如果那块内存被释放了,就会出现其他指针指向一块被释放的内存,出现未定义的值的问题,如果深拷贝,就不会出现问题,因为深拷贝,不会出现指向堆中同一块内存的问题,因为每一次拷贝,都会开辟新的内存供对象存放其值。
下面是浅拷贝构造函数的代码:
#include <iostream>using namespace std;class A{private: int* n;public: A() { n = new int[10]; n[0] = 1; cout<<"constructor is called\n"; } A(const A& a) { n = a.n; cout<<"copy constructor is called\n"; } ~A() { cout<<"destructor is called\n"; delete n; } void get() { cout<<"n[0]: "<<n[0]<<endl; }};int main(){ A* a = new A(); A b = *a; delete a; b.get(); return 0;}
运行结果如下:
下面是深拷贝构造函数的代码:
#include <iostream>#include <string.h>using namespace std;class A{private: int* n;public: A() { n = new int[10]; n[0] = 1; cout<<"constructor is called\n"; } A(const A& a) { n = new int[10]; memcpy(n, a.n, 10); //通过按字节拷贝,将堆中一块内存存储到另一块内存 cout<<"copy constructor is called\n"; } ~A() { cout<<"destructor is called\n"; delete n; } void get() { cout<<"n[0]: "<<n[0]<<endl; }};int main(){ A* a = new A(); A b = *a; delete a; b.get(); return 0;}
运行截图如下:
但是赋值构造函数是将一个参数对象中私有成员赋给一个已经在内存中占据内存的对象的私有成员,赋值构造函数被赋值的对象必须已经在内存中,否则调用的将是拷贝构造函数,当然赋值构造函数也有深拷贝和浅拷贝的问题。当然赋值构造函数必须能够处理自我赋值的问题,因为自我赋值会出现指针指向一个已经释放的内存。还有赋值构造函数必须注意它的函数原型,参数必须是引用类型,返回值也必须是引用类型,否则在传参和返回的时候都会再次调用一次拷贝构造函数。
#include <iostream>#include <string.h>using namespace std;class A{private: int* n;public: A() { n = new int[10]; n[0] = 1; cout<<"constructor is called\n"; } A(const A& a) //拷贝构造函数的参数一定是引用,不能不是引用,不然会出现无限递归 { n = new int[10]; memcpy(n, a.n, 10); //通过按字节拷贝,将堆中一块内存存储到另一块内存 cout<<"copy constructor is called\n"; } A& operator=(const A& a) //记住形参和返回值一定要是引用类型,否则传参和返回时会自动调用拷贝构造函数 { if(this == &a) //为什么需要进行自我赋值判断呢?因为下面要进行释放n的操作,如果是自我赋值,而没有进行判断的话,那么就会出现讲一个释放了的内存赋给一个指针 return *this; if(n != NULL) { delete n; n == NULL; //记住释放完内存将指针赋为NULL } n = new int[10]; memcpy(n, a.n, 10); cout<<"assign constructor is called\n"; return *this; } ~A() { cout<<"destructor is called\n"; delete n; n = NULL; //记住释放完内存将指针赋为NULL } void get() { cout<<"n[0]: "<<n[0]<<endl; }};int main(){ A* a = new A(); A* b = new A(); *b = *a; delete a; b->get();return 0;}
运行截图如下:
如果我们在赋值构造函数的形参和返回值不用引用类型,代码如下:
#include <iostream>#include <string.h>using namespace std;class A{private: int* n;public: A() { n = new int[10]; n[0] = 1; cout<<"constructor is called\n"; } A(const A& a) //拷贝构造函数的参数一定是引用,不能不是引用,不然会出现无限递归 { n = new int[10]; memcpy(n, a.n, 10); //通过按字节拷贝,将堆中一块内存存储到另一块内存 cout<<"copy constructor is called\n"; } A operator=(const A a) //传参和返回值设置错误 { if(this == &a) return *this; if(n != NULL) { delete n; n == NULL; } n = new int[10]; memcpy(n, a.n, 10); cout<<"assign constructor is called\n"; return *this; } ~A() { cout<<"destructor is called\n"; delete n; } void get() { cout<<"n[0]: "<<n[0]<<endl; }};int main(){ A* a = new A(); A* b = new A(); *b = *a; delete a; b->get(); while(1) {} return 0;}
运行截图如下:
多了两次的拷贝构造函数的调用和两次析构函数的调用。
三、拷贝构造函数总结:
class CTest
{
public:
CTest(); //普通构造函数
CTest(const CTest &); //拷贝构造函数
CTest & operator = (const CTest &); //赋值构造函数
};
CTest::CTest()
{
cout<<"Constructor of CTest"<<endl;
}
CTest::CTest(const CTest & arg)
{
cout<<"Copy Constructor of CTest"<<endl;
}
CTest & CTest::operator = (const CTest & arg)
{
cout<<"Assign function of CTest"<<endl;
}
int main()
{
CTest a; //调用构造函数
CTest b(a); //调用拷贝构造函数
CTest c = a; //仍然调用拷贝构造函数
a = c; //这里调用赋值构造函数,因为"="左右的对象均已存在.
return 0;
}
运行的结果如下:
Constructor of CTest
Copy Constructor of CTest
Copy Constructor of CTest
Assign function of CTest
复制构造函数与赋值操作符之间的区别
复制构造函数又称拷贝构造函数,它与赋值操作符间的区别体现在以下几个方面
1.从概念上区分:
复制构造函数是构造函数,而赋值操作符属于操作符重载范畴,它通常是类的成员函数
2.从原型上来区分:
复制构造函数原型ClassType(const ClassType &);无返回值
赋值操作符原型ClassType& operator=(const ClassType &);返回值为ClassType的引用,便于连续赋值操作
3.从使用的场合来区分:
复制构造函数用于产生对象,它用于以下几个地方:函数参数为类的值类型时、函数返回值为类类型时以及初始化语句,例如(示例了初始化语句,函数参数与函数返回值为类的值类型时较简单,这里没给出示例)
ClassType a; //
ClassType b(a); //调用复制构造函数
ClassType c = a; //调用复制构造函数
而赋值操作符要求‘=’的左右对象均已存在,它的作用就是把‘=’右边的对象的值赋给左边的对象
ClassType e;
Class Type f;
f = e; //调用赋值操作符
4.当类中含有指针成员时,两者的意义有很大区别
复制构造函数需为指针变量分配内存空间,并将实参的值拷贝到其中;而赋值操作符它实现的功能仅仅是将‘=’号右边的值拷贝至左值,在左边对象内存不足时,先释放然后再申请。当然赋值操作符必须检测是否是自身赋值,若是则直接返回当前对象的引用而不进行赋值操作