C語言:第11天筆記

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()); // 帶有返回值的函數作為實參
  1. 類型轉換:
  • 當實參和形參類型不同時,會按照賦值規則進行類型轉換。

  • 類型轉換可能導致精度丟失。

  • 例如:

  • #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);
    }
    

    注意:函數調用的時候,通過實參給形參賦值。形參類似于變量,實參類似于變

    量的值。

函數調用時:

主調函數通過實參給被調函數的形參賦值,可理解為:將主調函數的

值賦值給被調函數的變量。

函數返回時:

被調函數通過返回值給主調函數賦值,可理解為:將被調函數的值賦

值給主調函數的變量。

  1. 單向值傳遞:
  • 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
    }
    
  1. 內存獨立性
  • 實參和形參在內存中占據不同的空間
  • 形參擁有獨立的內存地址

演示

#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);

變量和函數底層工作原理【擴展】

變量的底層執行機制

變量本質是內存中的一塊存儲空間,其底層處理涉及編譯期的符號解析運行時的內存分

配與訪問

  1. 編譯階段:符號表與地址映射
  • 編譯器在編譯時會為每個變量創建符號表條目,記錄變量名、類型、作用域和內

? 存偏移量(而非實際地址)。

  • 對于全局變量和靜態變量,編譯器會將其分配到數據段(已初始化)或BSS

? (未初始化),并計算其在段內的偏移量。

  • 對于局部變量,編譯器會記錄其在棧幀中的相對位置(基于棧指針的偏移量)。
  1. 運行階段:內存分配與訪問
  • 全局 / 靜態變量:程序加載時,操作系統會將數據段和 BSS 段加載到內存的固定

? 位置,變量的實際地址 = 段起始地址 + 編譯期計算的偏移量。

  • 局部變量:函數調用時,CPU 會為函數創建棧幀,局部變量的地址 = 棧指針

? (SP) + 編譯期確定的偏移量(通常為負數,因為棧向下生長)。

  • 動態變量(malloc:通過系統調用在中分配內存,返回的指針是堆中實際地

? 址,由內存管理模塊(如 glibc 的 ptmalloc)維護。

  1. 訪問變量的底層指令

訪問變量時,CPU 通過地址計算得到內存地址,再執行加載( load )或存儲

store)指令。

例如, int a = 5; 會被編譯為:計算a的地址,然后執行store 5該地

址 。

函數的底層執行機制

函數的執行本質是指令流的跳轉與棧幀管理,涉及函數調用、棧幀創建、參數傳遞和返回

值處理。

  1. 編譯階段:函數地址與指令生成
  • 編譯器將函數體編譯為一系列機器指令,存儲在代碼段(只讀),并在符號表中

記錄函數名與起始地址。

  • 函數參數和返回值的傳遞方式(如棧傳遞、寄存器傳遞)由調用約定(如 cdecl、

stdcall)決定,編譯器會按約定生成對應指令。

  1. 函數調用的底層步驟
  • 步驟 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. 函數調用的底層步驟
  • 步驟 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 指令返回。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/91131.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/91131.shtml
英文地址,請注明出處:http://en.pswp.cn/web/91131.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

java導出pdf(使用html)

引入maven <dependencies><!-- Thymeleaf --><dependency><groupId>org.thymeleaf</groupId><artifactId>thymeleaf</artifactId><version>3.1.1.RELEASE</version> <!-- 或與 Spring Boot 匹配的版本 --></de…

Qt 遠程過程調用(RPC)實現方案

在分布式系統開發中&#xff0c;遠程過程調用&#xff08;RPC&#xff09;是實現跨進程、跨機器通信的重要技術。Qt 作為一個強大的跨平臺框架&#xff0c;提供了多種 RPC 實現方案&#xff0c;能夠滿足不同場景下的通信需求。本文將深入探討 Qt 中 RPC 的各種實現方式&#xf…

攻防世界-引導-Web_php_unserialize

題目內容&#xff1a;出現一段源代碼&#xff0c;分段分析第一部分如下<?php class Demo { private $file index.php;public function __construct($file) { $this->file $file; }function __destruct() { echo highlight_file($this->file, true); }function __w…

pytorch學習筆記-自定義卷積

未完結的草稿———&#xff01;大概是準備整合一下常見的層&#xff0c;整合完感覺就可以進行搭建了&#xff08;還沒進行到這一步所以不太確定版&#xff09; &#xff08;ps我將在完結這一篇的時候刪除上面的小字and二編一下整篇文章的結構&#xff0c;如果看到了這部分文字…

[明道云]-基礎教學2-工作表字段 vs 控件:選哪種?

本文深入解析“工作表字段”與“控件”的關系與差別,并從結構、功能、使用場景和選型建議等方面進行對比。 一、基礎概念厘清 ? 工作表字段 = 數據模型中的列 工作表字段相當于數據庫表中的列,是記錄每條業務對象(如訂單、客戶等)屬性的數據項,每個字段都有明確的名稱和…

C++-一篇文章入門coroutines協程

文章目錄前言什么是協程協程實現原理C協程的最小例子12345協程等效代碼協程傳值的例子前言 最近學習了一下C協程&#xff0c;這篇文章將介紹協程的相關概念&#xff0c;以及在C中如何使用協程。 什么是協程 C中&#xff0c;協程&#xff08;coroutines&#xff09;可以理解為…

數字經濟專業的就業全景指南

CDA數據分析師證書含金量高&#xff0c;適應了未來數字化經濟和AI發展趨勢&#xff0c;難度不高&#xff0c;行業認可度高&#xff0c;對于找工作很有幫助。一、數字經濟就業熱力圖二、核心崗位發展路徑1. 互聯網數字運營崗2. 金融科技崗崗位類型技能組合證書加持5年薪資范圍智…

