1. 程式人生 > >effective C++筆記——實現

effective C++筆記——實現

文章目錄

儘可能延後變數定義式的出現時間

. 當定義了一個變數並且它的型別存在建構函式和解構函式,那麼程式需要承受它的構造成本和析構成本,即使這個變數並不被使用,仍需要耗費這些成本,所以應該儘量避免這種情況的發生。
  通常我們不會定義一個不被使用的變量出來,但是定義變數過早可能會應為程式的執行情況而沒有被使用到,比如:

string entrypassword(const string& password){
	string entryret;									//過早的定義
	if(password.length() < 5){
		throw logic_error("password is too short!");
	}
	...														//其他操作
	entryret = password;
	return entryret;
}

. 假設在其中丟擲了異常,變數entry熱帖就沒有被使用,但是仍然要承擔entryret的構造成本和析構成本。另一個問題是定義變數的操作是使用的預設的建構函式,然後再使用賦值符號進行賦值,這樣顯得效率低下,應該儘量跳過預設的預設的構造:

string entrypassword(const string& password){
	if(password.length() < 5){
		throw logic_error("password is too short!");
	}
	...														//其他操作
	string entryret(password);				//使用拷貝建構函式完成定義和初始化					
	return entryret;
}

. 另外一種比較常見的定義變數的情況是在迴圈中的變數定義,是應該將變數定義在迴圈內部還是定義在迴圈外部每次進行賦值?兩種情況分別對應的成本如下:
  做法1:n個建構函式+n個解構函式
  做法2:1個建構函式+1個解構函式+n個賦值操作
  所以通常除了在知道賦值操作的成本比“構造+析構”的成本低,或則正在處理程式碼中效率的高敏感部分時,都應該使用做法1。

儘量少做轉型操作

. 轉型破壞了型別系統,可能帶來任何種類的麻煩,有些容易辨識,有些則會非常隱晦。所以這個特性應當得到重視。
  先回顧兩種舊式轉型的形式:

//C風格的轉型動作
(T)expression;      //將expression轉型為T
//函式風格的轉型動作
expression(T);			//將expression轉型為T

兩種形式並無差別,知識括號的擺放位置不同,C++還提供四中新式轉型:
1.const_cast<T>(expression)
 const_cast通常被用來將物件的常量性轉除,它也是唯一有此能力的C++風格的轉型操作符。
2.dynamic_cast<T>()expression)
 dynamic_cast主要用來執行“安全向下轉型”,也就是用來決定某物件是否歸屬繼承體系中的某個型別。它是唯一無法由舊式語法執行的動作,也是唯一可能耗費重大執行成本的動作。
3.reinterpret_cast<T>(expression)
 reinterpret_cast意圖執行低階轉型,實際動作及結果可能取決於編譯器,這也就表示它不可移植,例如將一個指向整型的指標轉為一個整型。
4.static_cast<T>(expression)
 static_cast用來強迫隱式轉換,例如將非常量物件轉換為常量物件、將void指標轉為typed指標,或者將int轉為double等。
  新式的轉型較受歡迎,因為容易在程式碼中辨識出來,也更加目標明確。
  轉型操作並不是簡單的告訴編譯器將變數視作另一種型別,而是往往真的產出需要執行的程式碼,比如:

int x,y;
...
double d = static_cast<double>(x)/y;		//使用浮點數除法

. 將int轉型為double幾乎肯定會產生一些程式碼,因為大部分的計算器體系中,int的底層描述不同於double的底層描述。又比如在繼承體系中父類指標指向子類物件的時候,實際上是隱式的將子類指標轉型為父類指標,這操作在執行期間有一個偏移量的存在,確保能夠取得正確的Base部分。
  另一種情況是可能會寫出事實而非的程式碼,比如:

class Window{
public:
	virtual void onResize(){...}
};

class SpecialWindow : public Window{
public:
	virtual void onResize(){
		static_cast<Window>(*this).onResize();
		...
	}
	...
};

