【C 语言】C 语言 函数 详解 ( 函数本质 | 顺序点 | 可变参数 | 函数调用 | 函数活动记录 | 函数设计 ) [ C语言核心概念 ]
相關文章鏈接 :
1.【嵌入式開發】C語言 指針數組 多維數組
2.【嵌入式開發】C語言 命令行參數 函數指針 gdb調試
3.【嵌入式開發】C語言 結構體相關 的 函數 指針 數組
4.【嵌入式開發】gcc 學習筆記(一) - 編譯C程序 及 編譯過程
5.【C語言】 C 語言 關鍵字分析 ( 屬性關鍵字 | 常量關鍵字 | 結構體關鍵字 | 聯合體關鍵字 | 枚舉關鍵字 | 命名關鍵字 | 雜項關鍵字)
6.【C 語言】編譯過程 分析 ( 預處理 | 編譯 | 匯編 | 鏈接 | 宏定義 | 條件編譯 | 編譯器指示字 )
7.【C 語言】指針 與 數組 ( 指針 | 數組 | 指針運算 | 數組訪問方式 | 字符串 | 指針數組 | 數組指針 | 多維數組 | 多維指針 | 數組參數 | 函數指針 | 復雜指針解讀)
- 一. 函數本質
- 1. 函數意義
- (1) 函數來源
- (2) 模塊化程序設計
- 2. 面向過程的程序設計
- (1) 程序結構
- 3. 函數的聲明和定義
- (1) 聲明 和 定義 的區別
- (2) 代碼示例 ( 函數 聲明 和 定義區別 )
- 1. 函數意義
- 二. 參數 可變參數 順序點 類型缺省認定
- 1. 函數參數
- (1) 參數分析
- (2) 代碼示例 ( 函數參數 求值順序 )
- 2. 程序中的順序點
- (1) 順序點簡介
- 3. C 語言 函數 的 缺省認定
- (n) 標題3
- 4.可變參數 的 定義 和 使用
- (1) 簡介
- (2) 代碼示例 ( 定義 使用 可變參數 )
- 1. 函數參數
- 三. 函數 與 宏
- 1. 函數 與 宏 對比案例
- (1) 函數 和 宏 的案例
- 2. 函數 和 宏 的分析
- (1) 函數 和 宏 分析
- 3. 函數 與 宏 的 利弊
- (1) 宏 優勢 和 弊端
- (2) 函數 的 優勢 和 弊端
- (3) 宏的無可替代性
- 4. 總結
- (1) 宏 定義 和 函數 總結
- 1. 函數 與 宏 對比案例
- 四. 函數的調用約定
- 1. 函數的活動記錄 分析
- (1) 函數的活動記錄
- 2. 函數的調用約定概述
- (1) 參數入棧 問題描述
- (2) 參數傳遞順序的調用約定
- 1. 函數的活動記錄 分析
- 五. 函數設計技巧
一. 函數本質
1. 函數意義
(1) 函數來源
C 程序結構 由 數據 和 函數 組成;
函數是由匯編跳轉發展而來的 :
- 1.匯編操作 : 匯編語言中由一系列的指令組成, 這些指令從上到下順序執行,
- 2.跳轉操作 : 匯編中需要做分支循環操作的時候, 就是使用跳轉指令;
- 3.指令代碼模塊 : 在匯編中有一組指令代碼, 總是需要執行這一組代碼, 需要時跳轉到該代碼處執行, 執行完畢后在跳轉回去, 這就是一個函數的雛形;
- 4.發展 : 跳轉過來 和 跳轉回去 相當于函數的 入棧 和 出棧;
(2) 模塊化程序設計
模塊化程序設計 :
- 1.思想 : 復雜問題拆解, 將一個復雜問題拆解成一個個的簡單問題, 這些簡單問題就可以作為一個個的函數來編寫;
- 2.C語言程序 : 將一個復雜的程序拆解成一個個模塊 和 庫函數;
一個復雜的 C 語言程序有幾十上百萬行代碼, 這些代碼可以分解成若干模塊來實現, 即分解成一個個的函數來實現.
2. 面向過程的程序設計
(1) 程序結構
面向過程程序設計思想 :
- 1.中心 : 整體的設計 以 過程 為中心;
- 2.問題分解 : 將復雜問題分解為若干容易解決的問題;
- 3.函數體現 : 面向過程 的思想在 C 語言 中的核心就是 函數;
- 4.分解函數 : 復雜問題 分解后的過程可以分為一個個函數一步步實現;
3. 函數的聲明和定義
(1) 聲明 和 定義 的區別
聲明和定義的區別 :
- 1.聲明 : 程序中 聲明 只是告訴編譯器 某個 實體 存在, 這個實體可以是 變量 或者 函數 等;
- 2.定義 : 程序中定義 指的就是 某個實體 ( 函數 或 變量 ) 的實際意義;
在 test_1.c 中定義變量 int i = 10; 這是定義了 int 類型的變量, 需要為該變量分配內存空間;
在 test_2.c 中聲明變量 extern int i; 這是聲明了 int 類型的變量, 變量定義在了別的文件中, 不必為該變量分配內存空間;
(2) 代碼示例 ( 函數 聲明 和 定義區別 )
代碼示例 :
- 1.代碼 test_1.c :
- 2.代碼test_2.c :
- 3.編譯運行結果 :
二. 參數 可變參數 順序點 類型缺省認定
1. 函數參數
(1) 參數分析
函數參數分析 :
- 1.本質 : 函數參數的本質 與 局部變量 基本相同, 這兩種數據都存放在棧空間中 ( 中間隔著 返回地址 寄存器 EBP 數據 ) 詳情參考上一篇博客內存管理 ;
- 2.參數值 : 函數調用的 初始值 是 函數調用時的實參值 ;
函數參數的求值順序 (盲點) :
- 1.實現 : 函數參數的求值順序 依賴 編譯器的實現;
- 2.操作數順序沒有在規范中 : C 語言規范中沒有規定函數參數必須從左到右進行計算賦值;
- 3.運算符編程注意點 : C語言中大多數的運算符的操作數求值順序也是不固定的, 依賴于編譯器的實現;
- 4.示例 : 如 int ret = fun1() * fun2(); fun1 和 fun2 函數哪個先執行, 哪個后執行 不一定;
編程時盡量不要編寫的代碼依賴于操作數的實現順序;
(2) 代碼示例 ( 函數參數 求值順序 )
代碼示例 :
- 1.代碼 :
- 2.編譯運行結果 :
分析 :
函數參數計算說明 : fun(m, m ++); 進入函數體之前先計算 m 和 m++ 的值, m 和 m++ 是實參, 在計算完成之后才賦值給 i 和 j 形參;
順序點 : 在進入函數體前是一個順序點, 需要將計算完畢的實參 賦值給形參;
實參 m 賦值 : 賦值給 形參 i, 此處已經到達順序點, m 自增操作已經反映到內存中, 因此 從 內存中獲取的 i 的值是 2;
實參 m++ 賦值 : 賦值給 形參 j, m++ 表達式的計算結果是 1, 因此 j 的值是1;
2. 程序中的順序點
(1) 順序點簡介
順序點介紹 :
- 1.順序點位置 : 順序點存在于程序之中;
- 2.順序點定義 : 順序點是 代碼 執行過程中, 修改變量值 的 最晚時刻 ;
- 3.順序點操作 : 程序運行到順序點時, 之前的代碼操作 都要反映到后續訪問中 ;
順序點列舉 :
- 1.表達式結束 : 每個表達式結束都是順序點, 以分號 “;” 結尾, 每個分號的位置都是順序點;
- 2.某些表達式的運算對象計算 : &&, || (邏輯運算), ? :(三目運算符), 逗號 表達式 中每個 運算對象計算后 是順序點;
- 3.函數運行前 : 函數調用并且在執行函數體之前, 所有實際參數求值完之后是一個順序點, 如參數是表達式, 需要將表達式計算出結果;
順序點代碼示例 :
#include <stdio.h>int fun(int i, int j) {printf("%d, %d\n", i, j); }//注意 : 這個知識點可能過時, k = k++ + k++; 在 Ubuntu 中執行結果是 5int main() {//順序點 : 在 k = 2; 表達式以分號結束, 這是一個順序點int k = 2;int a = 1;/*順序點 : 分號結尾處是順序點, 該順序點第 1 個 k++, 計算時 k 先是 2, 自增操作到順序點時執行; 第 2 個 k++, 計算時 k 還是 2, 自增操作到順序點時執行;加法計算完畢后 k 變成 4, 兩次自增后變為 6*/k = k++ + k++;printf("k = %d\n", k);/*a-- && a 進行邏輯運算, 其中 && 是順序點, a-- 在 && 時執行 自減操作, 然后 a-- 結果變成了 0, a 的值也變成了 0, 進行邏輯與操作結果為 0 */printf("a--&&a = %d\n",a--&&a);return 0; }
3. C 語言 函數 的 缺省認定
(n) 標題3
函數缺省認定簡介 :
- 1.描述 : C 語言中 默認 沒有類型的 參數 和 返回值 為 int 類型;
- 2.舉例 :
等價于
int fun(int i) {return i; }- 3.代碼示例 :
4.可變參數 的 定義 和 使用
(1) 簡介
可變參數簡介 :
- 1.描述 : 函數可以接收的參數個數是不定的, 根據調用的需求決定有幾個參數;
- 2.依賴頭文件 : 如果要使用可變參數, 需要導入 stdarg.h 頭文件;
- 3.核心用法 : va_list, va_start, va_end, va_arg 配合使用, 訪問可變參數值;
可變參數示例 :
- 1.函數名相同, 參數個數不同 : open 函數, 有兩種用法, 一個有 2 個參數 int open(const char *pathname, int flags) , 一個有三個參數 int open(const char *pathname, int flags, mode_t mode) , C 語言中明顯沒有重載, 這里是用可變參數來實現的 ; 使用 man 2 open 命令查看 open 函數的文檔;
可變參數的注意點 :
- 1.取值必須順序進行 : 讀取可變參數的值時, 必須從頭到尾按照前后順序讀取, 不能跳過任何一個參數;
- 2.必須確定1個參數 : 參數列表中必須有一個命名確定的參數;
- 3.可變參數數量無法確定 : 使用 va_arg 獲取 va_list 中的值時, 無法判斷實際有多少個參數;
- 4.可變參數類型無法確定 : 使用 va_arg 獲取 va_list 中的值時, 無法判斷某個參數是什么類型的;
依次讀取可變參數時, 注意 可變參數 的 數量 和 類型, 每個位置的參數 是 什么類型, 一定不要讀取錯誤, 否則會產生不可預測的后果;
(2) 代碼示例 ( 定義 使用 可變參數 )
代碼示例 :
- 1.代碼 :
- 2.編譯運行結果 :
三. 函數 與 宏
1. 函數 與 宏 對比案例
(1) 函數 和 宏 的案例
代碼示例 : 分別使用 函數 和 宏 將數組數據清零;
- 1.代碼 :
- 2.編譯運行結果 :
雖然看起來 函數 和 宏實現了相同的功能, 但是它們有很大的區別;
2. 函數 和 宏 的分析
(1) 函數 和 宏 分析
函數 和 宏 分析 :
- 1.宏處理 : 宏定義是在預處理階段直接進行宏替換, 代碼直接復制到宏調用的位置, 由于宏在預處理階段就被處理了, 編譯器是不知道宏的存在的;
- 2.函數處理 : 函數是需要編譯器進行編譯的, 編譯器有決定函數調用行為的義務;
- 3.宏的弊端 ( 代碼量 ) : 每調用一次宏, 在預處理階段都要進行一次宏替換, 會造成代碼量的增加;
- 4.函數優勢 ( 代碼量 ) : 函數執行是通過跳轉來實現的, 代碼量不會增加;
- 5.宏的優勢 ( 效率 ) : 宏 的執行效率 高于 函數, 宏定義是在預編譯階段直接進行代碼替換, 沒有調用開銷;
- 6.函數的弊端 ( 效率 ) : 函數執行的時候需要跳轉, 以及創建對應的活動記錄( 棧 ), 效率要低于宏;
3. 函數 與 宏 的 利弊
(1) 宏 優勢 和 弊端
宏的優勢和弊端 : 宏的執行效率要高于函數, 但是使用宏會有很大的副作用, 非常容易出錯, 下面的例子說明這種弊端;
代碼示例 :
- 1.代碼 :
- 2.編譯運行結果 :
- 3.查看預編譯文件 : 使用 gcc -E test_1.c -o test_1.i 指令, 將預編譯文件輸出到 test_1.i 目錄中; 下面是預編譯文件的一部分 ;
(2) 函數 的 優勢 和 弊端
函數的優缺點 :
- 1.函數優勢 : 函數調用需要將實參傳遞給形參, 沒有宏替換這樣的副作用;
- 2.弊端 ( 效率低 ) : 函數執行需要跳轉, 同時也需要建立活動對象對象 ( 如 函數棧 ) 來存儲相關的信息, 需要犧牲一些性能;
(3) 宏的無可替代性
宏 定義 優勢 :
- 1.宏參數不限定類型 : 宏參數 可以是 任何 C 語言 的實體類型, 如 int, float, char, double 等;
- 2.宏參數可以使類型名稱 : 類型的名稱也可以作為宏的參數;
4. 總結
(1) 宏 定義 和 函數 總結
宏定義 和 函數 小結 :
- 1.宏定義 : 宏 的 參數 可以 是 C 語言中 的 任何類型的 ( 優勢 ) , 宏的執行效率 高 ( 優勢 ), 但是容易出錯 ( 弊端 );
- 2.函數 : 函數 參數 的 類型是固定的, 其 執行效率低于宏, 但是不容易出錯;
- 3.宏定義 和 函數之間的關系 : 這兩者不是競爭對手, 宏定義可以實現一些函數無法實現的功能;
四. 函數的調用約定
1. 函數的活動記錄 分析
(1) 函數的活動記錄
活動記錄概述 : 函數調用時 將 下面一系列的信息 記錄在 活動記錄中 ;
1.臨時變量域 : 存放一些運算的臨時變量的值, 如自增運算, 在到順序點之前的數值是存在臨時變量域中的;
后置操作 自增 原理 : i++ 自增運算 進行的操作 :
( 1 ) 生成臨時變量 : 在內存中生成臨時變量 tmp ;
( 2 ) 臨時變量賦值 : 將 i 的值賦值給臨時變量, tmp = i ;
( 3 ) 進行加 1 操作 : 將 i + 1 并賦值給 i;
示例 : 定義函數 fun(int a, int b), 傳入 fun(i, i++), 傳入后 獲取的實參值分別是 2 和 1;
在函數傳入參數達到順序點之后開始取值, 函數到達順序點之后, 上面的三個步驟就執行完畢, 形參 a 從內存中取值, i 的值是2, 形參 b 從臨時變量域中取值, 即 tmp 的值, 取值是 1;2.局部變量域 : 用于存放 函數 中定義 的局部變量, 該變量的生命周期是局部變量執行完畢;
- 3.機器狀態域 : 保存 函數調用 之前 機器狀態 相關信息, 包括 寄存器值 和 返回地址, 如 esp 指針, ebp 指針;
- 4.實參數域 : 保存 函數的實參信息 ;
- 5.返回值域 : 存放 函數的返回值 ;
2. 函數的調用約定概述
(1) 參數入棧 問題描述
參數入棧問題 : 函數參數的計算次序是不固定的, 嚴重依賴于編譯器的實現, 編譯器中函數參數入棧次序;
- 1.參數傳遞順序 : 函數的參數 實參傳遞給形參 是從左到右傳遞 還是 從右到左傳遞;
- 2.堆棧清理 : 是函數的調用者清理 還是 由 函數本身清理 ;
參數入棧 棧維護 問題示例 :
- 1.多參數函數定義 : 定義一個函數 fun(int a, int b, int c) , 其中有 3 個參數;
- 2.函數調用 : 當發生函數調用時 fun(1, 2, 3), 傳入三個 int 類型的參數, 這三個參數肯定有一個傳遞順序, 這個傳遞順序可以約定;
- ( 1 ) 從左向右入棧 : 將 1, 2, 3 依次 傳入 函數中 ;
- ( 2 ) 從右向左入棧 : 將 3, 2, 1 依次 傳入 函數中 ;
- 3.棧維護 : 在 fun1() 函數中 調用 fun2() 函數, 會創建 fun2() 函數的 活動記錄 (棧), 當 fun2() 函數執行完畢 返回的時候, 該 fun2 函數的棧空間是由誰 ( fun1 或 fun2 函數 ) 負責釋放的;
函數參數計算次序依賴于編輯器實現, 函數參數入棧的順序可以自己設置;
(2) 參數傳遞順序的調用約定
函數參數調用約定 :
- 1.函數調用行為 : 函數調用時 參數 傳遞給 被調用的 函數, 返回值被返回給 調用函數 ;
- 2.調用約定作用 : 調用約定 是 用來規定 ① 參數 是通過什么方式 傳遞到 棧空間 ( 活動記錄 ) 中, ② 棧 由誰來 清理 ;
- 3.參數傳遞順序 ( 右到左 ) : 從右到左入棧使用 __stdcall, __cdecl, __thiscall 關鍵字, 放在 函數返回值之前;
- 4.參數傳遞順序 ( 左到右 ) : 從左到右入棧使用 __pascal, __fastcall 關鍵字, 放在 函數返回值之前;
- 5.調用堆棧的清理工作 : ① 調用者負責清理調用堆棧; ② 被調用的函數返回之前清理堆棧;
五. 函數設計技巧
函數設計技巧 :
- 1.避免使用全局變量 : 在函數中盡量避免使用全局變量, 讓函數形成一個獨立功能模塊;
- 2.參數傳遞全局變量 : 如果必須使用到全局變量, 那么多設計一個參數, 用于傳入全局變量;
- 3.參數名稱可讀性 : 盡量不要使用無意義的字符串作為參數變量名;
- 4.參數常量 : 如果參數是一個指針, 該指針僅用于輸入作用, 盡量使用 const 修飾該指針參數, 防止該指針在函數體內被修改;
- 5.返回類型不能省略 : 函數的返回類型不能省略, 如果省略了返回值, 那么返回值默認 int;
- 6.參數檢測 : 在函數開始位置, 需要檢測函數參數的合法性, 避免不必要的錯誤, 尤其是指針類型的參數;
- 7.棧內存指針 : 返回值 絕對不能是 局部變量指針, 即 指針指向的位置是 棧內存位置, 棧內存在返回時會銷毀, 不能再函數運行結束后使用 ;
- 8.代碼量 : 函數的代碼量盡量控制在一定數目, 50 ~ 80 行, 符合模塊化設計規則;
- 9.輸入輸出固定 : 函數在輸入相同的參數, 其輸出也要相同, 盡量不要在函數體內使用 static 局部變量, 這樣函數帶記憶功能, 增加函數的復雜度;
- 10.參數控制 : 編寫函數的時候, 函數的參數盡量控制在 4 個以內, 方便使用;
- 11.函數返回值設計 : 有時候函數不需要返回值, 或者返回值使用指針參數設置, 但是為了增加靈活性, 可以附加返回值; 如 支持 鏈式表達式 功能;
總結
以上是生活随笔為你收集整理的【C 语言】C 语言 函数 详解 ( 函数本质 | 顺序点 | 可变参数 | 函数调用 | 函数活动记录 | 函数设计 ) [ C语言核心概念 ]的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【C 语言】内存管理 ( 动态内存分配
- 下一篇: 【 Markdown 】Markdown