08. C語言函式

阿狸喜羊羊發表於2024-05-08


【函式基礎】

函式用於將程式程式碼分類管理,實現不同功能的程式碼放在不同函式內,一個函式等於一種功能,其它函式可以呼叫本函式執行。

C語言規定所有的指令資料必須定義在函式內部,比如之前介紹的程式執行流程控制語句,另外修改全域性變數的操作也是透過指令進行的,所以全域性變數只能在函式內修改。

資料作用域

定義的資料有使用區域限制,分為以下三種:
1.全域性資料,在函式外定義的資料,所有函式都能使用。
2.區域性資料,在函式內定義的資料,只有本函式可以使用(可透過指標繞過這一限制)。
3.語句內資料,在程式執行流程控制語句內定義的資料,只能在本語句內部使用,本質上屬於區域性資料的一種,儲存方式與區域性資料相同,都是在棧空間儲存。

不同作用域的資料可以同名,比如全域性變數、區域性變數、語句內變數三者可以同名,呼叫同名資料時使用距離最近的資料,但是為了避免混亂一般不會定義同名資料。

在C語言中定義全域性資料並引用另一個全域性資料賦值時,不能引用全域性變數賦值,可以引用全域性常量賦值,而在C++中沒有這個限制。

#include <stdio.h>

int a = 1;          //全域性變數
const int b = 2;    //全域性常量

//int c = a;    //錯誤,禁止編譯
int c = b;      //可以編譯

int main()
{
    return 0;
}

函式之間的通訊

函式之間可以互相呼叫執行,從一個函式跳轉到另一個函式,呼叫者一般使用call指令跳轉到接任者,接任者執行完畢後使用ret指令返回到呼叫者。

有些函式執行時需要與呼叫者進行通訊,執行前需要呼叫者傳入資料,執行完畢後需要向呼叫者傳送執行結果,最簡單的函式通訊方式是使用全域性變數進行中轉,呼叫者將通訊資料寫入全域性變數,接任者讀取全域性變數獲得通訊資料,接任者執行完畢後將執行結果寫入全域性變數並返回,呼叫者讀取全域性變數接收執行結果,此方式可以實現雙向通訊,但是無法保密,所有函式都可以讀取通訊資料。

為了實現保密通訊,C語言為函式提供了引數和返回值。
1.引數,用於呼叫者向接任者傳送資料,引數由接任者定義、由呼叫者賦值,引數在語意上屬於定義者的區域性資料,只不過可以在函式執行時由呼叫者設定一個初始值,引數可以有多個,也可以沒有。
2.返回值,用於接任者向呼叫者傳送執行結果,返回值由編譯器自動定義,接任者使用return關鍵詞為返回值賦值,呼叫者定義一個變數接收返回值,返回值最多隻能有一個,也可以沒有。

函式定義方式:
1.設定返回值型別,若沒有返回值則設定為void。
2.設定函式名稱。
3.編寫()符號,並在內部定義引數。
4.編寫{}符號,並在內部定義執行語句,若有返回值則在函式末尾使用return關鍵詞為返回值賦值。

#include <stdio.h>

/* 定義函式,int為返回值型別,add為函式名,引數定義時不賦值,執行時由呼叫者賦值 */
int add(const int a, const int b)
{
    return a+b;    //return語句為返回值賦值,同時終止函式執行
}
int main()
{
    int result = add(1,2);    //呼叫add函式執行,需要為引數賦值,同時定義變數result接收返回值,若不接收返回值則返回值自動廢棄
    printf("%d\n", result);
    
    return 0;
}


引數使用方式:
1.引數的定義和使用與基礎型別資料相同。
2.為引數賦值時,若型別不同則編譯器會自動進行型別轉換,轉換方式與使用 = 符號為資料賦值時相同。
3.引數可以定義為常量,常量引數由呼叫者賦值後禁止在接任者內部修改。

返回值使用方式:
1.使用return關鍵詞為編譯器自動管理的返回值賦值,可以直接使用資料賦值,也可以使用運算式、有返回值的另一個函式為本函式的返回值賦值,此時會首先執行一遍運算式或另一個函式。
2.函式執行到return語句後終止執行並返回,若return語句之後還有程式碼也不會再執行,可以使用return語句當做終止函式語句,在符合指定條件時終止函式執行。
3.若函式無需返回值,可以將返回值型別定義為void,此時函式內無需定義return語句,函式會執行到最後定義的語句。
4.呼叫函式的語句本身表示函式返回值,可以使用此語句為一個資料賦值,這與運算式本身表示運算結果相同,比如上述程式碼中的 add(1,2); 即表示執行add函式也表示此函式的返回值,add函式執行完畢後返回main函式,併為變數result賦值。

