面試題1---賦值運算符函數詳解

1.題目

如下類型CMyString的聲明,請爲該類型添加賦值運算符函數。

class CMyString
{
  public:
  CMyString(char* pData=nullptr);
  CMyString(const CMyString& str);
  ~CMyString(void);
  private:
  char* m_pData;
}

2.疑問

  • 1.賦值運算符函數是什麼?
  • 2.nullptr是什麼?是null嗎?
  • 3.我寫出來的會是什麼東西?

3.涉及知識點

(一)類的構造函數

1.構造函數與類名相同,是特殊的公有成員函數
2.構造函數無函數返回類型說明,實際上構造函數是有返回值的,其返回值類型即爲構造函數所構建到的對象。
3.當新對象被建立時,構造函數便被自動調用,實例化的每個對象僅調用一次構造函數。
4.構造函數可以被重載(即允許有多個構造函數),重載由不同參數進行區分,構造時系統按照函數重載規則選擇一個進行執行。
5.如果類中沒有構造函數,則系統自動會生成缺省的構造函數。
6.只要我們定義了構造函數,則系統便不會生成缺省的構造函數。
7.構造函數也可在類外進行定義。
8.若構造函數是無參的或者各個參數均有缺省值,C++編譯器均認爲是缺省的構造函數。但是注意,缺省的構造函數只允許有一個。

(二)類的析構函數

1.析構函數無返回值無參數,其名字與類名相同,只在類名前加上~ 即:~類名(){…}
2.析構函數有且只有一個
3.對象註銷時自動調用析構函數,先構造的對象後析構。

#include<iostream>
using namespace std;
class Text
{
  private:
     long b;
     char a;
     double c;
  public:
     Text();
     //Text(char a=0);無參數的和各個參數均有缺省值的構造函數均被認爲是缺省構造函數
     Text(char a);
     Text(long b,double c);//參數列表不同的構造函數的重載
     ~Text()//析構函數有且只能有一個
     { 
      cout<<"The Text was free."<<this<<endl;
     }
     void print();
};
Text::Text()
{
  cout<<"The Text was built."<<this<<endl;
  this->a=0;
  this->b=0;
  this->C=0;
}
Text::Text(char a)
{
  cout<<"The Text was built."<<this<<endl;
  this->a=a;
}
Text::Text(long b,double c)
{
  cout<<"The Text was built."<<this<<endl;
  this->a='0';
  this->b=b;
  this->c=c;
}
void Text:print()
{
  cout<<"a= "<<this->a<<" b= "<<" c="<<this->c<<endl; 
}

(三)引用

1.引用簡介
引用就是某個變量(目標)的一個別名,對引用的操作與對變量直接操作完全一樣。
引用的聲明方法:類型標識符 &引用名=目標變量名。
【例1】:int a;int &ra=a;//定義引用ra,它是變量a的引用,即別名。
說明:
(1)&在此不是求地址運算,而是起標識作用。
(2)類型標識符是指目標變量的類型。
(3)聲明引用時,必須同時對其進行初始化。
(4)引用聲明完畢後,相當於目標變量名有兩個名稱,即該目標原名稱和引用名,且不能再把該引用名作爲其他變量名的別名。
ra=1;等價於a=1;
(5)聲明一個引用,不是新定義了一個變量,它只表示該引用名是目標變量名的一個別名,它本身不是一種數據類型,因此引用本身不佔存儲單元,系統也不給引用分配存儲單元。故:對引用求地址,就是對目標變量求地址。&ra與&a相等。
(6)不能建立數組的引用。因爲數組是一個由若干個元素所組成的集合,所以無法建立一個數組的別名。
2.引用應用
(1)引用作爲參數:引用的一個重要作用就是作爲函數的參數。以前的C語言中函數參數傳遞是值傳遞,如果有大塊數據作爲參數傳遞的時候,採用的方案往往是指針,因爲這樣可以避免將整塊數據全部壓棧,可以提高程序的效率。但是現在(C++中)又增加了一種同樣有效率的選擇(在某些特殊情況下又是必須的選擇),就是引用。
【例2】:void swap(int &p1,int &p2);//此處函數的形參p1,p2都是引用
{int p;p=p1;p1=p2;p2=p;}
爲在程序中調用該函數,則相應的主調函數的調用點處,直接以變量作爲實參進行調用即可,而不需要實參變量有任何的特殊要求。如:對應上面定義的swap函數,相應的主調函數可寫爲:

