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