指標作為引數

引數可以定義為指標,若呼叫者將指標引數賦值為本函式區域性資料的地址,則接任者可以使用呼叫者的區域性資料。

#include <stdio.h>
void f1(int * const arg)    //指標引數本身無需修改時,定義為常量更合適
{
    *arg = 0;
}
int main()
{
    int a = 9;
    f1(&a);
    
    printf("%d\n", a);    //輸出0,變數a的值被f1函式修改
    
    return 0;
}

指標作為返回值

指標返回值一般用於返回全域性資料的地址,不能返回本函式區域性資料的地址,因為函式執行完畢後其使用的棧空間區域將會被回收,此區域儲存的區域性資料將會被其它資料覆蓋,但是若引數本身是個指標,則可以直接返回指標引數,指標引數由呼叫者賦值,呼叫者不會將其賦值為接任者定義的區域性資料地址。

若返回本函式區域性資料的地址會導致如下情況:
1.在GCC編譯器中,會自動將指標賦值為0,返回的指標不能使用,並會發出警告。
2.在VC++編譯器中,不會進行任何干預,這將會導致呼叫了錯誤的資料,當進行函式內聯後,函式會合並,會執行成功,但是這種使用方式是錯誤的,不要被行內函數所迷惑。

#include <stdio.h>
int * f1()
{
    int a = 9;
    return &a;
}
int main()
{
    printf("%d\n", f1());    //不同編譯器有不同的執行結果
    
    return 0;
}

main函式

main函式是使用者自定義程式碼的執行入口,使用者編寫的程式碼從main函式開始執行,main函式只能有一個,返回值是int型別,一般返回0,作用是告知作業系統此程式正常執行完畢,沒有出現異常情況。

#include <stdio.h>
int main()
{
    /*
      自定義程式碼
     */
    return 0;
}

注:main函式並非程式最早執行的程式碼,main只是使用者編寫程式碼的入口,編譯器會自動新增一些程式碼在main函式之前執行,這些程式碼用於設定程式執行必要的功能,之後再跳轉到main函式執行。

擴充套件全域性資料使用範圍

編譯器在編譯程式碼時是從前向後進行的,首先定義的程式碼首先編譯,如果一個全域性資料在定義之前呼叫它則會報錯,編譯器找不到此資料,此時可以使用 extern 關鍵詞將全域性資料或函式的使用範圍擴充套件到之前的程式碼,extern 關鍵詞擴充套件資料使用範圍時只需定義主體部分即可,若是全域性資料則定義資料的型別和名稱,若是函式則無需定義{}符號以及內部程式碼。

#include <stdio.h>
extern void f1();         //函式外的extern針對所有函式
int main()
{
    extern int a;         //函式內的extern只針對本函式
    printf("%d\n", a);
    
    f1();
    
    return 0;
}
int a = 9;
void f1()
{
    printf("阿狸\n");
}

函式內聯

函式內聯是一種最佳化方式,將一個函式的程式碼合併到呼叫者中,此時呼叫函式時無需執行跳轉與返回,執行速度更快,缺點是函式被多次呼叫時編譯後的程式體積會增加很多。

函式內聯由編譯器自動控制,在GCC編譯器中可以為函式新增如下程式碼人為控制內聯:
__attribute__((noinline)),禁用內聯最佳化。
__attribute__((always_inline)),強制進行內聯最佳化。

__attribute__((noinline)) void f1()    //本函式禁止內聯
{
    //......
}

函式指標

函式指標儲存函式的執行入口地址,定義語法如下:返回值型別(*指標名)(引數型別)。

#include <stdio.h>
int add(const int data1, const int data2)
{
    return data1 + data2;
}
int main()
{
    int (*p1)(int,int) = add;    //定義函式指標,p1為指標名,直接使用函式名賦值
    printf("%d\n", p1(1,2));     //透過指標呼叫函式,將指標名作為函式名使用即可
    
    return 0;
}


