# 第?24?章?函數接口
**目錄**
+ [1\. 本章的預備知識](ch24s01.html)
+ [1.1\. `strcpy`與`strncpy`](ch24s01.html#id2819066)
+ [1.2\. `malloc`與`free`](ch24s01.html#id2820062)
+ [2\. 傳入參數與傳出參數](ch24s02.html)
+ [3\. 兩層指針的參數](ch24s03.html)
+ [4\. 返回值是指針的情況](ch24s04.html)
+ [5\. 回調函數](ch24s05.html)
+ [6\. 可變參數](ch24s06.html)
我們在[第?6?節 “折半查找”](ch11s06.html#sortsearch.binary)講過,函數的調用者和函數的實現者之間訂立了一個契約,在調用函數之前,調用者要為實現者提供某些條件,在函數返回時,實現者要對調用者盡到某些義務。如何描述這個契約呢?首先靠函數接口來描述,即函數名,參數,返回值,只要函數和參數的名字起得合理,參數和返回值的類型定得準確,至于這個函數怎么用,調用者單看函數接口就能猜出八九分了。函數接口并不能表達函數的全部語義,這時文檔就起了重要的補充作用,函數的文檔該寫什么,怎么寫,Man Page為我們做了很好的榜樣。
函數接口一旦和指針結合起來就變得異常靈活,有五花八門的用法,但是萬變不離其宗,只要像[圖?23.1 “指針的基本概念”](ch23s01.html#pointer.pointer0)那樣畫圖分析,指針的任何用法都能分析清楚,所以,如果上一章你真正學明白了,本章不用學也能自己領悟出來,之所以寫這一章是為了照顧悟性不高的讀者。本章把函數接口總結成幾類常見的模式,對于每種模式,一方面講函數接口怎么寫,另一方面講函數的文檔怎么寫。
## 1.?本章的預備知識
這一節介紹本章的范例代碼要用的幾個C標準庫函數。我們先體會一下這幾個函數的接口是怎么設計的,Man Page是怎么寫的。其它常用的C標準庫函數將在下一章介紹。
### 1.1.?`strcpy`與`strncpy`
從現在開始我們要用到很多庫函數,在學習每個庫函數時一定要看Man Page。Man Page隨時都在我們手邊,想查什么只要敲一個命令就行,然而很多初學者就是不喜歡看Man Page,寧可滿世界去查書、查資料,也不愿意看Man Page。據我分析原因有三:
1. 英文不好。那還是先學好了英文再學編程吧,否則即使你把這本書都學透了也一樣無法勝任開發工作,因為你沒有進一步學習的能力。
2. Man Page的語言不夠友好。Man Page不像本書這樣由淺入深地講解,而是平鋪直敘,不過看習慣了就好了,每個Man Page都不長,多看幾遍自然可以抓住重點,理清頭緒。本節分析一個例子,幫助讀者把握Man Page的語言特點。
3. Man Page通常沒有例子。描述一個函數怎么用,一靠接口,二靠文檔,而不是靠例子。函數的用法無非是本章所總結的幾種模式,只要把本章學透了,你就不需要每個函數都得有個例子教你怎么用了。
總之,Man Page是一定要看的,一開始看不懂硬著頭皮也要看,為了鼓勵讀者看Man Page,本書不會像[[K&R]](bi01.html#bibli.kr "The C Programming Language")那樣把庫函數總結成一個附錄附在書后面。現在我們來分析`strcpy(3)`。
**圖?24.1.?`strcpy(3)`**

這個Man Page描述了兩個函數,`strcpy`和`strncpy`,敲命令`man strcpy`或者`man strncpy`都可以看到這個Man Page。這兩個函數的作用是把一個字符串拷貝給另一個字符串。_SYNOPSIS_部分給出了這兩個函數的原型,以及要用這些函數需要包含哪些頭文件。參數`dest`、`src`和`n`都加了下劃線,有時候并不想從頭到尾閱讀整個Man Page,而是想查一下某個參數的含義,通過下劃線和參數名就能很快找到你關心的部分。
`dest`表示Destination,`src`表示Source,看名字就能猜到是把`src`所指向的字符串拷貝到`dest`所指向的內存空間。這一點從兩個參數的類型也能看出來,`dest`是`char *`型的,而`src`是`const char *`型的,說明`src`所指向的內存空間在函數中只能讀不能改寫,而`dest`所指向的內存空間在函數中是要改寫的,顯然改寫的目的是當函數返回后調用者可以讀取改寫的結果。因此可以猜到`strcpy`函數是這樣用的:
```
char buf[10];
strcpy(buf, "hello");
printf(buf);
```
至于`strncpy`的參數`n`是干什么用的,單從函數接口猜不出來,就需要看下面的文檔。
**圖?24.2.?`strcpy(3)`**

在文檔中強調了`strcpy`在拷貝字符串時會把結尾的`'\0'`也拷到`dest`中,因此保證了`dest`中是以`'\0'`結尾的字符串。但另外一個要注意的問題是,`strcpy`只知道`src`字符串的首地址,不知道長度,它會一直拷貝到`'\0'`為止,所以`dest`所指向的內存空間要足夠大,否則有可能寫越界,例如:
```
char buf[10];
strcpy(buf, "hello world");
```
如果沒有保證`src`所指向的內存空間以`'\0'`結尾,也有可能讀越界,例如:
```
char buf[10] = "abcdefghij", str[4] = "hell";
strcpy(buf, str);
```
因為`strcpy`函數的實現者通過函數接口無法得知`src`字符串的長度和`dest`內存空間的大小,所以“確保不會寫越界”應該是調用者的責任,調用者提供的`dest`參數應該指向足夠大的內存空間,“確保不會讀越界”也是調用者的責任,調用者提供的`src`參數指向的內存應該確保以`'\0'`結尾。
此外,文檔中還強調了`src`和`dest`所指向的內存空間不能有重疊。凡是有指針參數的C標準庫函數基本上都有這條要求,每個指針參數所指向的內存空間互不重疊,例如這樣調用是不允許的:
```
char buf[10] = "hello";
strcpy(buf, buf+1);
```
`strncpy`的參數`n`指定最多從`src`中拷貝`n`個字節到`dest`中,換句話說,如果拷貝到`'\0'`就結束,如果拷貝到`n`個字節還沒有碰到`'\0'`,那么也結束,調用者負責提供適當的`n`值,以確保讀寫不會越界,比如讓`n`的值等于`dest`所指向的內存空間的大小:
```
char buf[10];
strncpy(buf, "hello world", sizeof(buf));
```
然而這意味著什么呢?文檔中特別用了_Warning_指出,這意味著`dest`有可能不是以`'\0'`結尾的。例如上面的調用,雖然把`"hello world"`截斷到10個字符拷貝至`buf`中,但`buf`不是以`'\0'`結尾的,如果再`printf(buf)`就會讀越界。如果你需要確保`dest`以`'\0'`結束,可以這么調用:
```
char buf[10];
strncpy(buf, "hello world", sizeof(buf));
buf[sizeof(buf)-1] = '\0';
```
`strncpy`還有一個特性,如果`src`字符串全部拷完了不足`n`個字節,那么還差多少個字節就補多少個`'\0'`,但是正如上面所述,這并不保證`dest`一定以`'\0'`結束,當`src`字符串的長度大于`n`時,不但不補多余的`'\0'`,連字符串的結尾`'\0'`也不拷貝。`strcpy(3)`的文檔已經相當友好了,為了幫助理解,還給出一個`strncpy`的簡單實現。
**圖?24.3.?`strcpy(3)`**

函數的Man Page都有一部分專門講返回值的。這兩個函數的返回值都是`dest`指針。可是為什么要返回`dest`指針呢?`dest`指針本來就是調用者傳過去的,再返回一遍`dest`指針并沒有提供任何有用的信息。之所以這么規定是為了把函數調用當作一個指針類型的表達式使用,比如`printf("%s\n", strcpy(buf, "hello"))`,一舉兩得,如果`strcpy`的返回值是`void`就沒有這么方便了。
_CONFORMING TO_部分描述了這個函數是遵照哪些標準實現的。`strcpy`和`strncpy`是C標準庫函數,當然遵照C99標準。以后我們還會看到`libc`中有些函數屬于POSIX標準但并不屬于C標準,例如`write(2)`。
_NOTES_部分給出一些提示信息。這里指出如何確保`strncpy`的`dest`以`'\0'`結尾,和我們上面給出的代碼類似,但由于`n`是個變量,在執行`buf[n - 1]= '\0';`之前先檢查一下`n`是否大于0,如果`n`不大于0,`buf[n - 1]`就訪問越界了,所以要避免。
**圖?24.4.?`strcpy(3)`**

_BUGS_部分說明了使用這些函數可能引起的Bug,這部分一定要仔細看。用`strcpy`比用`strncpy`更加不安全,如果在調用`strcpy`之前不仔細檢查`src`字符串的長度就有可能寫越界,這是一個很常見的錯誤,例如:
```
void foo(char *str)
{
char buf[10];
strcpy(buf, str);
...
}
```
`str`所指向的字符串有可能超過10個字符而導致寫越界,在[第?4?節 “段錯誤”](ch10s04.html#gdb.segfault)我們看到過,這種寫越界可能當時不出錯,而在函數返回時出現段錯誤,原因是寫越界覆蓋了保存在棧幀上的返回地址,函數返回時跳轉到非法地址,因而出錯。像`buf`這種由調用者分配并傳給函數讀或寫的一段內存通常稱為緩沖區(Buffer),緩沖區寫越界的錯誤稱為緩沖區溢出(Buffer Overflow)。如果只是出現段錯誤那還不算嚴重,更嚴重的是緩沖區溢出Bug經常被惡意用戶利用,使函數返回時跳轉到一個事先設好的地址,執行事先設好的指令,如果設計得巧妙甚至可以啟動一個Shell,然后隨心所欲執行任何命令,可想而知,如果一個用`root`權限執行的程序存在這樣的Bug,被攻陷了,后果將很嚴重。至于怎樣巧妙設計和攻陷一個有緩沖區溢出Bug的程序,有興趣的讀者可以參考[[SmashStack]](bi01.html#bibli.smashstack "Smashing The Stack For Fun And Profit,網上到處都可以搜到這篇文章")。
#### 習題
1、自己實現一個`strcpy`函數,盡可能簡潔,按照本書的編碼風格你能用三行代碼寫出函數體嗎?
2、編一個函數,輸入一個字符串,要求做一個新字符串,把其中所有的一個或多個連續的空白字符都壓縮為一個空格。這里所說的空白包括空格、'\t'、'\n'、'\r'。例如原來的字符串是:
```
This Content hoho is ok
ok?
file system
uttered words ok ok ?
end.
```
壓縮了空白之后就是:
```
This Content hoho is ok ok? file system uttered words ok ok ? end.
```
實現該功能的函數接口要求符合下述規范:
```
char *shrink_space(char *dest, const char *src, size_t n);
```
各項參數和返回值的含義和`strncpy`類似。完成之后,為自己實現的函數寫一個Man Page。
### 1.2.?`malloc`與`free`
程序中需要動態分配一塊內存時怎么辦呢?可以像上一節那樣定義一個緩沖區數組。這種方法不夠靈活,C89要求定義的數組是固定長度的,而程序往往在運行時才知道要動態分配多大的內存,例如:
```
void foo(char *str, int n)
{
char buf[?];
strncpy(buf, str, n);
...
}
```
`n`是由參數傳進來的,事先不知道是多少,那么`buf`該定義多大呢?在[第?1?節 “數組的基本概念”](ch08s01.html#array.intro)講過C99引入VLA特性,可以定義`char buf[n+1] = {};`,這樣可確保`buf`是以`'\0'`結尾的。但即使用VLA仍然不夠靈活,VLA是在棧上動態分配的,函數返回時就要釋放,如果我們希望動態分配一塊全局的內存空間,在各函數中都可以訪問呢?由于全局數組無法定義成VLA,所以仍然不能滿足要求。
其實在[第?5?節 “虛擬內存管理”](ch20s05.html#link.vm)提過,進程有一個堆空間,C標準庫函數`malloc`可以在堆空間動態分配內存,它的底層通過`brk`系統調用向操作系統申請內存。動態分配的內存用完之后可以用`free`釋放,更準確地說是歸還給`malloc`,這樣下次調用`malloc`時這塊內存可以再次被分配。本節學習這兩個函數的用法和工作原理。
```
#include <stdlib.h>
void *malloc(size_t size);
返回值:成功返回所分配內存空間的首地址,出錯返回NULL
void free(void *ptr);
```
`malloc`的參數`size`表示要分配的字節數,如果分配失敗(可能是由于系統內存耗盡)則返回`NULL`。由于`malloc`函數不知道用戶拿到這塊內存要存放什么類型的數據,所以返回通用指針`void *`,用戶程序可以轉換成其它類型的指針再訪問這塊內存。`malloc`函數保證它返回的指針所指向的地址滿足系統的對齊要求,例如在32位平臺上返回的指針一定對齊到4字節邊界,以保證用戶程序把它轉換成任何類型的指針都能用。
動態分配的內存用完之后可以用`free`釋放掉,傳給`free`的參數正是先前`malloc`返回的內存塊首地址。舉例如下:
**例?24.1.?malloc和free**
```
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
int number;
char *msg;
} unit_t;
int main(void)
{
unit_t *p = malloc(sizeof(unit_t));
if (p == NULL) {
printf("out of memory\n");
exit(1);
}
p->number = 3;
p->msg = malloc(20);
strcpy(p->msg, "Hello world!");
printf("number: %d\nmsg: %s\n", p->number, p->msg);
free(p->msg);
free(p);
p = NULL;
return 0;
}
```
關于這個程序要注意以下幾點:
* `unit_t *p = malloc(sizeof(unit_t));`這一句,等號右邊是`void *`類型,等號左邊是`unit_t *`類型,編譯器會做隱式類型轉換,我們講過`void *`類型和任何指針類型之間可以相互隱式轉換。
* 雖然內存耗盡是很不常見的錯誤,但寫程序要規范,`malloc`之后應該判斷是否成功。以后要學習的大部分系統函數都有成功的返回值和失敗的返回值,每次調用系統函數都應該判斷是否成功。
* `free(p);`之后,`p`所指的內存空間是歸還了,但是`p`的值并沒有變,因為從`free`的函數接口來看根本就沒法改變`p`的值,`p`現在指向的內存空間已經不屬于用戶,換句話說,`p`成了野指針,為避免出現野指針,我們應該在`free(p);`之后手動置`p = NULL;`。
* 應該先`free(p->msg)`,再`free(p)`。如果先`free(p)`,`p`成了野指針,就不能再通過`p->msg`訪問內存了。
上面的例子只有一個簡單的順序控制流程,分配內存,賦值,打印,釋放內存,退出程序。這種情況下即使不用`free`釋放內存也可以,因為程序退出時整個進程地址空間都會釋放,包括堆空間,該進程占用的所有內存都會歸還給操作系統。但如果一個程序長年累月運行(例如網絡服務器程序),并且在循環或遞歸中調用`malloc`分配內存,則必須有`free`與之配對,分配一次就要釋放一次,否則每次循環都分配內存,分配完了又不釋放,就會慢慢耗盡系統內存,這種錯誤稱為內存泄漏(Memory Leak)。另外,`malloc`返回的指針一定要保存好,只有把它傳給`free`才能釋放這塊內存,如果這個指針丟失了,就沒有辦法`free`這塊內存了,也會造成內存泄漏。例如:
```
void foo(void)
{
char *p = malloc(10);
...
}
```
`foo`函數返回時要釋放局部變量`p`的內存空間,它所指向的內存地址就丟失了,這10個字節也就沒法釋放了。內存泄漏的Bug很難找到,因為它不會像訪問越界一樣導致程序運行錯誤,少量內存泄漏并不影響程序的正確運行,大量的內存泄漏會使系統內存緊缺,導致頻繁換頁,不僅影響當前進程,而且把整個系統都拖得很慢。
關于`malloc`和`free`還有一些特殊情況。`malloc(0)`這種調用也是合法的,也會返回一個非`NULL`的指針,這個指針也可以傳給`free`釋放,但是不能通過這個指針訪問內存。`free(NULL)`也是合法的,不做任何事情,但是`free`一個野指針是不合法的,例如先調用`malloc`返回一個指針`p`,然后連著調用兩次`free(p);`,則后一次調用會產生運行時錯誤。
[[K&R]](bi01.html#bibli.kr "The C Programming Language")的8.7節給出了`malloc`和`free`的簡單實現,基于環形鏈表。目前讀者還沒有學習鏈表,看那段代碼會有點困難,我再做一些簡化,圖示如下,目的是讓讀者理解`malloc`和`free`的工作原理。`libc`的實現比這要復雜得多,但基本工作原理也是如此。讀者只要理解了基本工作原理,就很容易分析在使用`malloc`和`free`時遇到的各種Bug了。
**圖?24.5.?簡單的`malloc`和`free`實現**

圖中白色背景的框表示`malloc`管理的空閑內存塊,深色背景的框不歸`malloc`管,可能是已經分配給用戶的內存塊,也可能不屬于當前進程,Break之上的地址不屬于當前進程,需要通過`brk`系統調用向內核申請。每個內存塊開頭都有一個頭節點,里面有一個指針字段和一個長度字段,指針字段把所有空閑塊的頭節點串在一起,組成一個環形鏈表,長度字段記錄著頭節點和后面的內存塊加起來一共有多長,以8字節為單位(也就是以頭節點的長度為單位)。
1. 一開始堆空間由一個空閑塊組成,長度為7×8=56字節,除頭節點之外的長度為48字節。
2. 調用`malloc`分配8個字節,要在這個空閑塊的末尾截出16個字節,其中新的頭節點占了8個字節,另外8個字節返回給用戶使用,注意返回的指針`p1`指向頭節點后面的內存塊。
3. 又調用`malloc`分配16個字節,又在空閑塊的末尾截出24個字節,步驟和上一步類似。
4. 調用`free`釋放`p1`所指向的內存塊,內存塊(包括頭節點在內)歸還給了`malloc`,現在`malloc`管理著兩塊不連續的內存,用環形鏈表串起來。注意這時`p1`成了野指針,指向不屬于用戶的內存,`p1`所指向的內存地址在Break之下,是屬于當前進程的,所以訪問`p1`時不會出現段錯誤,但在訪問`p1`時這段內存可能已經被`malloc`再次分配出去了,可能會讀到意外改寫數據。另外注意,此時如果通過`p2`向右寫越界,有可能覆蓋右邊的頭節點,從而破壞`malloc`管理的環形鏈表,`malloc`就無法從一個空閑塊的指針字段找到下一個空閑塊了,找到哪去都不一定,全亂套了。
5. 調用`malloc`分配16個字節,現在雖然有兩個空閑塊,各有8個字節可分配,但是這兩塊不連續,`malloc`只好通過`brk`系統調用抬高Break,獲得新的內存空間。在[[K&R]](bi01.html#bibli.kr "The C Programming Language")的實現中,每次調用`sbrk`函數時申請1024×8=8192個字節,在Linux系統上`sbrk`函數也是通過`brk`實現的,這里為了畫圖方便,我們假設每次調用`sbrk`申請32個字節,建立一個新的空閑塊。
6. 新申請的空閑塊和前一個空閑塊連續,因此可以合并成一個。在能合并時要盡量合并,以免空閑塊越割越小,無法滿足大的分配請求。
7. 在合并后的這個空閑塊末尾截出24個字節,新的頭節點占8個字節,另外16個字節返回給用戶。
8. 調用`free(p3)`釋放這個內存塊,由于它和前一個空閑塊連續,又重新合并成一個空閑塊。注意,Break只能抬高而不能降低,從內核申請到的內存以后都歸`malloc`管了,即使調用`free`也不會還給內核。
#### 習題
1、小練習:編寫一個小程序讓它耗盡系統內存。觀察一下,分配了多少內存后才會出現分配失敗?內存耗盡之后會怎么樣?會不會死機?
## 2.?傳入參數與傳出參數
如果函數接口有指針參數,既可以把指針所指向的數據傳給函數使用(稱為傳入參數),也可以由函數填充指針所指的內存空間,傳回給調用者使用(稱為傳出參數),例如`strcpy`的`src`參數是傳入參數,`dest`參數是傳出參數。有些函數的指針參數同時擔當了這兩種角色,如`select(2)`的`fd_set *`參數,既是傳入參數又是傳出參數,這稱為Value-result參數。
**表?24.1.?傳入參數示例:`void func(const unit_t *p);`**
| 調用者 | 實現者 |
| --- | --- |
| 1. 分配`p`所指的內存空間 2. 在`p`所指的內存空間中保存數據 3. 調用函數 4. 由于有`const`限定符,調用者可以確信`p`所指的內存空間不會被改變 | 1. 規定指針參數的類型`unit_t *` 2. 讀取`p`所指的內存空間 |
想一想,如果有函數接口`void func(const int p);`這里的`const`有意義嗎?
**表?24.2.?傳出參數示例:`void func(unit_t *p);`**
| 調用者 | 實現者 |
| --- | --- |
| 1. 分配`p`所指的內存空間 2. 調用函數 3. 讀取`p`所指的內存空間 | 1. 規定指針參數的類型`unit_t *` 2. 在`p`所指的內存空間中保存數據 |
**表?24.3.?Value-result參數示例:`void func(unit_t *p);`**
| 調用者 | 實現者 |
| --- | --- |
| 1. 分配p所指的內存空間 2. 在`p`所指的內存空間保存數據 3. 調用函數 4. 讀取`p`所指的內存空間 | 1. 規定指針參數的類型`unit_t *` 2. 讀取`p`所指的內存空間 3. 改寫`p`所指的內存空間 |
由于傳出參數和Value-result參數的函數接口完全相同,應該在文檔中說明是哪種參數。
以下是一個傳出參數的完整例子:
**例?24.2.?傳出參數**
```
/* populator.h */
#ifndef POPULATOR_H
#define POPULATOR_H
typedef struct {
int number;
char msg[20];
} unit_t;
extern void set_unit(unit_t *);
#endif
```
```
/* populator.c */
#include <string.h>
#include "populator.h"
void set_unit(unit_t *p)
{
if (p == NULL)
return; /* ignore NULL parameter */
p->number = 3;
strcpy(p->msg, "Hello World!");
}
```
```
/* main.c */
#include <stdio.h>
#include "populator.h"
int main(void)
{
unit_t u;
set_unit(&u);
printf("number: %d\nmsg: %s\n", u.number, u.msg);
return 0;
}
```
很多系統函數對于指針參數是`NULL`的情況有特殊規定:如果傳入參數是`NULL`表示取缺省值,例如`pthread_create(3)`的`pthread_attr_t *`參數,也可能表示不做特別處理,例如`free`的參數;如果傳出參數是`NULL`表示調用者不需要傳出值,例如`time(2)`的參數。這些特殊規定應該在文檔中寫清楚。
## 3.?兩層指針的參數
兩層指針也是指針,同樣可以表示傳入參數、傳出參數或者Value-result參數,只不過該參數所指的內存空間應該解釋成一個指針變量。用兩層指針做傳出參數的系統函數也很常見,比如`pthread_join(3)`的`void **`參數。下面看一個簡單的例子。
**例?24.3.?兩層指針做傳出參數**
```
/* redirect_ptr.h */
#ifndef REDIRECT_PTR_H
#define REDIRECT_PTR_H
extern void get_a_day(const char **);
#endif
```
想一想,這里的參數指針是`const char **`,有`const`限定符,卻不是傳入參數而是傳出參數,為什么?如果是傳入參數應該怎么表示?
```
/* redirect_ptr.c */
#include "redirect_ptr.h"
static const char *msg[] = {"Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"};
void get_a_day(const char **pp)
{
static int i = 0;
*pp = msg[i%7];
i++;
}
```
```
/* main.c */
#include <stdio.h>
#include "redirect_ptr.h"
int main(void)
{
const char *firstday = NULL;
const char *secondday = NULL;
get_a_day(&firstday);
get_a_day(&secondday);
printf("%s\t%s\n", firstday, secondday);
return 0;
}
```
兩層指針作為傳出參數還有一種特別的用法,可以在函數中分配內存,調用者通過傳出參數取得指向該內存的指針,比如`getaddrinfo(3)`的`struct addrinfo **`參數。一般來說,實現一個分配內存的函數就要實現一個釋放內存的函數,所以`getaddrinfo(3)`有一個對應的`freeaddrinfo(3)`函數。
**表?24.4.?通過參數分配內存示例:`void alloc_unit(unit_t **pp);` `void free_unit(unit_t *p);`**
| 調用者 | 實現者 |
| --- | --- |
| 1. 分配`pp`所指的指針變量的空間 2. 調用`alloc_unit`分配內存 3. 讀取`pp`所指的指針變量,通過后者使用`alloc_unit`分配的內存 4. 調用`free_unit`釋放內存 | 1. 規定指針參數的類型`unit_t **` 2. `alloc_unit`分配`unit_t`的內存并初始化,為`pp`所指的指針變量賦值 3. `free_unit`釋放在`alloc_unit`中分配的內存 |
**例?24.4.?通過兩層指針參數分配內存**
```
/* para_allocator.h */
#ifndef PARA_ALLOCATOR_H
#define PARA_ALLOCATOR_H
typedef struct {
int number;
char *msg;
} unit_t;
extern void alloc_unit(unit_t **);
extern void free_unit(unit_t *);
#endif
```
```
/* para_allocator.c */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "para_allocator.h"
void alloc_unit(unit_t **pp)
{
unit_t *p = malloc(sizeof(unit_t));
if(p == NULL) {
printf("out of memory\n");
exit(1);
}
p->number = 3;
p->msg = malloc(20);
strcpy(p->msg, "Hello World!");
*pp = p;
}
void free_unit(unit_t *p)
{
free(p->msg);
free(p);
}
```
```
/* main.c */
#include <stdio.h>
#include "para_allocator.h"
int main(void)
{
unit_t *p = NULL;
alloc_unit(&p);
printf("number: %d\nmsg: %s\n", p->number, p->msg);
free_unit(p);
p = NULL;
return 0;
}
```
思考一下,為什么在`main`函數中不能直接調用`free(p)`釋放內存,而要調用`free_unit(p)`?為什么一層指針的函數接口`void alloc_unit(unit_t *p);`不能分配內存,而一定要用兩層指針的函數接口?
總結一下,兩層指針參數如果是傳出的,可以有兩種情況:第一種情況,傳出的指針指向靜態內存(比如上面的例子),或者指向已分配的動態內存(比如指向某個鏈表的節點);第二種情況是在函數中動態分配內存,然后傳出的指針指向這塊內存空間,這種情況下調用者應該在使用內存之后調用釋放內存的函數,調用者的責任是請求分配和請求釋放內存,實現者的責任是完成分配內存和釋放內存的操作。由于這兩種情況的函數接口相同,應該在文檔中說明是哪一種情況。
## 4.?返回值是指針的情況
返回值顯然是傳出的而不是傳入的,如果返回值傳出的是指針,和上一節通過參數傳出指針類似,也分為兩種情況:第一種是傳出指向靜態內存或已分配的動態內存的指針,例如`localtime(3)`和`inet_ntoa(3)`,第二種是在函數中動態分配內存并傳出指向這塊內存的指針,例如`malloc(3)`,這種情況通常還要實現一個釋放內存的函數,所以有和`malloc(3)`對應的`free(3)`。由于這兩種情況的函數接口相同,應該在文檔中說明是哪一種情況。
**表?24.5.?返回指向已分配內存的指針示例`:unit_t *func(void);`**
| 調用者 | 實現者 |
| --- | --- |
| 1. 調用函數 2. 將返回值保存下來以備后用 | 1. 規定返回值指針的類型`unit_t *` 2. 返回一個指針 |
以下是一個完整的例子。
**例?24.5.?返回指向已分配內存的指針**
```
/* ret_ptr.h */
#ifndef RET_PTR_H
#define RET_PTR_H
extern char *get_a_day(int idx);
#endif
```
```
/* ret_ptr.c */
#include <string.h>
#include "ret_ptr.h"
static const char *msg[] = {"Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"};
char *get_a_day(int idx)
{
static char buf[20];
strcpy(buf, msg[idx]);
return buf;
}
```
```
/* main.c */
#include <stdio.h>
#include "ret_ptr.h"
int main(void)
{
printf("%s %s\n", get_a_day(0), get_a_day(1));
return 0;
}
```
這個程序的運行結果是`Sunday Monday`嗎?請讀者自己分析一下。
**表?24.6.?動態分配內存并返回指針示例:`unit_t *alloc_unit(void);` `void free_unit(unit_t *p)`;**
| 調用者 | 實現者 |
| --- | --- |
| 1. 調用`alloc_unit`分配內存 2. 將返回值保存下來以備后用 3. 調用`free_unit`釋放內存 | 1. 規定返回值指針的類型`unit_t *` 2. `alloc_unit`分配內存并返回指向該內存的指針 3. `free_unit`釋放由`alloc_unit`分配的內存 |
以下是一個完整的例子。
**例?24.6.?動態分配內存并返回指針**
```
/* ret_allocator.h */
#ifndef RET_ALLOCATOR_H
#define RET_ALLOCATOR_H
typedef struct {
int number;
char *msg;
} unit_t;
extern unit_t *alloc_unit(void);
extern void free_unit(unit_t *);
#endif
```
```
/* ret_allocator.c */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "ret_allocator.h"
unit_t *alloc_unit(void)
{
unit_t *p = malloc(sizeof(unit_t));
if(p == NULL) {
printf("out of memory\n");
exit(1);
}
p->number = 3;
p->msg = malloc(20);
strcpy(p->msg, "Hello world!");
return p;
}
void free_unit(unit_t *p)
{
free(p->msg);
free(p);
}
```
```
/* main.c */
#include <stdio.h>
#include "ret_allocator.h"
int main(void)
{
unit_t *p = alloc_unit();
printf("number: %d\nmsg: %s\n", p->number, p->msg);
free_unit(p);
p = NULL;
return 0;
}
```
思考一下,通過參數分配內存需要兩層的指針,而通過返回值分配內存就只需要返回一層的指針,為什么?
## 5.?回調函數
如果參數是一個函數指針,調用者可以傳遞一個函數的地址給實現者,讓實現者去調用它,這稱為回調函數(Callback Function)。例如`qsort(3)`和`bsearch(3)`。
**表?24.7.?回調函數示例:`void func(void (*f)(void *), void *p);`**
| 調用者 | 實現者 |
| --- | --- |
| 1. 提供一個回調函數,再提供一個準備傳給回調函數的參數。 2. 把回調函數傳給參數`f`,把準備傳給回調函數的參數按`void *`類型傳給參數`p` | 1. 在適當的時候根據調用者傳來的函數指針`f`調用回調函數,將調用者傳來的參數`p`轉交給回調函數,即調用`f(p);` |
以下是一個簡單的例子。實現了一個`repeat_three_times`函數,可以把調用者傳來的任何回調函數連續執行三次。
**例?24.7.?回調函數**
```
/* para_callback.h */
#ifndef PARA_CALLBACK_H
#define PARA_CALLBACK_H
typedef void (*callback_t)(void *);
extern void repeat_three_times(callback_t, void *);
#endif
```
```
/* para_callback.c */
#include "para_callback.h"
void repeat_three_times(callback_t f, void *para)
{
f(para);
f(para);
f(para);
}
```
```
/* main.c */
#include <stdio.h>
#include "para_callback.h"
void say_hello(void *str)
{
printf("Hello %s\n", (const char *)str);
}
void count_numbers(void *num)
{
int i;
for(i=1; i<=(int)num; i++)
printf("%d ", i);
putchar('\n');
}
int main(void)
{
repeat_three_times(say_hello, "Guys");
repeat_three_times(count_numbers, (void *)4);
return 0;
}
```
回顧一下前面幾節的例子,參數類型都是由實現者規定的。而本例中回調函數的參數按什么類型解釋由調用者規定,對于實現者來說就是一個`void *`指針,實現者只負責將這個指針轉交給回調函數,而不關心它到底指向什么數據類型。調用者知道自己傳的參數是`char *`型的,那么在自己提供的回調函數中就應該知道參數要轉換成`char *`型來解釋。
回調函數的一個典型應用就是實現類似C++的泛型算法(Generics Algorithm)。下面實現的`max`函數可以在任意一組對象中找出最大值,可以是一組`int`、一組`char`或者一組結構體,但是實現者并不知道怎樣去比較兩個對象的大小,調用者需要提供一個做比較操作的回調函數。
**例?24.8.?泛型算法**
```
/* generics.h */
#ifndef GENERICS_H
#define GENERICS_H
typedef int (*cmp_t)(void *, void *);
extern void *max(void *data[], int num, cmp_t cmp);
#endif
```
```
/* generics.c */
#include "generics.h"
void *max(void *data[], int num, cmp_t cmp)
{
int i;
void *temp = data[0];
for(i=1; i<num; i++) {
if(cmp(temp, data[i])<0)
temp = data[i];
}
return temp;
}
```
```
/* main.c */
#include <stdio.h>
#include "generics.h"
typedef struct {
const char *name;
int score;
} student_t;
int cmp_student(void *a, void *b)
{
if(((student_t *)a)->score > ((student_t *)b)->score)
return 1;
else if(((student_t *)a)->score == ((student_t *)b)->score)
return 0;
else
return -1;
}
int main(void)
{
student_t list[4] = {{"Tom", 68}, {"Jerry", 72},
{"Moby", 60}, {"Kirby", 89}};
student_t *plist[4] = {&list[0], &list[1], &list[2], &list[3]};
student_t *pmax = max((void **)plist, 4, cmp_student);
printf("%s gets the highest score %d\n", pmax->name, pmax->score);
return 0;
}
```
`max`函數之所以能對一組任意類型的對象進行操作,關鍵在于傳給`max`的是指向對象的指針所構成的數組,而不是對象本身所構成的數組,這樣`max`不必關心對象到底是什么類型,只需轉給比較函數`cmp`,然后根據比較結果做相應操作即可,`cmp`是調用者提供的回調函數,調用者當然知道對象是什么類型以及如何比較。
以上舉例的回調函數是被同步調用的,調用者調用`max`函數,`max`函數則調用`cmp`函數,相當于調用者間接調了自己提供的回調函數。在實際系統中,異步調用也是回調函數的一種典型用法,調用者首先將回調函數傳給實現者,實現者記住這個函數,這稱為_注冊_一個回調函數,然后當某個事件發生時實現者再調用先前注冊的函數,比如`sigaction(2)`注冊一個信號處理函數,當信號產生時由系統調用該函數進行處理,再比如`pthread_create(3)`注冊一個線程函數,當發生調度時系統切換到新注冊的線程函數中運行,在GUI編程中異步回調函數更是有普遍的應用,例如為某個按鈕注冊一個回調函數,當用戶點擊按鈕時調用它。
以下是一個代碼框架。
```
/* registry.h */
#ifndef REGISTRY_H
#define REGISTRY_H
typedef void (*registry_t)(void);
extern void register_func(registry_t);
#endif
```
```
/* registry.c */
#include <unistd.h>
#include "registry.h"
static registry_t func;
void register_func(registry_t f)
{
func = f;
}
static void on_some_event(void)
{
...
func();
...
}
```
既然參數可以是函數指針,返回值同樣也可以是函數指針,因此可以有`func()();`這樣的調用。返回函數的函數在C語言中很少見,在一些函數式編程語言(例如LISP)中則很常見,基本思想是把函數也當作一種數據來操作,輸入、輸出和參與運算,操作函數的函數稱為高階函數(High-order Function)。
### 習題
1、[[K&R]](bi01.html#bibli.kr "The C Programming Language")的5.6節有一個`qsort`函數的實現,可以對一組任意類型的對象做快速排序。請讀者仿照那個例子,寫一個插入排序的函數和一個折半查找的函數。
## 6.?可變參數
到目前為止我們只見過一個帶有可變參數的函數`printf`:
```
int printf(const char *format, ...);
```
以后還會見到更多這樣的函數。現在我們實現一個簡單的`myprintf`函數:
**例?24.9.?用可變參數實現簡單的printf函數**
```
#include <stdio.h>
#include <stdarg.h>
void myprintf(const char *format, ...)
{
va_list ap;
char c;
va_start(ap, format);
while (c = *format++) {
switch(c) {
case 'c': {
/* char is promoted to int when passed through '...' */
char ch = va_arg(ap, int);
putchar(ch);
break;
}
case 's': {
char *p = va_arg(ap, char *);
fputs(p, stdout);
break;
}
default:
putchar(c);
}
}
va_end(ap);
}
int main(void)
{
myprintf("c\ts\n", '1', "hello");
return 0;
}
```
要處理可變參數,需要用C到標準庫的`va_list`類型和`va_start`、`va_arg`、`va_end`宏,這些定義在`stdarg.h`頭文件中。這些宏是如何取出可變參數的呢?我們首先對照反匯編分析在調用`myprintf`函數時這些參數的內存布局。
```
myprintf("c\ts\n", '1', "hello");
80484c5: c7 44 24 08 b0 85 04 movl $0x80485b0,0x8(%esp)
80484cc: 08
80484cd: c7 44 24 04 31 00 00 movl $0x31,0x4(%esp)
80484d4: 00
80484d5: c7 04 24 b6 85 04 08 movl $0x80485b6,(%esp)
80484dc: e8 43 ff ff ff call 8048424 <myprintf>
```
**圖?24.6.?`myprintf`函數的參數布局**

這些參數是從右向左依次壓棧的,所以第一個參數靠近棧頂,第三個參數靠近棧底。這些參數在內存中是連續存放的,每個參數都對齊到4字節邊界。第一個和第三個參數都是指針類型,各占4個字節,雖然第二個參數只占一個字節,但為了使第三個參數對齊到4字節邊界,所以第二個參數也占4個字節。現在給出一個`stdarg.h`的簡單實現,這個實現出自[[Standard C Library]](bi01.html#bibli.standardclib "The Standard C Library"):
**例?24.10.?stdarg.h的一種實現**
```
/* stdarg.h standard header */
#ifndef _STDARG
#define _STDARG
/* type definitions */
typedef char *va_list;
/* macros */
#define va_arg(ap, T) \
(* (T *)(((ap) += _Bnd(T, 3U)) - _Bnd(T, 3U)))
#define va_end(ap) (void)0
#define va_start(ap, A) \
(void)((ap) = (char *)&(A) + _Bnd(A, 3U))
#define _Bnd(X, bnd) (sizeof (X) + (bnd) & ~(bnd))
#endif
```
這個頭文件中的內部宏定義`_Bnd(X, bnd)`將類型或變量`X`的長度對齊到`bnd+1`字節的整數倍,例如`_Bnd(char, 3U)`的值是4,`_Bnd(int, 3U)`也是4。
在`myprintf`中定義的`va_list ap;`其實是一個指針,`va_start(ap, format)`使`ap`指向`format`參數的下一個參數,也就是指向上圖中`esp+4`的位置。然后`va_arg(ap, int)`把第二個參數的值按`int`型取出來,同時使`ap`指向第三個參數,也就是指向上圖中`esp+8`的位置。然后`va_arg(ap, char *)`把第三個參數的值按`char *`型取出來,同時使`ap`指向更高的地址。`va_end(ap)`在我們的簡單實現中不起任何作用,在有些實現中可能會把`ap`改寫成無效值,C標準要求在函數返回前調用`va_end`。
如果把`myprintf`中的`char ch = va_arg(ap, int);`改成`char ch = va_arg(ap, char);`,用我們這個`stdarg.h`的簡單實現是沒有問題的。但如果改用`libc`提供的`stdarg.h`,在編譯時會報錯:
```
$ gcc main.c
main.c: In function ‘myprintf’:
main.c:33: warning: ‘char’ is promoted to ‘int’ when passed through ‘...’
main.c:33: note: (so you should pass ‘int’ not ‘char’ to ‘va_arg’)
main.c:33: note: if this code is reached, the program will abort
$ ./a.out
Illegal instruction
```
因此要求`char`型的可變參數必須按`int`型來取,這是為了與C標準一致,我們在[第?3.1?節 “Integer Promotion”](ch15s03.html#type.intpromo)講過Default Argument Promotion規則,傳遞`char`型的可變參數時要提升為`int`型。
從`myprintf`的例子可以理解`printf`的實現原理,`printf`函數根據第一個參數(格式化字符串)來確定后面有幾個參數,分別是什么類型。保證參數的類型、個數與格式化字符串的描述相匹配是調用者的責任,實現者只管按格式化字符串的描述從棧上取數據,如果調用者傳遞的參數類型或個數不正確,實現者是沒有辦法避免錯誤的。
還有一種方法可以確定可變參數的個數,就是在參數列表的末尾傳一個Sentinel,例如`NULL`。`execl(3)`就采用這種方法確定參數的個數。下面實現一個`printlist`函數,可以打印若干個傳入的字符串。
**例?24.11.?根據Sentinel判斷可變參數的個數**
```
#include <stdio.h>
#include <stdarg.h>
void printlist(int begin, ...)
{
va_list ap;
char *p;
va_start(ap, begin);
p = va_arg(ap, char *);
while (p != NULL) {
fputs(p, stdout);
putchar('\n');
p = va_arg(ap, char*);
}
va_end(ap);
}
int main(void)
{
printlist(0, "hello", "world", "foo", "bar", NULL);
return 0;
}
```
`printlist`的第一個參數`begin`的值并沒有用到,但是C語言規定至少要定義一個有名字的參數,因為`va_start`宏要用到參數列表中最后一個有名字的參數,從它的地址開始找可變參數的位置。實現者應該在文檔中說明參數列表必須以`NULL`結尾,如果調用者不遵守這個約定,實現者是沒有辦法避免錯誤的。
### 習題
1、實現一個功能更完整的`printf`,能夠識別`%`,能夠處理`%d`、`%f`對應的整數參數。在實現中不許調用`printf(3)`這個Man Page中描述的任何函數。
- Linux C編程一站式學習
- 歷史
- 前言
- 部分?I.?C語言入門
- 第?1?章?程序的基本概念
- 第?2?章?常量、變量和表達式
- 第?3?章?簡單函數
- 第?4?章?分支語句
- 第?5?章?深入理解函數
- 第?6?章?循環語句
- 第?7?章?結構體
- 第?8?章?數組
- 第?9?章?編碼風格
- 第?10?章?gdb
- 第?11?章?排序與查找
- 第?12?章?棧與隊列
- 第?13?章?本階段總結
- 部分?II.?C語言本質
- 第?14?章?計算機中數的表示
- 第?15?章?數據類型詳解
- 第?16?章?運算符詳解
- 第?17?章?計算機體系結構基礎
- 第?18?章?x86匯編程序基礎
- 第?19?章?匯編與C之間的關系
- 第?20?章?鏈接詳解
- 第?21?章?預處理
- 第?22?章?Makefile基礎
- 第?23?章?指針
- 第?24?章?函數接口
- 第?25?章?C標準庫
- 第?26?章?鏈表、二叉樹和哈希表
- 第?27?章?本階段總結
- 部分?III.?Linux系統編程
- 第?28?章?文件與I/O
- 第?29?章?文件系統
- 第?30?章?進程
- 第?31?章?Shell腳本
- 第?32?章?正則表達式
- 第?33?章?信號
- 第?34?章?終端、作業控制與守護進程
- 第?35?章?線程
- 第?36?章?TCP/IP協議基礎
- 第?37?章?socket編程
- 附錄?A.?字符編碼
- 附錄?B.?GNU Free Documentation License Version 1.3, 3 November 2008
- 參考書目
- 索引