【進階】關於宏定義和內聯函數
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