一. 什么是拷贝构造函数

首先对于普通类型的对象来说,它们之间的复制是很简单的,例如:

[c-sharp] view plain copy

  1. int a = 100;
  2. int b = a;

而类对象与普通对象不同,类对象内部结构一般较为复杂,存在各种成员变量。
下面看一个类对象拷贝的简单例子。

  1. #include <iostream>
  2. using namespace std;
  3. class CExample {
  4. private:
  5.  int a;
  6. public:
  7. //构造函数
  8.  CExample(int b)
  9.  { a = b;}
  10. //一般函数
  11.  void Show ()
  12.  {
  13. cout<<a<<endl;
  14. }
  15. };
  16. int main()
  17. {
  18.  CExample A(100);
  19.  CExample B = A; //注意这里的对象初始化要调用拷贝构造函数,而非赋值
  20.   B.Show ();
  21.  return 0;
  22. }

运行程序,屏幕输出100。从以上代码的运行结果可以看出,系统为对象 B 分配了内存并完成了与对象 A 的复制过程。就类对象而言,相同类型的类对象是通过拷贝构造函数来完成整个复制过程的。

下面举例说明拷贝构造函数的工作过程。

  1. #include <iostream>
  2. using namespace std;
  3. class CExample {
  4. private:
  5. int a;
  6. public:
  7. //构造函数
  8. CExample(int b)
  9. { a = b;}
  10. //拷贝构造函数
  11. CExample(const CExample& C)
  12. {
  13. a = C.a;
  14. }
  15. //一般函数
  16. void Show ()
  17. {
  18. cout<<a<<endl;
  19. }
  20. };
  21. int main()
  22. {
  23. CExample A(100);
  24. CExample B = A; // CExample B(A); 也是一样的
  25. B.Show ();
  26. return 0;
  27. }

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

二. 拷贝构造函数的调用时机

在C++中,下面三种对象需要调用拷贝构造函数!
1. 对象以值传递的方式传入函数参数

  1. class CExample   
  2. {  
  3. private:  
  4.  int a;  
  5.   
  6. public:  
  7.  //构造函数  
  8.  CExample(int b)  
  9.  {   
  10.   a = b;  
  11.   cout<<"creat: "<<a<<endl;  
  12.  }  
  13.   
  14.  //拷贝构造  
  15.  CExample(const CExample& C)  
  16.  {  
  17.   a = C.a;  
  18.   cout<<"copy"<<endl;  
  19.  }  
  20.    
  21.  //析构函数  
  22.  ~CExample()  
  23.  {  
  24.   cout<< "delete: "<<a<<endl;  
  25.  }  
  26.   
  27.      void Show ()  
  28.  {  
  29.          cout<<a<<endl;  
  30.      }  
  31. };  
  32.   
  33. //全局函数,传入的是对象  
  34. void g_Fun(CExample C)  
  35. {  
  36.  cout<<"test"<<endl;  
  37. }  
  38.   
  39. int main()  
  40. {  
  41.  CExample test(1);  
  42.  //传入对象  
  43.  g_Fun(test);  
  44.   
  45.  return 0;  
  46. }  


调用g_Fun()时,会产生以下几个重要步骤:
(1).test对象传入形参时,会先会产生一个临时变量,就叫 C 吧。
(2).然后调用拷贝构造函数把test的值给C。 整个这两个步骤有点像:CExample C(test);
(3).等g_Fun()执行完后, 析构掉 C 对象。

2. 对象以值传递的方式从函数返回

  1. class CExample   
  2. {  
  3. private:  
  4.  int a;  
  5.   
  6. public:  
  7.  //构造函数  
  8.  CExample(int b)  
  9.  {   
  10.   a = b;  
  11.  }  
  12.   
  13.  //拷贝构造  
  14.  CExample(const CExample& C)  
  15.  {  
  16.   a = C.a;  
  17.   cout<<"copy"<<endl;  
  18.  }  
  19.   
  20.      void Show ()  
  21.      {  
  22.          cout<<a<<endl;  
  23.      }  
  24. };  
  25.   
  26. //全局函数  
  27. CExample g_Fun()  
  28. {  
  29.  CExample temp(0);  
  30.  return temp;  
  31. }  
  32.   
  33. int main()  
  34. {  
  35.  g_Fun();  
  36.  return 0;  
  37. }  


