拷贝构造函数(一)——哲学三连

本文章以下内容均总结自诸如博客园和CSDN内大佬文章,并加上自己的理解,重新整理

 

拷贝构造函数(一)——哲学三连:http://www.cnblogs.com/tenjl-exv/p/8017814.html

拷贝构造函数(二)——深拷贝与浅拷贝:http://www.cnblogs.com/tenjl-exv/p/8017909.html

拷贝构造函数(三)——重载赋值运算符:http://www.cnblogs.com/tenjl-exv/p/8017983.html

参数传递时的问题
 
把参数传递给函数有三种方法,

一种是值传递,一种是地址传递,还有一种是引用传递。

前者与后两者不同的地方在于:当使用值传递的时候,会在函数里面生成传递参数的一个副本,这个副本的内容是按位从原始参数那里拷贝过来的,两者的内容是相同的。

当原始参数是一个类的对象时,它也会产生一个对象的副本,不过在这里要注意,

一般对象产生时都会触发构造函数的执行,但是在产生对象的副本时却不会这样,这时执行的是对象的拷贝构造函数。

看下面这个例子:

 

 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   
22 int Rect::count = 0;        // 初始化计数器  
23   
24 int main()  
25 {  
26     Rect rect1;  
27     cout<<"The count of Rect: "<<Rect::getCount()<<endl;  
28   
29     Rect rect2(rect1);   // 使用rect1复制rect2,此时应该有两个对象  
30      cout<<"The count of Rect: "<<Rect::getCount()<<endl;  
31   
32     return 0;  
33 }      

这段代码对前面的类,加入了一个静态成员,目的是进行计数。

在主函数中,首先创建对象rect1,输出此时的对象个数,

然后使用rect1复制出对象rect2,再输出此时的对象个数,

按照理解,此时应该有两个对象存在,但实际程序运行时,输出的都是1,反应出只有1个对象。

此外,在销毁对象时,由于会调用销毁两个对象,类的析构函数会调用两次,此时的计数器将变为负数。

为什么会这样?

一般的构造函数会完成一些成员属性初始化的工作,在对象传递给某一函数之前,

对象的一些属性可能已经被改变了,如果在产生对象副本的时候再执行对象的构造函数,

那么这个对象的属性又再恢复到原始状态,这并不是我们想要的。

所以在产生对象副本的时候,构造函数不会被执行,被执行的是一个默认的拷贝构造函数。

当函数执行完毕要返回的时候,对象副本会执行析构函数,如果你的析构函数是空的话,就不会发生什么问题,

但一般的析构函数都是要完成一些清理工作,如释放指针所指向的内存空间。这时候问题就可能要出现了。

假如你在构造函数里面为一个指针变量分配了内存,在析构函数里面释放分配给这个指针所指向的内存空间,

那么在把对象传递给函数至函数结束返回这一过程会发生什么事情呢?

首先有一个对象的副本产生了,这个副本也有一个指针,它和原始对象的指针是指向同块内存空间的。

函数返回时,对象的析构函数被执行了,即释放了对象副本里面指针所指向的内存空间,

但是这个内存空间对原始对象还是有用的,就程序本身而言,这是一个严重的错误。

然而错误还没结束,

当原始对象也被销毁的时候,析构函数再次执行,对同一块系统动态分配的内存空间释放两次是一个未知的操作,将会产生严重的错误。     

为什么需要拷贝构造函数

上面说的就是我们会遇到的问题。解决问题的方法是什么呢?

首先我们想到的是不要以传值的方式来传递参数,我们可以用传地址或传引用。

这样的确可以避免上面的情况,而且在允许的情况下,传地址或传引用是最好的方法,

但这并不适合所有的情况,有时我们不希望在函数里面的一些操作会影响到函数外部的变量。

那要怎么办呢?可以利用拷贝构造函数来解决这一问题。

拷贝构造函数就是在产生对象副本的时候执行的,我们可以定义自己的拷贝构造函数。

在拷贝构造函数里面我们申请一个新的内存空间来保存构造函数里面的那个指针所指向的内容。

这样在执行对象副本的析构函数时,释放的就是拷贝构造函数里面所申请的那个内存空间。

除了将对象传递给函数时会存在以上问题,还有一种情况也会存在以上问题,

就是当函数返回对象时,会产生一个临时对象,这个临时对象和对象的副本性质差不多。

拷贝构造函数,是一种特殊的构造函数,它由编译器调用,来完成一些基于同一类的其他对象的构件及初始化。

它的唯一的一个参数(对象的引用)是不可变的(因为是const型的)。

这个函数经常用在函数调用期间于用户定义类型的值传递及返回。

