閱讀676 返回首頁    go 阿裏雲 go 技術社區[雲棲]


【進階】關於宏定義和內聯函數

Tips:

1. 對於單純常量,盡量用const對象或者enums替換 #define

2. 對於形似函數的宏(marcos),最好改用inline函數替換#define

我們先來看一般的宏定義

 #define ASPECT_RATIO 1.653;

記號名稱為ASPECT_RATIO也許從未被編譯器看見: 也許在編譯器開始處理源碼之前它就被預處理器取走了。於是記號ASPECT_RATIO有可能沒有進入到幾號表(symbol table)內,於是當你運用此常量獲得一個編譯錯誤信息時,可能會帶來困惑,因為這個錯誤信息也許會提到1.653而不是ASPECT_RATIO。如果ASPECT_RATIO被定義在一個非你所寫的頭文件內,你肯定會對1.653是從何而來的感到費解。

解決之道是以一個常量替換上述宏:

const double AspectRatio = 1.653;
作為一個語言常量,AspectRadio肯定會被編譯器看到,當然會進入記號表內。

當我們以常量替換#define,有兩種特殊情況需要注意:

第一是定義常量指針(constant pointers)。 由於常量定義通常被放在頭文件內(以便被不同的源碼含入),因此有必要將指針(而不隻是指針指向之物)聲明為const。即指向常量的常量指針。

第二是class專屬常量。為了將常量的作用域(scope)限製於class內,必須將它成為class的一個成員(member);而為了確保此常量之多隻有一份實體,必須讓它成為一個static成員:

class GamePlayer{
    private:
    static const int NumTurns = 6;//常量聲明式
    int scores[NumTurns];//使用該常量
    ...
    };  

然而你所看到的NumTurns的聲明式而非定義式通常C++要求你對你所使用的任何一個東西提供一個定義式,但如果它是一個class的專屬常量又是static並且為整數類型(integral type,例如ints, chars, bools)則需要特殊處理隻要不取它們的地址,你可以聲明並且使用他們而無需提供定義式。但如果你取某個class專屬常量的地址,或者縱使你不取其地址而編譯器卻(不正確地)堅持要看到一個定義式,你就必須另外提供定義如下:

const int GamePlayer:: NumTurns;// NumTurns的定義式,下麵告訴大家為什麼沒有給予數值

請把這個式子放進一個實現文件而非頭文件,由於class常量已在聲明時獲得了初值(例如先前聲明時將其賦值為6),因此定義時 不可以 在定義初值。

順帶一提:我們無法利用#define創建一個class的專屬常量,因為#define並不重視scope。一旦宏被定義了,它就在其後的編譯過程中有效(除非在某處被#undef)。同時意味著#define不僅不能用來定義class專屬常量,也不能提供任何封裝性,也就是說沒有private #define之類的東西。

如果你的編譯器(錯誤地)不允許“static整數型class常量完成in class 初值設定”可改用所謂的“the enum hack”補償做法。理論基礎是“一個屬於枚舉類型(enumerated type)的數值可權充ints被使用”:

class GamePlayer{
private: 
	enum{NumTurns = 5} ;
	int scores[NumTurns];
	...
}


基於數個理由enum hack值得我們認識:

第一: enum hack的行為某方麵說比較像#define而不像const,有時候這正是你所想要的。例如取一個const的地址是合法的,而取一個enum的地址就不合法,而取一個define的地址通常也不合法。如果你不想要一個pointer或者reference指向你的某個整數常量,enum可以幫助你實現這個約束。

第二:實用主義。許多代碼用了它,所以看到它的時候你必須認識它- -! ,好吧我承認我剛認識。。。事實上,enum hack是template metaprogramming(模板元編程)的基礎


另一種常見的#define的誤用情況是以它實現宏(marcos)。宏看起來像函數,但不會招致函數調用(function call)帶來的額外開銷。例如:

#define CALL_WITH_MAX(a,b) f((a)>(b) ? (a):(b))

這般長相的宏有太多缺點,光是想到它們就讓人苦不堪言。

無論何時你寫出這種宏,必須記住為宏中的所有實參加上小括號。但縱使你為所有實參都加上了小括號,看看下麵意想不到的事

int a = 5,  b = 0;
CALL_WITH_MAX(++a,b);		//a 被累加二次
CALL_WITH_MAX(++a, b+10);  //a 被累加一次

調用f之前, a的遞增次數竟然取決於 它被拿來和誰比較!!

到了template inline上場的時候了!

tips:

1. inline函數的代碼被放在符號表中,像宏一樣展開,效率高

2. 類的inline函數是一個真正的函數,檢查參數類型,確保調用正確

3. inline函數可作為類的成員函數,可在其中使用private和protect成員。

4. 將大多數inline限製在小型,被頻繁調用的函數身上,這可使得日後的調試過程和二進製升級(binary upgradability)變得更容易。也可使得潛在的代碼膨脹問題最小化,使程序的速度提升機會更大。

inline函數,看起來像函數,動作像函數,比宏好的多,可以調用它們又不需要蒙受函數調用所招致的額外開銷。實際上,能夠獲取的比想到的更多,因為“免除函數調用成本”隻是故事的一部分而已。編譯器最優化機製通常被設計用來濃縮那些“不含函數調用”的代碼,所以當你inline某個函數,或許編譯器就因此有能力為它執行語境相關最優化,大部分編譯器絕不會對著一個“outline函數調用”動作執行如此之最優化。