int main()
{
 int a,b;
 cin>>a>>b;//輸入a,b兩變量的值
 swap(a,b);//直接以變量a和b作爲實參調用swap函數
 cout<<a<<' '<<b;//輸出結果
}

上述程序運行,如果輸入數據10 20並回車後,則輸出結果爲20 10。
由【例2】可看出:
(1)傳遞引用給函數與傳遞指針的效果是一樣的。這時,被調用函數的形參就稱爲原來主調函數中的實參變量或對象的一個別名來使用,所以在被調函數中對形參變量的操作就是對其相應的目標對象(在主調函數中)的操作。
(2)使用引用傳遞函數的參數,在內存中並沒有產生實參的副本,它是直接對實參操作;而使用一般變量傳遞函數的參數,當發生函數調用是,需要給形參分配存儲單元,形參變量是實參變量的副本;如果傳遞的是對象,還將調用拷貝構造函數。因此,當參數傳遞的數據較大時,用引用比用一般變量傳遞參數的效率和所佔空間都好。
(3)使用指針作爲函數的參數隨二胺也能達到與使用引用的效果,但是,在被掉函數中同樣要給形參分配存儲單元,且需要重複使用“*指針變量名”的形式進行運算,這很容易產生錯誤且程序的閱讀性較差;另一方面,在主調函數的調用點處,必須用變量的地址作爲實參。而引用更容易使用,更清晰。
如果既要利用引用提高程序的效率,又要保護傳遞給函數的數據不在函數中被改變,就應使用常引用。
3.常引用
常引用聲明方式:const類型標識符 &引用名=目標變量名;
用這種方式聲明的引用,不能通過引用對目標變量的值進行修改,從而使引用的目標成爲const,達到了引用的安全性。
【例3】:

int a;
const int &ra=a;
ra=1;//錯誤
a=1;//正確

這不光是讓代碼更加健壯,也有些其它方面的需要。
【例4】:假設有如下函數聲明:

string foo();
void bar(string & s);

那麼下面的表達式將是非法的:

bar(foo());
bar("hello world");

原因在於foo()和"hello world"串都會產生一個臨時對象,而在C++中,這些臨時對象都是const類型的。因此上面的表達式就是試圖將一個const類型的對象轉換爲非const類型,這是非法的。引用型參數應該在能被定義爲const的情況下,儘量定義爲const。
4.引用作爲返回值
要以引用返回函數值,則函數定義時要按以下格式:
類型標識符 &函數名(形參列表及類型說明){函數體}
說明:(1)以引用返回函數值,定義函數時需要在函數名前加&
(2)用引用返回一個函數值的最大好處是,在內存中不產生被返回值的副本。
【例5】以下程序中定義了一個普通的函數fn1(它用返回值的方法返回函數值),另外一個函數fn2,它以引用的方法返回函數值。

#include<iostream.h>
float temp;//定義全局變量temp
float fn1(float r);//聲明函數fn1
float &f2(float r);//聲明函數fn2
float fn1(float r)//定義函數fn1,它以返回值的方法返回函數值
{
  temp=(float)(r*r*3.14);
  return temp;
}
float &fn2(float r)//定義函數fn2,它以引用方式返回函數值
{
   temp=(float)(r*r*3.14);
   return temp;
}
void main()//主函數
{
    float a=fn1(10.0);//第1種情況,系統生成要返回值的副本(即臨時變量)
    float &b=fn1(10.0);//第2種情況,可能會出錯(不同 C++系統有不同規定)
    //不能從被調函數中返回一個臨時變量或局部變量的引用
    float c=fn2(10.0);//第3種情況,系統不生成返回值的副本
    //可以從背調函數中返回一個全局變量的引用
    float &d=fn2(10.0);//第4種情況,系統不生成返回值的副本
    //可以從被調函數中返回一個全局變量的引用
    cout<<a<<c<<d;
}