当g_Fun()函数执行到return时,会产生以下几个重要步骤:
(1). 先会产生一个临时变量,就叫XXXX吧。
(2). 然后调用拷贝构造函数把temp的值给XXXX。整个这两个步骤有点像:CExample XXXX(temp);
(3). 在函数执行到最后先析构temp局部变量。
(4). 等g_Fun()执行完后再析构掉XXXX对象。

3. 对象需要通过另外一个对象进行初始化;

  1. CExample A(100);  
  2. CExample B = A;   
  3. // CExample B(A);   

后两句都会调用拷贝构造函数。

三. 浅拷贝和深拷贝

1. 默认拷贝构造函数

很多时候在我们都不知道拷贝构造函数的情况下,传递对象给函数参数或者函数返回对象都能很好的进行,这是因为编译器会给我们自动产生一个拷贝构造函数,这就是“默认拷贝构造函数”,这个构造函数很简单,仅仅使用“老对象”的数据成员的值对“新对象”的数据成员一一进行赋值,它一般具有以下形式:

  1. Rect::Rect(const Rect& r)
  2. {
  3. width = r.width;
  4. height = r.height;
  5. }

当然,以上代码不用我们编写,编译器会为我们自动生成。但是如果认为这样就可以解决对象的复制问题,那就错了,让我们来考虑以下一段代码:

  1. class Rect
  2. {
  3. public:
  4. Rect()      // 构造函数,计数器加1
  5. {
  6. count++;
  7. }
  8. ~Rect()     // 析构函数,计数器减1
  9. {
  10. count--;
  11. }
  12. static int getCount()       // 返回计数器的值
  13. {
  14. return count;
  15. }
  16. private:
  17. int width;
  18. int height;
  19. static int count;       // 一静态成员做为计数器
  20. };
  21. int Rect::count = 0;        // 初始化计数器
  22. int main()
  23. {
  24. Rect rect1;
  25. cout<<"The count of Rect: "<<Rect::getCount()<<endl;
  26. Rect rect2(rect1);   // 使用rect1复制rect2,此时应该有两个对象
  27. cout<<"The count of Rect: "<<Rect::getCount()<<endl;
  28. return 0;
  29. }

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

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

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

  1. class Rect
  2. {
  3. public:
  4. Rect()      // 构造函数,计数器加1
  5. {
  6. count++;
  7. }
  8. Rect(const Rect& r)   // 拷贝构造函数
  9. {
  10. width = r.width;
  11. height = r.height;
  12. count++;          // 计数器加1
  13. }
  14. ~Rect()     // 析构函数,计数器减1
  15. {
  16. count--;
  17. }
  18. static int getCount()   // 返回计数器的值
  19. {
  20. return count;
  21. }
  22. private:
  23. int width;
  24. int height;
  25. static int count;       // 一静态成员做为计数器
  26. };

2. 浅拷贝

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

  1. class Rect
  2. {
  3. public:
  4. Rect()      // 构造函数,p指向堆中分配的一空间
  5. {
  6. p = new int(100);
  7. }
  8. ~Rect()     // 析构函数,释放动态分配的空间
  9. {
  10. if(p != NULL)
  11. {
  12. delete p;
  13. }
  14. }
  15. private:
  16. int width;
  17. int height;
  18. int *p;     // 一指针成员
  19. };
  20. int main()
  21. {
  22. Rect rect1;
  23. Rect rect2(rect1);   // 复制对象
  24. return 0;
  25. }

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

在运行定义rect1对象后,由于在构造函数中有一个动态分配的语句,因此执行后的内存情况大致如下:

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

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

3. 深拷贝

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

  1. class Rect
  2. {
  3. public:
  4. Rect()      // 构造函数,p指向堆中分配的一空间
  5. {
  6. p = new int(100);
  7. }
  8. Rect(const Rect& r)
  9. {
  10. width = r.width;
  11. height = r.height;
  12. p = new int;    // 为新对象重新动态分配空间
  13. *p = *(r.p);
  14. }
  15. ~Rect()     // 析构函数,释放动态分配的空间
  16. {
  17. if(p != NULL)
  18. {
  19. delete p;
  20. }
  21. }
  22. private:
  23. int width;
  24. int height;
  25. int *p;     // 一指针成员
  26. };

