C語言:第11天筆記
內容提要
-
函數
-
函數的概述
-
函數的分類
-
函數的定義
-
形參和實參
-
函數的返回值
-
函數的調用
-
函數的聲明
-
函數
函數的概述
- **函數:**實現一定功能的,獨立的代碼模塊,對于函數的使用,一定是先定義,后使
? 用。
使用函數的優勢:
① 我們可以通過函數提供功能給別人使用。當然我們也可以使用別人提供的函數,減
少代碼量。
② 借助函數可以減少重復性的代碼。
③ 實現結構化(模塊化:C語言中的模塊化其實就是多文件+函數)程序設計思想。
關于結構化設計思想:將大型的任務功能劃分為相互獨立的小型的任務模塊來設計
(多文件 + 函數)
- 函數的作用:
(1) 代碼復用:避免重復編寫相同功能的代碼。
(2) 模塊化設計:將復雜程序拆分成多個小功能模塊,每個函數負責一個獨立任務,使
代碼邏輯結構更加清晰。
(3) 便于維護和調試:單個函數功能單一,出現問題容易定位和修改,不需要改動整個
程序。
(4) 提高開發效率:便于多人協同開發時,分工明確,編寫不同函數,最終組合成完整
程序
- 函數是C語言程序的基本組成單元:
C語言程序必須包含一個main函數,可以包含零個或多個其他函數。
函數的分類
-
按來源分:
-
**庫函數:**C語言標準庫實現的并提供使用的函數,如:scanf()、printf()、fgets()、
? fputs()、strlen()…
-
自定義函數:需要程序員自行實現,開發中大部分函數都是自定義函數。
-
按參數分:
-
**無參函數:**函數調用時,無需傳遞參數,可有可無返回值,如:show_all();
-
**有參函數:**函數調用時,需要參數傳遞數據,經常需要配套返回值來使用,如:
? printf(“%d\n”, 12);
-
按返回值分:
-
**有返回值函數:**函數執行后返回一個值,如: if (scanf(“%d”, &num) != 1)
-
**無返回值函數(void):**函數僅執行操作,不返回值
-
-
從函數調用的角度分:
-
**主調函數:**主動去調用其他函數的函數。(main函數只能作為主調函數)
-
**被調函數:**被其他函數調用的函數。
舉例:
// 主調函數
int main()
{
// 被調函數
printf("hello world!\n");
}
注意:很多時候,尤其是對于自定義函數,一個函數有可能既是主調函數,又是
被調函數。
int fun_b()
{
printf("函數B\n");
}
// fun_a是主調函數
int fun_a()
{
printf("函數A\n");
// fun_b是背調函數
fun_b();
}
int main()
{
// fun_a是被調函數
fun_a();
}
以上案例中,fun_a()相對fun_b()來說是主調函數,同時對于main()函數來說,他
又是被調函數。
函數的定義
定義
語法:
[返回類型] 函數名([形參列表]) -- 函數頭 | 函數首部
{
函數體語句; -- 函數體:整個{}包裹的內容都屬于函數體,
函數的{}不能省略
}
-
函數頭:
-
**返回類型:**函數返回值的類型
-
**函數名:**函數的名稱,遵循標識符命名(不能以數字開頭,只能包含大小寫字母、下
? 換線、數字。建議:小寫+下劃線,舉例: show_all() ,或者小駝峰命名法,第一
? 個單詞首字母小寫,其他單詞首字母大寫,舉例: showAll() )
- **形參列表:**用于接收主調函數傳遞的數據,如果有多個參數,使用 , 分隔,切每一個
? 形參都需要指明類型。
小貼士:
① 形參:主調函數給被調函數傳遞數據:主調函數 → 被調函數
② 返回值:被調函數給主調函數返回數據:被調函數 → 主調函數
通過生活中的案例理解函數調用:
假設:飲料店的工作人員通過榨汁機榨取新鮮果汁
理解:
工作人員:主調函數
榨汁機: 被調函數
水果: 傳遞的參數
果汁: 函數的返回值
工作人員向榨汁機放入一個水果:主調函數調用被調函數,并傳遞數據
工作人員用杯子接收榨汁機榨出的果汁:主調函數接收被調函數返回的數據
說明:
- 函數的返回值:就是返回值的類型,兩個類型可以不同,但是必須能夠進行轉換,舉
例:
double fun_a() // 函數的返回類型是:double
{return 12; // 函數的返回值是:int
}
// 分析:此時需要轉換,函數在執行的時候,會自動提升int的類型為double,此
時屬于隱式轉換,正常轉換,以上正確
int[] fun_b() // 函數的返回類型是:int[]
{return 12; // 函數的返回值是:int
}
// 分析:此時需要提升int的類型為int[],int不能轉換為int[],以上錯誤!
int fun_c() // 函數的返回類型是:int
{return 12.5;// 函數的返回值是:double
}
// 分析:此時需要將double類型轉換為int類型,浮點型轉整型,會丟失小數部
分,保留整數位,以上正確
大家可以這樣理解(非官方):
大家可以將函數的返回類型理解為 變量的類型,將函數的返回值理解為 變量的
值。
范例:
#include <stdio.h>double fun_a(){return 12;// 就是將int類型的12賦值給double類型的匿名變量 int -->double}int fun_b(){return 12.5;// 就是將double類型的12.5賦值給int類型的匿名變量
double --> int 此時會舍棄掉小數部分
}
double fun_c()
{return 12.5; // 就是將double類型的12.5賦值給double類型的匿名變量
double --> double
}
int main(int argc,char *argv[])
{
// 接收函數返回值,函數返回什么類型,就用什么類型接收double result1 = fun_a();// 主調函數使用double來接收被調函數返回
的double,double --> doubleprintf("%lf\n",result1);int result2 = fun_b(); // 主調函數使用int來接收被調函數返回的
int,int --> intprintf("%d\n",result2);int result3 = (int)fun_c(); // 主調函數使用int來接收被調函數返回
的double,int --> (int)doubleprintf("%d\n",result3);return 0;
}
在C語言中無返回值時應明確使用 void
類型(空類型/無類型),舉例:
void test() // 此時這個函數,沒有返回值,如果需要提前結束函數,寫法:
return;
{printf("hello world!\n");
}
// 下面寫法等價于上面寫法
void test()
{printf("hello world!\n");return; // 一般,這個return;省略不寫
}
在C語言中,C89標準允許函數的返回類型標識符可以省略,如果省略,默認返回int。
C99/C11標準要求必須明確指定返回類型,不再支持默認int類型,舉例:
// 寫法1:(C89標準),main的返回類型是int類型,默認返回值是0,等價于寫
法2 不推薦
main()
{
...
}
// 寫法2:(C99后推薦),等價于上面寫法
int main()
{return 0;
}
函數中返回語句的形式為return``(表達式)
或者 return 表達式
// 寫法1
int main()
{return(0);
}
// 寫法2
int main()
{return 0;
}
如果 參數列表
中有多個參數,則它們之間要用 , 分隔;即使它們的類型相同,在形
式參數中只能逐個進行說明,舉例:
// 正確舉例
int avg(int x, int y, int z)
{...
}
// 錯誤舉例
int avg(int x, y, z)
{...
}// 正確舉例
int avg(int x, int y, int z)
{...
}
// 錯誤舉例
int avg(int x, y, z)
{...
}
如果 形參列表
中沒有參數,我們可以不寫,也可以用void標識,舉例:
// 寫法1:推薦
int main()
{...
}
// 寫法2:
int main(void)
{...
}
C89開始,提供了變長參數,也就是一個函數的參數個數可以是不確定的。需要引入
<stdarg.h>
,擴展:
語法:
[返回類型] 函數名(參數列表, ...)
{
...
}
舉例:
#include <stdio.h>
#include <stdarg.h>
// 計算n個整數的平均值
double average(int n, ...) { // ...只能放在 具體的參數列表的后面va_list args; // 聲明參數列表對象int sum = 0;va_start(args, n); // 初始化參數列表,n是最后一個固定參數// 遍歷所有可變參數for (int i = 0; i < n; i++) {// 獲取一個int類型的參數sum += va_arg(args, int);}va_end(args); // 清理參數列表return (double)sum / n;
}
int main() {printf("平均值: %.2f\n", average(3, 10, 20, 30)); // 20.00printf("平均值: %.2f\n", average(5, 1, 2, 3, 4, 5)); // 3.00return 0;
}
案例
案例1
- 需求:計算1~n之間自然數的階乘值
- 代碼:
#include <stdio.h>
/**
* 定義一個函數,實現1~n之間的階乘計算
* @param n:階乘上限
* @return n的階乘值
*/
size_t fun_1(int n)
{int i; // 循環變量size_t s = 1; // 階乘值,初始值是1for (i = 1; i <= n; i++) s *= i;return s;
}
int main(int argc,char *argv[])
{printf("1~12的階乘結果是:%lu\n", fun_1(12));printf("1~20的階乘結果是:%lu\n", fun_1(20));printf("1~30的階乘結果是:%lu\n", fun_1(30));printf("1~40的階乘結果是:%lu\n", fun_1(40));return 0;
}
運行結果:
注意:這里計算結果為0,是因為數據太大,超過int存儲范圍,高位數據丟失,低位
數據轉出來為0,建議
使用 unsigned long
類型。
案例2
需求:計算一個圓臺兩個面的面積之和。
代碼:
#include <stdio.h>
#include <math.h>
#define PI 3.1415926
/**
* 定義一個函數:實現圓的面積計算
* @param r:圓的半徑
* @return 圓的面積
*/
double cicle_area(double r)
{
// return PI * r * r;return PI * pow(r,2.0); // pow(底數,指數);編譯需要加 -lm
}
int main(int argc,char *argv[])
{
// 定義兩個半徑,兩個面積double r1,r2,area1,area2;printf("請輸入兩個圓的半徑:\n");scanf("%lf,%lf", &r1, &r2);// 計算面積area1 = cicle_area(r1);area2 = cicle_area(r2);printf("一個圓臺兩個面的面積之和是%lf\n", area1 + area2);return 0;
}
-
編譯命令
gcc demo03.c -lm
形參和實參
形參(形式參數)
定義
函數定義時指定的參數,形參是用來接收數據的。函數定義時,系統不會為形參申請內
存,只有當函數調用時,系統才會為形參申請內存。主要用于存儲實際參數,并且當函數
返回時(執行return),系統會自動回收為形參申請的內存資源。
- C****語言中所有的參數傳遞都是值傳遞。
- 若要修改實參,需要傳遞指針,指針傳遞本質上也是值傳遞(后續章節講)。
案例
- 需求:判斷一個數是偶數還是奇數
- 代碼:
#include <stdio.h>
/**
* 方式1
*/
void fun1(int n) // 這里的n就是形參
{if (n % 2 == 0){printf("%d是偶數!\n", n);return; // 提前結束函數,后續代碼不再執行}printf("%d是奇數!\n", n);
}
/**
* 方式2
*/
int fun2(int n)
{if (n % 2 == 0){printf("%d是偶數!\n", n);return -1;}printf("%d是奇數!\n", n);return 0;
}
int main(int argc,char *argv[])
{fun1(5);fun2(5);return 0;
}
實參(實際參數)
定義
實參是函數調用時由主調函數傳遞給被調函數的具體的數據。實參可以是常量、變量、表
達式、帶有返回值的函數等。
關鍵特性
1、類型多樣性:
- 實參可以是常量、變量或者表達式…。
- 例如:
fun(12); // 常量作為實參
fun(a); // 變量作為實參
fun(a + 12); // 表達式作為實參
fun(func()); // 帶有返回值的函數作為實參
- 類型轉換:
-
當實參和形參類型不同時,會按照賦值規則進行類型轉換。
-
類型轉換可能導致精度丟失。
-
例如:
-
#include <stdio.h> /** * 求一個數的絕對值 */ double fabs(double a) {return a < 0 ? -a : a; } int main() {int x = 12, y = -12;int x1 = (int)fabs(x); // x會被隱式轉換為double,fabs返回的 是double類型數據int y1 = (int)fabs(y); }
注意:函數調用的時候,通過實參給形參賦值。形參類似于變量,實參類似于變
量的值。
函數調用時:
主調函數通過實參給被調函數的形參賦值,可理解為:將主調函數的
值賦值給被調函數的變量。
函數返回時:
被調函數通過返回值給主調函數賦值,可理解為:將被調函數的值賦
值給主調函數的變量。
- 單向值傳遞:
-
C語言采用單向值傳遞機制(賦值的方向:實參 → 形參)
-
實參僅將其值賦值給形參,不傳遞實參本身。
-
形參值的改變不會影響實參。
-
案例:
int modify(int n) // n的變量地址:0x11 {n = 20; // 修改 0x11這個空間的數據位20 n = 20return n; } int main() {int n = 10; // n的變量地址:0x21modify(n); // 將0x21中的數據賦值給0x11這個空間printf("%d\n", n); // 10 }
- 內存獨立性
- 實參和形參在內存中占據不同的空間
- 形參擁有獨立的內存地址
演示
#include <stdio.h>
int fun(int n) // n是形參
{printf("形參n的值:%d\n", n);n += 5; // 修改形參的數據return n;
}
int main()
{int a = 10;printf("調用前實參a的值:%d\n", a); // 10// 變量作為實參int res = fun(a); // a 是實參printf("調用前實參a的值:%d\n", a); // 10printf("函數返回值:%d\n", res); // 15// 常量作為實參fun(12); //字面量12作為實參// 表達式作為實參fun(a + 12); // 表達式作為實參return 0;}
上述示例程序會輸出:
調用前實參a的值: 10
形參n的值: 10
調用后實參a的值: 10
函數返回值: 15
形參n的值: 12
形參n的值: 22
案例
-
需求:輸入4個整數,要求用一個函數求出最大數。
-
分析:
-
設計一個函數,這個函數只求2個數的最大數
-
多次復用這個函數實現最終的求值
-
-
代碼:
#include <stdio.h>
/**
* 定義一個函數,求2個數的最大值
* @param x,y:參與比較的整數
* @return 返回最大值
*/
int get_max(int x, int y)
{return x > y ? x : y;
}
int main(int argc,char *argv[])
{// 定義4個變量,用來接收控制臺輸入int a,b,c,d;// 定義一個變量,存儲最大值int max;printf("請輸入4個整數:\n");scanf("%d%d%d%d", &a, &b, &c, &d);// 求a,b最大值max = get_max(a,b);// 求a,b,c最大值max = get_max(max,c);// 求a,b,c,d最大值max = get_max(max,d);printf("%d,%d,%d,%d中的最大值是%d\n",a,b,c,d,max);return 0;
}
運行結果:
函數的返回值
定義
- 若不需要返回值,函數可以沒有return語句;
// 如果返回類型是void,return關鍵字可以省略
void fun1()
{... // return;
}
// 這種寫法,return關鍵字也可以省略,但是此時默認返回是 return 0
int fun2()
{... // return 0;
}
// 這種寫法,return關鍵字也可以省略,但是此時默認返回是 return 0
fun3() // 如果不寫返回類型,默認返回int,C99/C11之后不再支持省略返回類
型
{... // return 0;
}
- 一個函數中可以有多個return語句,但是同一時刻只有一個return語句被執行。
\#include <stdio.h>
int eq(int num)
{if (num % 2 == 0) return 0;return 1;
}
int main()
{int num = 5;printf("%d是一個%s\n", num, eq(num) == 0 ? "偶數" : "奇數"); //
5是一個奇數
}
-
返回類型一般情況下要和函數中return語句返回的數據類型一致,如果不一致,要符
合C語言中的隱式轉換規則。
double add(int a, int b) // 返回類型是double
{return a + b; // 返回值的類型是int
}
// 簡化理解: double add = a + b;
案例
- 需求:輸入兩個整數,要求用一個函數求出最大值
- 實現1:不涉及類型轉換
#include <stdio.h>
int get_max(int x, int y)
{if (x > y) return x;return y;
}
int main()
{int a,b,max;printf("請輸入兩個整數:\n");scanf("%d%d",&a,&b);max = get_max(a,b);printf("%d,%d中的最大值是%d\n",a,b,max);
}
- 實現2:設計類型轉換-隱式轉換
#include <stdio.h>
double get_max(int x, int y) // int隱身轉換為double
{if (x > y) return x;return y;
}
int main()
{int a,b,max;printf("請輸入兩個整數:\n");scanf("%d%d",&a,&b);max = (int)get_max(a,b); // 顯示轉換printf("%d,%d中的最大值是%d\n",a,b,max);
}
- 實現3:涉及類型轉換-顯示轉換
#include <stdio.h>
int get_max(int x, int y) // 將double類型轉換為int類型,可以隱式轉
換,也可以顯示轉換
{double z;z = x > y ? x : y;return (int)z; // 顯示轉換
}
int main()
{int a,b,max;printf("請輸入兩個整數:\n");scanf("%d%d",&a,&b);max = get_max(a,b);printf("%d,%d中的最大值是%d\n",a,b,max);
}
函數的調用
調用方式
① 函數語句:
test(); // 對于無返回值的函數,直接調用
int res = max(2,4); // 對于有返回值的函數,一般需要在主調函數中接收被調函數
的返回值
② 函數表達式:
printf("%d", (int)fabs(number)); // 函數作為實參
注意:函數可以作為函數的實參,如果要作為形參,必須使用函數指針。
在一個函數中調用另一個函數具備以下條件:
- 被調用的函數必須是已經定義的函數。
- 若使用庫函數,應在本文件開頭用 #include 包含其對應的頭文件。
- 若使用自定義函數,自定義函數又在主調函數的后面,則應在主調函數中對被調函數
? 進行聲明。聲明的作用是把函數名、函數參數的個數和類型等信息通知編譯系統,以
? 便于在遇到函數時,編譯系統能正確識別函數,并檢查函數調用的合法性。
函數的聲明
函數調用時,往往要遵循先定義,后使用,但如果我們對函數的調用操作出現在函數定義
之前,則需要對函數進行聲明。
定義
完整的函數使用分為三部分:
-
**[**函數聲明]
int max(int x, int y, double z); // 函數聲明只保留函數頭,便于編譯 系統進行檢查 int max(int, int, double); // 函數聲明的時候,可以省略形參名 稱
函數聲明如果是在同一個文件,一定要定義在文件中所有函數定義的最前面。如果有
對應的 .h 文件,可以將函數的聲明抽取到.h中。
-
函數定義
-
int max(int x, int y, double z) // 函數定義時,一定不能省略形參名 稱 { return x > y ? x : y > z ? y : (int)z; }
函數定義的時候,不能省略形參的數據類型、參數個數、參數名稱,位置要和函數聲
明完全一致。
注意:函數定義時參數列表要和函數聲明時的參數列表完全對應,同時函數定
義要保留形參名稱
- 函數調用
int main()
{
printf("%d\n", max(4,5,6));
}
作用
? C語言的函數聲明時為了提前告訴編譯系統函數的名稱、返回類型和參數,這樣在函
數實際定義之前就能安全調用它,避免編譯錯誤,同時檢查參數和返回值是否正確。相當
于給編譯器一個“預告”,確保代碼正確編譯和運行。
使用
- 錯誤示例:被調函數寫在主調函數之后
// 主調函數
int main()
{printf("%d\n", add(12,13));// 編譯報錯,因為函數未經過聲明,編譯
系統無法檢查函數的合法性
}
// 被調函數
int add(int x, int y)
{return x + y;
}
- 正確示例:主調函數寫在被調函數之后
// 被調函數
int add(int x, int y)
{return x + y;
}
// 主調函數
int main()
{printf("%d\n", add(12,13));
}
注意:如果函數的調用比較簡單,如a函數調用b函數,b函數定義在a函數之前,此時
是可以省略函數聲明的。
- 正確演示:被調函數和主調函數無法區分前后,必須要增加函數聲明
// 函數聲明
void funa(int, int);
void funb(int, int);
// 函數定義
void funb(int a, int b)
{...// 函數調用funa();
}
void funa(int a, int b)
{...// 函數調用funb();
}
int main()
{// 函數調用funa(12,13);
}
聲明的方式:
- 函數頭加上分號
int add(int a, int b);
- 函數頭加上分號,可省略形參名稱,但不能省略參數類型
int add(int, int);
變量和函數底層工作原理【擴展】
變量的底層執行機制
變量本質是內存中的一塊存儲空間,其底層處理涉及編譯期的符號解析和運行時的內存分
配與訪問。
- 編譯階段:符號表與地址映射
- 編譯器在編譯時會為每個變量創建符號表條目,記錄變量名、類型、作用域和內
? 存偏移量(而非實際地址)。
- 對于全局變量和靜態變量,編譯器會將其分配到數據段(已初始化)或BSS 段
? (未初始化),并計算其在段內的偏移量。
- 對于局部變量,編譯器會記錄其在棧幀中的相對位置(基于棧指針的偏移量)。
- 運行階段:內存分配與訪問
- 全局 / 靜態變量:程序加載時,操作系統會將數據段和 BSS 段加載到內存的固定
? 位置,變量的實際地址 = 段起始地址 + 編譯期計算的偏移量。
- 局部變量:函數調用時,CPU 會為函數創建棧幀,局部變量的地址 = 棧指針
? (SP) + 編譯期確定的偏移量(通常為負數,因為棧向下生長)。
- 動態變量(malloc):通過系統調用在堆中分配內存,返回的指針是堆中實際地
? 址,由內存管理模塊(如 glibc 的 ptmalloc)維護。
- 訪問變量的底層指令
訪問變量時,CPU 通過地址計算得到內存地址,再執行加載( load
)或存儲
(store
)指令。
例如, int a = 5;
會被編譯為:計算a
的地址,然后執行store 5
到該地
址 。
函數的底層執行機制
函數的執行本質是指令流的跳轉與棧幀管理,涉及函數調用、棧幀創建、參數傳遞和返回
值處理。
- 編譯階段:函數地址與指令生成
- 編譯器將函數體編譯為一系列機器指令,存儲在代碼段(只讀),并在符號表中
記錄函數名與起始地址。
- 函數參數和返回值的傳遞方式(如棧傳遞、寄存器傳遞)由調用約定(如 cdecl、
stdcall)決定,編譯器會按約定生成對應指令。
- 函數調用的底層步驟
- 步驟 1:參數入棧
調用者將參數按約定順序(通常從右到左)壓入棧中,或放入指定寄存器(如
x86-64 的部分參數用寄存器傳遞)。
- 步驟 2:保存返回地址
CPU 將下一條指令的地址(函數調用后的執行點)壓入棧中,供函數返回時使
用。
- 步驟 3:跳轉至函數入口
執行 call 指令,將程序計數器(PC)設置為函數的起始地址,開始執行函數指
令。
- 步驟 4:創建棧幀
函數執行的第一條指令通常是:asm
push ebp ; 保存調用者的棧幀基址
mov ebp, esp ; 用當前棧指針作為新棧幀的基址
sub esp, N ; 為局部變量分配N字節的棧空間
此時棧幀包含:參數、返回地址、上一個棧幀基址(ebp)、局部變量。
- 步驟 5:執行函數體
按編譯生成的指令執行邏輯,訪問局部變量(通過 ebp 偏移)、操作參數(通過
ebp 正偏移)。
- 步驟 6:返回結果
返回值通常存入指定寄存器(如 x86 的 eax ,x86-64 的 rax ),或通過棧傳遞
(大型結構體)。
- 步驟 7:恢復棧幀并返回
執行:asm
mov esp, ebp ; 釋放局部變量的棧空間
pop ebp ; 恢復調用者的棧幀基址
ret ; 彈出返回地址到PC,跳轉回調用者
關鍵底層概念
- 內存分段:代碼段(指令)、數據段(全局變量)、BSS 段(未初始化全局變量)、
棧(局部變量 / 函數調用)、堆(動態內存)。
- 棧幀:每個函數調用對應一個棧幀,包含參數、返回地址、局部變量,由 ebp(基址
指針)和 esp(棧指針)界定。
- 地址綁定:變量和函數的地址在編譯期(靜態綁定)或加載 / 運行期(動態綁定,如
共享庫)確定。
總結
- 變量:通過編譯期符號表記錄偏移量,運行時映射到實際內存地址,通過 CPU 的加載
/ 存儲指令訪問。
- 函數:通過
call
指令跳轉至代碼段執行,借助棧幀管理參數、局部變量和返回地
址,最終通過 ret
指令返回。
stdcall)決定,編譯器會按約定生成對應指令。
- 函數調用的底層步驟
- 步驟 1:參數入棧
調用者將參數按約定順序(通常從右到左)壓入棧中,或放入指定寄存器(如
x86-64 的部分參數用寄存器傳遞)。
- 步驟 2:保存返回地址
CPU 將下一條指令的地址(函數調用后的執行點)壓入棧中,供函數返回時使
用。
- 步驟 3:跳轉至函數入口
執行 call 指令,將程序計數器(PC)設置為函數的起始地址,開始執行函數指
令。
- 步驟 4:創建棧幀
函數執行的第一條指令通常是:asm
push ebp ; 保存調用者的棧幀基址
mov ebp, esp ; 用當前棧指針作為新棧幀的基址
sub esp, N ; 為局部變量分配N字節的棧空間
此時棧幀包含:參數、返回地址、上一個棧幀基址(ebp)、局部變量。
- 步驟 5:執行函數體
按編譯生成的指令執行邏輯,訪問局部變量(通過 ebp 偏移)、操作參數(通過
ebp 正偏移)。
- 步驟 6:返回結果
返回值通常存入指定寄存器(如 x86 的 eax ,x86-64 的 rax ),或通過棧傳遞
(大型結構體)。
- 步驟 7:恢復棧幀并返回
執行:asm
mov esp, ebp ; 釋放局部變量的棧空間
pop ebp ; 恢復調用者的棧幀基址
ret ; 彈出返回地址到PC,跳轉回調用者
關鍵底層概念
- 內存分段:代碼段(指令)、數據段(全局變量)、BSS 段(未初始化全局變量)、
棧(局部變量 / 函數調用)、堆(動態內存)。
- 棧幀:每個函數調用對應一個棧幀,包含參數、返回地址、局部變量,由 ebp(基址
指針)和 esp(棧指針)界定。
- 地址綁定:變量和函數的地址在編譯期(靜態綁定)或加載 / 運行期(動態綁定,如
共享庫)確定。
總結
- 變量:通過編譯期符號表記錄偏移量,運行時映射到實際內存地址,通過 CPU 的加載
/ 存儲指令訪問。
- 函數:通過
call
指令跳轉至代碼段執行,借助棧幀管理參數、局部變量和返回地
址,最終通過 ret
指令返回。