C++ 拷贝构造和拷贝赋值运算符

来源:互联网 发布:c语言是什么语言 编辑:程序博客网 时间:2024/06/07 06:27

第一部分:拷贝构造函数

类类型的变量需要使用拷贝构造函数来完成整个复制过程

拷贝构造函数的形式:

     A (const A &a)   // A为类名

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

  X::X(const X&)    X::X(&)

/*拷贝构造函数*/#include <iostream>using namespace std;class A{public:    A(int n):m_n(n){        cout<<"A的构造函数被调用"<<endl;    }    A(const A&a){        m_n = a.m_n;        cout<<"A的拷贝构造函数被调用"<<endl;    }    // 析构函数    ~A(void){        cout<<"A的析构函数被调用"<<endl;    }    A foo4(){        cout<<"成员函数foo4"<<endl;        A a(1);        return a;    }    void print(){        cout<<m_n<<endl;        A a(1);    }private:    int m_n;};void gfoo1(A a){    cout<<"全局函数foo1"<<endl;}A gfoo2(){    cout<<"全局函数foo2"<<endl;    A a(1);    return a;}int main(void){    return 0;}

一、拷贝构造函数的工作过程:下面两种写法都会调用拷贝构造函数

A a(1);A a1 = a;  或者 A a2(a);

2、拷贝构造函数被调用的时机 即 对象复制的过程发生

        1. 对象以值传递的方式传入函数参数

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

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

 1、对象以值传递的方式传入函数参数