拷贝构造函数要调用基类的拷贝构造函数和成员函数。

如果可以的话,它将用常量方式调用,另外,也可以用非常量方式调用。 

在C++中,下面三种对象需要拷贝的情况。因此,拷贝构造函数将会被调用。 

1). 一个对象以值传递的方式传入函数体 

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

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

如果在前两种情况不使用拷贝构造函数的时候,就会导致一个指针指向已经被删除的内存空间。

对于第三种情况来说,初始化和赋值的不同含义是拷贝构造函数调用的原因。

事实上,拷贝构造函数是由普通构造函数和赋值操作符(=)共同实现的。

描述拷贝构造函数和赋值运算符的异同的参考资料有很多。 

拷贝构造函数不可以改变它所引用的对象,其原因如下:

当一个对象以传递值的方式传一个函数的时候,拷贝构造函数自动的被调用,来生成函数中的对象。

如果一个对象是被传入自己的拷贝构造函数,它的拷贝构造函数将会被调用来拷贝这个对象

这样拷贝才可以传入它自己的拷贝构造函数,这会导致无限循环。 

除了当对象传入函数的时候被隐式调用以外,拷贝构造函数在对象被函数返回的时候也同样的被调用。

换句话说,你从函数返回得到的只是对象的一份拷贝。但是同样的,拷贝构造函数被正确的调用了,你不必担心。 

如果在类中没有显式的声明一个拷贝构造函数,

那么,编译器会私下里为你制定一个函数来进行对象之间的位拷贝(bitwise copy)。

这个隐含的拷贝构造函数简单的关联了所有的类成员。

许多作者都会提及这个默认的拷贝构造函数。

注意到这个隐式的拷贝构造函数和显式声明的拷贝构造函数的不同在于对于成员的关联方式。

显式声明的拷贝构造函数关联的只是被实例化的类成员的缺省构造函数,

除非另外一个构造函数在类初始化或者在构造列表的时候被调用。 

拷贝构造函数使程序更加有效率,因为它不用在构造一个对象的时候改变构造函数的参数列表。

设计拷贝构造函数是一个良好的风格,即使是编译系统帮助你申请内存的默认拷贝构造函数。

事实上,默认拷贝构造函数可以应付许多情况。

但最好的方法是创建自己的拷贝构造函数而不要指望编译器创建,这样就能保证程序在我们自己的控制之下。

什么是拷贝构造函数

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

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

而类对象与普通对象不同,类对象内部结构一般较为复杂,存在各种成员变量。

 

下面看一个类对象拷贝的简单例子。

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

运行程序,屏幕输出100。

从以上代码的运行结果可以看出,系统为对象 B 分配了内存并完成了与对象 A 的复制过程。

就类对象而言,相同类型的类对象是通过拷贝构造函数来完成整个复制过程的。

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

 
#include <iostream>  
using namespace std;  
  
class CExample {  
private:  
    int a;  
public:  
    //构造函数  
    CExample(int b)  
    { a = b;}  
      
    //拷贝构造函数  
    CExample(const CExample& C)  
    {  
        a = C.a;  
    }  
  
    //一般函数  
    void Show ()  
    {  
        cout<<a<<endl;  
    }  
};  
  
int main()  
{  
    CExample A(100);  
    CExample B = A; // CExample B(A); 也是一样的  
     B.Show ();  
    return 0;  
}   

CExample(const CExample& C) 就是我们自定义的拷贝构造函数。

可见,拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它必须的一个参数是本类型的一个引用变量。

拷贝构造函数的调用时机

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

class CExample   
{  
private:  
 int a;  
  
public:  
 //构造函数  
 CExample(int b)  
 {   
  a = b;  
  cout<<"creat: "<<a<<endl;  
 }  
  
 //拷贝构造  
 CExample(const CExample& C)  
 {  
  a = C.a;  
  cout<<"copy"<<endl;  
 }  
   
 //析构函数  
 ~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;  
}  

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

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

class CExample   
{  
    private:  
         int a;  
    public:  
     //构造函数  
     CExample(int b)  
     {   
          a = b;  
     }  
     //拷贝构造  
     CExample(const CExample& C)  
     {  
          a = C.a;  
          cout<<"copy"<<endl;  
     }  
     void Show ()  
     {  
         cout<<a<<endl;  
     }  
};  
//全局函数  
CExample g_Fun()  
{  
     CExample temp(0);  
     return temp;  
}   
int main()  
{  
     g_Fun();  
     return 0;  
}  
                    

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

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

CExample A(100);  
CExample B = A;   
// CExample B(A);  

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

posted @ 2017-12-10 19:29  T丶jl  阅读(532)  评论(0编辑  收藏  举报