天天看點

Effective c++(筆記) 之 類與函數的設計聲明中常遇到的問題

1.當我們開始去敲代碼的時候,想過這個問題麼?怎麼去設計一個類?

或者對于程式員來說,寫代碼真的就如同搬磚一樣,每天都幹的事情,但是我們是否曾想過,在c++的代碼中怎麼樣去設計一個類?我覺得這個問題可比我們“搬磚”重要的多,大家說不是麼?

這個答案在本部落格中會細細道來,當我們設計一個類時,其實會出現很多問題,例如:我們是否應該在類中編寫copy constructor 和assignment運算符(這個上篇部落格中已說明),另外,我們是讓編寫的函數成為類的成員函數還是友元還是非成員函數,函數的參數使用傳引用的方式還是傳值的方式,這個函數該不該聲明為const,函數的傳回值是該設計成const麼?等一系列的問題,我會在下文分成各個小問題來解釋。

首先,我覺得應該考慮的一個重要問題,我們設計的類該有多大,每當有新的需求時,我們是否應該随意的添加。

請在設計類的時候遵循下面的原則:

讓你的class類接口即完美又最小化

原因------設計的類就相當于定義了一個新的類型,如果不能滿足我們的需求,那麼就不用談其他的了,是以首先我們應該讓設計的類滿足我們的需求,在滿足我們的需求時,盡可能的使類最小化,類的最小化是指,當類有新的需求時,我們看這個需求是否跟已經編寫的函數沖突,是否可以和以前的整合,也就是說看這個成員函數是否是必要寫到類裡面的,因為大型class接口的缺點可維護性差。

2.類中的資料成員是設計成public、protected還是private?

答案:盡量使自己的data members設計成私有,不讓外部通路,使其封裝性更好,如果類的資料成員設計成public的話,外界随便通路,這對于c++的封裝性而言就不是很好。

我們通常設計為pirvate,如果需要得到或者改變這些值,我們會編寫專門的成員函數來操作,如下所示

如果所設計的類為基類,同時希望基類的資料成員被派生類繼承,那麼一個很好的方法常常将資料成員設計為保護類型protected

這樣,當繼承方式為公有繼承時,基類的公有成員和保護成員均以原有的狀态繼承下來,派生類中的成員函數和友元可以通路到基類的資料成員。

3.類class與結構體struct 的差別在哪裡?

答:定義類等于定義了一種新的類型,結構體其實也可以達到這樣的結果,有兩個非常明顯的不同點

類中如果不注明資料成員的通路級别預設的資料成員是私有的private,當繼承的時候,如果不注明繼承方式,則是私有private繼承

結構體正好相反,它定義是預設的資料成員是公有的public,同時它的預設繼承方式也是公有public的

4.設計類的函數時,應該将其設計為成員函數、非成員函數還是友元函數?

答:首先簡單說一下它們之間的差別

成員函數與非成員函數最大的差別是---------------成員函數可以為虛函數,而其他的函數不可以為虛函數(最大最明顯的差別)。

友元函數相當于該類的一個朋友友元,它可以通路該類的所有資料成員,但有時候朋友多并不是什麼好事,是以,在設計類的時候,如果這個函數不能為成員函數但同時它又必須通路到該類的資料成員(輸入>> 輸出<< 操作符重載)此時再設計成友元,如果可能不設計為友元那就盡量這樣。

用一個例子來教我們怎麼判斷這個函數是設計成成員函數還是非成員函數!

下面是一個分數的類,其中有個實作分數乘法的函數,我們暫且先将它設計為類的成員函數來讨論

看上述代碼,将Rational類中的分數乘法設計為類的成員函數看似沒什麼錯誤,看下面的執行個體就知道了

如果我們寫成上述的成員函數,那麼乘号*左邊一定得是Rational對象,因為成員函數的形式決定了這樣,

在此你可能會說不對,兩邊都必須是Rational對象,乘号*右邊可以為int對象,原因如下:

當我們在類中的構造函數設計為

而不是

這二者的差別就是,當函數的參數是類類型的時候,當你傳入函數的參數并不是類類型,恰該類類型的構造函數沒有申明explicit,那麼便會産生隐式轉換,使int ------->   Rational,其内部的轉換過程大緻如下:

再次說明:

隻有當類類型的構造函數沒有聲明explicit時才會産生類型轉換

這裡的類型轉換隻針對于出現在參數表上的類類型形參有效,而對于(*this)是無效的,是以最後一個執行個體會報錯,因為左邊不會進行轉換。

是以這樣看來将分數乘法的這個函數設計為成員函數顯然不合理,因為沒辦法乘号*左邊是int類型的資料,這跟現實不符合。

有人說了再寫一個類似能實作左邊是int值的函數就可以了,别忘了我們設計類最初要遵循的:盡量接口最小化,我們完全可以通過一個函數實作所有的類型的分數相乘,為什麼要再寫一個函數呢,如果再寫一個函數就違背了接口最小化的原則。

我們可以這樣改變operator*函數 , 将其設計為非成員函數,如下所示

