以下介紹更多通用的程式設計概念。這些概念在許多程式語言中都有,只是語法略有差異。介紹的同時會把它們整合進類 C 語法的虛擬碼範例中——到最後,虛擬碼看起來會非常接近真正的 C 程式碼。

變數#

for 迴圈裡用的計數器其實就是一種變數(variable)。變數可以單純理解成「一個裝著可變資料的物件」——名稱由此而來。也有不會改變的變數,恰如其名地稱為常數(constant)。回到行車的例子:車速是變數,車子的顏色是常數。

在虛擬碼中變數只是抽象概念,但在 C(以及許多其他語言)中,變數使用前必須先宣告並賦予型別

為什麼要宣告?因為 C 程式最終會被編譯成可執行程式。就像食譜先列出所有食材再進入步驟,變數宣告讓你在進入正題前先做好準備;所有變數終究都被存在記憶體的某處,宣告讓編譯器能更有效率地組織這塊記憶體。

但說到底,即使有型別宣告,一切也都只是記憶體而已

C 中最常見的幾種型別:

  • int:整數值
  • float:十進位浮點值
  • char:單一字元值

宣告方式是在變數前加上這些關鍵字:

int a, b;
float k;
char z;

變數可以在宣告時、或宣告後任何時候用 = 運算子賦值:

int a = 13, b;
float k;
char z = 'A';

k = 3.14;
z = 'w';
b = a + 5;

執行完這些指令後:a 是 13、k 是 3.14、z 是字元 wb 是 18(13 + 5)。變數只是記住數值的方式;只不過在 C 裡,你必須先宣告每個變數的型別。

算術運算子#

b = a + 7 就是最簡單的算術運算子範例。C 使用以下符號:

運算符號範例
加法+b = a + 5
減法-b = a - 5
乘法*b = a * 5
除法/b = a / 5
取餘數%b = a % 5

前四個運算應該都很熟悉。取餘數(modulo reduction) 看似新概念,其實就是取除法後的餘數:若 a 是 13,13 除以 5 等於 2 餘 3,所以 a % 5 = 3

由於 ab 都是整數,b = a / 5 會讓 b 得到 2——只保留整數部分。想留住較精確的 2.6,必須使用浮點變數。

遞增與遞減的簡寫#

完整寫法簡寫說明
i = i + 1i++++i變數加 1
i = i - 1i----i變數減 1

這些簡寫可以與其他算術運算組合成更複雜的運算式——而 i++++i 的差異也在此顯現:

  • i++先求值,後加 1
  • ++i先加 1,後求值
int a, b;
a = 5;
b = a++ * 6;

結束時 b 是 30、a 是 6,因為 b = a++ * 6; 等價於:

b = a * 6;
a = a + 1;

但若改成 b = ++a * 6;,加法的順序改變,等價於:

a = a + 1;
b = a * 6;

順序一變,b 就會是 36,而 a 仍是 6。

就地修改的簡寫#

程式中經常需要把某個值加到變數上、再把結果存回同一個變數(例如 i = i + 12)。這種情況夠常見,所以也有簡寫:

完整寫法簡寫說明
i = i + 12i += 12對變數加上某值
i = i - 12i -= 12對變數減去某值
i = i * 12i *= 12把變數乘上某值
i = i / 12i /= 12把變數除以某值

比較運算子#

變數常被用在控制結構的條件敘述裡,而這些條件敘述都建立在某種比較之上。C 的比較運算子語法在許多語言間相當通用:

條件符號範例
小於<(a < b)
大於>(a > b)
小於等於<=(a <= b)
大於等於>=(a >= b)
等於==(a == b)
不等於!=(a != b)

a = 7 的意思是「把 7 放進變數 a」;a == 7 的意思是「檢查變數 a 是否等於 7」。(Pascal 等語言乾脆用 := 表示賦值,以消除視覺混淆。)

驚嘆號一般代表 not,可單獨用來反轉任何運算式:

!(a < b)   等價於   (a >= b)

比較運算子也能用 OR 與 AND 的簡寫串接:

邏輯符號範例
OR||((a < b) || (a < c))
AND&&((a < b) && !(a < c))

以 OR 連接的範例,在「a 小於 ba 小於 c」時為真;以 AND 連接的範例,則在「a 小於 ba 不小於 c」時為真。這些敘述應以括號分組,且可以有許多不同的變化組合。

C 沒有布林型別:非零即真#

很多東西都可以化約成變數、比較運算子與控制結構。回到那隻找食物的老鼠——飢餓可以轉譯成布林(Boolean)真/假變數,1 代表真、0 代表假:

While (hungry == 1)
{
  Find some food;
  Eat the food;
}

C 其實沒有真正的布林運算子:任何非零值都視為真,含 0 者視為假。事實上比較運算子在比較為真時回傳 1、為假時回傳 0。

檢查 hungry 是否等於 1,在 hungry 為 1 時回傳 1、為 0 時回傳 0。既然程式只用到這兩種情況,比較運算子可以整個省掉——這是程式設計師與駭客常用的簡寫:

While (hungry)
{
  Find some food;
  Eat the food;
}

