1. return語句

之前我們一直在main函數中使用return語句,現在是時候全面深入地學習一下了。在有返回值的函數中,return語句的作用是提供整個函數的返回值,並結束當前函數返回到調用它的地方。在沒有返回值的函數中也可以使用return語句,例如當檢查到一個錯誤時提前結束當前函數的執行並返回:

#include <math.h>

void print_logarithm(double x)
{
	if (x <= 0.0) {
		printf("Positive numbers only, please.\n");
		return;
	}
	printf("The log of x is %f", log(x));
}

這個函數首先檢查參數x是否大於0,如果x不大於0就打印錯誤提示,然後提前結束函數的執行返回到調用者,只有當x大於0時才能求對數,在打印了對數結果之後到達函數體的末尾,自然地結束執行並返回。注意,使用數學函數log需要包含標頭檔math.h,由於x是浮點數,應該與同類型的數做比較,所以寫成0.0。

第 2 節 “if/else語句”中我們定義了一個檢查奇偶性的函數,如果是奇數就打印x is odd.,如果是偶數就打印x is even.。事實上這個函數並不十分好用,我們定義一個檢查奇偶性的函數往往不是為了打印兩個字元串就完了,而是為了根據奇偶性的不同分別執行不同的後續動作。我們可以把它改成一個返回布爾值的函數:

int is_even(int x)
{
	if (x % 2 == 0)
		return 1;
	else
		return 0;
}

有些人喜歡寫成return(1);這種形式也可以,表達式外面套括號表示改變運算符優先順序,在這裡不起任何作用。我們可以這樣調用這個函數:

int i = 19;
if (is_even(i)) {
	/* do something */
} else {
	/* do some other thing */
}

返回布爾值的函數是一類非常有用的函數,在程序中通常充當控製表達式,函數名通常帶有isif等表示判斷的詞,這類函數也叫做謂詞(Predicate)is_even這個函數寫得有點囉嗦,x % 2這個表達式本來就有0值或非0值,直接把這個值當作布爾值返回就可以了:

int is_even(int x)
{
	return !(x % 2);
}

函數的返回值應該這樣理解:函數返回一個值相當於定義一個和返回值類型相同的臨時變數並用return後面的表達式來初始化。例如上面的函數調用相當於這樣的過程:

int 臨時變數 = !(x % 2);
函數退出,局部變數x的存儲空間釋放;
if (臨時變數) { /* 臨時變數用完就釋放 */
	/* do something */
} else {
	/* do some other thing */
}

if語句對函數的返回值做判斷時,函數已經退出,局部變數x已經釋放,所以不可能在這時候才計算表達式!(x % 2)的值,表達式的值必然是事先計算好了存在一個臨時變數裡的,然後函數退出,局部變數釋放,if語句對這個臨時變數的值做判斷。注意,雖然函數的返回值可以看作是一個臨時變數,但我們只是讀一下它的值,讀完值就釋放它,而不能往它裡面存新的值,換句話說,函數的返回值不是左值,或者說函數調用表達式不能做左值,因此下面的賦值語句是非法的:

is_even(20) = 1;

第 3 節 “形參和實參”中講過,C語言的傳參規則是Call by Value,按值傳遞,現在我們知道返回值也是按值傳遞的,即便返回語句寫成return x;,返回的也是變數x的值,而非變數x本身,因為變數x馬上就要被釋放了。

在寫帶有return語句的函數時要小心檢查所有的代碼路徑(Code Path)。有些代碼路徑在任何條件下都執行不到,這稱為Dead Code,例如把&&和||運算符記混了(據我瞭解初學者犯這個低級錯誤的不在少數),寫出如下代碼:

void foo(int x, int y)
{
	if (x >= 0 || y >= 0) {
		printf("both x and y are positive.\n");
		return;
	} else if (x < 0 || y < 0) {
		printf("both x and y are negetive.\n");
		return;
	}
	printf("x has a different sign from y.\n");
}

最後一行printf永遠都沒機會被執行到,是一行Dead Code。有Dead Code就一定有Bug,你寫的每一行代碼都是想讓程序在某種情況下去執行的,你不可能故意寫出一行永遠不會被執行的代碼,如果程序在任何情況下都不會去執行它,說明跟你預想的不一樣,要麼是你對所有可能的情況分析得不正確,也就是邏輯錯誤,要麼就是像上例這樣的筆誤,語義錯誤。還有一些時候,對程序中所有可能的情況分析得不夠全面將導致漏掉一些代碼路徑,例如:

int absolute_value(int x)
{
	if (x < 0) {
		return -x;
	} else if (x > 0) {
		return x;
	}
}

這個函數被定義為返回int,就應該在任何情況下都返回int,但是上面這個程序在x==0時安靜地退出函數,什麼也不返回,C語言對於這種情況會返回什麼結果是未定義的,通常返回不確定的值,等學到第 1 節 “函數調用”你就知道為什麼了。另外注意這個例子中把-號當負號用而不是當減號用,事實上+號也可以這麼用。正負號是單目運算符,而加減號是雙目運算符,正負號的優先順序和邏輯非運算符相同,比加減的優先順序要高。

以上兩段代碼都不會產生編譯錯誤,編譯器只做語法檢查和最簡單的語義檢查,而不檢查程序的邏輯[7]。雖然到現在為止你見到了各種各樣的編譯器錯誤提示,也許你已經十分討厭編譯器報錯了,但很快你就會認識到,如果程序中有錯誤編譯器還不報錯,那一定比報錯更糟糕。比如上面的絶對值函數,在你測試的時候運行得很好,也許是你沒有測到x==0的情況,也許剛好在你的環境中x==0時返回的不確定值就是0,然後你放心地把它整合到一個數萬行的程序之中。然後你把這個程序交給用戶,起初的幾天裡相安無事,之後每過幾個星期就有用戶報告說程序出錯,但每次出錯的現象都不一樣,而且這個錯誤很難復現,你想讓它出現時它就不出現,在你毫無防備時它又突然冒出來了。然後你花了大量的時間在數萬行的程序中排查哪裡錯了,幾天之後終於幸運地找到了這個函數的Bug,這時候你就會想,如果當初編譯器能報個錯多好啊!所以,如果編譯器報錯了,不要責怪編譯器太過于挑剔,它幫你節省了大量的調試時間。另外,在math.h中有一個fabs函數就是求絶對值的,我們通常不必自己寫絶對值函數。

習題

1、編寫一個布爾函數int is_leap_year(int year),判斷參數year是不是閏年。如果某年份能被4整除,但不能被100整除,那麼這一年就是閏年,此外,能被400整除的年份也是閏年。

2、編寫一個函數double myround(double x),輸入一個小數,將它四捨五入。例如myround(-3.51)的值是-4.0,myround(4.49)的值是4.0。可以調用math.h中的庫函數ceilfloor實現這個函數。



[7] 有的代碼路徑沒有返回值的問題編譯器是可以檢查出來的,如果編譯時加-Wall選項會報警告。