【資料的儲存方式】

全域性資料

1.全域性變數,作業系統分配專用的一組記憶體頁儲存,可以稱其為全域性變數區,全域性變數區未使用的部分全部設定為0,所以定義全域性變數不賦值時預設值為0。

2.全域性常量,作業系統分配專用的一組記憶體頁儲存,可以稱其為全域性常量區,這裡的記憶體頁會被作業系統設定為只讀,修改其中的資料會被CPU禁止,開啟編譯最佳化後某些常量也會轉換為立即數定址,增加執行速度。

區域性資料

1.區域性變數(包括引數),它們不需要在程式執行期間一直存在,所屬函式執行完畢後既刪除,區域性資料會在函式執行期間頻繁讀寫,為了增加區域性變數讀寫速度作業系統為程式分配一段地址連續的記憶體空間儲存,同時函式為了更快的讀寫資料經常以棧的方式使用這段記憶體空間,所以此段記憶體也稱為程式的棧空間,棧空間不會進行初始化操作,定義區域性變數不賦值時預設值無法預測。作業系統按執行緒分配棧空間,執行緒內所有函式共用此棧空間,不同函式使用不同的區域,函式執行完畢後釋放此段棧空間的使用權,供其他函式使用,此時函式內定義的區域性資料將會被其它資料覆蓋。

2.區域性常量,開啟編譯最佳化後區域性常量會盡量轉換為立即數定址,若需參與其它運算則將立即數寫入暫存器,若需使用指標呼叫則將立即數寫入棧空間,而長度很大的臨時常量(比如臨時字串)會放在全域性常量區中儲存,這樣無需使用多個指令的組合透過立即數儲存。

靜態區域性變數

定義區域性變數時新增static關鍵詞表示靜態區域性變數,靜態區域性變數儲存在全域性變數區中,所以它在程式執行期間一直存在,它本質是在函式外定義的全域性資料,但是隻允許本函式使用,這個限制是由編譯器提供的,對程式進行逆向分析並修改時並不存在此限制。

靜態區域性變數的作用是儲存本函式的執行結果,並禁止其它函式使用,本函式再次執行時直接取上次儲存的結果使用。

#include <stdio.h>
int f1()
{
    static int a = 0;    //靜態區域性資料只會定義一次,下次執行函式時不會重複定義,而是直接使用上次的值
    a++;
    
    return a;            //可以使用靜態區域性變數為返回值賦值
}
int main()
{
    printf("f1函式執行第%d次\n", f1());
    printf("f1函式執行第%d次\n", f1());
    
    return 0;
}

上述程式碼等同於如下程式碼,只不過編譯器禁止其它函式使用變數a。

#include <stdio.h>
int a = 0;
int f1()
{
    a++;
    return a;
}
int main()
{
    printf("f1函式執行第%d次\n", f1());
    printf("f1函式執行第%d次\n", f1());
    
    return 0;
}

因為靜態區域性變數等於全域性變數,所以在C語言中不能在定義它時引用其它全域性變數賦值,也不能使用函式區域性資料進行賦值。

void f(int arg)
{
    static int a = arg;    //錯誤
}


【函式實現原理】

儲存現場與還原現場

函式使用某個暫存器時會首先將暫存器原值入棧儲存,函式執行完畢後取棧中的資料還原暫存器,這種行為稱為儲存現場與還原現場,防止返回上一級函式時暫存器原值丟失。

棧空間設定

程式執行時作業系統按執行緒分配棧空間,執行緒內所有函式共用此棧空間,不同的函式使用不同區域,防止混亂,棧空間可以使用push/pop、mov分別進行讀寫,push/pop只能按固定順序讀寫,但是連續讀寫速度快,mov可以在任意位置讀寫,但是讀寫速度稍慢,兩種讀寫方式互補,功能複雜的函式經常同時使用這兩種指令讀寫棧空間,為了防止混亂編譯器將一個函式使用的棧空間區域再分為兩部分,並使用bp、sp暫存器分別儲存兩部分的地址,push/pop使用sp暫存器確定操作地址,mov使用bp暫存器確定操作地址。

引數、返回值

