C/C++开发语言系列之26---C++复制(拷贝)构造函数

C++拷贝构造函数的细节

在知道什么是拷贝构造函数前,先了解一下系统什么时候为我们创建 默认的构造函数和拷贝构造函数

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

每个类只有一个析构函数 和一个赋值函数 ,但可以有多个构造函数 (包含一个拷贝构造函数 ,其它的称为普通构造函数 )。对于任意一个类 A ,如果不想编写上述函数, C++ 编译器将自动为 A 产生四个缺省的函数(也只是在需要的时候才会产生) ,如

A(void); // 缺省的无参数构造函数

A(const A &a); // 缺省的拷贝构造函数

~A(void); // 缺省的析构函数

A & operate =(const A &a); // 缺省的赋值函数

这不禁让人疑惑,既然能自动生成函数,为什么还要程序员编写?

原因如下:

( 1 )如果使用 “ 缺省的无参数构造函数 ” 和 “ 缺省的析构函数 ” ,等于放弃了自主 “ 初始化 ” 和 “ 清除 ” 的机会, C++发明人 Stroustrup 的好心好意白费了。

( 2 ) “ 缺省的拷贝构造函数 ” 和 “ 缺省的赋值函数 ” 均采用 “ 位拷贝 ” 而非 “ 值拷贝 ” 的方式来实现,倘若类中含有指针变量,这两个函数注定将出错 。

对于那些没有吃够苦头的 C++ 程序员,如果他说编写构造函数、析构函数与赋值函数很容易,可以不用动脑筋,表明他的认识还比较肤浅,水平有待于提高。






第1:
什么时候系统会为我们定义无参数构造函数
答案:
首先,编译器“可能”为我们定义一个无参的构造函数,一个拷贝构造函数


当自己没有定义任何构造函数(包括无参数,有参数的普通构造函数,还有拷贝构造函数)的时候
系统才定义一个 无参数的空函数体的 构造函数


如果没有定义一个拷贝构造函数,系统也会为我们定义一个拷贝构造函数,完成简单的位(bit)复制


第2:
拷贝构造函数的深度复制,类中包括指针变量,然后指向相同的一块内存区域,可能由于一个对象释放了内存,另一个不知道已经释放,极有可能导致程序崩溃


VC6.0++ 可以通过改变配置,可以暂时运行程序,而不导致程序运行崩溃的临时解决方法,但是其实还是我们的程序不健壮,具体设置方法可以看下面的参考




第3:
C++拷贝构造函数的几个问题 

class CopyConstructorTest
{
public:
    CopyConstructorTest() {}
    CopyConstructorTest(const CopyConstructorTest& cct) {}
};
 
问题1:const 是不是必须的?
不是必须的,但是你省去了const,下面的代码就会出错:
const CopyConstructorTest cct1;
CopyConstructorTest cct2(cct1);

所以,如果希望const对象可以被拷贝,那么写上const,否则不写,那么记住const对象不能再出现副本,有时这是有用的。
 
问题2:&是不是必须的,YES OR NO?
答案是YES!否则,呵呵,会出现一个编译错误:
error C2652: 'CopyConstructorTest' : illegal copy constructor: first parameter must not be a 'CopyConstructorTest'



第4:

为什么拷贝构造函数必须为引用传递,不能是值传递?  

拷贝构造函数的标准写法如下:

class Base
{
public:
   Base(){}
   Base(const Base &b){..}
  //
}
上述写法见得最多,甚至你认为理所当然。
那么如果我们不写成引用传递呢,而是值传递,那么会怎样?
class Base
{
public:
   Base(){}
   Base(const Base b){ }
  //
}编译出错:error C2652: 'Base' : illegal copy constructor: first parameter must not be a 'Base'

事实上,你可以从这个小小的问题认真搞清楚2件事:
1) 拷贝构造函数的作用就是用来复制对象的,在使用这个对象的实例来初始化这个对象的一个新的实例。其实,个人认为不应该叫这些constructor(default constructor, copy constructor....)为构造函数,更佳的名字应该是"初始化函数"(见我的另一片文章).

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

上述1) 2)回答了为什么拷贝构造函数使用值传递会产生无限递归调用...

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

上述3)回答了在类中有指针数据成员时,拷贝构造函数使用值传递等于白显式定义了拷贝构造函数,因为默认的拷贝构造函数就是这么干的...


第5

一 拷贝构造函数是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,int  b=2);  

 2. 一个类中可以存在多于一个的拷贝构造函数吗?

