c++拷贝构造函数详解

1、什么是拷贝构造函数

      首先对于普通类型的对象来讲,它们之间的复制是很简单的,例如:
int a=100; int b=a;
 
  而类对象与普通对象不一样,类对象内部结构通常较为复杂,存在各类成员变量。
  下面看一个类对象拷贝的简单例子。
   
复制代码
#include<iostream>
using namespace std; class CExample { private: int a; public: //构造函数 CExample(int b) { a=b; printf("constructor is called\n"); } //拷贝构造函数 CExample(const CExample & c) { a=c.a; printf("copy constructor is called\n"); } //析构函数 ~CExample() { cout<<"destructor is called\n"; } void Show() { cout<<a<<endl; } }; int main() { CExample A(100); CExample B=A; B.Show(); return 0; }
复制代码
   运行程序,屏幕输出100。从以上代码的运行结果能够看出,系统为对象 B 分配了内存并完成了与对象 A 的复制过程。就类对象而言,相同类型的类对象是经过拷贝构造函数来完成整个复制过程的。
  

 CExample(const CExample& C) 就是咱们自定义的拷贝构造函数。可见,拷贝构造函数是一种 特殊的 构造函数,函数的名称必须和类名称一致,它必须的一个参数是本类型的一个 引用变量

2、拷贝构造函数的调用时机
     1. 当函数的参数为类的对象时
  
复制代码
  
#include<iostream>
using namespace std; class CExample { private: int a; public: CExample(int b) { a=b; printf("constructor is called\n"); } CExample(const CExample & c) { a=c.a; printf("copy constructor is called\n"); } ~CExample() { cout<<"destructor is called\n"; } void Show() { cout<<a<<endl; } }; void g_fun(CExample c) { cout<<"g_func"<<endl; } int main() { CExample A(100); CExample B=A; B.Show(); g_fun(A); return 0; }
复制代码
 
 调用g_fun()时,会产生如下几个重要步骤:
(1).A对象传入形参时,会先会产生一个临时变量,就叫 C 吧。
(2).而后调用拷贝构造函数把A的值给C。 整个这两个步骤有点像:CExample C(A);
(3).等g_fun()执行完后, 析构掉 C 对象。  

    

2. 函数的返回值是类的对象
  
复制代码
#include<iostream>
using namespace std; class CExample { private: int a; public: //构造函数 CExample(int b) { a=b; printf("constructor is called\n"); } //拷贝构造函数 CExample(const CExample & c) { a=c.a; printf("copy constructor is called\n"); } //析构函数 ~CExample() { cout<<"destructor is called\n"; } void Show() { cout<<a<<endl; } }; CExample g_fun() { CExample temp(0); return temp; } int main() { g_fun(); return 0; }
复制代码
 
   当g_Fun()函数执行到return时,会产生如下几个重要步骤:
(1). 先会产生一个临时变量,就叫XXXX吧。
(2). 而后调用拷贝构造函数把temp的值给XXXX。整个这两个步骤有点像:CExample XXXX(temp);
(3). 在函数执行到最后先析构temp局部变量。
(4). 等g_fun()执行完后再析构掉XXXX对象。  
  
 
3. 对象须要经过另一个对象进行初始化
CExample A(100); CExample B=A;
3、浅拷贝与深拷贝
     1. 默认拷贝构造函数
   不少时候在咱们都不知道拷贝构造函数的状况下,传递对象给函数参数或者函数返回对象都能很好的进行,这是由于编译器会给咱们自动产生一个拷贝构造函数,这就是“默认拷贝构造函数”,这个构造函数很简单,仅仅使用“老对象”的数据成员的值对“新对象”的数据成员一一进行赋值,它通常具备如下形式:
Rect::Rect(const Rect& r) { width=r.width; height=r.height; }
固然,以上代码不用咱们编写,编译器会为咱们自动生成。可是若是认为这样就能够解决对象的复制问题,那就错了,让咱们来考虑如下一段代码:
 
复制代码
 
#include<iostream>
using namespace std; class Rect { public: Rect() { count++; } ~Rect() { count--; } static int getCount() { return count; } private: int width; int height; static int count; }; int Rect::count=0; int main() { Rect rect1; cout<<"The count of Rect:"<<Rect::getCount()<<endl; Rect rect2(rect1); cout<<"The count of Rect:"<<Rect::getCount()<<endl; return 0; } 
复制代码

这段代码对前面的类,加入了一个静态成员,目的是进行计数。在主函数中,首先建立对象rect1,输出此时的对象个数,而后使用rect1复制出对象rect2,再输出此时的对象个数,按照理解,此时应该有两个对象存在,但实际程序运行时,输出的都是1,反应出只有1个对象。此外,在销毁对象时,因为会调用销毁两个对象,类的析构函数会调用两次,此时的计数器将变为负数。html

说白了,就是拷贝构造函数没有处理静态数据成员。ios

出现这些问题最根本就在于在复制对象时,计数器没有递增,咱们从新编写拷贝构造函数,以下:面试

复制代码
#include<iostream>
using namespace std; class Rect { public: Rect() { count++; } Rect(const Rect& r) { width=r.width; height=r.height; count++; } ~Rect() { count--; } static int getCount() { return count; } private: int width; int height; static int count; }; int Rect::count=0; int main() { Rect rect1; cout<<"The count of Rect:"<<Rect::getCount()<<endl; Rect rect2(rect1); cout<<"The count of Rect:"<<Rect::getCount()<<endl; return 0; }
复制代码

  2. 浅拷贝函数

    所谓浅拷贝,指的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。大多状况下“浅拷贝”已经能很好地工做了,可是一旦对象存在了动态成员,那么浅拷贝就会出问题了,让咱们考虑以下一段代码:ui

 
复制代码
#include<iostream>
#include<assert.h>
using namespace std; class Rect { public: Rect() { p=new int(100); } ~Rect() { assert(p!=NULL); delete p; } private: int width; int height; int *p; }; int main() { Rect rect1; Rect rect2(rect1); return 0; }
复制代码
 

 在这段代码运行结束以前,会出现一个运行错误。缘由就在于在进行对象复制时,对于动态分配的内容没有进行正确的操做。咱们来分析一下:spa

    在运行定义rect1对象后,因为在构造函数中有一个动态分配的语句,所以执行后的内存状况大体以下:.net

     
  在使用rect1复制rect2时,因为执行的是浅拷贝,只是将成员的值进行赋值,这时 rect1.p = rect2.p,也即这两个指针指向了堆里的同一个空间,以下图所示:
    

 固然,这不是咱们所指望的结果,在销毁对象时,两个对象的析构函数将对同一个内存空间释放两次,这就是错误出现的缘由。咱们须要的不是两个p有相同的值,而是两个p指向的空间有相同的值,解决办法就是使用“深拷贝”。
 
   3. 深拷贝

  在“深拷贝”的状况下,对于对象中动态成员,就不能仅仅简单地赋值了,而应该从新动态分配空间,如上面的例子就应该按照以下的方式进行处理:

 
复制代码
#include<iostream>
#include<assert.h>
using namespace std; class Rect { public: Rect() { p=new int(100); } Rect(const Rect& r) { width=r.width; height=r.height; p=new int(100); *p=*(r.p); } ~Rect() { assert(p!=NULL); delete p; } private: int width; int height; int *p; }; int main() { Rect rect1; Rect rect2(rect1); return 0; }
复制代码
 
  此时,在完成对象的复制后,内存的一个大体状况以下:
   
此时rect1的p和rect2的p各自指向一段内存空间,但它们指向的空间具备相同的内容,这就是所谓的“深拷贝”。

3. 防止默认拷贝发生unix

    经过对对象复制的分析,咱们发现对象的复制大多在进行“值传递”时发生,这里有一个小技巧能够防止按值传递——声明一个私有拷贝构造函数。甚至没必要去定义这个拷贝构造函数,这样由于拷贝构造函数是私有的,若是用户试图按值传递或函数返回该类对象,将获得一个编译错误,从而能够避免按值传递或返回对象。指针

复制代码
 
//防止按值传递 class CExample { private: int a; public: //构造函数 CExample(int b) { a = b; cout<<"creat: "<<a<<endl; } private: //拷贝构造函数,只是声明 CExample(const CExample& C); public: ~CExample() { cout<< "delete: "<<a<<endl; } void Show () { cout<<a<<endl; } }; //???? void g_Fun(CExample C) { cout<<"test"<<endl; } int main() { CExample test(1); //g_Fun(test); //按值传递将出错 return 0; }
复制代码
小结:
    拷贝有两种:深拷贝,浅拷贝。

      当出现类的等号赋值时,会调用拷贝函数,在未定义显示拷贝构造函数的状况下,系统会调用默认的拷贝函数——即浅拷贝,它可以完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的。但当数据成员中有指针时,若是采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而致使指针悬挂现象。因此,这时,必须采用深拷贝。code

     深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。简而言之,当数据成员中有指针时,必需要用深拷贝


4、拷贝构造函数的几个细节
   1.为何拷贝构造函数必须是引用传递,不能是值传递?
     简单的回答是为了防止递归引用。
          具体一些能够这么讲:
              当 一个对象须要以值方式传递时,编译器会生成代码调用它的拷贝构造函数以生成一个复本。若是类A的拷贝构造函数是以值方式传递一个类A对象做为参数的话,当 须要调用类A的拷贝构造函数时,须要以值方式传进一个A的对象做为实参; 而以值方式传递须要调用类A的拷贝构造函数;结果就是调用类A的拷贝构造函数导 致又一次调用类A的拷贝构造函数,这就是一个无限递归。
    
   2. 拷贝构造函数的做用。     
           做用就是用来复制对象的,在使用这个对象的实例来初始化这个对象的一个新的实例。

     3.参数传递过程到底发生了什么?
      将地址传递和值传递统一块儿来,归根结底仍是传递的是"值"(地址也是值,只不过经过它能够找到另外一个值)!
      i)值传递:
         对于内置数据类型的传递时,直接赋值拷贝给形参(注意形参是函数内局部变量);
         对于类类型的传递时,须要首先调用该类的拷贝构造函数来初始化形参(局部对象);如void foo(class_type obj_local){}, 若是调用foo(obj);  首先class_type obj_local(obj) ,这样就定义了局部变量obj_local供函数内部使用
     ii)引用传递:
        不管对内置类型仍是类类型,传递引用或指针最终都是传递的地址值!而地址老是指针类型(属于简单类型), 显然参数传递时,按简单类型的赋值拷贝,而不会有拷贝构造函数的调用(对于类类型).

   4. 在类中有指针数据成员时,拷贝构造函数的使用?
        若是不显式声明拷贝构造函数的时候,编译器也会生成一个默认的拷贝构造函数,并且在通常的状况下运行的也很好。可是在遇到类有指针数据成员时就出现问题 了:由于默认的拷贝构造函数是按成员拷贝构造,这致使了两个不一样的指针(如ptr1=ptr2)指向了相同的内存。当一个实例销毁时,调用析构函数 free(ptr1)释放了这段内存,那么剩下的一个实例的指针ptr2就无效了,在被销毁的时候free(ptr2)就会出现错误了, 这至关于重复释放一块内存两次。这种状况必须显式声明并实现本身的拷贝构造函数,来为新的实例的指针分配新的内存。