A  a(1);gfoo1(a);     

          其执行的过程:

             1> 对象 a传入形参时,会先产生临时变量temp  (临时变量的产生是不会调用构造函数

             2>然后调用拷贝构造函数把temp的值复制给形参param

             3>等gfoo1函数执行完,先析构掉临时对象temp  、再析构掉param对象
结果:

A的构造函数被调用A的拷贝构造函数被调用全局函数foo1A的析构函数被调用A的析构函数被调用

 

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

gfoo2();

          其执行的过程:

      1>先会产生一个临时变量,就叫temp吧。
      2> 然后调用拷贝构造函数把a的值给temp。整个这两个步骤有点像:A temp(a)                                                                                                            
      3> 在函数执行到最后先析构a局部变量。
     4> 等gfoo2函数执行完后 再析构掉临时temp对象。

结果:

全局函数foo2A的构造函数被调用A的析构函数被调用

注意:增加一个成员函数 结果是一样的

A a(1);a.foo4();
A的构造函数被调用成员函数foo4A的构造函数被调用A的析构函数被调用A的析构函数被调用

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

A  a(1);A a1 = a;A a2(a);

 

二、浅拷贝和深拷贝

系统提供的默认拷贝构造函数就是浅拷贝

/*含有静态成员的拷贝构造函数*/#include <iostream>using namespace std;class A{public:    A(){ sn++;  }    ~A(){ sn--; }    static int GetValue(){        return sn;     } private:    static int sn; };int A::sn = 0;int main(void){    A a1;     cout<<"1:"<<A::GetValue()<<endl;    A a2(a1);    cout<<"2:"<<A::GetValue()<<endl;    return 0;}

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

原因:拷贝构造函数没有处理静态数据成员

再看下面程序:输出为 2

/*含有静态成员的拷贝构造函数*/#include <iostream>using namespace std;class A{public:    A(){ sn++;  }    ~A(){ sn--; }    static int GetValue(){        return sn;     }     A (const A&a){        sn++;    }private:    static int sn; };int A::sn = 0;int main(void){    A a1;     cout<<"1:"<<A::GetValue()<<endl;    A a2(a1);    cout<<"2:"<<A::GetValue()<<endl;    return 0;}

结论:

浅拷贝是以字节的方式复制,申请的是同一块内存区域、如果是指针在析构的时候,同一块内存就会出现释放两次的情况

三、防止默认拷贝构造函数---

声明一个私有拷贝构造函数,甚至不必去定义这个拷贝构造函数,这样因为拷贝构造函数是私有的,

如果用户试图按值传递或函数返回该类对象,将得到一个编译错误,从而可以避免按值传递或返回对象。

 

四、拷贝构造函数和赋值运算符的区别:

调用系统默认的拷贝构造函数,不再新分配资源内存。深拷贝:调用自己的拷贝构造函数,分配新的资源内存。

拷贝构造函数用已存在的对象创建一个相同的新对象。而赋值运算符用已存在的对象赋予一个已存在的同类对象。

拷贝构造函数发生在创建对象时,只发生一次拷贝赋值可以重复发生;  

 实例代码:

/* Sample实例  */#include <iostream>using namespace std;class Sample{public:    Sample(void):m_n(*new int(0)),m_ch('a'){}    Sample(int n,char ch):m_n(*new int(n)),m_ch(ch){}    ~Sample(){        delete &m_n;        }       Sample(const Sample&sample):m_n(*new int(sample.m_n)),m_ch(sample.m_ch){}    Sample&operator =(const Sample& sample){        if(&sample != this){            m_n = sample.m_n;            return *this;        }    }       void printInfo(void){        cout<<m_n<<","<<m_ch<<endl;    }   private:    int &m_n;    const char m_ch;};int main(void){    Sample s1;     s1.printInfo();    Sample s2(100,'B');    s2.printInfo();    return 0;}


 拷贝构造函数可以借鉴:http://blog.csdn.net/lwbeyond/article/details/6202256 点击打开链接

=============================================================================================================

 第二部分:拷贝赋值运算符

1、赋值运算符为什么要返回引用

一、c/c++赋值运算符的本意为“返回左值的引用”(左值:赋值号左面的变量而非其值)

 实例:

     int a, b = 3, c = 2;

 (a = b) = c;

 cout<<a<<b<<c<<endl;

   结果:232

二、为了进行连续赋值,即 x = y = z

1、赋值返回引用

    x = y = z 先执行y = z,返回y的引用,执行x = y

2、赋值不返回引用

 x = y = z 先执行y = z,返回用y初始化的临时对象(注意临时对象都是常对象),再执行x = y的临时对象(要求operator=(const X&) ),

    返回用x初始化的临时对象(此处要求拷贝构造函数必须为X(const X&) )。

    所以也并非必须返回引用,返回引用的好处既可以于赋值的原始语义已知,又可避免拷贝构造函数和析构函数的调用。

 

下面介绍类的赋值运算符


1.C++中对象的内存分配方式
  在C++中,对象的实例在编译的时候,就需要为其分配内存大小,因此系统都是在stack上为其分配内存的。因此,在C++中,只要申明该实例,在程序编译后,就要为其分配相应的内存空间,至于实体内的各个域的值,就由其构造函数决定了。
例如:

class A{public:    A()    {    }    A(int id,char *t_name)    {    _id=id;    name=new char[strlen(t_name)+1];    strcpy(name,t_name);    }    private:        char *username;        int _id;}int main(){A a(1,"herengang");A b;}


在程序编译之后,a和b在stack上都被分配相应的内存大小。只不过对象a的域都被初始化,而b则都为随机值。
其内存分配如下:


2. 缺省情况下的赋值运算符
   如果我们执行以下:
    b=a;
   则其执行的是缺省定义的缺省的赋值运算。所谓缺省的赋值运算,是指对象中的所有位于stack中的域,进行相应的复制。但是,如果对象有位于heap上的域的话,其不会为拷贝对象分配heap上的空间,而只是指向相同的heap上的同一个地址。
   执行b=a这样的缺省的赋值运算后,其内存分配如下:

因此,对于缺省的赋值运算,如果对象域内没有heap上的空间,其不会产生任何问题。但是,如果对象域内需要申请heap上的空间,那么在析构对象的时候,就会连续两次释放heap上的同一块内存区域,从而导致异常。

~A()   {                delete name;   }

 

3.解决办法--重载(overload)赋值运算符
        因此,对于对象的域在heap上分配内存的情况,我们必须重载赋值运算符。当对象间进行拷贝的时候,我们必须让不同对象的成员域指向其不同的heap地址--如果成员域属于heap的话。    
因此,重载赋值运算符后的代码如下:

class A{public:    A()    {    }    A(int id,char *t_name)    {        _id=id;        name=new char[strlen(t_name)+1];        strcpy(name,t_name);    }        A& operator =(A& a)                   //注意:此处一定要返回对象的引用,否则返回后其值立即消失!    {        if(name!=NULL)                delete name;        this->_id=a._id;        int len=strlen(a.name);        name=new char[len+1];        strcpy(name,a.name);        return *this;    }    ~A()    {        cout<<"~destructor"<<endl;        delete name;    }    int _id;    char *name;};int main(){ A a(1,"herengang"); A b; b=a;}

其内存分配如下:



这样,在对象a,b退出相应的作用域,其调用相应的析构函数,然后释放分别属于不同heap空间的内存,程序正常结束。

 

=======================================================================================================

第三部分:关于拷贝构造函数、拷贝赋值函数的写法

类的深拷贝函数的重载
    public class A
{
    public:
        ...
        A(A &a);//重载拷贝函数
        A& operator=(A &b);//重载赋值函数
       
 //或者 我们也可以这样重载赋值运算符void operator=(A &a);即不返回任何值。如果这样的话,他将不支持客户代买中的链式赋值 ,例如a=b=c will be prohibited!
    private:
        int _id;
        char *username;
}

A::A(A &a)
{
    _id=a._id;
    username=new char[strlen(a.username)+1];
    if(username!=NULL)
        strcpy(username,a.usernam);
}

A& A::operaton=(A &a)
{
        if(this==&a)//  问:什么需要判断这个条件?(不是必须,只是优化而已)。答案:提示:考虑a=a这样的操作。
            return *this;
     
   if(username!=NULL)
            delete username;
        _id=a._id;
        username=new char[strlen(a.username)+1];
        if(username!=NULL)
            strcpy(username,a.usernam);
        return *this;    
}
//另外一种写法:
void A::operation=(A &a)
{
     
   if(username!=NULL)
            delete username;
        _id=a._id;
        username=new char[strlen(a.username)+1];
        if(username!=NULL)
            strcpy(username,a.usernam);
}

其实,从上可以看出,赋值运算符和拷贝函数很相似。不过赋值函数最好有返回值(进行链式赋值),返回也最好是对象的引用(为什么不是对象本身呢?note2有讲解), 而拷贝函数不需要返回任何同时,赋值函数首先要释放掉对象自身的堆空间(如果需要的话),然后进行其他的operation.而拷贝函数不需要如此,因为对象此时还没有分配堆空间。

note1:
    不要按值向函数传递对象。如果对象有内部指针指向动态分配的堆内存,丝毫不要考虑把对象按值传递给函数,要按引用传递。并记住:若函数不能改变参数对象的状态和目标对象的状态,则要使用const修饰符

note2:问题:

    对于类的成员需要动态申请堆空间的类的对象,大家都知道,我们都最好要overload其赋值函数和拷贝函数。拷贝构造函数是没有任何返回类型的,这点毋庸置疑。 而赋值函数可以返回多种类型,例如以上讲的void,类本身class1,以及类的引用 class &?

问:这几种赋值函数的返回各有什么异同?
答:

   1 如果赋值函数返回的是void ,我们知道,其唯一一点需要注意的是,其不支持链式赋值运算,即a=b=c这样是不允许的!
   2 对于返回的是类对象本身,还是类对象的引用,其有着本质的区别!
       
第一:如果其返回的是类对象本身
   A operator =(A& a)
    
{
            if(name!=NULL)
                delete name;
        
this->_id=
a._id;
        
int len=
strlen(a.name);
       name
=new char[len+1
];
        strcpy(name,a.name);
        
return *this
;
    }

          其过程是这样的:
                       class1 A("herengnag");
                        class1 B;   
                        B=A;
                    看似简单的赋值操作,其所有的过程如下:
                       1 释放对象原来的堆资源
                       2 重新申请堆空间
                       3 拷贝源的值到对象的堆空间的值
                     
  4 创建临时对象(调用临时对象拷贝构造函数),将临时对象返回
                       
5. 临时对象结束,调用临时对象析构函数,释放临时对象堆内存
my god,还真复杂!!但是,在这些步骤里面,如果第4步,我们没有overload 拷贝函数,也就是没有进行深拷贝。那么在进行第5步释放临时对象的heap 空间时,将释放掉的是和目标对象同一块的heap空间。这样当目标对象B作用域结束调用析构函数时,就会产生错误!!
            
因此,如果赋值运算符返回的是类对象本身,那么一定要overload 类的拷贝函数(进行深拷贝)!
       第二:如果赋值运算符返回的是对象的引用,
   A& operator =(A& a)
    
{
            if(name!=NULL)
                delete name;
        
this->_id=
a._id;
        
int len=
strlen(a.name);
       name
=new char[len+1
];
        strcpy(name,a.name);
        
return *this
;
    }

        那么其过程如下:
                   1 释放掉原来对象所占有的堆空间
                   1.申请一块新的堆内存
                   2 将源对象的堆内存的值copy给新的堆内存
                   3 返回源对象的引用
                   4 结束。
    因此,如果赋值运算符返回的是对象引用,那么其不会调用类的拷贝构造函数,这是问题的关键所在!!