此时,在完成对象的复制后,内存的一个大致情况如下:

此时rect1的p和rect2的p各自指向一段内存空间,但它们指向的空间具有相同的内容,这就是所谓的“深拷贝”。

3. 防止默认拷贝发生

通过对对象复制的分析,我们发现对象的复制大多在进行“值传递”时发生,这里有一个小技巧可以防止按值传递——声明一个私有拷贝构造函数。甚至不必去定义这个拷贝构造函数,这样因为拷贝构造函数是私有的,如果用户试图按值传递或函数返回该类对象,将得到一个编译错误,从而可以避免按值传递或返回对象。

  1. // 防止按值传递
  2. class CExample
  3. {
  4. private:
  5. int a;
  6. public:
  7. //构造函数
  8. CExample(int b)
  9. {
  10. a = b;
  11. cout<<"creat: "<<a<<endl;
  12. }
  13. private:
  14. //拷贝构造,只是声明
  15. CExample(const CExample& C);
  16. public:
  17. ~CExample()
  18. {
  19. cout<< "delete: "<<a<<endl;
  20. }
  21. void Show ()
  22. {
  23. cout<<a<<endl;
  24. }
  25. };
  26. //全局函数
  27. void g_Fun(CExample C)
  28. {
  29. cout<<"test"<<endl;
  30. }
  31. int main()
  32. {
  33. CExample test(1);
  34. //g_Fun(test); 按值传递将出错
  35. return 0;
  36. }

四. 拷贝构造函数的几个细节

1. 拷贝构造函数里能调用private成员变量吗?
解答:
这个问题是在网上见的,当时一下子有点晕。其时从名子我们就知道拷贝构造函数其时就是一个特殊的构造函数,操作的还是自己类的成员变量,所以不受private的限制。

2. 以下函数哪个是拷贝构造函数,为什么?

  1. X::X(const X&);
  2. X::X(X);
  3. X::X(X&, int a=1);
  4. X::X(X&, int a=1, int b=2);


解答:对于一个类X, 如果一个构造函数的第一个参数是下列之一:
a) X&
b) const X&
c) volatile X&
d) const volatile X&
且没有其他参数或其他参数都有默认值,那么这个函数是拷贝构造函数.

  1. X::X(const X&);  //是拷贝构造函数
  2. X::X(X&, int=1); //是拷贝构造函数
  3. X::X(X&, int a=1, int b=2); //当然也是拷贝构造函数

3. 一个类中可以存在多于一个的拷贝构造函数吗?
解答:
类中可以存在超过一个拷贝构造函数。

  1. class X {
  2. public:
  3. X(const X&);      // const 的拷贝构造
  4. X(X&);            // 非const的拷贝构造
  5. };

注意,如果一个类中只存在一个参数为 X& 的拷贝构造函数,那么就不能使用const X或volatile X的对象实行拷贝初始化.

  1. class X {
  2. public:
  3. X();
  4. X(X&);
  5. };
  6. const X cx;
  7. X x = cx;    // error

如果一个类中没有定义拷贝构造函数,那么编译器会自动产生一个默认的拷贝构造函数。
这个默认的参数可能为 X::X(const X&)或 X::X(X&),由编译器根据上下文决定选择哪一个。

 
43