加入更多輸入的聰明老鼠程式,示範了比較運算子如何與變數組合:

While ((hungry) && !(cat_present))
{
  Find some food;
  If(!(food_is_on_a_mousetrap))
    Eat the food;
}

這個例子假設另有變數描述貓的存在與食物的位置,同樣以 1 為真、0 為假。

函式#

有時程式設計師知道某組指令會用到好幾次,這些指令就能被歸成一個較小的子程式,稱為函式(function)。在其他語言中,函式也被稱為副程式(subroutine)或程序(procedure)。

以轉彎為例——這個動作其實由許多更小的指令組成:打對應方向燈、減速、注意來車、把方向盤轉向對應方向……等等。開頭的行車路線需要好幾次轉彎,若每次都把所有細部指令列出來會既冗長又難讀。

你可以把變數當作**引數(argument)**傳給函式,藉此改變函式的運作方式。這裡傳入的是轉彎方向:

Function Turn(variable_direction)
{
  Activate the variable_direction blinker;
  Slow down;
  Check for oncoming traffic;
  while(there is oncoming traffic)
  {
    Stop;
    Watch for oncoming traffic;
  }
  Turn the steering wheel to the variable_direction;
  while(turn is not complete)
  {
    if(speed < 5 mph)
      Accelerate;
  }
  Turn the steering wheel back to the original position;
  Turn off the variable_direction blinker;
}

當程式需要轉彎時,只要呼叫這個函式即可。呼叫時,函式內的指令會以傳入的引數執行;執行完畢後,控制流程回到程式中函式呼叫之後的位置。傳入 leftright 就會讓函式往該方向轉。

回傳值與型別宣告#

在 C 中,函式預設可以回傳一個值給呼叫者。對熟悉數學函數的人來說這很自然——想像一個計算階乘的函式,它當然要回傳結果。

C 不用 function 關鍵字標示函式,而是以它回傳的資料型別來宣告,格式看起來很像變數宣告。

int factorial(int x)
{
  int i;
  for(i=1; i < x; i++)
    x *= i;
  return x;
}

這個函式宣告為 int,因為它把 1 到 x 的每個值相乘並回傳結果,而結果是整數。結尾的 return 敘述把變數 x 的內容傳回並結束函式。之後,這個 factorial 函式就能在任何知道它的程式主體裡像整數變數一樣使用:

int a=5, b;
b = factorial(a);

執行結束時 b 會是 120,因為 factorial 以引數 5 被呼叫並回傳 120。

函式原型#

在 C 中,編譯器必須先「認識」函式才能使用它。做法有二:把整支函式寫在使用它之前,或使用函式原型(function prototype)

函式原型只是告訴編譯器:「預期會有一個叫這個名字、回傳這種型別、接受這些型別引數的函式。」實際的函式可以擺在程式尾端,但因為編譯器已經知道它,就能在任何地方使用:

int factorial(int);

函式原型通常放在程式開頭。原型中不需要定義變數名稱(那是實際函式的事)——編譯器只在乎函式的名稱、回傳型別,以及引數的資料型別。

若函式沒有值要回傳,就應宣告為 void,例如前面的 turn() 函式。

讓 turn() 更完整#

不過先前的 turn() 還沒涵蓋行車路線所需的全部功能:路線中每次轉彎都同時有方向街名。這表示轉彎函式該有兩個變數:轉彎方向,以及要轉進的街道。這讓轉彎變得複雜,因為得先找到正確的街道才能轉:

void turn(variable_direction, target_street_name)
{
  Look for a street sign;
  current_intersection_name = read street sign name;
  while(current_intersection_name != target_street_name)
  {
    Look for another street sign;
    current_intersection_name = read street sign name;
  }

  Activate the variable_direction blinker;
  Slow down;
  Check for oncoming traffic;
  while(there is oncoming traffic)
  {
    Stop;
    Watch for oncoming traffic;
  }
  Turn the steering wheel to the variable_direction;
  while(turn is not complete)
  {
    if(speed < 5 mph)
      Accelerate;
  }
  Turn the steering wheel right back to the original position;
  Turn off the variable_direction blinker;
}

函式開頭多了一段:尋找路牌、讀取牌上名稱、存進 current_intersection_name,持續尋找直到找到目標街道,接著才執行其餘轉彎指令。行車指令現在可以改用這個函式:

Begin going East on Main Street;
while (there is not a church on the right)
   Drive down Main Street;
if (street is blocked)
{
  Turn(right, 15th Street);
  Turn(left, Pine Street);
  Turn(right, 16th Street);
}
else
  Turn(right, 16th Street);
Turn(left, Destination Road);
for (i=0; i<5; i++)
  Drive straight for 1 mile;
Stop at 743 Destination Road;

虛擬碼中其實不常使用函式,因為虛擬碼多半只是程式設計師在寫出可編譯程式碼前用來勾勒概念的工具。既然虛擬碼不必真的能跑,完整的函式也不必寫出來——寫個「Do some complex stuff here」就夠了。

但在 C 這樣的語言中,函式被大量使用。C 真正的用處,大多來自被稱為函式庫(library)的既有函式集合。