. 在子類中做了一個轉型操作,將this指標指向的物件轉型為Window,燃火呼叫了父類的onResize函式,看起來想法沒什麼問題,但是呼叫的卻不是當前物件上的函式,而是轉型操作時所建立的Base部分的副本身上的函式,也就是說這樣呼叫後並不對該物件的基類部分做改動,只對該基類部分的一個副本進行了改動,所以正常情況還是應該這麼寫:

class SpecialWindow : public Window{
public:
	virtual void onResize(){
		Window::onResize();			//呼叫onResize作用於this指標指向的物件
		...
	}
	...
};

優良的C++程式碼很少使用轉型,但是要完全擺脫他們又不太實際,通常應該將轉型動作儘可能隔離開,將它隱藏在某個函式中,函式的介面會保護呼叫者不受內部的動作所影響。

避免返回handles指向物件的內部成分

我的理解是不要將私有變數直接用引用的方式返回,這樣外部將能對他們進行修改,破壞了封裝性。

為“異常安全”而努力是值得的

. 異常安全性對函式來說是很重要的,對一個異常安全函式應該提供三個保證之一:
1.基本承諾:如果異常被丟擲,程式內的任何事物仍然保持在有效狀態下。沒有任何物件或是資料結構被破壞,所有物件應當處於一種內部前後一致的狀態。
2.強烈保證:如果異常被丟擲,程式狀態不改變。也就是說如果函式呼叫成功,那麼就應該完全成功,如果呼叫失敗,程式應該恢復到呼叫函式之前的狀態。關於這點,往往能夠以copy-and-swap實現出來,即修改物件資料的副本,然後在不丟擲異常的函式中將修改後的資料和原件進行置換。
3.不拋擲保證:承諾絕不丟擲異常,因為他們總是能夠完成他們原先承諾的功能(沒懂,是說盡量不丟擲異常,讓函式完成它的工作的意思嗎?)

透徹瞭解inlining的裡裡外外

. inline函式看起來像是函式,動作像是函式,比巨集要更加高效,不要承擔函式呼叫所招致的額外開銷,編譯器最優化機制通常被設計用來濃縮那些“不含函式呼叫”的程式碼,所以當inline某個函式時,編譯器就有能力對它執行語境相關最優化。
  但是天下沒有白吃的午餐,inline函式也不例外,inline函式的背後理念是,將“對此函式的每一次呼叫”都以函式本體替換之,這樣做可能會增加目標碼的大小,在一臺記憶體有限的機器上,過度使用inlining會造成程式體積太大,帶來效率損失。
  隱喻宣告inline函式的方式是將函式定義在class內部,明確的定義inline函式是在其定義式前加上inline關鍵字,但是一個函式被你宣告為inline後是否真的是inline的,取決於編譯器環境,通常編譯器會拒絕將過於複雜的函式inlining,而所有對virtual函式的呼叫,也會使inlining落空。幸運的是大多數編譯器提供了一個診斷級別:如果不能對函式進行inline化,會給出一個警告資訊。
  雖然編譯器有意願將某個函式inlined,但還是有可能為函式宣告一個函式本體,比如當程式要取某個inline函式的地址的時候(函式指標啊之類的),編譯器通常必須為這個函式生成一個函式本體,因為編譯器沒有能力提出一個指標指向並不存在的函式。
  實際上建構函式和解構函式往往是inline的糟糕人選,這是因為C++的設計原理中對“物件被建立和銷燬的時候做了什麼”要做出各種保證,即使你的建構函式或解構函式中什麼都沒有寫,但是可以知道的是這些情況中肯定是有事情發生的,程式內一定有某些程式碼讓這些保證發生,而這些程式碼,是編譯器在編譯期間代為產生並安插到程式中的,有時候就放在建構函式與解構函式中。
  程式設計者應當評估將函式宣告為inline帶來的衝擊:inline函式無法隨著程式庫的升級而升級,一旦需要修改這個inline函式,所有用到它的客戶端程式都必須重新編譯。
  還有比較實際的一個問題是:大部分偵錯程式對inline函式都束手無策,因為無法在不存在的函式內打斷點。
  所以建議將inline限制在小型、被頻繁使用的函式身上,降低程式碼膨脹為題,提高程式執行速度。

