1. while語句

第 3 節 “遞歸”中,我們介紹了用遞歸求n!的方法,其實每次遞歸調用都在重複做同樣一件事,就是把n乘到(n-1)!上然後把結果返回。雖說是重複,但每次做都稍微有一點區別(n的值不一樣),這種每次都有一點區別的重複工作稱為迭代(Iteration)。我們使用計算機的主要目的之一就是讓它做重複迭代的工作,因為把一件工作重複做成千上萬次而不出錯正是計算機最擅長的,也是人類最不擅長的。雖然迭代用遞歸來做就夠了,但C語言提供了循環語句使迭代程序寫起來更方便。例如factorialwhile語句可以寫成:

int factorial(int n)
{
	int result = 1;
	while (n > 0) {
		result = result * n;
		n = n - 1;
	}
	return result;
}

if語句類似,while語句由一個控製表達式和一個子語句組成,子語句可以是由若干條語句組成的語句塊。

語句 → while (控製表達式) 語句

如果控製表達式的值為真,子語句就被執行,然後再次測試控製表達式的值,如果還是真,就把子語句再執行一遍,再測試控製表達式的值……這種控制流程稱為循環(Loop),子語句稱為循環體。如果某次測試控製表達式的值為假,就跳出循環執行後面的return語句,如果第一次測試控製表達式的值就是假,那麼直接跳到return語句,循環體一次都不執行。

變數result在這個循環中的作用是累加器(Accumulator),把每次循環的中間結果累積起來,循環結束後得到的累積值就是最終結果,由於這個例子是用乘法來累積的,所以result的初值是1,如果用加法累積則result的初值應該是0。變數n是循環變數(Loop Variable),每次循環要改變它的值,在控製表達式中要測試它的值,這兩點合起來起到控制循環次數的作用,在這個例子中n的值是遞減的,也有些循環採用遞增的循環變數。這個例子具有一定的典型性,累加器和循環變數這兩種模式在循環中都很常見。

可見,遞歸能解決的問題用循環也能解決,但解決問題的思路不一樣。用遞歸解決這個問題靠的是遞推關係n!=n·(n-1)!,用循環解決這個問題則更像是把這個公式展開了:n!=n·(n-1)·(n-2)·…·3·2·1。把公式展開了理解會更直觀一些,所以有些時候循環程序比遞歸程序更容易理解。但也有一些公式要展開是非常複雜的甚至是不可能的,反倒是遞推關係更直觀一些,這種情況下遞歸程序比循環程序更容易理解。此外還有一點不同:看圖 5.2 “factorial(3)的調用過程”,在整個遞歸調用過程中,雖然分配和釋放了很多變數,但所有變數都只在初始化時賦值,沒有任何變數的值發生過改變,而上面的循環程序則通過對nresult這兩個變數多次賦值來達到同樣的目的。前一種思路稱為函數式編程(Functional Programming),而後一種思路稱為命令式編程(Imperative Programming),這個區別類似於第 1 節 “程序和編程語言”講的Declarative和Imperative的區別。函數式編程的“函數”類似於數學函數的概念,回顧一下第 1 節 “數學函數”所講的,數學函數是沒有Side Effect的,而C語言的函數可以有Side Effect,比如在一個函數中修改某個全局變數的值就是一種Side Effect。第 4 節 “全局變數、局部變數和作用域”指出,全局變數被多次賦值會給調試帶來麻煩,如果一個函數體很長,控制流程很複雜,那麼局部變數被多次賦值也會有同樣的問題。因此,不要以為“變數可以多次賦值”是天經地義的,有很多編程語言可以完全採用函數式編程的模式,避免Side Effect,例如LISP、Haskell、Erlang等。用C語言編程主要還是採用Imperative的模式,但要記住,給變數多次賦值時要格外小心,在代碼中多次讀寫同一變數應該以一種一致的方式進行。所謂“一致的方式”是說應該有一套統一的規則,規定在一段代碼中哪裡會對某個變數賦值、哪裡會讀取它的值,比如在第 2.4 節 “errno與perror函數”會講到訪問errno的規則。

遞歸函數如果寫得不小心就會變成無窮遞歸,同樣道理,循環如果寫得不小心就會變成無限循環(Infinite Loop)或者叫死循環。如果while語句的控製表達式永遠為真就成了一個死循環,例如while (1) {...}。在寫循環時要小心檢查你寫的控製表達式有沒有可能取值為假,除非你故意寫死循環(有的時候這是必要的)。在上面的例子中,不管n一開始是幾,每次循環都會把n減掉1,n越來越小最後必然等於0,所以控製表達式最後必然取值為假,但如果把n = n - 1;這句漏掉就成了死循環。有的時候是不是死循環並不是那麼一目瞭然:

while (n != 1) {
	if (n % 2 == 0) {
		n = n / 2;
	} else {
		n = n * 3 + 1;
	}
}

如果n為正整數,這個循環能跳出來嗎?循環體所做的事情是:如果n是偶數,就把n除以2,如果n是奇數,就把n乘3加1。一般來說循環變數要麼遞增要麼遞減,可是這個例子中的n一會兒變大一會兒變小,最終會不會變成1呢?可以找個數試試,例如一開始n等於7,每次循環後n的值依次是:7、22、11、34、17、52、26、13、40、20、10、5、16、8、4、2、1。最後n確實等於1了。讀者可以再試幾個數都是如此,但無論試多少個數也不能代替證明,這個循環有沒有可能對某些正整數n是死循環呢?其實這個例子只是給讀者提提興趣,同時提醒讀者寫循環時要有意識地檢查控製表達式。至于這個循環有沒有可能是死循環,這是著名的3x+1問題,目前世界上還無人能證明。許多世界難題都是這樣的:描述無比簡單,連小學生都能看懂,但證明卻無比困難。

習題

1、用循環解決第 3 節 “遞歸”的所有習題,體會遞歸和循環這兩種不同的思路。

2、編寫程序數一下1到100的所有整數中出現多少次數字9。在寫程序之前先把這些問題考慮清楚:

  1. 這個問題中的循環變數是什麼?

  2. 這個問題中的累加器是什麼?用加法還是用乘法累積?

  3. 第 2 節 “if/else語句”的習題1寫過取一個整數的個位和十位的表達式,這兩個表達式怎樣用到程序中?