内联函数
內(nèi)聯(lián)函數(shù)
轉(zhuǎn)自《C++編程思想》,這本書寫得真的是太棒了。
在C語言中,保持效率的一個方法是使用宏(macro)。宏可以不要普通的函數(shù)調(diào)用代碼就可以是之看起來像函數(shù)調(diào)用。宏的實(shí)現(xiàn)不是預(yù)處理器而是編譯器。預(yù)處理器直接用宏代碼代替宏調(diào)用,所以沒有了參數(shù)壓棧、生成匯編語言的CALL、返回參數(shù)、執(zhí)行匯編語言的RETURN等的開銷。所有的工作由處理器來完成,因此不用花費(fèi)什么就具有了程序調(diào)用的便利和可讀性。
但是C++有一個特有的問題:預(yù)處理器不允許訪問類的成員數(shù)據(jù)。 這意味著預(yù)處理器宏不能用作類的成員函數(shù)。
為了既保持預(yù)處理器宏的效率又增加安全性,而且還能像一般成員函數(shù)一樣可以在類里訪問自如,C++引入了內(nèi)聯(lián)函數(shù)。
1 預(yù)處理器的缺陷
當(dāng)在宏調(diào)用中使用表達(dá)式作為參數(shù)時,問題出現(xiàn)。
#define FLOOR(x,b) x>=b?0:1
假如使用表達(dá)式作參數(shù):
if(FLOOR(a&0x0f,0x07)) //...
宏就將展開成
if(a&0x0f>=0x07?0:1)
因?yàn)?amp;的優(yōu)先級比>=的低,所以宏的展開結(jié)果將會使我們驚訝。一旦發(fā)現(xiàn)這個問題,可以通過在宏定義內(nèi)的各個地方使用括弧來解決。(這是創(chuàng)建預(yù)處理器宏時使用的好方法。)上面的定義可以改寫如下:
#define FLOOR(x,b) ((x)>=(b)?0:1)
再看一個例子。
下面這個宏決定它的參數(shù)是否在一定的范圍:
#define BAND(x) (((x)>5 && (x)<10) ? (x):0)
然后看一段程序:
#include <iostream>
#include <algorithm>
#include <cmath>
#include <vector>
#include <string>
#include <cstring>
#pragma warning(disable:4996)
using namespace std;
#define BAND(x) (((x)>5 && (x)<10) ? (x):0)
int main()
{
//freopen("i.txt", "r", stdin);
//freopen("o.txt", "w", stdout);
for (int i = 4; i < 11; i++)
{
int a = i;
cout << "a=" << a << endl << ' ';
cout << "BAND(++a)=" << BAND(++a) << endl;
cout << " a = " << a << endl;
}
system("pause");
return 0;
}
注意宏名中所有大寫字母的使用。這是一種很有用的做法,因?yàn)榇髮懙淖帜父嬖V讀者這是一個宏而不是一個函數(shù),所以如果出現(xiàn)問題,也可以起到一定的提示作用。
執(zhí)行效果如圖
原因在于,當(dāng)a等于4時僅測試了條件表達(dá)式的第一部分,表達(dá)式只求值一次,所以宏調(diào)用的副作用是a等于5,這是在相同的情況下從普通函數(shù)調(diào)用所期望得到的。但當(dāng)數(shù)字在值域范圍內(nèi)時,兩個表達(dá)式都測試,產(chǎn)生兩次自增操作。產(chǎn)生這個結(jié)果是由于再次對參數(shù)操作。一旦數(shù)組出了范圍,兩個條件仍然測試,所以也產(chǎn)生兩次自增操作。根據(jù)參數(shù)不同產(chǎn)生的副作用也不同。
2 內(nèi)聯(lián)函數(shù)
在C++中,宏的概念是作為內(nèi)聯(lián)函數(shù)(inline function)來實(shí)現(xiàn)的,而內(nèi)聯(lián)函數(shù)無論從哪一方面上說都是真正的函數(shù)。內(nèi)聯(lián)函數(shù)能夠像普通函數(shù)一樣具有我們所有期望的任何行為。唯一不同之處是內(nèi)聯(lián)函數(shù)在適當(dāng)?shù)牡胤较窈暌粯诱归_,所以不需要函數(shù)調(diào)用的開銷。因此,應(yīng)該(幾乎)永遠(yuǎn)不使用宏,只使用內(nèi)聯(lián)函數(shù)。
任何在類中定義的函數(shù)自動地成為內(nèi)聯(lián)函數(shù),但也可以在非類的函數(shù)前面加上inline關(guān)鍵字使之成為內(nèi)聯(lián)函數(shù)。但為了使之有效,必須使函數(shù)體和聲明結(jié)合在一起,否則,編譯器將它作為普通函數(shù)對待。因此
inline int plusOne(int x);
沒有任何效果,僅僅是聲明函數(shù),成功的方法如下:
inline int plusOne(int x) {return ++x;}
注意,編譯器將檢查函數(shù)參數(shù)列表使用是否正確,并返回值(進(jìn)行必要的轉(zhuǎn)換)。這些事情是預(yù)處理器無法完成的。假如對于上面的內(nèi)聯(lián)函數(shù)寫成一個預(yù)處理器宏的話,將得到不想要的副作用。
一般應(yīng)該把內(nèi)聯(lián)定義放在頭文件里。當(dāng)編譯器看到這個定義時,它把函數(shù)類型(函數(shù)名,返回值)和函數(shù)體放到符號表里。當(dāng)使用函數(shù)時,編譯器檢查以確保調(diào)用時正確的且返回值被正確使用,然后將函數(shù)調(diào)用替換為函數(shù)體,因此消除了開銷。內(nèi)聯(lián)代碼的確占用空間,這實(shí)際上比為了一個普通函數(shù)調(diào)用而產(chǎn)生的代碼(參數(shù)壓棧和執(zhí)行CALL)占用的空間還少。
在頭文件中,內(nèi)聯(lián)函數(shù)處于一種特殊狀態(tài),因?yàn)樵陬^文件中聲明該函數(shù),所以必須包含頭文件和該函數(shù)的定義,這些定義在每個用到該函數(shù)的文件中,但是不會產(chǎn)生多個定義錯誤的情況(不過,在任何使用內(nèi)聯(lián)函數(shù)地方該內(nèi)聯(lián)函數(shù)的定義都必須是相同的)。
2.1 類內(nèi)部的內(nèi)聯(lián)函數(shù)
任何類內(nèi)部定義的函數(shù)自動地成為內(nèi)聯(lián)函數(shù)。
因?yàn)轭悆?nèi)部的內(nèi)聯(lián)函數(shù)節(jié)省了在外部定義成員函數(shù)的額外步驟,所以我們一定想在類聲明內(nèi)每一處都使用內(nèi)聯(lián)函數(shù)。但應(yīng)記住,使用內(nèi)聯(lián)函數(shù)的目的是減少函數(shù)調(diào)用的開銷。但是如果函數(shù)較大,由于需要在調(diào)用函數(shù)的每一處都重復(fù)復(fù)制代碼,這樣使代碼膨脹,在速度方面獲得的好處就會減少。
2.2 訪問函數(shù)
不用內(nèi)聯(lián)函數(shù),考慮效率的類設(shè)計(jì)者將忍不住簡單地使變量成為公共成員,從而通過讓用戶直接訪問變量來消除開銷。從設(shè)計(jì)的角度看,這是很不好的。
4 內(nèi)聯(lián)函數(shù)和編譯器
為了理解內(nèi)聯(lián)函數(shù)何時有效,應(yīng)該先理解當(dāng)編譯器遇到一個內(nèi)聯(lián)函數(shù)時將做什么。
對于任何函數(shù),編譯器在它的符號表里放入函數(shù)類型(即包括名字和參數(shù)類型的函數(shù)原型及函數(shù)的返回類型)。另外,當(dāng)編譯器看到內(nèi)聯(lián)函數(shù)和對內(nèi)聯(lián)函數(shù)體的進(jìn)行分析沒有發(fā)現(xiàn)錯誤時,就將對應(yīng)與函數(shù)體的代碼也放入符號表。源代碼是以源程序的形式存放還是以編譯過的匯編指令形式存放取決于編譯器。
當(dāng)調(diào)用一個內(nèi)聯(lián)函數(shù)時,編譯器首先確保調(diào)用正確,即所有的參數(shù)類型必須滿足:要么與函數(shù)參數(shù)表中的參數(shù)類型一樣,要么編譯器能夠?qū)⑵滢D(zhuǎn)換為正確類型,并且返回值在目標(biāo)表達(dá)式里應(yīng)該是正確類型或可改變?yōu)檎_類型。當(dāng)然,編譯器為任何類型函數(shù)都是這樣做的,并且這是與預(yù)處理器顯著的不同之處,因?yàn)轭A(yù)處理器不能檢查類型和進(jìn)行轉(zhuǎn)換。
假如所有的函數(shù)類型信息符合調(diào)用的上下文的話,內(nèi)聯(lián)函數(shù)代碼就會直接替換函數(shù)調(diào)用,這消除了調(diào)用的開銷,也考慮了編譯器的進(jìn)一步優(yōu)化。
4.1 限制
有兩種編譯器不能執(zhí)行內(nèi)聯(lián)的情況。在這些情況下,它就像對非內(nèi)聯(lián)函數(shù)一樣,根據(jù)內(nèi)聯(lián)函數(shù)定義和為函數(shù)建立存儲空間,簡單地將其轉(zhuǎn)換為函數(shù)的普通形式。假如它必須在多重編譯單元里做這些(通常將產(chǎn)生一個多定義錯誤),連接器就會被告知忽略多重定義。
1.假如函數(shù)太復(fù)雜,編譯器將不能執(zhí)行內(nèi)聯(lián)。這取決于特定的編譯器,但對于大多數(shù)編譯器這時都會放棄內(nèi)聯(lián)方式,這是內(nèi)聯(lián)將可能不能提高任何效率。一般地,任何種類的循環(huán)都會被認(rèn)為太復(fù)雜而不擴(kuò)展為內(nèi)聯(lián)函數(shù)。循環(huán)在函數(shù)里可能比調(diào)用要花費(fèi)更多的時間。假如函數(shù)僅由簡單語句組成,編譯器可能沒有任何內(nèi)聯(lián)的麻煩,但假如函數(shù)有很多語句,調(diào)用函數(shù)的開銷將比執(zhí)行函數(shù)體的開銷少多了。記住,每次調(diào)用一個大的內(nèi)聯(lián)函數(shù),整個函數(shù)體就被插入在函數(shù)調(diào)用的地方,所以很容易使代碼膨脹,而程序性能上沒有任何顯著的改進(jìn)。
2.假如要顯式地或隱式地取一個函數(shù)的地址,編譯器也不能執(zhí)行內(nèi)聯(lián)。因?yàn)檫@時編譯器必須為函數(shù)代碼分配內(nèi)存而產(chǎn)生一個函數(shù)的地址,但當(dāng)?shù)刂凡恍枰獣r,編譯器仍將可能內(nèi)聯(lián)代碼。
內(nèi)聯(lián)僅是編譯器的一個建議,編譯器不會被強(qiáng)迫內(nèi)聯(lián)任何代碼。一個好的編譯器將會內(nèi)聯(lián)小的、簡單的函數(shù),同時明智地忽略那些太復(fù)雜的內(nèi)聯(lián),這將給我們想要的結(jié)果–具有宏效率的函數(shù)調(diào)用的真正的語義學(xué)。
4.2 向前引用
如果猜想編譯器執(zhí)行內(nèi)聯(lián)函數(shù)時將會做什么事情,就可能會糊涂地認(rèn)為限制比實(shí)際存在的要多。特別當(dāng)一個內(nèi)聯(lián)函數(shù)在類中向前引用一個還沒有聲明的函數(shù)時,看起來好像實(shí)際編譯器不能處理。
class Forward
{
int i;
public:
Forward() :i(0) {}
int f()const { return g() + 1; }
int g()const { return i; }
};
int main()
{
Forward frwd;
frwd.f();
}
函數(shù)f()調(diào)用g(),但此時還沒有聲明g()。這也能正常工作,因?yàn)镃++語言規(guī)定:只有在類函數(shù)聲明結(jié)束后,其中的內(nèi)聯(lián)函數(shù)才會被計(jì)算。
當(dāng)然,如果g()反過來調(diào)用f(),就會產(chǎn)生遞歸調(diào)用,這對于編譯器來說太復(fù)雜而不能執(zhí)行內(nèi)聯(lián)。
4.3 在構(gòu)造函數(shù)和析構(gòu)函數(shù)里隱藏行為
在構(gòu)造函數(shù)和析構(gòu)函數(shù)中,可能易于認(rèn)為內(nèi)聯(lián)的作用比它實(shí)際上更有效。構(gòu)造函數(shù)和析構(gòu)函數(shù)都可能隱藏行為,因?yàn)轭惪梢园訉ο螅訉ο蟮臉?gòu)造函數(shù)和析構(gòu)函數(shù)必須被調(diào)用。這些子對象可能是成員對象,或可能由于集成而存在。看例子。
#include <iostream>
#include <algorithm>
#include <cmath>
#include <vector>
#include <string>
#include <cstring>
#pragma warning(disable:4996)
using namespace std;
class Member
{
int i, j, k;
public:
Member(int x=0):i(x),j(x),k(x){}
~Member() { cout << "~Member" << endl; }
};
class WithMember
{
Member q, r, s;
int i;
public:
WithMember(int ii):i(ii){}
~WithMember() {
cout << "~WithMember" << endl;
}
};
int main()
{
WithMember wm(1);
return 0;
}
Member的構(gòu)造函數(shù)對于內(nèi)聯(lián)是足夠簡單的,它不做什么特別的事情。沒有繼承和成員對象會引起的額外隱藏行為。但是在類WithMembers里,內(nèi)聯(lián)的構(gòu)造函數(shù)和析構(gòu)函數(shù)看起來似乎很直接很簡單,但其實(shí)很復(fù)雜。成員對象q、r和s的構(gòu)造函數(shù)和析構(gòu)函數(shù)將被自動調(diào)用,這些構(gòu)造函數(shù)和析構(gòu)函數(shù)也是內(nèi)聯(lián)的,所以它們和普通的成員函數(shù)的差別是非常顯著的。這并不意味著應(yīng)該使構(gòu)造函數(shù)和析構(gòu)函數(shù)定義為非內(nèi)聯(lián)的,只是在一些特定的情況下,這樣做才是合理的。一般來說,快速地寫代碼來建立一個程序的初始“輪廓”時,使用內(nèi)聯(lián)函數(shù)經(jīng)常是便利的,但假如要考慮效率,內(nèi)聯(lián)是值得注意的一個問題。
5 預(yù)處理器的更多特征
前面說過,我們幾乎總是希望使用內(nèi)聯(lián)函數(shù)代替預(yù)處理宏。然而當(dāng)需要在標(biāo)準(zhǔn)C預(yù)處理器(通過繼承也是C++預(yù)處理器)里使用3個特殊特征時卻是例外:字符串定義、字符串拼接和標(biāo)志粘貼。字符串定義的完成是用#指示,他容許一個標(biāo)識符并把它轉(zhuǎn)化為字符數(shù)組,然而字符串拼接在當(dāng)兩個相鄰的字符串沒有分隔符時發(fā)生,在這種情況下字符串組合在一起。在寫調(diào)試代碼時,這兩個特征特別有用。
#define DEBUG(x) cout<<#x"="<<x<<endl;
上面的這個定義可以打印任何變量的值。也可以得到一個跟蹤信息,在此信息里打印出他們執(zhí)行的語句。
#define TRACE(s) cerr<<#s<<endl;s
#s將輸出語句字符。第2個s重申了該語句,所以這個語句被執(zhí)行。當(dāng)然,這可能會產(chǎn)生問題,尤其是在一行for循環(huán)中。
for (int i = 0; i < 100; i++)
TRACE(f(i));
因?yàn)樵赥RACE()宏里實(shí)際上有兩個語句,所以一行for循環(huán)只執(zhí)一個,解決辦法是在宏中用逗號代替分號。
總結(jié)
- 上一篇: 【TCP/IP详解】BOOTP:引导程序
- 下一篇: 观点|APUS创始人李涛:生成式AI将对