使用x86-64處理器、Linux作業系統的C語言程式引數儲存方式如下:
1.整數引數,使用rdi、rsi、rdx、rcx、r8、r9暫存器按順序儲存,若整數引數超過6個,剩餘引數使用棧空間儲存,棧引數使用C規範,最後定義的引數最先使用push入棧,最先定義的引數最後入棧,函式執行完畢後由呼叫者修改sp暫存器釋放引數佔用的棧空間。
2.浮點數引數,使用 xmm0 - xmm7 暫存器按順序儲存,若超過8個則剩餘浮點數使用棧儲存。

另外Linux的某些API函式會使用eax傳遞一個額外的引數,用於說明要操作的浮點數數量,比如printf函式,若要輸出一個浮點數,則會設定eax為1。

返回值儲存方式如下:
1.整數返回值,使用rax、rdx暫存器儲存,長度不超過64位使用rax,超過64位使用rax+rdx。
2.浮點數返回值,使用xmm0、xmm1暫存器儲存。

#include <stdio.h>
int add(int data1, int data2)
{
    return data1 + data2;
}
int main()
{
    int a,b;
    scanf("%d%d", &a, &b);       //輸入a、b的值
    
    printf("%d\n", add(a,b));    //輸出add函式返回值
    
    return 0;
}


GCC -O0 彙編程式碼:

0000000000401132 <add>:
  401132:	push   rbp                        ;儲存現場
  401133:	mov    rbp,rsp                    ;rsp寫入rbp,mov使用rbp讀寫棧空間

  401136:	mov    DWORD PTR [rbp-0x4],edi    ;data1入棧儲存
  401139:	mov    DWORD PTR [rbp-0x8],esi    ;data2入棧儲存
  40113c:	mov    edx,DWORD PTR [rbp-0x4]    ;data1寫入edx
  40113f:	mov    eax,DWORD PTR [rbp-0x8]    ;data2寫入eax
  401142:	add    eax,edx                    ;data1 + data2,計算結果儲存在eax,直接作為返回值

  401144:	pop    rbp                        ;還原現場
  401145:	ret                               ;返回


0000000000401146 <main>:
  401146:	push   rbp                        ;儲存現場
  401147:	mov    rbp,rsp                    ;rsp寫入rbp
  40114a:	sub    rsp,0x10                   ;棧頂地址減0x10,將棧空間分為兩段使用,rsp原值 至 rsp減0x10 這段區域為mov操作區域,push/pop操作之後的區域,執行入棧指令時不影響mov操作的棧區域

  40114e:	lea    rdx,[rbp-0x8]              ;變數b地址寫入rdx傳參
  401152:	lea    rax,[rbp-0x4]              ;變數a地址寫入rax,之後寫入rsi傳參
  401156:	mov    rsi,rax
  401159:	mov    edi,0x402004               ;"%d%d"字串地址作為第一個引數
  40115e:	mov    eax,0x0                    ;0個浮點數
  401163:	call   401040                     ;執行scanf函式

  401168:	mov    edx,DWORD PTR [rbp-0x8]    ;變數b寫入edx,之後寫入esi傳參
  40116b:	mov    eax,DWORD PTR [rbp-0x4]    ;變數a寫入eax,之後寫入edi傳參
  40116e:	mov    esi,edx
  401170:	mov    edi,eax
  401172:	call   401132                     ;執行add函式

  401177:	mov    esi,eax                    ;add返回值寫入esi,作為printf的引數
  401179:	mov    edi,0x402009               ;"%d\n"字串地址
  40117e:	mov    eax,0x0                    ;0個浮點數
  401183:	call   401030                     ;執行printf函式

  401188:	mov    eax,0x0                    ;設定返回值
  40118d:	leave                             ;還原rbp、rsp
  40118e:	ret                               ;返回


Contents of section .rodata:                   ;rodata段儲存全域性常量
 402000 01000200 25642564 0025640a 00          ;"%d%d" "%d\n" 字元編碼

棧空間是從上到下使用的,在main函式中,使用rbp暫存器之前首先將其入棧儲存原值,之後將儲存棧頂的rsp寫入rbp,再將rsp減0x10,mov指令使用bp操作棧空間,操作範圍是 rsp原值 到 rsp原值減0x10,push/pop指令使用 rsp原值減0x10 之後的棧空間。

函式末尾使用leave指令還原rsp、rbp的值,leave等同於如下兩條指令的組合:

mov rsp, rbp
pop rbp


GCC -O3,禁用函式內聯,彙編程式碼:

0000000000401050 <main>:
  401050:	sub    rsp,0x18                   ;棧頂地址減0x18,棧空間從高地址向低地址使用,為了讓lea、mov指令使用rsp+x的方式呼叫資料需要將棧頂減去所需位元組
  401054:	mov    edi,0x402004               ;"%d%d"字串地址寫入edi
  401059:	xor    eax,eax                    ;0個浮點數
  40105b:	lea    rdx,[rsp+0xc]              ;變數b地址寫入rdx傳參
  401060:	lea    rsi,[rsp+0x8]              ;變數a地址寫入rsi傳參
  401065:	call   401040                     ;scanf

  40106a:	mov    esi,DWORD PTR [rsp+0xc]    ;變數b寫入esi傳參
  40106e:	mov    edi,DWORD PTR [rsp+0x8]    ;變數a寫入edi傳參
  401072:	call   401180                     ;add

  401077:	mov    edi,0x402009               ;"%d\n"字串地址寫入edi
  40107c:	mov    esi,eax                    ;add返回值寫入esi
  40107e:	xor    eax,eax                    ;0個浮點數
  401080:	call   401030                     ;printf

  401085:	xor    eax,eax                    ;設定返回值
  401087:	add    rsp,0x18                   ;還原rsp
  40108b:	ret                               ;返回

0000000000401180 <add>:
  401180:	lea    eax,[rdi+rsi*1]            ;data1 + data2,結果儲存在eax作為返回值
  401183:	ret                               ;返回


【函式遞迴呼叫】

函式可以呼叫自己執行,稱為遞迴執行,可以直接呼叫也可以間接呼叫,比如 A->B,B->C,C->A,這種遞迴稱為間接遞迴。

函式遞迴執行與迴圈語句的作用相同,都是將一段程式碼迴圈執行,區別在於迴圈語句是在本語句內部迴圈執行,而函式遞迴是整個函式迴圈執行,函式遞迴執行會從函式的起始地址處開始迴圈,函式起始地址處是儲存現場、設定棧空間相關指令,每次遞迴執行都會消耗一些棧空間,遞迴次數過多會導致棧頂超界,程式將會被作業系統強制退出,並且遞迴執行效率也不高。

#include <stdio.h>
void f1(int arg)
{
	/* 遞迴程式碼 */
	printf("%d\n", arg);
	
	/* 每次遞迴後引數+1 */
	arg++;
	
	/* 若arg小於10則遞迴執行 */
	if(arg < 10)
	{
		f1(arg);    //遞迴執行時可以使用本次的引數值為下次執行時的引數賦值
	}
}
int main()
{
	f1(0);
	
	return 0;
}


【陣列作為引數和返回值】

陣列作為引數

C語言不支援陣列整體作為引數,陣列作為引數時可以透過指標實現,若直接將陣列設定為引數則編譯器會自動轉換為指標,比如printf終端輸出函式的第一個引數為字串指標,但是可以直接使用字串賦值,編譯器會自動轉換為指標。

#include <stdio.h>
void output(const char * arg)    //常量指標引數,即可賦值為變數地址也可賦值為常量地址
{
    printf("%s\n", arg);
}
int main()
{
    char name[100] = "阿狸";
    output(name);              //陣列引數自動轉換為指標,等於 output(&name[0]);
    
    return 0;
}


也可以定義為如下程式碼形式:

void output(const char arg[])    //編譯器自動轉換為指標引數,變數陣列轉換為變數指標,常量陣列轉換為常量指標
{
    printf("%s\n", arg);
}

上述程式碼中,函式定義的陣列引數並不等同於本函式的區域性陣列成員,而是區域性指標成員,這一點新手很容易誤解,修改陣列引數時會透過指標修改指向的資料,比如下面的程式碼:

#include <stdio.h>
void f1(char arg[])
{
    arg[0] = 0;
}
int main()
{
    char name[100] = "阿狸";
    f1(name);
    
    printf("%s\n", name);    //輸出換行,字串的首元素被f1函式修改為0,等同於字串有效字元為空
    
    return 0;
}

二維陣列引數

二維陣列作為引數時會轉換為雙重指標。

#include <stdio.h>
void output(char arg[][100])    //二維陣列引數需要指定內部一維陣列的長度
{
	printf("%s\n%s\n", arg[0], arg[1]);
}
int main()
{
	char fox[2][100] = {"阿狸", "桃子"};
	output(fox);
	
	return 0;
}