08--C++拷贝构造函数详解的更多相关文章

  1. 转 C++拷贝构造函数详解

    C++拷贝构造函数详解 一. 什么是拷贝构造函数 首先对于普通类型的对象来说,它们之间的复制是很简单的,例如: int a = 100; int b = a; 而类对象与普通对象不同,类对象内部结构一 ...

  2. [Reprint]C++友元函数与拷贝构造函数详解

    这篇文章主要介绍了C++友元函数与拷贝构造函数,需要的朋友可以参考下   一.友元函数 1.友元函数概述: (1)友元函数是定义在一个类外的普通函数.友元函数和普通函数的定义一样;在类内必须将该普通函 ...

  3. C++拷贝构造函数详解(转载)

    一. 什么是拷贝构造函数 首先对于普通类型的对象来说,它们之间的复制是很简单的,例如: int a = 100; int b = a; 而类对象与普通对象不同,类对象内部结构一般较为复杂,存在各种成员 ...

  4. [016]转--C++拷贝构造函数详解

    一. 什么是拷贝构造函数 首先对于普通类型的对象来说,它们之间的复制是很简单的,例如: int a = 100; int b = a; 而类对象与普通对象不同,类对象内部结构一般较为复杂,存在各种成员 ...

  5. C++拷贝构造函数详解 转

    一. 什么是拷贝构造函数 首先对于普通类型的对象来说,它们之间的复制是很简单的,例如: [c-sharp] view plaincopy int a = 100; int b = a; 而类对象与普通 ...

  6. C++拷贝构造函数详解

    转自:http://blog.csdn.net/lwbeyond/article/details/6202256 对于一个空类,编译器默认生成四个成员函数:默认构造函数.析构函数.拷贝构造函数.赋值函 ...

  7. 【转】C++拷贝构造函数详解

    一.什么是拷贝构造函数 首先对于普通类型的对象来说,它们之间的复制是很简单的,例如: ; int b=a; 而类对象与普通对象不同,类对象内部结构一般较为复杂,存在各种成员变量. 下面看一个类对象拷贝 ...

  8. c++之拷贝构造函数详解

    C++中经常使用一个常量或变量初始化另一个变量,例如: double x=5.0: double y=x; 使用类创建对象时,构造函数被自动调用以完成对象的初始化,那么能否象简单变量的初始化一样,直接 ...

  9. C++构造函数和拷贝构造函数详解

    构造函数.析构函数与赋值函数是每个类最基本的函数.它们太普通以致让人容易麻痹大意,其实这些貌似简单的函数就象没有顶盖的下水道那样危险. 每个类只有一个析构函数和一个赋值函数,但可以有多个构造函数(包含 ...

随机推荐

  1. GlobalSign 多域型(SNAs) SSL 证书

    GlobalSign 多域型(SNAs) SSL 证书 GlobalSign 多域型(SNAs) SSL 证书,有别于通配符 SSL 证书可以同时保护一个域名下所有的子域名网站,SANs 证书更进一步 ...

  2. 赛门铁克扩展验证EV SSL证书

      申请EV SSL证书,将接受最严格验证企业域名所有权和企业身份信息,属于最高信任级别扩展验证(EV)的 EV SSL证书,最高达256位自适应加密.Symantec不仅提供先进的SSL加密技术,同 ...

  3. 【codeforces 785D】Anton and School - 2

    [题目链接]:http://codeforces.com/contest/785/problem/D [题意] 给你一个长度为n的括号序列; 让你删掉若干个括号之后,整个序列变成前x个括号为左括号,后 ...

  4. Linux中安装MongoDB出现的问题记录

    mongoDB安装完成后,运行sudo service mongod start 查看程序状态:ps ajx | grep mongod   ,启动失败 查看失败信息提示,终端命令:tail -f / ...

  5. Debug : array type has incomplete element type

    array type has incomplete element type extern   struct  SoundReport SoundList[32];     ///// 多写了  st ...

  6. 开源GIS软件 1

    1. 在线地图浏览器 GMap.NET GMap.NET 是一个强大.免费.跨平台.开源的.NET控件,它在Windows Forms 和WPF环境中能够通过Google, Yahoo!, Bing, ...

  7. lsyncd + rsync 实时同步搭建

    一.inotify和lsync inotify和lsyncd对比一下,发现虽然lsyncd没有inotify那么真正的实时同步,但是lsyncd的同步基本上可以满足基本实时同步的要求,而且lsyncd ...

  8. 在CentOS VPS上源代码安装高版本号git

    背景:个别软件在国内下载非常慢,在vps下载就非常快. 可是下载好后的文件通过scp弄出来的时候又非常慢,所以想通过在vps里安装git,通过gitlab或oschina来进行中转.但遗憾的是,上传到 ...

  9. 《Java程序猿面试笔试宝典》之字符串创建与存储的机制是什么

    在Java语言中.字符串起着非常关键的数据.字符串的声明与初始化主要有例如以下两种情况:(1) 对于String s1=new String("abc")语句与String s2= ...

  10. HTML_项目符号使用图片

    本文出自:http://blog.csdn.net/svitter 创建一个HTML页面. 其内容为一个无序列表. 列表中至少包括了5本畅销书,每本书之前的项目符号必须採用概述封面的缩略图. 这些信息 ...