[TOC]
# 簡介
在c中我們經常把一些短并且執行頻繁的計算寫成宏,而不是函數,這樣做的理由是為了執行效率,宏可以避免函數調用的開銷,這些都由預處理來完成。
但是在c++出現之后,使用預處理宏會出現兩個問題:
* 第一個在c中也會出現,宏看起來像一個函數調用,但是會有隱藏一些難以發現的錯誤。
* 第二個問題是c++特有的,預處理器不允許訪問類的成員,也就是說預處理器宏不能用作類類的成員函數。
為了保持預處理宏的效率又增加安全性,而且還能像一般成員函數那樣可以在類里訪問自如,c++引入了內聯函數(inline function).
內聯函數為了繼承宏函數的效率,沒有函數調用時開銷,然后又可以像普通函數那樣,可以進行參數,返回值類型的安全檢查,又可以作為成員函數。
# 預處理宏的缺陷
預處理器宏存在問題的關鍵是我們可能認為預處理器的行為和編譯器的行為是一樣的。當然也是由于宏函數調用和函數調用在外表看起來是一樣的,因為也容易被混淆。但是其中也會有一些微妙的問題出現:
問題一:
~~~
#define ADD(x,y) x+y
inline int Add(int x,int y){
return x + y;
}
void test(){
int ret1 = ADD(10, 20) * 10; //希望的結果是300
int ret2 = Add(10, 20) * 10; //希望結果也是300
cout << "ret1:" << ret1 << endl; //210
cout << "ret2:" << ret2 << endl; //300
}
~~~
問題二:
~~~
#define COMPARE(x,y) ((x) < (y) ? (x) : (y))
int Compare(int x,int y){
return x < y ? x : y;
}
void test02(){
int a = 1;
int b = 3;
//cout << "COMPARE(++a, b):" << COMPARE(++a, b) << endl; // 3
cout << "Compare(int x,int y):" << Compare(++a, b) << endl; //2
}
~~~
問題三:
預定義宏函數沒有作用域概念,無法作為一個類的成員函數,也就是說預定義宏沒有辦法表示類的范圍。
# 內聯函數
## 內聯函數基本概念
在c++中,預定義宏的概念是用內聯函數來實現的,而內聯函數本身也是一個真正的函數。內聯函數具有普通函數的所有行為。唯一不同之處在于內聯函數會在適當的地方像預定義宏一樣展開,所以不需要函數調用的開銷。因此應該不使用宏,使用內聯函數。
* 在普通函數(非成員函數)函數前面加上inline關鍵字使之成為內聯函數。但是必須注意必須函數體和聲明結合在一起,否則編譯器將它作為普通函數來對待。
~~~
inline void func(int a);
~~~
以上寫法沒有任何效果,僅僅是聲明函數,應該如下方式來做:
~~~
inline int func(int a){return ++;}
~~~
~~~
inline void mycompare(int a, int b) {
int rel = a < b ? a : b;
cout << "結果是: " << rel << endl;
}
int main() {
mycompare(1, 2);
system("pause");
return EXIT_SUCCESS;
}
~~~
注意: 編譯器將會檢查函數參數列表使用是否正確,并返回值(進行必要的轉換)。這些事預處理器無法完成的。
內聯函數的確占用空間,但是內聯函數相對于普通函數的優勢只是省去了函數調用時候的壓棧,跳轉,返回的開銷。我們可以理解為內聯函數是以空間換時間。
## 類內部的內聯函數
為了定義內聯函數,通常必須在函數定義前面放一個inline關鍵字。但是**在類內部定義內聯函數時并不是必須的。任何在類內部定義的函數自動成為內聯函數。**
~~~
class Person{
public:
Person(){ cout << "構造函數!" << endl; }
void PrintPerson(){ cout << "輸出Person!" << endl; }
}
~~~
構造函數Person,成員函數PrintPerson在類的內部定義,自動成為內聯函數。
## 內聯函數和編譯器
內聯函數并不是何時何地都有效,為了理解內聯函數何時有效,應該要知道編譯器碰到內聯函數會怎么處理?
對于任何類型的函數,編譯器會將函數類型(包括函數名字,參數類型,返回值類型)放入到符號表中。同樣,當編譯器看到內聯函數,并且對內聯函數體進行分析沒有發現錯誤時,也會將內聯函數放入符號表。
當調用一個內聯函數的時候,編譯器首先確保傳入參數類型是正確匹配的,或者如果類型不正完全匹配,但是可以將其轉換為正確類型,并且返回值在目標表達式里匹配正確類型,或者可以轉換為目標類型,內聯函數就會直接替換函數調用,這就消除了函數調用的開銷。假如內聯函數是成員函數,對象this指針也會被放入合適位置。
類型檢查和類型轉換、包括在合適位置放入對象this指針這些都是預處理器不能完成的。
# 限制
c++內聯編譯會有一些限制,以下情況編譯器可能考慮不會將函數進行內聯編譯:
* 不能存在任何形式的循環語句
* 不能存在過多的條件判斷語句
* 函數體不能過于龐大
* 不能對函數進行取址操作
**內聯僅僅只是給編譯器一個建議,編譯器不一定會接受這種建議,如果你沒有將函數聲明為內聯函數,那么編譯器也可能將此函數做內聯編譯。一個好的編譯器將會內聯小的、簡單的函數。**
盡管大多數教科書中在函數聲明和函數定義處都增加了 inline 關鍵字,但我認為 inline 關鍵字不應該出現在函數聲明處。這個細節雖然不會影響函數的功能,但是體現了高質量 C++ 程序設計風格的一個基本原則:聲明與定義不可混為一談,用戶沒有必要、也不應該知道函數是否需要內聯。
**更為嚴格地說,內聯函數不應該有聲明,應該將函數定義放在本應該出現函數聲明的地方,這是一種良好的編程風格。 **
在多文件編程中,我們通常將函數的定義放在源文件中,將函數的聲明放在頭文件中,希望調用函數時,引入對應的頭文件即可,我們鼓勵這種將函數定義和函數聲明分開的做法。但這種做法不適用于內聯函數,將內聯函數的聲明和定義分散到不同的文件中會出錯,請看下面的例子
main.cpp
~~~
#include <iostream>
using namespace std;
//內聯函數聲明
void func();
int main(){
func();
return 0;
}
~~~
module.cpp
~~~
#include <iostream>
using namespace std;
//內聯函數定義
inline void func(){
cout<<"inline function"<<endl;
}
~~~
上面的代碼能夠正常編譯,但在鏈接時會出錯。func() 是內聯函數,編譯期間會用它來替換函數調用處,編譯完成后函數就不存在了,鏈接器在將多個目標文件(`.o`或`.obj`文件)合并成一個可執行文件時找不到 func() 函數的定義,所以會產生鏈接錯誤。
內聯函數雖然叫做函數,在定義和聲明的語法上也和普通函數一樣,但它已經失去了函數的本質。函數是一段可以重復使用的代碼,它位于虛擬地址空間中的代碼區,也占用可執行文件的體積,而內聯函數的代碼在編譯后就被消除了,不存在于虛擬地址空間中,沒法重復使用
聯函數看起來簡單,但是有很多細節需要注意,從代碼重復利用的角度講,內聯函數已經不再是函數了。我認為將內聯函數作為帶參宏的替代方案更為靠譜,而不是真的當做函數使用。
**在多文件編程時,我建議將內聯函數的定義直接放在頭文件中,并且禁用內聯函數的聲明(聲明是多此一舉**
# 內聯函數定義在類外部
~~~
class Student{
public:
char *name;
int age;
float score;
void say(); //內聯函數聲明,可以增加 inline 關鍵字,但編譯器會忽略
};
//函數定義
inline void Student::say(){
cout<<name<<"的年齡是"<<age<<",成績是"<<score<<endl;
}
~~~
這樣,say() 就會變成內聯函數。
這種在類體外定義 inline 函數的方式,必須將類的定義和成員函數的定義都放在同一個頭文件中(或者同一個源文件中),否則編譯時無法進行嵌入(將函數代碼的嵌入到函數調用出)
- c語言
- 基礎知識
- 變量和常量
- 宏定義和預處理
- 隨機數
- register變量
- errno全局變量
- 靜態變量
- 類型
- 數組
- 類型轉換
- vs中c4996錯誤
- 數據類型和長度
- 二進制數,八進制數和十六進制數
- 位域
- typedef定義類型
- 函數和編譯
- 函數調用慣例
- 函數進棧和出棧
- 函數
- 編譯
- sizeof
- main函數接收參數
- 宏函數
- 目標文件和可執行文件有什么
- 強符號和弱符號
- 什么是鏈接
- 符號
- 強引用和弱引用
- 字符串處理函數
- sscanf
- 查找子字符串
- 字符串指針
- qt
- MFC
- 指針
- 簡介
- 指針詳解
- 案例
- 指針數組
- 偏移量
- 間接賦值
- 易錯點
- 二級指針
- 結構體指針
- 字節對齊
- 函數指針
- 指針例子
- main接收用戶輸入
- 內存布局
- 內存分區
- 空間開辟和釋放
- 堆空間操作字符串
- 內存處理函數
- 內存分頁
- 內存模型
- 棧
- 棧溢出攻擊
- 內存泄露
- 大小端存儲法
- 寄存器
- 結構體
- 共用體
- 枚舉
- 文件操作
- 文件到底是什么
- 文件打開和關閉
- 文件的順序讀寫
- 文件的隨機讀寫
- 文件復制
- FILE和緩沖區
- 文件大小
- 插入,刪除,更改文件內容
- typeid
- 內部鏈接和外部鏈接
- 動態庫
- 調試器
- 調試的概念
- vs調試
- 多文件編程
- extern關鍵字
- 頭文件規范
- 標準庫以及標準頭文件
- 頭文件只包含一次
- static
- 多線程
- 簡介
- 創建線程threads.h
- 創建線程pthread
- gdb
- 簡介
- mac使用gdb
- setjump和longjump
- 零拷貝
- gc
- 調試器原理
- c++
- c++簡介
- c++對c的擴展
- ::作用域運算符
- 名字控制
- cpp對c的增強
- const
- 變量定義數組
- 盡量以const替換#define
- 引用
- 內聯函數
- 函數默認參數
- 函數占位參數
- 函數重載
- extern "C"
- 類和對象
- 類封裝
- 構造和析構
- 深淺拷貝
- explicit關鍵字
- 動態對象創建
- 靜態成員
- 對象模型
- this
- 友元
- 單例
- 繼承
- 多態
- 運算符重載
- 賦值重載
- 指針運算符(*,->)重載
- 前置和后置++
- 左移<<運算符重載
- 函數調用符重載
- 總結
- bool重載
- 模板
- 簡介
- 普通函數和模板函數調用
- 模板的局限性
- 類模板
- 復數的模板類
- 類模板作為參數
- 類模板繼承
- 類模板類內和類外實現
- 類模板和友元函數
- 類模板實現數組
- 類型轉換
- 異常
- 異常基本語法
- 異常的接口聲明
- 異常的棧解旋
- 異常的多態
- 標準異常庫
- 自定義異常
- io
- 流的概念和類庫結構
- 標準io流
- 標準輸入流
- 標準輸出流
- 文件讀寫
- STL
- 簡介
- string容器
- vector容器
- deque容器
- stack容器
- queue容器
- list容器
- set/multiset容器
- map/multimap容器
- pair對組
- 深淺拷貝問題
- 使用時機
- 常用算法
- 函數對象
- 謂詞
- 內建函數對象
- 函數對象適配器
- 空間適配器
- 常用遍歷算法
- 查找算法
- 排序算法
- 拷貝和替換算法
- 算術生成算法
- 集合算法
- gcc
- GDB
- makefile
- visualstudio
- VisualAssistX
- 各種插件
- utf8編碼
- 制作安裝項目
- 編譯模式
- 內存對齊
- 快捷鍵
- 自動補全
- 查看c++類內存布局
- FFmpeg
- ffmpeg架構
- 命令的基本格式
- 分解與復用
- 處理原始數據
- 錄屏和音
- 濾鏡
- 水印
- 音視頻的拼接與裁剪
- 視頻圖片轉換
- 直播
- ffplay
- 常見問題
- 多媒體文件處理
- ffmpeg代碼結構
- 日志系統
- 處理流數據
- linux
- 系統調用
- 常用IO函數
- 文件操作函數
- 文件描述符復制
- 目錄相關操作
- 時間相關函數
- 進程
- valgrind
- 進程通信
- 信號
- 信號產生函數
- 信號集
- 信號捕捉
- SIGCHLD信號
- 不可重入函數和可重入函數
- 進程組
- 會話
- 守護進程
- 線程
- 線程屬性
- 互斥鎖
- 讀寫鎖
- 條件變量
- 信號量
- 網絡
- 分層模型
- 協議格式
- TCP協議
- socket
- socket概念
- 網絡字節序
- ip地址轉換函數
- sockaddr數據結構
- 網絡套接字函數
- socket模型創建流程圖
- socket函數
- bind函數
- listen函數
- accept函數
- connect函數
- C/S模型-TCP
- 出錯處理封裝函數
- 多進程并發服務器
- 多線程并發服務器
- 多路I/O復用服務器
- select
- poll
- epoll
- epoll事件
- epoll例子
- epoll反應堆思想
- udp
- socket IPC(本地套接字domain)
- 其他常用函數
- libevent
- libevent簡介