引用作爲返回值,必須遵守以下規則:
(1)不能返回局部變量的引用。主要原因是局部變量會在函數返回後被銷燬,因此被返回的引用就成爲了“無所指”的引用,程序會進入未知狀態。
(2)不能返回函數內部new分配的內存的引用。雖然不存在局部變量的被動銷燬問題,可對於這種情況(返回函數內部new分配內存的引用),又面臨其它尷尬局面。例如,被函數返回的引用只是作爲一個臨時變量出現,而沒有被賦予一個實際的變量,那麼這個引用所指向的空間就無法釋放,造成memory leak。
(3)可以返回類成員,但最好是const。主要原因是當對象的屬性與某種業務規則相關聯的時候,其賦值常常與某些其它屬性或者對象的狀態有關,因此有必要將賦值操作封裝在一個業務規則當中。如果其它對象可以獲得該屬性的屬性的非常量引用(或指針),那麼對該屬性的單純賦值就會破壞業務規則的完整性。

(四)this指針

(1)我們知道類的成員函數可以訪問類的數據(限定符只是限定於類外的一些操作,類內的一切對於成員函數來說都是透明的),那麼成員函數如何知道哪個對象的數據成員要被操作呢,原因在於每個對象都擁有一個指針:this指針,通過this指針來訪問自己的住址。注:this指針並不是對象的一部分,this指針所佔的內存大小是不會反應在sizeof操作符上的。this指針的類型取決於使用this指針的成員函數類型以及對象類型,(1)假如this指針所在類的類型是Stu_Info_Manage類型,(下面的測試用例中的類的類型)並且如果成員函數是非常量的,則this的類型是:Stu_Info_Manager const類型,(2)即一個指向非const Stu_Info_Manager對象的常量(const)指針。
(2)this指針常用概念
this只能在成員函數中使用。全局函數,靜態函數都不能使用this。實際上,成員函數默認第一個參數爲T* const register this。
爲什麼this指針不能在靜態函數中使用?
大家可以這樣理解,靜態函數如同靜態變量一樣,它不屬於具體的哪一個對象,靜態函數表示了整個範圍意義上的信息,而this指針卻實實在在的對應一個對象,所以this指針當然不能被靜態函數使用了,同理,全局函數也一樣。
this指針是什麼時候創建的?
this在成員函數的開始執行前構造的,在成員的執行結束後清除。
this指針只有在成員函數中才有定義。因此,你獲得一個對象後,也不能通過對象使用this指針。所以,我們也無法知道一個對象的this指針的位置(只有在成員函數裏纔有this指針的位置)。當然,在成員函數裏,你是可以知道this指針的位置的(可以&this獲得),也可以直接使用的。
(3)this指針的使用:
一種情況就是,在類的非靜態成員函數中返回類對象本身的時候,我們可以使用源點運算符(this).,箭頭運算符this->,另外,我們也可以返回關於this的引用。

(五)深拷貝與淺拷貝(位拷貝)