編譯器會轉換為類似如下的程式碼:

#include <stdio.h>
void output(char ** arg)    //雙重指標引數
{
	printf("%s\n%s\n", arg[0], arg[1]);
}
int main()
{
	char ali[100] = "阿狸";
	char taozi[100] = "桃子";
	char * fox[2] = {&ali[0], &taozi[0]};    //定義指標陣列
	output(&fox[0]);                         //指標陣列本身轉換為指標傳參,相當於雙重指標
	
	return 0;
}

二維陣列傳參時,編譯器首先將其內部的一維陣列轉換為指標,此時二維陣列變成元素為指標的一維陣列,之後繼續轉換為指標進行傳參,最終形成雙重指標引數。


雙重指標可以像二維陣列一樣使用,透過兩個下標呼叫資料。

#include <stdio.h>
void output(int ** arg)
{
	printf("%d\n%d\n", arg[0][0], arg[1][0]);    //輸出0、5
}
int main()
{
	int a[5] = {0,1,2,3,4};
	int b[5] = {5,6,7,8,9};
	int * p1[2] = {&a[0], &b[0]};
	output(&p1[0]);
	
	return 0;
}

陣列作為返回值

C語言不支援陣列整體作為返回值,若需返回陣列可以使用如下方式:

1.使用全域性陣列進行通訊,無需返回。
2.接任者透過指標引數使用呼叫者內部定義的陣列,無需返回。
3.接任者透過指標引數修改呼叫者內部定義的陣列,實現返回。

方式3示例:

#include <stdio.h>
void f1(int * arg)
{
    int a[5] = {1,2,3,4,5};
    
    /* 返回陣列a */
    for(int i = 0; i < 5; i++)
    {
        arg[i] = a[i];
    }
}
int main()
{
    int b[5];
    f1(b);
    
    /* 輸出返回的陣列 */
    for(int i = 0; i < 5; i++)
    {
        printf("%d\n", b[i]);
    }
    
    return 0;
}


【結構體作為引數、返回值】

結構體在功能上是異型陣列,在語意上是使用者自定義型別的單個資料,全域性宣告的結構體可以作為引數和返回值,此時函式會保證結構體的值能夠完全傳遞。

結構體作為引數

結構體作為引數時編譯器會將其所有成員作為引數,但並非每個成員都佔用一個暫存器,長度小的成員可能會進行合併,之後將合併資料使用一個暫存器傳參,引數過多時剩餘成員使用棧傳參。

#include <stdio.h>
struct k
{
    int a;
    char b;
    float c;
};
void f1(struct k arg)
{
    printf("%d\n%c\n%f\n", arg.a, arg.b, arg.c);
}
int main()
{
    struct k k1 = {9, 'a', 3.14};
    f1(k1);
    
    return 0;
}

編譯後的程式碼功能相當於如下C程式碼:

#include <stdio.h>
struct k
{
    int a;
    char b;
    float c;
};
void f1(int arg1, char arg2, float arg3)
{
    printf("%d\n%c\n%f\n", arg1, arg2, arg3);
}
int main()
{
    struct k k1 = {9, 'a', 3.14};
    f1(k1.a, k1.b, k1.c);
    
    return 0;
}


使用結構體作為引數時,若結構體長度較大,可以將引數定義為結構體指標,這樣只需要傳遞一個指標引數即可,執行速度更快,但前提是接任者不會修改此結構體,或者修改後不會出錯。

#include <stdio.h>
struct zoo
{
    char name[100];
    int age;
};
void output(const struct zoo * const p1)
{
    printf("姓名:%s\n年齡:%d歲\n", p1->name, p1->age);
}
int main()
{
    struct zoo ali = {"阿狸", 8};
    output(&ali);
    
    return 0;
}

結構體作為返回值

在x86-64 Linux程式中,整數返回值使用rax、rdx暫存器儲存,浮點數返回值使用xmm0、xmm1暫存器儲存。

若結構體成員總長度不超過如上暫存器容量,則編譯器將結構體成員直接使用暫存器返回、或合併後使用暫存器返回。
若結構體成員總長度超過如上暫存器容量,則透過指標返回,返回原理與之前介紹的返回陣列方式相同。