PDF轉Word免費工具!批量處理PDF壓縮,合并, OCR識別, 去水印, 簽名等全功能詳解

大家好&#xff0c;歡迎來到程序視點&#xff01;我是你們的老朋友.小二&#xff01;前言PDF軟件我發的非常多&#xff0c;但今天這款工具是大家公認最值得推薦的&#xff0c;這款軟件就是PDF24PDF24幾乎包含了PDF的所有功能&#xff0c;目前是更新到了最新版本&#xff01;文末…

Flutter開發實戰之Widget體系與布局原理

第3章:Widget體系與布局原理 在前面兩章中,我們已經搭建好了Flutter開發環境,并且了解了Dart語言的基礎知識。現在是時候深入Flutter的核心——Widget體系了。如果說Dart是Flutter的語言基礎,那么Widget就是Flutter的靈魂。理解Widget體系,是掌握Flutter開發的關鍵所在。…

C++:stack與queue的使用

stack與queue的使用一.stack與queuej基礎1.stack1.1基本認識1.2示例代碼代碼功能解析2.queue2.1基礎知識操作說明2.2示例代碼代碼分析 一.stack與queuej基礎 1.stack 1.1基本認識以上圖片展示了棧&#xff08;stack&#xff09;這種數據結構的基本操作示意。棧是一種遵循后進先…

Unity 編輯器開發 之 Excel導表工具

一個簡單的Excel導表工具&#xff0c;可以用來熱更數據配置工具使用&#xff1a;&#xfeff;&#xfeff;執行菜單 SDGSupporter/Excel/1.Excel2Cs 生成c#腳本。&#xfeff;&#xfeff;等待C#類編譯完成&#xfeff;&#xfeff;執行菜單 SDGSupporter/Excel/2.Excel2Bytes …

【數據結構與算法】力扣 415. 字符串相加

題目描述 415. 字符串相加 給定兩個字符串形式的非負整數 num1 和num2 &#xff0c;計算它們的和并同樣以字符串形式返回。 你不能使用任何內建的用于處理大整數的庫&#xff08;比如 BigInteger&#xff09;&#xff0c; 也不能直接將輸入的字符串轉換為整數形式。 示例 1…

進階向:Manus AI與多語言手寫識別

Manus AI與多語言手寫識別:從零開始理解 手寫識別技術作為人工智能領域的重要應用之一,近年來在智能設備、教育、金融等行業得到了廣泛運用。根據市場調研機構IDC的數據顯示,2022年全球手寫識別市場規模已達到45億美元,預計到2025年將突破70億美元。其中,多語言手寫識別技…

Javaweb————HTTP請求頭屬性講解

??????????????????????前面我們已經說過http請求分為三部分&#xff0c;請求行&#xff0c;請求頭和請求體 請求頭包含若干個屬性&#xff1a;格式為屬性名&#xff1a;屬性值&#xff0c;這篇文章我們就來介紹一下http請求頭中一些常見屬性的含義 我們…

9.c語言常用算法

查找順序查找&#xff08;線性查找&#xff09;算法思想&#xff1a;從數組的第一個元素開始&#xff0c;逐個與目標值進行比較&#xff0c;直到找到目標值或查找完整個數組。時間復雜度&#xff1a;最好情況&#xff1a;O(1)&#xff08;目標在第一個位置&#xff09;最壞情況…

AI小智源碼分析——音頻部分(一)

一、源碼跳轉這里采用了函數重載來進行代碼復用&#xff0c;當需要對I2S接口的數據進行配置&#xff0c;比如左右音道切換&#xff0c;可以使用第二個構造函數&#xff0c;這里小智使用的是第一個構造函數&#xff0c;即只傳遞I2S相關的引腳參數&#xff08;不帶slot mask&…

【GNSS原理】【LAMBDA】Chapter.12 GNSS定位算法——模糊度固定LAMBDA算法[2025年7月]

Chapter.12 GNSS定位算法——模糊度固定LAMBDA算法 作者&#xff1a;齊花Guyc(CAUC) 文章目錄Chapter.12 GNSS定位算法——模糊度固定LAMBDA算法一.整周模糊度理論1.LAMBDA算法干了一件什么事情&#xff1f;2.LAMBDA算法步驟&#xff08;1&#xff09;去相關&#xff08;Z變換…

計算機畢業設計java在線二手系統的設計與實現 基于Java的在線二手交易平臺開發 Java技術驅動的二手物品管理系統

計算機畢業設計java在線二手系統的設計與實現z2n189&#xff08;配套有源碼 程序 mysql數據庫 論文&#xff09; 本套源碼可以在文本聯xi,先看具體系統功能演示視頻領取&#xff0c;可分享源碼參考。隨著互聯網技術的飛速發展&#xff0c;二手交易市場也逐漸從傳統的線下模式轉…

如何進行項目復盤?核心要點分析

進行項目復盤需要明確復盤目標、確定復盤參與人員、選擇合適的復盤方法、梳理項目過程與關鍵節點、分析成功與失敗的原因、總結經驗教訓并制定改進計劃。其中&#xff0c;選擇合適的復盤方法尤其關鍵&#xff0c;常見的復盤方法包括魚骨圖分析法、SWOT分析法、PDCA循環法&#…

LeetCode 923.多重三數之和

給定一個整數數組 arr &#xff0c;以及一個整數 target 作為目標值&#xff0c;返回滿足 i < j < k 且 arr[i] arr[j] arr[k] target 的元組 i, j, k 的數量。 由于結果會非常大&#xff0c;請返回 109 7 的模。 示例 1&#xff1a; 輸入&#xff1a;arr [1,1,2,2,…