C++本質:類的賦值運算符=的重載,以及深拷貝和淺拷貝

關鍵詞:構造函數,淺拷貝,深拷貝,堆棧(stack),堆heap,賦值運算符
摘要:
    在面向對象程序設計中,對象間的相互拷貝和賦值是常常進行的操做。
    若是對象在申明的同時立刻進行的初始化操做,則稱之爲拷貝運算。例如:
        class1 A("af"); class1 B=A;
     此時其實際調用的是B(A)這樣的淺拷貝操做。
    若是對象在申明以後,在進行的賦值運算,咱們稱之爲賦值運算。例如:
        class1 A("af"); class1 B;
        B=A;html

  此時實際調用的類的缺省賦值函數B.operator=(A);
        不論是淺拷貝仍是賦值運算,其都有缺省的定義。也就是說,即便咱們不overload這兩種operation,仍然能夠運行。
那麼,咱們到底需不須要overload這兩種operation 呢?
        答案就是:通常,咱們咱們須要手動編寫析構函數的類,都須要overload 拷貝函數和賦值運算符。

        
 下面介紹類的賦值運算符
1.C++中對象的內存分配方式
        在C++中,對象的實例在編譯的時候,就須要爲其分配內存大小,所以,系統都是在stack上爲其分配內存的。這一點和C#徹底不一樣!千 萬記住:在C#中,全部類都是reference type,要建立類的實體,必須經過new在heap上爲其分配空間,同時返回在stack上指向其地址的reference.
        所以,在C++中,只要申明該實例,在程序編譯後,就要爲其分配相應的內存空間,至於實體內的各個域的值,就由其構造函數決定了。
    例如:app

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空間的內存,程序正常結束。


references:
類的深拷貝函數的重載
    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 結束。
    所以,若是賦值運算符返回的是對象引用,那麼其不會調用類的拷貝構造函數,這是問題的關鍵所在!!
 
完整代碼以下:
// virtual.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include "string.h"
#include "stdlib.h"
#include "assert.h"

class complex
{
public:
        int real;
        int virt;
public:
    complex(){real=virt=0;}
    complex(int treal,int tvirt){real=treal;virt=tvirt;}
    complex operator+(const complex &x)
    {
        real+=x.real;
        virt+=x.virt;
        return *this;
    }
    complex operator=(const complex &x)
    {
        return complex(x.real,x.virt);
    }
};


class A
{
public:
    A(){m_username=NULL;printf("null constructor");}
    A(char *username)
    {
        int len;
        len=strlen(username);
        m_username=new char[len+1];//(char*)malloc(sizeof(len+1));
        strcpy(m_username,username);
        printf(""nUsername is %s"n",m_username);
    }
    
    A(A &a);
    A operator=(A &b);
    int test(const int &x)
    {
        return x;
    }

    virtual ~A()
    {
    //    if(m_username)
        {
        delete m_username;
        printf(""nA is destructed"n");
        }
    }



protected:
    char *m_username;

};



A::A(A &a)
{

    int len=strlen(a.m_username);
    this->m_username=new char[len+2];
    strcpy(m_username,a.m_username);
    strcat(m_username,"f");
    printf(""ndeep copy function");
}


A A::operator=(A &b)
{
    if(m_username)
        delete m_username;

    int len=strlen(b.m_username);
    this->m_username=new char[len+1];
    strcpy(m_username,b.m_username);
//    printf("copied successfully!");
     return *this;
}

 

class B:public A
{
public:
    B(char *username,char *password):A(username)
    {
        int len=strlen(password)+1;
        m_password=new char[len];//(char *)malloc(sizeof(len));
        strcpy(m_password,password);
        printf("username:%s,password:%s"n",m_username,m_password);
    }
    ~B()
    {
        delete m_password;
        printf("B is destructed"n");
    }
protected:
    char *m_password;
};

int main(int argc, char* argv[])
{
//    B b("herengang","982135");
//    A *a=&b;
//    delete a;
    A a("haha");
    A b;

    printf(""nbegin to invoke copy function");
    b=a;

//    printf("%d",b.test(2));
    //complex x(1,3),y(1,4);
    //x=(x+y);
    //printf("%d,%d",x.real,x.virt);
    return 0;


}

1 重載賦值運算符返回結果爲類對象的運行結果this

明顯, 運算符最後調用了拷貝構造函數

2 重載賦值運算符返回結果爲類對象引用的運行結果spa

很明顯,沒有調用拷貝構造函數設計

轉自:http://www.cnblogs.com/alexusli/archive/2008/08/27/1277683.html指針

相關文章
相關標籤/搜索