#include <stdio.h>
struct zoo
{
    char name[100];
    int age;
};
struct zoo f1()
{
    struct zoo ali = {"阿狸", 8};
    return ali;
}
int main()
{
    struct zoo fox = f1();
    printf("姓名:%s\n年齡:%d歲\n", fox.name, fox.age);
    
    return 0;
}

編譯器會轉換為類似如下的程式碼:

#include <stdio.h>
struct zoo
{
    char name[100];
    int age;
};
void f1(struct zoo * arg)
{
    struct zoo ali = {"阿狸", 8};
    
    arg->name[0] = ali.name[0];    //"阿狸"UTF8編碼,1箇中文字元佔3個位元組,末尾包含一個空字元
    arg->name[1] = ali.name[1];
    arg->name[2] = ali.name[2];
    arg->name[3] = ali.name[3];
    arg->name[4] = ali.name[4];
    arg->name[5] = ali.name[5];
    arg->name[6] = ali.name[6];
    arg->age = ali.age;
};
int main()
{
    struct zoo fox;
    f1(&fox);
    printf("姓名:%s\n年齡:%d歲\n", fox.name, fox.age);
    
    return 0;
}


若需要返回一個長度很大的結構體,使用編譯器的返回方式顯然不合適,此時可以在呼叫者內部定義一個結構體,之後使用指標引數將其提供給接任者使用,接任者無需返回此結構體。

#include <stdio.h>
#include <string.h>
struct zoo
{
    char name[100];
    int age;
};
void f1(struct zoo * const arg)
{
    strcpy(arg->name, "阿狸");    //為arg->name賦值
    arg->age = 8;
};
int main()
{
    struct zoo fox;
    f1(&fox);
    printf("姓名:%s\n年齡:%d歲\n", fox.name, fox.age);
    
    return 0;
}


【函式可變引數】

某些情況下定義函式時無法確定引數的數量,而是在執行時確定,C語言支援定義可變引數函式,在函式定義期間不指定所有的引數,而是在執行期間臨時確定引數的數量,比如終端輸入輸出函式就使用了可變引數。

void f(char *s, ...)    //不固定的引數使用 ... 符號代替,表示這是一個可變引數函式

定義可變引數函式時,至少需要有一個可以確定的引數,用於說明其他可變引數的屬性資訊,比如可變引數的數量、型別。


執行函式時,可變引數的傳遞方式與普通引數相同,也是優先使用暫存器傳參、之後使用棧傳參,可變引數沒有名稱,無法使用資料名呼叫,當然你可以在函式中內嵌彙編程式碼直接使用暫存器呼叫、或者使用指標呼叫棧中的引數,但是這樣做太繁瑣,而且不同的編譯器、不同的處理器傳參彙編程式碼不同,為此C語言標準函式庫提供了stdarg.h檔案,其提供了統一的呼叫可變引數的方式,具體呼叫原理我們無需深究,這是編譯器系統的工作。

stdarg.h檔案內定義了一些資料和宏程式碼用於呼叫可變引數,常用程式碼如下:
1.va_list,表示一個資料型別,用於繫結可變引數的型別。
2.va_start(v, l),執行功能初始化工作,v指定一個va_list型別變數,l指定函式第一個有名稱的引數。
3.va_arg(v, T),返回一個未使用的可變引數,v指定初始化後的va_list變數,T指定可變引數的型別。
4.va_end(v),使用完畢後執行清理工作,v指定初始化後的va_list變數。

#include <stdio.h>
#include <stdarg.h>
void f1(unsigned int format, ...)
{
	va_list v1;              //定義va_list變數v1,v1由編譯器自動管理
	va_start(v1, format);    //功能初始化
	
	/* 迴圈輸出可變引數 */
	for(int i = 0; i < format; i++)
	{
		printf("可變引數%d\n", va_arg(v1, int));    //va_arg返回一個未使用的可變引數,這裡約定可變引數都為int型別,所以無需做複雜判斷
	}
	
	va_end(v1);    //使用完畢之後執行清理工作
}
int main()
{
	int a, b, c;
	scanf("%d%d%d", &a, &b, &c);    //輸入a、b、c的值
	
	f1(3, a, b, c);    //這裡約定可變引數的型別都為int,無需在第一個有名稱引數中指定可變引數的型別,只需指定可變引數的數量
	
	return 0;
}

相關文章