這樣參數為兩個均為Rational類的引用,都為參數,在構造函數不為explicit時,均可以進行隐式轉換,實作了左右兩邊都可以是int型資料的可能。同時類中的成員設計為私有,通過numerator() 和denominator()來通路,提高了類的封裝性,參數的形式采用了引用的方式,當調用該函數時,不用複制實參,提高了效率,傳回值采用了const的形式,避免了分數乘積的結果被寫的危險,即有效率又有安全性的寫法。

很多情況下,都需要重載輸入輸出操作符,常常為了我們的程式設計習慣,把輸入輸出操作符重載的函數設計成了類的非成員友元函數。

結論

虛拟函數必須為類的成員函數

類的加減乘除運算符重載常常設計為類的非成員函數

輸入輸出操作符重載的函數一定不能為類的成員函數,常常為類的友元函數

隻有非成員函數才能在其左端對象(形參)身上使用類型轉換

5.函數的參數盡量使用傳址方式,而少使用傳值方式

這條幾乎是c++領域中公認的規則,在編寫函數中盡量使用引用(傳址)方式,而少用c語言中的傳值方式

原因有下面兩點

傳址方式效率比較高,不用在調用函數的時候複制實參調用拷貝構造函數,通常将對象以傳值的方式進行時,将實參複制給形參需要調用copy constructor 當傳回的時候将傳回值傳回對象又調用copy constructor 完了之後對局部對象會析構掉,還要調用析構函數,這樣的效率可知。

另外,當使用傳址的方式時,可以避免派生類對象傳入參數為基類對象的函數時發生的切割現象,當用傳址的方式,該基類對象的引用綁定的是派生類對象,是以在執行這個函數裡中如果該對象調用了虛函數,那麼就會根據其綁定的動态對象來決定執行基類還是派生類的對象,這樣很容易達到我們的目的。

6. 當傳回值是對象時,盡量不要采用傳回引用的方式

其實這點我感覺Effective c++沒有說清楚,我認為,當函數是類的成員函數,通常傳回的應該是該類的引用,為什麼呢?因為Effective c++上說這條的原因是,如果傳回的是對象,采用引用的方式,通常該引用指向不存在的對象,但是在類的成員函數中,常以T&作為傳回值,是因為調用該成員函數的對象肯定存在我們常常傳回時*this,是以對于成員函數而言傳回*this不可能指向不存在的對象。

是以我感覺書上這點應該指的是類的非成員函數,當類的非成員函數傳回對象時,我們的确不要傳回該對象的引用。

傳引用無非目标就是避免調用構造函數使效率提高,但是因為傳回的引用必然要綁定對象(因為引用其實就是别名,為某個對象某個變量起了另外一個名字,改變這個别名也就等于改變了本身,操作這個引用也就等于操作了本身) ,是以我們必然要産生一個對象,這樣傳回時,引用才能指向一個對象,棧空間和堆空間中産生

凡是局部變量都是在棧空間中産生的,

凡是通過new出來的都是在堆空間中産生的

還是拿上面的那個分數乘法的例子繼續讨論

首先,在棧空間中産生的result,仍然調用了構造函數,效率完全沒有提高,另外一個最大的bug是你傳回了局部對象,這樣當函數執行完後局部對象就析構不存在了,這是非常大的錯誤。

如果在堆空間中産生result,new産生的還是需要調用構造函數,同時也存在一個bug,那就是你new了什麼時候delete哈!new和delete必須要配對産生哈!

是以,在非成員函數中,如果傳回的是對象時,盡量傳回值盡量為對象而不應該為對象的引用

7.什麼時候應該使用const?

其實,在初學c++的時候感覺最郁悶的就是這個const關鍵字了,它無時不刻存在所有的c++代碼中,讓我看得頭暈眼花。

const表示常量,在定義全局變量時我們常用到,如下所示

這是在全局作用域定義一些常量,别告訴我你還在用#define,可以改改舊的c語言習慣了哈!

另外在類的成員函數的末尾也經常見到這個關鍵字const,如下所示

這表示調用該成員函數的對象的資料成員不可更改,說的簡單點就是這個函數中隐藏的this指針所指向的對象時const對象(注意,指針并不是const,而是指向的對象時const對象)

在函數的傳回值時有時也能見到const

這裡表示傳回的對象時const,不能被寫,隻能讀,函數傳回的是一個常量值。如下所示

常常函數的參數我們也使用const 引用的方式

此處,如果在函數中不打算改變參數的資料成員,就盡量設定成const,這樣當你不注意在函數體内試圖改變參數時編譯器便會給提醒。

怎麼去差別const關鍵字是限制指針還是變量的,下面是一種最簡單的方法

以*号為分界線,const在左邊就是限制變量,即指向的是const的變量,const在*号右邊指的是指針不能修改,const指針。

8.如果不想用編譯器産生的預設函數,盡量顯示的拒絕這個函數

怎麼去拒絕編譯器産生的預設函數,将它定義為類的私有方式即可,這樣執行個體化的對象也可以是客戶就沒法調用這個函數,或者嘗試去調用這個函數時,編譯器會提示錯誤。

為什麼要去拒絕編譯器為我們産生的預設函數,比如預設的指派操作符

當我們定義資料類的時候,數組是不能指派的,需要循環才可以,是以在設計類的時候,便不想允許這個函數存在,雖然自己沒有編寫,但是編譯器依然還會給我們合成一個,是以此時我們就必須顯示指出這個函數不能調用,如下所示

繼續閱讀