3. 写出以下程序段的输出结果, 并说明为什么? 如果你都能回答无误的话,那么你已经对拷贝构造函数有了相当的了解。

  1. #include <iostream>
    #include <string>
    using namespace std; 

    class X {
    public:
    template<typename T>
    X( T& ) { std::cout << "X( T& )" << std::endl; } 

    template<typename T>
    X& operator=( T& ) { std::cout << "X& operator=( T& )" << std::endl; } 
    }; 

    int main() { 
    int m =5, n=10.5;
    X a(m); //X( T& )
    X b(n);     //X( T& )

    X c = a;     //VC6这里只调用系统默认的拷贝构造函数,没有调用X( T& )
    X d(b); //VC6这里只调用系统默认的拷贝构造函数,没有调用X( T& )
    return 0;

解答如下:

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); //是拷贝构造函数  
  3. X::X(X&, int a=1,int  b=2);        //是拷贝构造函数  

 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.  拷贝构造函数不能由成员函数模版生成. 

  1. #include <iostream>
    #include <string>
    using namespace std; 


    class X {
    public:
    template<typename T>
    X( T& ) { std::cout << "X( T& )" << std::endl; } 


    template<typename T>
    X& operator=( T& ) { std::cout << "X& operator=( T& )" << std::endl; } 
    }; 

    int main() { 
    int m =5, n=10.5;

    X a(m);//X( T& )  ,这里会调用拷贝构造函数
    X b(n);     //X( T& ),这里会调用拷贝构造函数


    //下面2个,VC6这里只调用系统默认的拷贝构造函数,没有调用自己的拷贝构造函数
  2. //linux下如何?需要再测试
  3. X c = a;
    X d(b);

    return 0;

原因很简单, 成员函数模版并不改变语言的规则,而语言的规则说,如果程序需要一个拷贝构造函数而你没有声明它,那么编译器会为你自动生成一个. 所以成员函数模版并不会阻止编译器生成拷贝构造函数, 赋值运算符重载也遵循同样的规则.(参见Effective C++ 3edition, Item45)



下面是我的进一步的实验结果:

//#include "stdafx.h"
#include "stdio.h"
#include <iostream>
#include <string>
using namespace std;


/*
Test1(int i)
Test1(const Test1& test)
Test1(const Test1& test)
Test1(const Test1& test)
Test1& operator = (const Test1& test)

Test2(int num)
Test2(const Test2& test)
Test2( Test2& test)
Test2()
Test2& operator = (const Test2& test)
*/


struct Test1 
{
    Test1() { cout<<"Test1()"<<endl; }
    Test1(int i) { id = i;   cout<<"Test1(int i)"<<endl; }
    Test1(const Test1& test)
    {
        id = test.id;
cout<<"Test1(const Test1& test)"<<endl; 
    }
    Test1& operator = (const Test1& test)
    {
cout<<"Test1& operator = (const Test1& test)"<<endl; 
        if(this == &test)
            return *this;
        id = test.id;


        return *this;
    }
    int id;
};


class Test2
{
public:
    Test2(){ m_pChar = NULL;
cout<<"Test2()"<<endl; }
    Test2(char *pChar) { m_pChar = pChar;cout<<"Test2(char *pChar) "<<endl; }
    Test2(int num) 
    { 
cout<<" Test2(int num) "<<endl; 
        m_pChar = new char[num];
        for(int i = 0; i< num; ++i)
            m_pChar[i] = 'a';
        m_pChar[num-1] = '\0';
    }

    Test2(const Test2& test)
    {
cout<<"  Test2(const Test2& test) "<<endl; 
        char *pCharT = m_pChar;

        m_pChar = new char[strlen(test.m_pChar)];
        strcpy(m_pChar, test.m_pChar);

        if(!pCharT)
            delete []pCharT;
    }
//注意这里没有const关键字,说明const可以参与函数重载的区分
    Test2(Test2 &test)

    {
cout<<"  Test2( Test2& test) "<<endl; 
        char *pCharT = m_pChar;

        m_pChar = new char[strlen(test.m_pChar)];
        strcpy(m_pChar, test.m_pChar);

        if(!pCharT)
            delete []pCharT;
    }


    Test2& operator = (const Test2& test)
    {
cout<<" Test2& operator = (const Test2& test) "<<endl; 
        if(this == &test)
//取别名的地址,相同则为同一个对象
            return *this;   //*this 表示指针中的内容,就是当前对象首地址

        char *pCharT = m_pChar;
//定义一个临时变量
        m_pChar = new char[strlen(test.m_pChar)];
        strcpy(m_pChar, test.m_pChar);


        if(!pCharT)
            delete []pCharT;
//删除char *的方法 ,不要忘记[]


        return *this;
    }

private:
    char *m_pChar;
};

int main(int argc, char* argv[])
{
//const 普通对象和const对象指针 必须在定义的时候进行初始化
    const Test1 ts(1); // Test1()
    const Test1 ts2(ts); //Test(const Test1& test)
    const Test1 ts3 = ts; //Test(const Test1& test)
    Test1 ts4 = ts; 
    ts4 = ts;  //Test1& operator = (const Test1& test)

cout<<endl;

/*
Test2 t4  ;    //调用默认的构造函数
Test2 t4 = t ; //定义的同时 并且 用另外一个对象 初始化则调用复制(拷贝)构造函数,没有调用 operator = 运算符函数函数
t4 = t   //定义之后的赋值调用operator = 运算符函数,没有调用 复制(拷贝)构造函数

Test2 t4 = t ;  这个方式:定义且赋值     如果我们自己没有定义复制(拷贝)构造函数且只定义了operator = 函数,编译器将自己我们定义一个复制(拷贝)构造函数
                        也不会调用定义了operator = 函数,原因是 定义的同时且用另一个对象赋值,系统会调用编译器为我们定义的默认拷贝构造函数。具体的原因待查
*/
     const Test2 t(5); //
     Test2 t2(t);
  //由于参数为const,所以调用Test2(const Test2& test)
     Test2 t3 = t2;
  //由于参数为非const,所以调用 Test2( Test2& test)
     Test2 t4 ;   

//如果未定义operator = 函数,系统也不会调用自定义的复制(拷贝)构造函数.
//会调用编译器自己的默认赋值函数 operator = ()
t4 = t;
//Test2& operator = (const Test2& test)  
    return 0;
}

实验代码的位置:


参考:
http://blog.csdn.net/dengrk/article/details/1930789
http://blog.sina.com.cn/s/blog_6c6b2acd0100n0lj.html






猜你喜欢

转载自blog.csdn.net/maojudong/article/details/8220303