前提:在對象拷貝過程中,如果沒有自定義拷貝構造函數,系統會提供一個缺省的拷貝構造函數,缺省的拷貝構造函數對於基本類型的成員變量,按字節複製,對於類類型成員變量,調用其相應類型的拷貝構造函數。
閱讀《C++ primer》有一段這樣的話:
由於並非所有的對象都會使用拷貝構造函數和複製函數,程序員可能對這兩個函數有些輕視。請先記住以下的警告,在閱讀正文時就會多心:
本章開頭講過,如果不主動編寫拷貝構造函數和賦值函數,編譯器將以“位拷貝”的方式自動生成缺省的函數。倘若類中含有指針變量,那麼這兩個缺省的函數就隱含了錯誤。以類String的兩個對象a,b爲例,假設a.m_data的內容爲“hello”,b.m_data的內容爲“world”。
下年a賦值給b,缺省賦值函數的“位拷貝”意味着執行b.m_data=a.m_data。這將造成三個錯誤:一是b.m_data原有的內存沒被釋放,造成內存泄漏;二是b.m_data和a.m_data指向同一塊內存,a或b任何變動都會影響另一方;三是在對象被析構是,m_data被釋放了兩次。
拷貝構造函數和賦值函數非常容易混淆,常導致錯寫誤用。拷貝構造函數是在堆象被創建時調用的,而賦值函數只能被已經存在了對象調用。以下程序中,第三個語句和第四個語句很相似,你分得清楚哪個調用了拷貝構造函數,哪個調用了賦值函數嗎?
String a(“hello”);
String b(“world”);
String c=a;//調用了拷貝構造函數,最好寫成c(a);
c=b;//調用了賦值函數
本例中第三個語句的風格較差,宜改寫成String c(a)以區別第四個語句。
在這裏插入圖片描述 位拷貝(淺拷貝)舉例,a指向b,b的改變其實會影響a的改變,同時a原本指向的空間發生泄漏。
然後這種情況下有了深拷貝。
何時調用 :以下情況都會調用拷貝構造函數:

  • 一個對象以值傳遞的方式傳入函數體
  • 一個對象以值傳遞的方式從函數返回
  • 一個對象需要通過另外一個對象進行初始化。
    淺拷貝:位拷貝,拷貝構造函數,賦值重載,多個對象共用同一塊資源,同一塊資源釋放多次,崩潰或者內存泄漏。
    深拷貝:每個對象共同擁有自己的資源,必須顯示提供拷貝構造函數和賦值運算符。
    缺省拷貝構造函數在拷貝過程中是按字節複製的,對於指針型成員變量只複製指針本身,而不復制指針所指向的目標–淺拷貝。
    我們用自己編寫的string舉例。
class String
{
  private:
     char * _str;
  public:
    const char* c_str()
    {
      return _str;
    }
    String(const char* str=""):_str(new char[strlen(str)+1])
    {
      strcpy(_str,str);
    }
    String(const String &s):_str(NULL)
    {
       String tmp(s._str);
       swap(_str,tmp._str);
    }
    ~String()
    {
      if(_str)
      {
         delete[] _str;
      }
    }
}

通過開闢空間的方式,進行深拷貝,這種方式採取的拷貝構造,注意這個

String(const String &s):_str(NULL)
{
  String tmp(s.str);
  swap(_str,tmp._str);
}

代碼解析:其中this指向拷貝的對象,s指向試圖拷貝的原對象。其中利用構造函數開闢空間,建立臨時的tmp,然後進行交換完成拷貝。當然,我們也可以使用賦值操作符重載完成這一功能

String& operator =(const String& s)
{
  if(this!=&s)
  {
    String tmp(s._str);
    swap(tmp._str,_str);
    return *this;
  }
}//調用構造析構
//本代碼是tmp調用的構造函數
String (const char* str=""):_str(new char[strlen(str)+1])
{
 strcpy(_str,str);
}
//調用這個構造函數,開闢空間,建立一個和s1一樣大小的空間,並拷貝值

s1(this),s2(s)
建立tmp,tmp和s2一樣大的空間,一樣的數值(調用構造函數),然後交換使s1(this)指向2號空間,獲得拷貝,tmp指向3號空間,tmp聲明週期結束調用析構函數釋放,功能完成。
在這裏插入圖片描述

(六)const關鍵字

