3. 形參和實參

下面我們定義一個帶參數的函數,我們需要在函數定義中指明參數的個數和每個參數的類型,定義參數就像定義變數一樣,需要為每個參數指明類型,參數的命名也要遵循標識符命名規則。例如:

例 3.4. 帶參數的自定義函數

#include <stdio.h>

void print_time(int hour, int minute)
{
	printf("%d:%d\n", hour, minute);
}

int main(void)
{
	print_time(23, 59);
	return 0;
}

需要注意的是,定義變數時可以把相同類型的變數列在一起,而定義參數卻不可以,例如下面這樣的定義是錯的:

void print_time(int hour, minute)
{
	printf("%d:%d\n", hour, minute);
}

學習C語言的人肯定都樂意看到這句話:“變數是這樣定義的,參數也是這樣定義的,一模一樣”,這意味着不用專門去記住參數應該怎麼定義了。誰也不願意看到這句話:“定義變數可以這樣寫,而定義參數卻不可以”。C語言的設計者也不希望自己設計的語法規則裡到處都是例外,一個容易被用戶接受的設計應該遵循最少例外原則(Rule of Least Surprise)。其實關於參數的這條規定也不算十分例外,也是可以理解的,請讀者想想為什麼要這麼規定。學習編程語言不應該死記各種語法規定,如果能夠想清楚設計者這麼規定的原因(Rationale),不僅有助于記憶,而且會有更多收穫。本書在必要的地方會解釋一些Rationale,或者啟發讀者自己去思考,例如上一節在腳註中解釋了void關鍵字的Rationale。[C99 Rationale]是隨C99標準一起發佈的,值得參考。

總的來說,C語言的設計是非常優美的,只要理解了少數基本概念和基本原則就可以根據組合規則寫出任意複雜的程序,很少有例外的規定說這樣組合是不允許的,或者那樣類推是錯誤的。相反,C++的設計就非常複雜,充滿了例外,全世界沒幾個人能把C++的所有規則都牢記於心,因而C++的設計一直飽受爭議,這個觀點在[UNIX編程藝術]中有詳細闡述。

在本書中,凡是提醒讀者注意的地方都是多少有些Surprise的地方,初學者如果按常理來想很可能要想錯,所以需要特別提醒一下。而初學者容易犯的另外一些錯誤,完全是因為沒有掌握好基本概念和基本原理,或者根本無視組合規則而全憑自己主觀臆斷所致,對這一類問題本書不會做特別的提醒,例如有的初學者看完第 2 章 常量、變數和表達式之後會這樣打印π的值:

double pi=3.1416;
printf("pi\n");

之所以會犯這種錯誤,一是不理解Literal的含義,二是自己想當然地把變數名組合到字元串裡去,而事實上根本沒有這條語法規則。如果連這樣的錯誤都需要在書上專門提醒,就好比提醒小孩吃飯一定要吃到嘴裡,不要吃到鼻子裡,更不要吃到耳朵裡一樣。

回到正題。我們調用print_time(23, 59)時,函數中的參數hour就代表23,參數minute就代表59。確切地說,當我們討論函數中的hour這個參數時,我們所說的“參數”是指形參(Parameter),當我們討論傳一個參數23給函數時,我們所說的“參數”是指實參(Argument),但我習慣都叫參數而不習慣總把形參、實參這兩個文縐縐的詞掛在嘴邊(事實上大多數人都不習慣),讀者可以根據上下文判斷我說的到底是形參還是實參。記住這條基本原理:形參相當於函數中定義的變數,調用函數傳遞參數的過程相當於定義形參變數並且用實參的值來初始化。例如這樣調用:

void print_time(int hour, int minute)
{
	printf("%d:%d\n", hour, minute);
}

int main(void)
{
	int h = 23, m = 59;
	print_time(h, m);
	return 0;
}

相當於在函數print_time中執行了這樣一些語句:

int hour = h;
int minute = m;
printf("%d:%d\n", hour, minute);

main函數的變數hprint_time函數的參數hour是兩個不同的變數,只不過它們的存儲空間中都保存了相同的值23,因為變數h的值賦給了參數hour。同理,變數m的值賦給了參數minute。C語言的這種傳遞參數的方式稱為Call by Value。在調用函數時,每個參數都需要得到一個值,函數定義中有幾個形參,在調用時就要傳幾個實參,不能多也不能少,每個參數的類型也必須對應上。

肯定有讀者注意到了,為什麼我們每次調用printf傳的實參個數都不一樣呢?因為C語言規定了一種特殊的參數列表格式,用命令man 3 printf可以查看到printf函數的原型:

int printf(const char *format, ...);

第一個參數是const char *類型的,後面的...可以代表0個或任意多個參數,這些參數的類型也是不確定的,這稱為可變參數(Variable Argument)第 6 節 “可變參數”將會詳細討論這種格式。總之,每個函數的原型都明確規定了返回值類型以及參數的類型和個數,即使像printf這樣規定為“不確定”也是一種明確的規定,調用函數時要嚴格遵守這些規定,有時候我們把函數叫做介面(Interface),調用函數就是使用這個介面,使用介面的前提是必須和介面保持一致。

Man Page

Man Page是Linux開發最常用的參考手冊,由很多頁面組成,每個頁面描述一個主題,這些頁面被組織成若干個Section。FHS(Filesystem Hierarchy Standard)標準規定了Man Page各Section的含義如下:

表 3.1. Man Page的Section

Section描述
1用戶命令,例如ls(1)
2系統調用,例如_exit(2)
3庫函數,例如printf(3)
4特殊檔案,例如null(4)描述了設備檔案/dev/null/dev/zero的作用
5系統配置檔案的格式,例如passwd(5)描述了系統配置檔案/etc/passwd的格式
6遊戲
7其它雜項,例如bash-builtins(7)描述了bash的各種內建命令
8系統管理命令,例如ifconfig(8)

注意區分用戶命令和系統管理命令,用戶命令通常位於/bin/usr/bin目錄,系統管理命令通常位於/sbin/usr/sbin目錄,一般用戶可以執行用戶命令,而執行系統管理命令經常需要root權限。系統調用和庫函數的區別將在第 2 節 “main函數和啟動常式”說明。

Man Page中有些頁面有重名,比如敲man printf命令看到的並不是C函數printf,而是位於第1個Section的系統命令printf,要查看位於第3個Section的printf函數應該敲man 3 printf,也可以敲man -k printf命令搜索哪些頁面的主題包含printf關鍵字。本書會經常出現類似printf(3)這樣的寫法,括號中的3表示Man Page的第3個Section,或者表示“我這裡想說的是printf庫函數而不是printf命令”。

習題

1、定義一個函數increment,它的作用是把傳進來的參數加1。例如:

void increment(int x)
{
	x = x + 1;
}

int main(void)
{
	int i = 1, j = 2;
	increment(i); /* i now becomes 2 */
	increment(j); /* j now becomes 3 */
	return 0;
}

我們在main函數中調用increment增加變數ij的值,這樣能奏效嗎?為什麼?

2、如果在一個程序中調用了printf函數卻不包含標頭檔,例如int main(void) { printf("\n"); },編譯時會報警告:warning: incompatible implicit declaration of built-in function ‘printf’。請分析錯誤原因。