將檔案間的編譯依存關係降到最低

. 往往在編寫程式碼的時候會分成很多的檔案,比如某個類的定義和實現會分別在一個頭檔案和cpp檔案中,其他檔案包含了標頭檔案就能定義這個類並呼叫這個類的方法,但是當修改了這個類的某個屬性的時候,或者這個類所依賴的標頭檔案中有改變時,任何包含這個類的檔案都需要進行重新編譯,這將大大增加編譯的時間。
  可以使用一種“將物件的實現細節隱藏於一個指標背後”,很像java中的介面類這種東西,對C++來說可以將類分為兩個class,一個只提供介面,一個負責實現介面,例如:

class PersonImpl;				//Person實現類的前置宣告
class Date;							//Person類用到的class的前置宣告
class Address;
class Person{
public:
	Person(const std::string& name,const Date& birthday,const Address& addr);
	std::string name() const;
	std::string birthdate() const;
	std::string address() const;
	...
private:
	std::tr1::shared_ptr<PersonImpl> pImpl;			//指向實現物的指標
}

. 以上的Person類中只含有一個指標成員,指向其實現類,這樣的設計下,Person的客戶就與Date、Address以及Person的實現細節分離了,那些class的任何修改都不需要Person客戶端重新編譯。
  這個分離的關鍵在於以“宣告的依存性”替換“定義的依存性”,這就是確定編譯依存性最小化的本質:現實中讓標頭檔案儘可能自我滿足,如果做不到,則讓它與其他檔案內的宣告式相依:
  如果使用引用或指標能完成任務,就不要直接使用物件。可以只靠一個型別的宣告式就定義出指向該型別的引用或指標,但如果定義某型別的物件,就需要該型別的定義式;
  如果可以,儘量以class宣告式替換class定義式。比如說在一個函式的宣告中,它的引數或者返回值是一個類型別的物件,只需要在前面做這個類的宣告,而不需要這個類的定義。你呼叫這個函式的時候才需要這個類的定義;
  為宣告式和定義式提供不同的標頭檔案。根據以上兩點,需要兩個標頭檔案,一個用於宣告式,一個用於定義式。這兩個檔案需要保證一致性。這樣不需要大量的做前置宣告,而是包含這個宣告式的標頭檔案就可以了。
  像之前的Person類,往往被稱為Handle classes,如何製作這樣的類呢,辦法之一是將它的所有函式轉交給相應的實現類並由後者完成實際工作,例如:

#include "Person.h"					
#include "PersonImpl.h"				//兩個類有相同的成員函式
Person::Person(const std::string& name,
const Date& birthday,
const Address& addr):pImpl(new PersonImpl(name,birthday,addr)){}

std::string Person::name() const {
	return pImpl->name();
}

. 另一個製作Handle classes的辦法是,令Person成為一種特殊的抽象基類,這種class的目的是描述派生類的介面,因此它通常沒有成員變數,也沒有建構函式,只有一個virtual解構函式和一組虛擬函式,用來敘述整個介面,聽起來很像java的介面(Interfaces),但不同的地方在於java不允許在interface內實現成員變數或是成員函式,但C++不禁止,這樣的彈性是有用途的,比如非虛擬函式的實現對繼承體系內的所有類都應該相同。例如一個針對Person的interface class看起來像是這樣:

class Person{
public:
	virtual ~Person();
	virtual std::string name() const = 0;
	virtual std::string birthdate() const = 0;
	virtual std::string address() const = 0;
	...
};

. 這個class的客戶必須以Person的pointer或reference來撰寫應用程式,因為不能對有純虛擬函式的類具現出實體,通常通過一個特殊函式(工廠函式或虛建構函式)返回指標來指向動態分配所獲得的這個類的物件,而該物件支援interface介面。
  Handle class 和interface class解除了介面和實現之間的耦合關係,從而降低檔案間的依存性,不過這樣的代價是:執行期將喪失若干速度,又讓你對每個物件超額付出若干記憶體。