问题1和2回答了为何拷贝构造函数使用值传递会产生无限递归调用的问题;
问题3回答了回答了在类中有指针数据成员时,拷贝构造函数使用值传递等于白显式定义了拷贝构造函数,由于默认的拷贝构造函数就是这么干的。
 

  5. 拷贝构造函数里能调用private成员变量吗?
解答:
这个问题是在网上见的,当时一会儿有点晕。其时从名子咱们就知道拷贝构造函数其时就是一个特殊的构造函数,操做的仍是本身类的成员变量,因此不受private的限制。

 6. 如下函数哪一个是拷贝构造函数,为何?
 
X::X(const X&); //拷贝构造函数 X::X(X); X::X(X&, int a=1); //拷贝构造函数 X::X(X&, int a=1, int b=2); //拷贝构造函数
   解答:对于一个类X, 若是一个构造函数的第一个参数是下列之一:
   a) X&
   b) const X&
   c) volatile X&
   d) const volatile X&
   且没有其余参数或其余参数都有默认值,那么这个函数是拷贝构造函数.

  7. 一个类中能够存在多于一个的拷贝构造函数吗?
   解答:
类中能够存在超过一个拷贝构造函数。
  
class X { public: X(const X&); // const 的拷贝构造 X(X&); // 非const的拷贝构造 };
 