1.定義const對象:const修飾符可以把對象轉變爲常數對象,意思就是說利用const進行修飾的變量的值在程序的任意位置將不能再被修改,就如同常數一樣使用~任何修改該變量的嘗試都會導致編譯錯誤,因爲常量在定有就不能被修改,所以定義時必須初始化。對於類中的const成員變量必須通過初始化列表進行初始化,如下所示:

class A
{
public:
    A(int i);
    void print();
    const int &r;
 private:
    const int a;
    static const int b;
};
const int A::b=10;
A::A(int i):a(i),r(a)
{
}

2.const對象默認爲文件的局部變量:在全局作用域裏定義非const變量時,它在整個程序中可以訪問,我們可以把一個非const變量定義在一個文件中,假設已經做了合適的聲明,就可以在另外的文件中使用這個變量。在全局作用域聲明的const變量是定義該對象的文件的局部變量。此變量只存在於那個文件中,不能被其他文件訪問。通過指定const變量爲extern,就可以在整個程序中訪問const對象。

extern const int bufSize;

非const變量默認爲extern。要使const變量能夠在其他文件中訪問,必須在文件中顯示地指定它爲extern。
3.const對象的動態數組:如果我們在自由存儲區中創建的數組存儲了內置類型的const對象,則必須爲這個數組提供初始化:因爲數組元素都是const對象,無法賦值。實現這個要求的唯一方法是對數組做值初始化。

//Error
const int *pci_bad=new const int[100];
//ok
const int *pci_ok=new const int[100]();

C++允許定義類類型的const數組,但該類類型必須提供默認構造函數

const string *pcs=new string[100];//這裏便會調用string類的默認構造函數初始化數組元素。

4.指針和const限定符的關係
const限定符和指針結合起來常見的情況有以下幾種。

  • (1)指向常量的指針(指向const對象的指針):C++爲了保證不允許使用指針改變所指的const值這個特性,強制要求這個指針也必須具備const特性。
const double *cptr;

這裏cptr是一個指向double類型const對象的指針,const確定了cptr指向的對象的類型,而而並非cptr本身,所以cptr本身並不是const。所以定義的時候並不需要對它進行初始,如果需要的話,允許給cptr重新賦值,讓其指向另一個const對象。但不能通過cptr修改其所指對象的值。

*cptr=42;//error
  • (2)常指針(const指針):C++還提供了const指針—本身的值不能修改
int errNumb=0;
int *const curErr=&errNumb;//curErr是一個const指針

我們可以從右往左把上述定義語句讀作“指向int型對象的const指針”。與其他const量一樣,const指針的值不能被修改,這意味着不能使curErr指向其他對象。const指針也必須在定義的時候初始化。

curErr=curErr;//錯誤!即使賦給其相同的值

5.函數和const限定符的關係

  • (1)類中的const成員函數(常量成員函數):在一個類中,任何不會修改數據成員的函數都應該聲明爲const類型。使用const關鍵字進行說明的成員函數,稱爲常成員函數。只有常成員函數纔有資格操作常量或常對象,沒有使用const關鍵字說明的成員函數不能用來操作常對象。const是加載函數說明後面的類型修飾符,它是函數類型的一個組成部分,因此,在函數實現部分也要帶const關鍵字。下面舉例子說明常成員函數的特徵。
class Stack
{
  private:
     int m_num;
     int m_data[100];
  public:
     void Push(int elem);
     int Pop(void);
  int GetCount(void)const;//定義爲const成員函數
};
int Stack::GetCount(void)const
{
  ++m_num;//編譯錯誤,企圖修改數據成員,m_num
  Pop();//編譯錯誤,企圖非const成員函數
  return m_num;
}

既然const是定義爲const函數的組成部分,那麼就可以通過添加const實現函數重載。

class R
{
  public:
    R(int r1,int r2)
    {
      R1=r1;
      R2=r2;
    }
    void  print();
    void  print() const;
 private:
    int R1,R2;
};
void R::print()
{
  cout<<R1;
}
void R::print()const
{
  cout<<R2;
}
void main()
{
  R a(5,4);
  a.print();
  const R b(20,52);
  b.print();
}

