C程式設計 10 函式
函式
函式是一組一起執行一個任務的語句。每個 C 程式都至少有一個函式,即主函式 main() ,所有簡單的程式都可以定義其他額外的函式。
所有的函式的執行都是在棧上執行,在預編譯時引入標頭檔案中的內容
可以把程式碼劃分到不同的函式中。但在邏輯上,劃分通常是根據每個函式執行一個特定的任務來進行的。
函式宣告告訴編譯器函式的名稱、返回型別和引數。函式定義提供了函式的實際主體。
C 標準庫提供了大量的程式可以呼叫的內建函式。例如,函式 strcat() 用來連線兩個字串,函式 memcpy() 用來複制記憶體到另一個位置。
1、定義函式
C 語言中的函式定義的一般形式如下:
return_type function_name( parameter list ) { body of the function }
在 C 語言中,函式由一個函式頭和一個函式主體組成。下面列出一個函式的所有組成部分:
- 返回型別:一個函式可以返回一個值。return_type 是函式返回的值的資料型別。有些函式執行所需的操作而不返回值,在這種情況下,return_type 是關鍵字 void。
- 函式名稱:這是函式的實際名稱。函式名和引數列表一起構成了函式簽名。
- 引數:引數就像是佔位符。當函式被呼叫時,向引數傳遞一個值,這個值被稱為實際引數。引數列表包括函式引數的型別、順序、數量。引數是可選的,也就是說,函式可能不包含引數。
- 函式主體:函式主體包含一組定義函式執行任務的語句。
例項:
以下是 max() 函式的原始碼。該函式有兩個引數 num1 和 num2,會返回這兩個數中較大的那個數:
/* 函式返回兩個數中較大的那個數 */
int max(int num1, int num2)
{
return num1>num2?num1:num2;
}
2、函式宣告
函式宣告會告訴編譯器函式名稱及如何呼叫函式。函式的實際主體可以單獨定義。
函式宣告包括以下幾個部分:
return_type function_name( parameter list );
針對上面定義的函式 max(),以下是函式宣告:
int max(int num1, int num2);
在函式宣告中,引數的名稱並不重要,只有引數的型別是必需的,因此下面也是有效的宣告:
int max(int, int);
當您在一個原始檔中定義函式且在另一個檔案中呼叫函式時,函式宣告是必需的。在這種情況下,應該在呼叫函式的檔案頂部宣告函式。
3、呼叫函式
建立 C 函式時,會定義函式做什麼,然後通過呼叫函式來完成已定義的任務。
當程式呼叫函式時,程式控制權會轉移給被呼叫的函式。被呼叫的函式執行已定義的任務,當函式的返回語句被執行時,或到達函式的結束括號時,會把程式控制權交還給主程式。
呼叫函式時,傳遞所需引數,如果函式返回一個值,則可以儲存返回值。例如:
#include <stdio.h>
/* 函式宣告 */
int max(int num1, int num2);
int main ()
{
/* 區域性變數定義 */
int a = 100;
int b = 200;
int ret;
/* 呼叫函式來獲取最大值 */
ret = max(a, b);
printf( "Max value is : %d\n", ret );
return 0;
}
/* 函式返回兩個數中較大的那個數 */
int max(int num1, int num2)
{
return num1>num2?num1:num2;
}
}
4、函式引數
如果函式要使用引數,則必須宣告接受引數值的變數。這些變數稱為函式的形式引數。
形式引數就像函式內的其他區域性變數,在進入函式時被建立,退出函式時被銷燬。
對於函式的引數壓棧:從右往左進行壓棧
當呼叫函式時,有兩種向函式傳遞引數的方式:
呼叫型別 | 描述 |
---|---|
傳值呼叫 | 該方法把引數的實際值複製給函式的形式引數。在這種情況下,修改函式內的形式引數不會影響實際引數。 |
引用呼叫 | 通過指標傳遞方式,形參為指向實參地址的指標,當對形參的指向操作時,就相當於對實參本身進行的操作。 |
函式指標與指標函式
1、函式指標
定義:函式指標,其本質是一個指標變數,該指標指向這個函式。總結來說,函式指標就是指向函式的指標。
宣告格式:型別說明符 (*函式名) (引數)
int (*f)(int x); //宣告一個函式指標
f=func; //將func函式的首地址賦給指標f
指向函式的指標包含了函式的地址的入口地址,可以通過它來呼叫函式。 其實這裡不能稱為函式名,應該叫做指標的變數名。
void (*fptr)(); //fptr為函式指標
//把函式Function的地址賦值給函式指標,可以採用下面兩種形式:
fptr = &Function;
fptr = Function;
取地址運算子&不是必需的,因為單單一個函式識別符號就標號表示了它的地址,如果是函式呼叫,還必須包含一個圓括號括起來的引數表。
例:
#include<stdio.h>
int Max(int a,int b)
{
return a>b?a:b;
}
int main()
{
int (*Pfun)(int,int) = Max; //Pfun函式指標
int max = Pfun(10,20);
printf("%d",max);
return 0;
}
2、指標函式
定義:指標函式,簡單的來說,就是一個返回指標的函式,其本質是一個函式,而該函式的返回值是一個指標。
宣告格式為:型別識別符號 * 函式名(引數表)
int *fun(int x,int y);
首先它是一個函式,只不過這個函式的返回值是一個地址值。函式返回值必須用同類型的指標變數來接受,也就是說,指標函式一定有函式返回值,而且,在主調函式中,函式返回值必須賦給同類型的指標變數。
float *fun();
float *p;
p = fun(a);
3、區別
定義不同 指標函式本質是一個函式,其返回值為指標。 函式指標本質是一個指標,其指向一個函式。
寫法不同 函式指標:int (*Pfun)(int,int) 指標函式:int * Pfun(int,int) 再簡單一點,可以這樣辨別兩者:函式名帶*帶括號的就是函式指標,帶*不帶括號就是指標函式。
函式的返回值是如何帶出函式的
除了函式引數的傳遞之外,函式與呼叫方的另一個互動方式就是返回值。
在返回不同位元組大小的返回值編譯器的處理方式不一樣:
小於4個位元組:函式將返回值儲存在eax暫存器中,返回呼叫方之後在讀取eax暫存器的值
大於4位元組小於等於8位元組:函式返回值通過兩個暫存器,eax和edx儲存返回後讀取。
大於8位元組的返回值:將利用臨時物件進行返回值的傳遞
- 首先在mian函式中的棧上開闢一片額外的空間作為臨時物件
- 呼叫函式時將該臨時物件的地址通過隱藏的引數傳遞給函式
- 函式內部將返回值的資料拷貝到臨時物件,並將臨時物件的地址通過暫存器eax傳出。
- 函式返回後,將eax指向的臨時物件的記憶體拷貝到main函式中的變數
注意:在返回自定義型別時,無論自定義型別的大小是多少都是通過臨時變數進行傳遞的
函式的遞迴呼叫
編寫條件:
- 必須有一個明確的結束條件,即趨近於一個臨界值
- 每次遞迴都是為了讓問題規模變小,呼叫自己本身
- 注意遞迴層次過多會導致棧溢位,且效率不高
例1:
#include<stdio.h>
int Age(int n) //使用for迴圈編寫
{
int j=10;
for(int i=2;i<=n;i++)
{
j = j+2;
}
return j;
}
int Age2(int n) //使用函式的遞迴呼叫編寫
{
int tmp;
if(n == 1)
{
tmp = 10;
return tmp;
}
else
{
tmp = Age2(n-1)+2;
return tmp;
}
int main()
{
int k = Age2(5);
printf("%d",k);
return 0;
}
例2:
#include<stdio.h>
int Fun(int n) //使用for迴圈編寫
{
int i;
int sum = 1;
for(i = 1;i<=n;i++)
{
sum *= i;
}
return sum;
}
int Fun2(int n) //使用函式的遞迴呼叫編寫
{
int tmp = 1;
if(n == 1)
{
return tmp;
}
else
{
tmp = Fun2(n-1)*n;
return tmp;
}
}
int main()
{
int sum = fun2(3);
printf("%d",sum);
}
extern關鍵字
在C語言中,修飾符extern用在變數或者函式的宣告前,用來說明“此變數/函式是在別處定義的,要在此處引用”。
1. extern修飾變數的宣告
如果檔案a.c需要引用b.c中變數int k,就可以在a.c中宣告extern int k,然後就可以引用變數k。
這裡需要注意的是,被引用的變數k的連結屬性必須是外連結(external)的,也就是說a.c要引用到k,不只是取決於在a.c中宣告extern int k,還取決於變數k本身是能夠被引用到的。
這涉及到c語言中變數的作用域。能夠被其他模組以extern修飾符引用到的變數通常是全域性變數。
還有很重要的一點是,extern int k可以放在a.c中的任何地方,比如可以在a.c中的函式fun定義的開頭處宣告extern int k,然後就可以引用到變數k了,只不過這樣只能在函式fun作用域中引用k罷了,這還是變數作用域的問題。對於這一點來說,很多人使用的時候都心存顧慮。好像extern宣告只能用於檔案作用域似的。
2. extern修飾函式宣告
從本質上來講,變數和函式沒有區別。函式名是指向函式二進位制塊開頭處的指標。
如果檔案a.c需要引用b.c中的函式,比如在b.c中原型是int fun(int mu),那麼就可以在a.c中宣告extern int fun(int mu),然後就能使用fun來做任何事情。
就像變數的宣告一樣,extern int fun(int mu)可以放在a.c中任何地方,而不一定非要放在a.c的檔案作用域的範圍中。
對其他模組中函式的引用,最常用的方法是包含這些函式宣告的標頭檔案。使用extern和包含標頭檔案來引用函式有什麼區別呢?extern的引用方式比包含標頭檔案要簡潔得多!extern的使用方法是直接了當的,想引用哪個函式就用extern宣告哪個函式。
這樣做的一個明顯的好處是,程式在預編譯時引入標頭檔案中的內容,使用extern引用方式會加速程式的預處理過程,節省時間。在大型C程式編譯過程中,這種差異是非常明顯的。
3. extern修飾符可用於指示C/C++函式的呼叫規範
比如在C++中呼叫C庫函式,就需要在C++程式中用 extern "C" 宣告要引用的函式。
這是給連結器用的,告訴連結器在連結的時候用C函式規範來連結。主要原因是C++和C程式編譯完成後在目的碼中命名規則不同。
extern int *arr; //此時arr代表陣列0號下標的值
extern int arr[5]; //直接按型別引入即可
4、 使用extern修飾符注意型別匹配
使用extern修飾符不注意型別匹配會出現如下錯誤:
file1.c:
int arr[80]={0,5,9};
file2.c:
#include<stdio.h>
extern int *arr; //錯誤引用,型別不相容
int main()
{
printf("%d\n", arr); // 輸出陣列中第一個元素 0
printf("%d\n", arr+1); // 第一個元素 0 + 指標長度4 = 4
printf("%d\n", arr+2); // 第一個元素 0 + 指標長度4 * 2 = 8
return 0;
}
file1.c:
int arr[80];
file2.c:
extern int *arr; //錯誤引用,型別不相容,正確寫法:extern int arr[]
int main()
{
arr[1] = 100;
printf("%d\n", arr[1]);
return 0;
}
分析:該程式可以編譯通過,但執行時會出錯。為什麼呢?原因是,在另一個檔案中用 extern int *arr來外部宣告一個數組並不能得到實際的期望值,因為他們的型別並不匹配。所以導致指標實際並沒有指向那個陣列。注意:一個指向陣列的指標,並不等於一個數組。修改:extern int arr[]
作用域規則
#include<stdio.h>
int data1 = 10; // data1為普通全域性變數
void Fun1()
{
data1++;
printf("%d\n",data1); //輸出 11 12 13···20
}
void Fun2()
{
int data2 = 10; // data2為普通區域性變數
data2++;
printf("%d\n",data2); //輸出 11 11 11···11
}
static int data3 = 10; // data3為靜態全域性變數
void Fun3()
{
data3++;
printf("%d\n",data3); //輸出 11 12 13···20
}
void Fun4()
{
static int data4 = 10; // data4為靜態區域性變數
data4++;
printf("%d\n",data4); //輸出 11 12 13···20
}
int main()
{
for(int i = 0;i < 10;i++)
{
Fun1();
Fun2();
Fun3();
Fun4();
}
return 0;
}
由於被 static 修飾的變數總是存在記憶體的靜態區,所以即使這個函式執行結束,這個靜態變數的值還是不會被銷燬,函式下次使用時仍然能用到這個值
bss段的作用是什麼?
一般C語言的編譯後執行語句都編譯成機器程式碼,儲存在.text段;已初始化的全域性變數和區域性靜態變數都儲存在. data段;
未初始化的全域性變數和區域性靜態變數一般放在一個叫.“bss”的段裡。 我們知道未初始化的全域性變數和區域性靜態變數預設值都為0,本來它們也可以被放在.data段的,但是因為它們都是0,所以為它們在.data段分配空間並且存放資料0是沒有必要的。 程式執行的時候它們的確是要佔記憶體空間的,並且可執行檔案必須記錄所有未初始化的全域性變數和區域性靜態變數的大小總和,記為.bss段。 所以.bss段只是為未初始化的全域性變數和區域性靜態變數預留位置而已,它並沒有內容,所以它在檔案中也不佔據空間。text和data段都在可執行檔案中(在嵌入式系統裡一般是固化在映象檔案中),由系統從可執行檔案中載入;而bss段不在可執行檔案中,由系統初始化。
總體來說,程式原始碼被編譯以後主要分成兩種段:程式指令和程式資料。程式碼段(.text)屬於程式指令,而資料段(.data)和.bss段屬於程式資料。