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解除了接口和實現之間的耦合關係,從而降低文件間的依存性,不過這樣的代價是:運行期將喪失若干速度,又讓你對每個對象超額付出若干內存。

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