6.const的難點

int b=100;
const int *a=&b;//[1]
int const *a=&b;//[2]
int* const a=&b;//[3]
const int* const a=&b;//[4]

如果const位於星號的左側,則const就是用來修飾指針所指向的變量,即指針指向的對象爲常量;如果const位於星號的右側,const就是修飾指針本身,即指針本身是常量。
因此,[1]和[2]的情況相同,都是指針所指向的內容爲常量(const 放在變量聲明符的位置無關),這種情況下不允許對內容進行更改操作,如*a=3;[3]爲指針本身是常量,而指針所指向的內容不是常量,這種情況下不能對指針本身進行更改操作,如a++是錯誤的;[4]爲指針本身和指向的內容均爲常量。

(七)賦值運算符

1.概述:首先介紹爲什麼要對賦值運算符“=”進行重載。某些情況下,當我們編寫一個類的時候,並不需要爲該類重載“=”運算符,因爲編譯系統爲每個類提供了默認的賦值運算符“=”,使用這個默認的賦值運算符操作類對象時,該運算符會把這個類的所有數據成員都進行一次賦值操作。例如有如下類:

class A
{
  public:
     int a;
     int b;
     int c;
};

那麼對這個類的對象進行賦值時,使用默認的賦值運算符是沒有問題的。但是,在下面的示例中,使用編譯系統默認提供的賦值運算符,就會出現問題了。示例代碼如下:

#include<iostream>
#include<string.h>
using namespace std;
class ClassA
{
 public:
   ClassA
   {
   }
   ClassA(const char* pszInputStr)//深拷貝
   {
     pszTestStr=new char[strlen(pszInputStr)+1];
     strncpy(pszTestStr,pszInputStr,strlen(PszInputStr)+1);
   }
   virtual ~ClassA()
   { 
     delete pszTestStr;
   }
   public:
      char* pszTestStr;
};

我們修改一下前面出錯的代碼示例,現增加賦值運算符重載函數的類,代碼如下:

ClassA& operator=(const ClassA& cls)
{
   //避免自賦值
   if(this!=&cls)
   {
     //避免內存泄漏
     if(pszTestStr!=NULL)
     {
       delete pszTestStr;
       pszTestStr=NULL;
     }
     pszTestStr=new char[strlen(cls.pszTestStr)+1];
     strncpy(pszTestStr,cls.pszTestStr,strlen(cls.pszTestStr)+1);
   }
   return *this;
}

2.總結:綜合上述示例內容,我們可以知道針對一下情況,需要顯示地提供賦值運算符重載函數(即自定義賦值運算符重載函數):

  • 用非類A類型的值爲類A的對象賦值時(當然,這種情況下我們可以不提供相應的賦值運算符重載函數,而值提供相應的構造函數)。
  • 當用類A類型的值爲類A的對象賦值,且類A的數據成員中含有指針的情況下,必須顯示提供賦值運算符重載函數。

(八)nullptr關鍵字

爲了避免“野指針”(即指針在首次使用之前沒有進行初始化)的出現,我們聲明一個指針後最好馬上對其進行初始化操作。如果暫時不明確該指針指向哪個變量,則需要賦予NULL值。除了NULL指針之外,C++11新標準中又引入了nullptr來聲明一個“空指針”,這樣,我們就有下面三種方法來獲取一個“空指針”:
如下:

int *p1=NULL;//需要引入cstdlib頭文件
int *p2=0;
int *p3=nullptr;

新標準中建議使用nullptr代替NULL來聲明空指針。到這裏,可能有疑問爲什麼要引入nullptr?這裏有幾個原因。

  • NULL在C++中的定義,NULL在C++中被明確定義爲整數0;
  • nullptr關鍵字用於標識空指針,它可以轉換成任何指針類型和bool布爾類型(主要是爲了兼容普通指針可以作爲條件判斷語句的寫法),但是不能轉換爲整數。