inline函數當然也有缺點。inline函數背後的整體觀念是將“對此函數的每一次調用”都以函數本體替換之。這樣做的最直接結果就是增加目標碼(object code)大小。在一台內存有限的機器上,過渡熱衷inlining會造成程序體積太大(對可用空間而言)。即時擁有虛擬內存,inline造成的代碼膨脹也會導致額外的換頁行為(paging),降低指令高速緩存裝置的擊中率(instruction cache hit rate),以及伴隨這些而來的效率損失。

REMEMBER: inline隻是對編譯器的一個申請,不是強製命令,這項申請可以隱喻提出,也可以明確提出。

隱喻方式是將函數定義於class定義式內:

class Person{
public:
	...
	int age() const {return theAge;} // 一個隱喻的inline申請,age被定義域class定義式內;
	...
private:
	int theAge;
};
 這樣的函數通常是成員函數,friend函數也可被定義於class內,如果真是這樣, friend函數也是被隱喻聲明為inline

明確聲明inline函數的方式是在函數定義式前麵加上關鍵字inline。標準的max temple是這樣實現的:

template<typename T>
inline const T& std::max(const T&a, const T&b){ return a<b?a:b;}

max是個template  帶出了一個觀察結果:inline函數和template兩者通常都被定義於頭文件內,這使得某些程序員以為function template一定必須是inline。這個結論不但無效而且有害。

inline函數通常一定被置於頭文件內,因為大多數建置環境(build enviroment)在編譯過程中進行inlining,而為了將一個“函數調用”替換為“被調用函數的本體”,編譯器必須知道那個函數長什麼樣子。雖然有個例,但大多數c++程序中,inline是編譯期行為。

大部分編譯器拒絕將太過複雜(例如帶有循環和遞歸)的函數inline。而對所有vitual函數的調用(除非是最平淡無奇)的也都會使inlining落空。這應該不感到驚訝,因為virtual意味著“等待,知道運行期才確定調用哪個函數”,而inline意味著“執行前,現將調用動作置換為被調用函數的本體”。

構造函數和析構函數往往是inlining的糟糕候選人--雖然在漫不經心的情況下我們可能不會這樣認為,看下麵的代碼:

class Base{
public:
	...
private:
	std::string bm1, bm2;		//base 成員1和2
};
class Derived: public: Base{
public:
	Derived(){}			//Derived構造函數是空的,really?
private:
	std::string dm1, dm2, dm3;// derived 成員1,2和3
};
derived類的構造函數是空的,看起來像是個inlining的絕佳候選人,因為它根本不含任何代碼,但是。。。

C++對於“對象被構建和銷毀時發生什麼事情”做了各式各樣的保證。當你使用new,動態創建的對象被其構造函數自動初始化;當你使用delete,對應的析構函數會被調用。當你創建一個對象,其每一個base class及每一個成員變量都會被自動構造;當你銷毀一個對象,反向程序的析構行為也會自動發生。如果有個異常在對象構造期間被拋出,該對象已經構建好的那一部分會自動銷毀。在這些情況下C++描述了什麼一定會發生,但沒有描述是如何發生的。“事情如何發生”是編譯器的權責,不過有一點很清楚,就是它不可能憑空發生。程序內的一定有某些代碼讓這些事情發生,而那些代碼---由編譯器於編譯期間代為產生並安插到你的程序中的代碼--肯定存在某個地方,有時候就放在構造函數和析構函數中。下麵看一下編譯器為上麵代碼中空的Derived函數做了哪些工作:

Derived::Derived(){
	Base::Base();					//初始化Base成分
	try{dm1.std::string::string();}		//試圖構造dm1
	catch(...){						//如果異常,銷毀base class成分,並拋出該異常
		Base::~Base();
		throw;
	}
	
	try{dm2.std::string::string();}		//試圖構造dm2
	catch(...){						//如果異常,銷毀base class成分,並拋出該異常
		dm1.std::string::~string();		//還要銷毀已經創建的dm1
		Base::~Base();
		throw;
	}
	
	try{dm3.std::string::string();}		//試圖構造dm3
	catch(...){						//如果異常,銷毀base class成分,並拋出該異常
		dm2.std::string::~string();		//先銷毀創建的dm2
		dm1.std::string::~string();		//還要銷毀已經創建的dm1
		Base::~Base();
		throw;
	}
}

這段代碼不能代表編譯器真正製造出來的代碼,因為真正的編譯器會更精確複雜地做法來處理異常,盡管如此,這已經能準確反映Derived的空白構造函數必須提供的行為。相同的理由也適用於Base構造函數,所以如果它被inline,所以替換“base構造函數調用”而插入的代碼都會被插入到“Derived 構造函數調用”內。程序庫設計者必須評估“將函數聲明為inline”的衝擊:inline函數無法隨著程序庫的升級而升級。換句話說如果f是程序庫內的一個inline函數,客戶將“f函數本體”編進其程序中,一旦程序設計者決定改變f,所有用到f的客戶端程序都需要重新編譯。而如果f是non-inline函數,一旦它有任何改動,客戶端隻需要重新連接就好,遠比重新編譯的負擔少很多。如果程序庫采用動態連接,升級版函數甚至可以不知不覺地被應用程序吸納。


最後更新:2017-04-03 14:54:11

  上一篇:go [Qt教程] 第48篇 進階(八) 3D繪圖簡介
  下一篇:go [Qt教程] 第47篇 進階(七) 定製Qt幫助係統