行內函數 和 巨集定義 區別
用內聯取代巨集:
1.行內函數在執行時可除錯,而巨集定義不可以;
2.編譯器會對行內函數的引數型別做安全檢查或自動型別轉換(同普通函式),而巨集定義則不會;
3.行內函數可以訪問類的成員變數,巨集定義則不能;
4.在類中宣告同時定義的成員函式,自動轉化為行內函數。
文章(一)
行內函數與巨集定義
在C中,常用預處理語句#define來代替一個函式定義。例如:
#define MAX(a,b) ((a)>(b)?(a):(b))
該語句使得程式中每個出現MAX(a,b)函式呼叫的地方都被巨集定義中後面的表示式((a)>(b)?(a):(b))所替換。
巨集定義語句的書寫格式有過分的講究, MAX與括號之間不能有空格,所有的引數都要
放在括號裡。儘管如此,它還是有麻煩:
int a=1,b=0;
MAX(a++,b); //a被增值2次
MAX(a++,b+10); //a被增值1次
MAX(a,"Hello"); //錯誤地比較int和字串,沒有引數型別檢查
MAX( )函式的求值會由於兩個引數值的大小不同而產生不同的副作用。
MAX(a++,b)的值為2,同時a的值為3;
MAX(a++,b+10)的值為10,同時a的值為2。
inline int MAX(int a,int b)
{
return a>b?a:b;
}
1.行內函數與巨集的區別:
傳統的巨集定義函式可能會引起一些麻煩。
ex:
#define F(x) x+x
void main(){int i=1;F(i++);}
這裡x將被加兩次。
行內函數被編譯器自動的用函式的形勢新增進程式碼,而不會出現這種情況。
行內函數的使用提高了效率(省去了很多函式調用匯編程式碼如:call和ret等)。
2.行內函數的使用:
所有在類的宣告中定義的函式將被自動認為是行內函數。
class A()
{
void c();// not a inline function;
void d(){ print("d() is a inline function.");}
}
如果想將一個全域性函式定義為行內函數可用,inline 關鍵字。
inline a(){print("a() is a inline function.");}
注意:
在行內函數中如果有複雜操作將不被內聯。如:迴圈和遞迴呼叫。
總結:
將簡單短小的函式定義為行內函數將會提高效率。
文章(二)
8.5.1 用內聯取代巨集程式碼
C++ 語言支援函式內聯,其目的是為了提高函式的執行效率(速度)。
在 C程式中,可以用巨集程式碼提高執行效率。巨集程式碼本身不是函式,但使用起來象函式。前處理器用複製巨集程式碼的方式代替函式呼叫,省去了引數壓棧、生成組合語言的 CALL呼叫、返回引數、執行return等過程,從而提高了速度。使用巨集程式碼最大的缺點是容易出錯,前處理器在複製巨集程式碼時常常產生意想不到的邊際效應。例如
#define MAX(a, b) (a) > (b) ? (a) : (b)
語句
result = MAX(i, j) + 2 ;
將被前處理器解釋為
result = (i) > (j) ? (i) : (j) + 2 ;
由於運算子‘+’比運算子‘:’的優先順序高,所以上述語句並不等價於期望的
result = ( (i) > (j) ? (i) : (j) ) + 2 ;
如果把巨集程式碼改寫為
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
則可以解決由優先順序引起的錯誤。但是即使使用修改後的巨集程式碼也不是萬無一失的,例如語句
result = MAX(i++, j);
將被前處理器解釋為
result = (i++) > (j) ? (i++) : (j);
對於C++ 而言,使用巨集程式碼還有另一種缺點:無法操作類的私有資料成員。
讓我們看看C++ 的“函式內聯”是如何工作的。對於任何行內函數,編譯器在符號表裡放入函式的宣告(包括名字、引數型別、返回值型別)。如果編譯器沒有發現行內函數存在錯誤,那麼該函式的程式碼也被放入符號表裡。在呼叫一個行內函數時,編譯器首先檢查呼叫是否正確(進行型別安全檢查,或者進行自動型別轉換,當然對所有的函式都一樣)。如果正確,行內函數的程式碼就會直接替換函式呼叫,於是省去了函式呼叫的開銷。這個過程與預處理有顯著的不同,因為前處理器不能進行型別安全檢查,或者進行自動型別轉換。假如行內函數是成員函式,物件的地址(this)會被放在合適的地方,這也是前處理器辦不到的。
C++ 語言的函式內聯機制既具備巨集程式碼的效率,又增加了安全性,而且可以自由操作類的資料成員。所以在C++ 程式中,應該用行內函數取代所有巨集程式碼,“斷言assert”恐怕是唯一的例外。assert是僅在Debug版本起作用的巨集,它用於檢查“不應該”發生的情況。為了不在程式的Debug版本和Release版本引起差別,assert不應該產生任何副作用。如果assert是函式,由於函式呼叫會引起記憶體、程式碼的變動,那麼將導致Debug版本與Release版本存在差異。所以assert不是函式,而是巨集。(參見6.5節“使用斷言”)
8.5.2 行內函數的程式設計風格
關鍵字inline必須與函式定義體放在一起才能使函式成為內聯,僅將inline放在函式宣告前面不起任何作用。如下風格的函式Foo不能成為行內函數:
inline void Foo(int x, int y); // inline僅與函式宣告放在一起
void Foo(int x, int y)
{
…
}
而如下風格的函式Foo則成為行內函數:
void Foo(int x, int y);
inline void Foo(int x, int y) // inline與函式定義體放在一起
{
…
}
所以說,inline是一種“用於實現的關鍵字”,而不是一種“用於宣告的關鍵字”。一般地,使用者可以閱讀函式的宣告,但是看不到函式的定義。儘管在大多數教科書中行內函數的宣告、定義體前面都加了inline關鍵字,但我認為inline不應該出現在函式的宣告中。這個細節雖然不會影響函式的功能,但是體現了高質量C++/C程式設計風格的一個基本原則:宣告與定義不可混為一談,使用者沒有必要、也不應該知道函式是否需要內聯。
定義在類宣告之中的成員函式將自動地成為行內函數,例如
class A
{
public:
void Foo(int x, int y) { … } // 自動地成為行內函數
}
將成員函式的定義體放在類宣告之中雖然能帶來書寫上的方便,但不是一種良好的程式設計風格,上例應該改成:
// 標頭檔案
class A
{
public:
void Foo(int x, int y);
}
// 定義檔案
inline void A::Foo(int x, int y)
{
…
}
8.5.3 慎用內聯
內聯能提高函式的執行效率,為什麼不把所有的函式都定義成行內函數?
如果所有的函式都是行內函數,還用得著“內聯”這個關鍵字嗎?
內聯是以程式碼膨脹(複製)為代價,僅僅省去了函式呼叫的開銷,從而提高函式的執行效率。如果執行函式體內程式碼的時間,相比於函式呼叫的開銷較大,那麼效率的收穫會很少。另一方面,每一處行內函數的呼叫都要複製程式碼,將使程式的總程式碼量增大,消耗更多的記憶體空間。以下情況不宜使用內聯:
(1)如果函式體內的程式碼比較長,使用內聯將導致記憶體消耗代價較高。
(2)如果函式體內出現迴圈,那麼執行函式體內程式碼的時間要比函式呼叫的開銷大。
類的建構函式和解構函式容易讓人誤解成使用內聯更有效。要當心建構函式和解構函式可能會隱藏一些行為,如“偷偷地”執行了基類或成員物件的建構函式和解構函式。所以不要隨便地將建構函式和解構函式的定義體放在類宣告中。
一個好的編譯器將會根據函式的定義體,自動地取消不值得的內聯(這進一步說明了inline不應該出現在函式的宣告中)。
8.6 一些心得體會
C++ 語言中的過載、內聯、預設引數、隱式轉換等機制展現了很多優點,但是這些優點的背後都隱藏著一些隱患。正如人們的飲食,少食和暴食都不可取,應當恰到好處。我們要辨證地看待C++的新機制,應該恰如其分地使用它們。雖然這會使我們程式設計時多費一些心思,少了一些痛快,但這才是程式設計的藝術。
第9章 類的建構函式、解構函式與賦值函式
建構函式、解構函式與賦值函式是每個類最基本的函式。它們太普通以致讓人容易麻痺大意,其實這些貌似簡單的函式就象沒有頂蓋的下水道那樣危險。
每個類只有一個解構函式和一個賦值函式,但可以有多個建構函式(包含一個拷貝建構函式,其它的稱為普通建構函式)。對於任意一個類A,如果不想編寫上述函式,C++編譯器將自動為A產生四個預設的函式,如
A(void); // 預設的無引數建構函式
A(const A &a); // 預設的拷貝建構函式
~A(void); // 預設的解構函式
A & operate =(const A &a); // 預設的賦值函式
這不禁讓人疑惑,既然能自動生成函式,為什麼還要程式設計師編寫?
原因如下:
(1)如果使用“預設的無引數建構函式”和“預設的解構函式”,等於放棄了自主“初始化”和“清除”的機會,C++發明人Stroustrup的好心好意白費了。
(2)“預設的拷貝建構函式”和“預設的賦值函式”均採用“位拷貝”而非“值拷貝”的方式來實現,倘若類中含有指標變數,這兩個函式註定將出錯。
對於那些沒有吃夠苦頭的C++程式設計師,如果他說編寫建構函式、解構函式與賦值函式很容易,可以不用動腦筋,表明他的認識還比較膚淺,水平有待於提高。
本章以類String的設計與實現為例,深入闡述被很多教科書忽視了的道理。String的結構如下:
class String
{
public:
String(const char *str = NULL); // 普通建構函式
String(const String &other); // 拷貝建構函式
~ String(void); // 解構函式
String & operate =(const String &other); // 賦值函式
private:
char *m_data; // 用於儲存字串
};
======================================================================================================
在程式編譯時,編譯器將程式中出現的行內函數的呼叫表示式用行內函數的函式體來進行替換。顯然,這種做法不會產生轉去轉回的問題,但是由於在編譯時將函式休中的程式碼被替代到程式中,因此會增加目標程式程式碼量,進而增加空間開銷,而在時間代銷上不象函式呼叫時那麼大,可見它是以目的碼的增加為代價來換取時間的節省。
為了除錯方便,在程式處於除錯階段時,所有行內函數都不被實現。
三、使用行內函數時應注意以下幾個問題:
(1) 在一個檔案中定義的行內函數不能在另一個檔案中使用。它們通常放在標頭檔案中共享。
(2) 行內函數應該簡潔,只有幾個語句,如果語句較多,不適合於定義為行內函數。
(3) 行內函數體中,不能有迴圈語句、if語句或switch語句,否則,函式定義時即使有inline關鍵字,編譯器也會把該函式作為非行內函數處理。
(4) 行內函數要在函式被呼叫之前宣告。關鍵字inline 必須與函式定義體放在一起才能使函式成為內聯,僅將inline 放在函式宣告前面不起任何作用。