char *p1=nullptr;//正確
int *p2=nullptr;//正確
bool b=nullptr;//正確 if(b)判斷爲false
int a=nullptr;//error

4.就題論題

當面試官要求應聘者定義一個賦值運算符函數時,他會在檢查應聘者寫出的代碼時關注如下幾點:

  • 是否把返回值的類型聲明爲該類型的引用,並在函數結束前返回實例自身的引用(*this)。只有返回一個引用,纔可以允許連續賦值。否則,如果函數的返回值是void,則應用該賦值運算符將不能進行連續賦值。假設有3個CMyString的對象:str、str2和str3,在程序中語句str1=str2=str3將不能通過編譯。
  • 是否把傳入的參數的類型聲明爲常量引用。如果傳入的參數不是引用而是實例,那麼從形參到實參會調用一次複製構造函數。把參數聲明爲引用可以避免這樣的無謂消耗,能提高代碼的效率。同時,我們在賦值運算函數內不會改變傳入的實例的狀態,因此應該爲傳入的引用參數加上const關鍵字。
  • 是否釋放實例自身已有的內存。如果我們忘記在分配新內存之前釋放自己已有的空間,則程序將出現內存側漏。
  • 判斷傳入的參數和當前的實例(this)是不是同一個實例。如果是同一個,則不進行賦值操作,直接返回。如果事先不判斷就進行賦值,那麼釋放實例自身內存的時候就會導致嚴重的問題:當this和傳入的參數是同一個實例時,一旦釋放了自身的內存,傳入的參數的內存也同時釋放了,因此再也找不到需要賦值的內容了。
    經典解法,適用於初級程序員
CMyString& CMyString::operator =(const CMyString &str)
{
  if(this==&str)
  return *this;
  delete []m_pData;
  m_pData=nullptr;
  m_pData=new char[strlen(str.m_pData)+1];
  strcpy(m_pData,str.m_pData);
  return *this;
}

考慮異常安全性的解法,高級程序員必備
在前面的函數中,我們在分配內存之前先用delete釋放了實例m_pData的內存。如果此時內存不足導致new char拋出異常,則m_pData將是一個空指針,這樣非常容易導致程序崩潰。也就是說,一旦在賦值運算符函數內部拋出一個異常,CMyString的實例不再保持有效狀態,這就違背了異常安全性原則。
要想在賦值運算符函數中實現異常安全性,我們有兩種方法。一種簡單的辦法是我們先用new分配新內容,在用delete釋放已有的內容。這樣只在分配內容成功之後在釋放原來的內容,也就是當分配內存失敗時我們能確保CMyString的實例不會被修改。我們還有一種更好的辦法,即先創建一個臨時實例,在交換臨時實例和原來的實例。下面是這種思路的參考代碼:

CMyString& CMyString::operator =(const CMyString &str)
{ 
   if(this!=&str)
   {
      CMyString strTemp(str);
      char* pTemp=strTemp.m_pData;
      strTemp.m_pData=m_pData;
      m_pData=pTemp;
   }
   return *this;
} 

在這個函數中,我們先創建一個臨時實例strTemp,接着把strTemp.m_pData和實例自身的m_pData進行交換。由於strTemp是一個局部變量,但程序運行到if的外面時也就出了該變量的作用域,就會自動調用strTemp的析構函數,把strTemp.m_pData所指向的內存釋放掉。由於strTemp.m_pData指向的內存就是實例之前m_pData的內存,這六相當於自動調用析構函數釋放實例的內存。
在新的代碼中,我們在CMyString的構造函數裏用new分配內存。如果由於內存不足拋出注入bad_alloc異常,但我們還沒有修改原來實例的狀態,因此實例的狀態還是有效的,這也就確保了異常安全性。
如果應聘者在面試的時候能夠考慮到這個層面,面試官就會覺得他對代碼的異常安全性有很深的理解,那麼他自然也就能通過這輪面試了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章