# 練習25:變參函數
> 原文:[Exercise 25: Variable Argument Functions](http://c.learncodethehardway.org/book/ex25.html)
> 譯者:[飛龍](https://github.com/wizardforcel)
在C語言中,你可以通過創建“變參函數”來創建你自己的`printf`或者`scanf`版本。這些函數使用`stdarg.h`頭,它們可以讓你為你的庫創建更加便利的接口。它們對于創建特定類型的“構建”函數、格式化函數和任何用到可變參數的函數都非常實用。
理解“變參函數”對于C語言編程并不必要,我在編程生涯中也只有大約20次用到它。但是,理解變參函數如何工作有助于你對它的調試,并且讓你更加了解計算機。
```c
/** WARNING: This code is fresh and potentially isn't correct yet. */
#include <stdlib.h>
#include <stdio.h>
#include <stdarg.h>
#include "dbg.h"
#define MAX_DATA 100
int read_string(char **out_string, int max_buffer)
{
*out_string = calloc(1, max_buffer + 1);
check_mem(*out_string);
char *result = fgets(*out_string, max_buffer, stdin);
check(result != NULL, "Input error.");
return 0;
error:
if(*out_string) free(*out_string);
*out_string = NULL;
return -1;
}
int read_int(int *out_int)
{
char *input = NULL;
int rc = read_string(&input, MAX_DATA);
check(rc == 0, "Failed to read number.");
*out_int = atoi(input);
free(input);
return 0;
error:
if(input) free(input);
return -1;
}
int read_scan(const char *fmt, ...)
{
int i = 0;
int rc = 0;
int *out_int = NULL;
char *out_char = NULL;
char **out_string = NULL;
int max_buffer = 0;
va_list argp;
va_start(argp, fmt);
for(i = 0; fmt[i] != '\0'; i++) {
if(fmt[i] == '%') {
i++;
switch(fmt[i]) {
case '\0':
sentinel("Invalid format, you ended with %%.");
break;
case 'd':
out_int = va_arg(argp, int *);
rc = read_int(out_int);
check(rc == 0, "Failed to read int.");
break;
case 'c':
out_char = va_arg(argp, char *);
*out_char = fgetc(stdin);
break;
case 's':
max_buffer = va_arg(argp, int);
out_string = va_arg(argp, char **);
rc = read_string(out_string, max_buffer);
check(rc == 0, "Failed to read string.");
break;
default:
sentinel("Invalid format.");
}
} else {
fgetc(stdin);
}
check(!feof(stdin) && !ferror(stdin), "Input error.");
}
va_end(argp);
return 0;
error:
va_end(argp);
return -1;
}
int main(int argc, char *argv[])
{
char *first_name = NULL;
char initial = ' ';
char *last_name = NULL;
int age = 0;
printf("What's your first name? ");
int rc = read_scan("%s", MAX_DATA, &first_name);
check(rc == 0, "Failed first name.");
printf("What's your initial? ");
rc = read_scan("%c\n", &initial);
check(rc == 0, "Failed initial.");
printf("What's your last name? ");
rc = read_scan("%s", MAX_DATA, &last_name);
check(rc == 0, "Failed last name.");
printf("How old are you? ");
rc = read_scan("%d", &age);
printf("---- RESULTS ----\n");
printf("First Name: %s", first_name);
printf("Initial: '%c'\n", initial);
printf("Last Name: %s", last_name);
printf("Age: %d\n", age);
free(first_name);
free(last_name);
return 0;
error:
return -1;
}
```
這個程序和上一個練習很像,除了我編寫了自己的`scanf`風格函數,它以我自己的方式處理字符串。你應該對`main`函數很清楚了,以及`read_string`和`read_int`兩個函數,因為它們并沒有做什么新的東西。
這里的變參函數叫做`read_scan`,它使用了`va_list`數據結構執行和`scanf`相同的工作,并支持宏和函數。下面是它的工作原理:
+ 我將函數的最后一個參數設置為`...`,它向C表示這個函數在`fmt`參數之后接受任何數量的參數。我可以在它前面設置許多其它的參數,但是在它后面不能放置任何參數。
+ 在設置完一些參數時,我創建了`va_list`類型的變量,并且使用`va_list`來為其初始化。這配置了`stdarg.h`中的這一可以處理可變參數的組件。
+ 接著我使用了`for`循環,遍歷`fmt`格式化字符串,并且處理了類似`scanf`的格式,但比它略簡單。它里面只帶有整數、字符和字符串。
+ 當我碰到占位符時,我使用了`switch`語句來確定需要做什么。
+ 現在,為了從`va_list argp`中獲得遍歷,我需要使用`va_arg(argp, TYPE)`宏,其中`TYPE`是我將要向參數傳遞的準確類型。這一設計的后果是你會非常盲目,所以如果你沒有足夠的變量傳入,程序就會崩潰。
+ 和`scanf`的有趣的不同點是,當它碰到`'s'`占位符時,我使用`read_string`來創建字符串。`va_list argp`棧需要接受兩個函數:需要讀取的最大尺寸,以及用于輸出的字符串指針。`read_string`使用這些信息來執行實際工作。
+ 這使`read_scan`比`scan`更加一致,因為你總是使用`&`提供變量的地址,并且合理地設置它們。
+ 最后,如果它碰到了不在格式中的字符,它僅僅會讀取并跳過,而并不關心字符是什么,因為它只需要跳過。
## 你會看到什么
當你運行程序時,會得到與下面詳細的結果:
```sh
$ make ex25
cc -Wall -g -DNDEBUG ex25.c -o ex25
$ ./ex25
What's your first name? Zed
What's your initial? A
What's your last name? Shaw
How old are you? 37
---- RESULTS ----
First Name: Zed
Initial: 'A'
Last Name: Shaw
Age: 37
```
## 如何使它崩潰
這個程序對緩沖區溢出更加健壯,但是和`scanf`一樣,它不能夠處理輸入的格式錯誤。為了使它崩潰,試著修改代碼,把首先傳入用于`'%s'`格式的尺寸去掉。同時試著傳入多于`MAX_DATA`的數據,之后找到在`read_string`中不使用`calloc`的方法,并且修改它的工作方式。最后還有個問題是`fgets`會吃掉換行符,所以試著使用`fgetc`修復它,要注意字符串結尾應為`'\0'`。
## 附加題
+ 再三檢查確保你明白了每個`out_`變量的作用。最重要的是`out_string`,并且它是指針的指針。所以,理清當你設置時獲取到的是指針還是內容尤為重要。
+ 使用變參系統編寫一個和`printf`相似的函數,重新編寫`main`來使用它。
+ 像往常一樣,閱讀這些函數/宏的手冊頁,確保知道了它在你的平臺做了什么,一些平臺會使用宏而其它平臺會使用函數,還有一些平臺會讓它們不起作用。這完全取決于你所用的編譯器和平臺。
- 笨辦法學C 中文版
- 前言
- 導言:C的笛卡爾之夢
- 練習0:準備
- 練習1:啟用編譯器
- 練習2:用Make來代替Python
- 練習3:格式化輸出
- 練習4:Valgrind 介紹
- 練習5:一個C程序的結構
- 練習6:變量類型
- 練習7:更多變量和一些算術
- 練習8:大小和數組
- 練習9:數組和字符串
- 練習10:字符串數組和循環
- 練習11:While循環和布爾表達式
- 練習12:If,Else If,Else
- 練習13:Switch語句
- 練習14:編寫并使用函數
- 練習15:指針,可怕的指針
- 練習16:結構體和指向它們的指針
- 練習17:堆和棧的內存分配
- 練習18:函數指針
- 練習19:一個簡單的對象系統
- 練習20:Zed的強大的調試宏
- 練習21:高級數據類型和控制結構
- 練習22:棧、作用域和全局
- 練習23:認識達夫設備
- 練習24:輸入輸出和文件
- 練習25:變參函數
- 練習26:編寫第一個真正的程序
- 練習27:創造性和防御性編程
- 練習28:Makefile 進階
- 練習29:庫和鏈接
- 練習30:自動化測試
- 練習31:代碼調試
- 練習32:雙向鏈表
- 練習33:鏈表算法
- 練習34:動態數組
- 練習35:排序和搜索
- 練習36:更安全的字符串
- 練習37:哈希表
- 練習38:哈希算法
- 練習39:字符串算法
- 練習40:二叉搜索樹
- 練習41:將 Cachegrind 和 Callgrind 用于性能調優
- 練習42:棧和隊列
- 練習43:一個簡單的統計引擎
- 練習44:環形緩沖區
- 練習45:一個簡單的TCP/IP客戶端
- 練習46:三叉搜索樹
- 練習47:一個快速的URL路由
- 后記:“解構 K&R C” 已死