1. 程式人生 > >C/C++面試題(3)——劍指offer1(賦值運算子函式)

C/C++面試題(3)——劍指offer1(賦值運算子函式)

今天又複習了C++面試題,這道題的目的是:給出一個類的宣告,然後寫出這個類的建構函式、解構函式、拷貝建構函式和運算子過載函式。

題目

//題目:如下為型別CMyString的宣告,請為該型別新增賦值運算子函式。
class CMyString
{
public:
CMyString(char *pData=NULL);//建構函式
CMyString();//解構函式
private:
char* m_pData;//資料域,字元指標
};

接下來分析如何一步步從最一般的解法到考慮異常安全性的解法:

那麼什麼是賦值運算子函式呢?

賦值運算子函式,由operator後面跟所定義的操作符符號,通過定義名為operator=函式來對賦值進行定義。該操作符函式有兩個形參:第一個形參對應左邊的運算元(隱式繫結到this指標了),第二個形參對應右運算元。
返回型別
應該與內建賦值運算返回的型別相同,內建型別的賦值運算返回對右運算元的引用,賦值操作符也返回對同一型別的引用。例如: class B { public: B& operator=(const B &);//賦值運算子函式的宣告 };

編寫賦值運算子函式應該注意的地方:

對於定義一個賦值運算子函式時,需要注意一下幾點:

(1)函式的返回型別必須是一個引用,因為只有返回自身的引用(也即 *this),才可以連續賦值;

(2)傳入的引數型別宣告為常量引用。如果傳入的引數不是引用而是例項,那麼從形參到實參會呼叫一次複製建構函式。把引數宣告為常量引用,可以提高程式碼效率,同時賦值運算函式內不會改變傳入的例項狀態;

(3)一定要記得釋放例項自身已有的記憶體,否則程式容易出現記憶體洩露

(4)注意傳入的引數和當前的例項(*this)是不是同一個例項,如果是同一個,則不用進行賦值操作,直接返回即可。

那麼賦值運算子函式的宣告是什麼呢?

CMyString& operator=(const CMyString& str);//過載運算子
經典的解法如下:
#include<iostream>
#include<stdlib.h>
using namespace std;

class CMyString
{
public:
    CMyString(char *pData=NULL);<span style="white-space:pre">	</span>//建構函式
    CMyString(const CMyString& str);<span style="white-space:pre">	</span>//拷貝建構函式
    ~CMyString();<span style="white-space:pre">			</span>//解構函式
    CMyString& operator=(const CMyString& str);//過載運算子
    void Print();<span style="white-space:pre">				</span>//列印字串
private:
    char* m_pData;<span style="white-space:pre">				</span>//資料域,字元指標
};

void CMyString::Print()
{
    cout<<m_pData<<endl;
}
//建構函式
CMyString::CMyString(char *pData)
{
    if(pData==NULL)<span style="white-space:pre">		</span>//如果建構函式的引數為空
    {
        m_pData=new char[1];
        m_pData[0]='\0';<span style="white-space:pre">	</span>//初始化字串,內容為'\0'
    }
    else<span style="white-space:pre">	</span>//如果建構函式的引數不為NULL,那麼首先求出字串長度,然後new一個長度為len+1的字元陣列
    {
        int len=strlen(pData);
        m_pData=new char[len+1];
        strcpy(m_pData,pData);//字串拷貝
    }
}

//解構函式
CMyString::~CMyString()
{
    delete[] m_pData;
}

//拷貝建構函式,拷貝建構函式與建構函式的思路非常類似。
CMyString::CMyString(const CMyString& str)
{
    int len=strlen(str.m_pData);
    m_pData=new char[len+1];
    strcpy(m_pData,str.m_pData);
}

//過載運算子
CMyString& CMyString::operator=(const CMyString& str)
{
    //如果傳入的引數與當前的例項是同一個例項,則直接返回自身
    if(this==&str)
        return *this;

    //釋放例項自身已有記憶體
    delete[] m_pData;
    m_pData=NULL;

    //在刪除自身記憶體以後在重新new一個長度為len+1的字元陣列,類似拷貝建構函式
    int len=strlen(str.m_pData);
    m_pData=new char[len+1];
    strcpy(m_pData,str.m_pData);
}

void main()
{
    char* strs="hello xuxing";
    CMyString str1(strs);
    CMyString str2;
    str2=str1;
    str1.Print();
    str2.Print();

    system("pause");
}</span>

前面程式碼的不足之處:
前面函式中,我們在分配記憶體之前就先呼叫了delete [] m_pData; 釋放了m_pData的記憶體。但是現在問題出現了,加入我們此時釋放了記憶體,但是剩下的記憶體又不夠我們 new char的空間,這個時候m_pData 將會是一個空指標,這樣非常導致程式崩潰。那我們如何解決這個問題呢?由於我們擔心先釋放會發生記憶體不夠的情況,那麼我們就先非配記憶體,並且判斷記憶體是否非配成功,在分配成功之後,再行釋放記憶體,這樣就做到萬無一失了。

改進之後的程式碼:

<span style="font-family:Microsoft YaHei;font-size:14px;">//過載運算子
CMyString& CMyString::operator=(const CMyString& str)
{
    if(this!=&str)//
    {
        CMyString strTemp(str);<span style="white-space:pre">		</span>//使用建構函式建立一個臨時物件
        //交換臨時物件與當前物件的m_pData值
        char* pTemp=strTemp.m_pData;
        strTemp.m_pData=m_pData;
        m_pData=pTemp;
    }
    return *this;
}</span>


改進之後的優點分析: 這樣的一個好處是在執行完if語句以後,因為除了strTemp的作用於,該例項會自動呼叫解構函式,把strTemp.m_pData所指向的記憶體釋放掉,而此時strTemp.m_pData指向的是例項原先m_pData指向的記憶體,並沒有釋放當前指向的pTemp這一塊記憶體。還有一點是通過建構函式為臨時例項分配記憶體,如果在new char過程中丟擲異常,並沒有改變該例項m_pData所指向的內容,也沒有釋放記憶體,所以是異常安全性的。