  注意,若是一个类中只存在一个参数为 X& 的拷贝构造函数,那么就不能使用const X或volatile X的对象实行拷贝初始化.
  若是一个类中没有定义拷贝构造函数,那么编译器会自动产生一个默认的拷贝构造函数。
  这个默认的参数可能为 X::X(const X&)或 X::X(X&),由编译器根据上下文决定选择哪个。

     
5、C++构造函数以及析构函数的若干面试问题
 
 Q1:构造函数可否重载,析构函数可否重载,为何?
 A1:构造函数能够,析构函数不能够。
 
  Q2:析构函数为何通常状况下要声明为虚函数?
  A2:虚函数是实现多态的基础,当咱们经过基类的指针是析构子类对象时候,若是不定义成虚函数,那只调用基类的析构函数,子类的析构函数将不会被调用。如       果定义为虚函数,则子类父类的析构函数都会被调用。
  Q3:什么状况下必须定义拷贝构造函数?
  A3:当类的对象用于函数值传递时(值参数,返回类对象),拷贝构造函数会被调用。若是对象复制并不是简单的值拷贝,那就必须定义拷贝构造函数。例如大的堆       栈数据拷贝。若是定义了拷贝构造函数,那也必须重载赋值操做符。

参考博客:
  http://blog.csdn.net/lwbeyond/article/details/6202256
http://jaden.blog.51cto.com/1549175/324480
http://blog.chinaunix.net/uid-28662931-id-3496322.html
相关文章
相关标签/搜索