變數


何謂變數? 在談論與變數相關的問題時, 請記得所有程式語言到最後都要編譯或解譯成電腦看得懂的機器語言。 機器語言的層次, 沒有變數的概念, 只有 「記憶第幾個 byte 起, 到第幾個 byte 為止」 的概念。 也許 把每一個簡單變數想成是一個盒子, 下面的觀念就會比較具體了。

r-value 與 l-value

  1. x = 5; 與 5 = x; 這兩句話看起來很相似, 為什麼第一句正確; 而第二句不正確呢? 第二句錯在那裡呢? 5 與 $x 都可以 提供值 所以兩句的等號右側都沒有問題。 另一方面 $x 可以 提供空間 用來裝其他的值; 而 5 不是, 所以第二句錯了, 錯在等號左邊。
  2. 心得: 一個無辜的簡單變數, 其實沒有那麼單純。 寫法沒有改變, 但在某些場合它的功用是提供值 (我們有興趣的是去讀取它的值); 而在某些場合它的功用卻是提供空間。 一個變數出現在提供值的場合時, 我們稱它在此扮演 r-value 的角色; 當它出現在提供空間的場合時時, 我們稱它在此扮演 l-value 的角色。
  3. 誰來決定「場合」 (上下文, context)? 變數通常當做運算子的運算元, 或函數的參數, 所以決定一個變數究竟扮演 r-value 或 l-value 角色的, 通常就是運算子或函數。 例如 "=" (c 與 perl 裡面的 assignment operator) 這個運算子要把右邊的值放入左邊的盒子去, 所以我們看到 = 就知道它左邊的東西必須扮演 l-value 的角色; 而它右邊的東西必須扮演 r-value 的角色。
  4. 有些東西只能當 r-value, 例如 literals; 有些東西既可當 r-value 又可當 l-value, 例如變數的名字。
  5. 有些運算子需要 r-value, 例如 + 的兩側又例如 = 的右邊; 有些運算子需要 l-value, 例如 = 的 左邊; 有些運算子需要一個運算元同時可以扮演 r-value 與 l-value 的角色, 例如 ++。
  6. Q: 在 ++i; 這句話當中, i 扮演什麼角色? 在 x = y = z; 這句話當中, y 又扮演什麼角色?

作業: 請把下列敘述翻譯成白話文:

  1. 變數可以扮演 r-value 的角色, 也可以扮演 l-value 的角色; 像是 7, "hello" 這樣的 literal, 則只能扮演 r-value 的角色, 不能扮演 l-value 的角色。
  2. + 號要求它兩側的運算元扮演 r-value 的角色; ++ 要求它右邊的運算元先扮演 r-value 的角色, 再扮演 l-value 的角色。

Life Time

一個變數, 從它開始佔有一塊記憶體起, 一直到它佔用的記憶體空間被系統收回為止, 這段時間稱為它的 life time ("生命期")。

變數按照其產生與消失的時機/方式可分為:

  1. 靜態變數: 從程式開始執行起就存在, 到整個程式執行完畢才消失。 包含所有的 global scope 及 file scope 變數, 還有 block scope 變數當中加上 static 關鍵字的變數。
  2. 自動變數: 程式流程執行到某一區段 (例如進入一副程式) 即自動在 activation record 中產生, 程式流程將離開該區段前即消失。 Block scope 變數中, 除了以 static 宣告以外的所有變數都是。
  3. (手動的)動態變數: 程式設計師以 malloc 向系統要空間時產生, 以 free 將空間釋放回系統的 free store 時消失。

變數的起始值設定: 只在產生時設定一次。

副程式內的靜態變數的用途: 常用來記錄過去的呼叫歷史, 如統計總呼叫次數或記錄先前呼叫時的參數/結果。 作業: 參考系統副程式 strtok(), 自己寫一個簡化的版本。 (C 當中 "static" 一詞兩用, 且意義相差甚遠, 請特別注意!)

Scope

在最簡單的程式語言裡 (例如許多工程計算機的記憶功能), 一個變數的名字恰恰對應到一個固定的盒子; 從來不會有兩個不同的盒子 (也就是在記憶體中, 不同位址開始的兩串 bytes) 竟然共用一個名字。 如果世界一直都是這麼簡單就好了, 這一節要解釋的問題也就不存在了; 但如果你的程式包含 5 個以上的副程式, 很快就會發覺這個簡單的規則, 其實是一個惱人的限制: 在寫第 5 支副程式時, 你必須記得前面 4 支副程式用到了那些變數, 以免不小心重複使用同一個 (名稱不小心相同, 但在你心中其實完全不相干的數個) 變數, 造成副程式之間互相干擾。 如果程式更大, 作者不只一人, 甚至連記憶力也解決不了問題了。

於是有了 global variable (全域變數)local variable (局部變數) 的概念: 每個副程式可以有自己專屬的變數, 與其他副程式當中同名的變數互不相干, 稱為這個副程式的 local variables -- 就像四技一A 有一位同學叫總務; 二技三B 也有一位同學叫總務一樣。 至於需要讓所有 (或很多) 副程式共同使用的變數, 就將它放到所有副程式外面, 成為 global variables。 程式設計師當然應該多用 local variables, 避免使用 global variables, 才不需要像先前描述的一樣擔心和其他副程式用到相同的變數。

一個變數的「知名度」稱為它的 scope: 簡單的情況下, 例如一個 global variable, 它的 scope 就是整個程式; 而一個 local variable, 則可能只有它所在的副程式可以「看得見」它 (在其他副程式當中, 如果去用到這個名字, 你的 compiler 或 interpreter 就會抱怨 "undeclared identifier" 或 "undefined variable" 等等)。

如果同一個名字的兩個變數 (都叫做 x 好了), 一個是 global, 另一個是 local 呢? 筆者所知道的程式語言都遵循一個很自然的規則: 在「看得見」兩者的地方, 凡是談論 x, 必定是指 local 的 x; 在只「看得見」 global 的地方, 談論到 x 當然就是指 global 的 x 嘍。 ("我們的總務" 這個詞, 出現在班會的場合, 指的是誰? 出現在全系同學集會的場合, 指的又是誰?) 知名度低的變數 ("地頭蛇"), 會「覆蓋」過知名度高的變數 ("天高皇帝遠"), 有一些書或手冊把這個現象叫做 shadow (要注意區分文中 shadow 一詞是主動還是被動)。

很多語言 (例如 pascal 與 lisp 語系) 允許副程式的定義裡面又有副程式的定義, 所以局部/全域變數的觀念其實不限於上述的兩層。 ("我們的總務" 這個詞, 如果出現在全校同學集會的場合, 指的又是誰呢?) C 語言不允許副程式的定義嵌在副程式裡面, 但也有三個不同層次的 scopes: 互相連結 (link) 的所有程式檔都能夠看到的真正的全域變數 (用 extern 宣告全域變數; extern 省略也可以); 只有這個程式檔內的副程式可以看到的 file scope variable (在全域變數前面加 static, 降低它的知名度); 和副程式的局部變數。 詳見 個別編譯與相關議題。 ANSI C 更允許每個 block (例如 for 迴圈) 裡面有更局部的變數。

namespace

有時候我們希望在同一個場合使用到隸屬於不同 模組/類別/檔案/程式庫, 但具有相同名字的兩個副程式 (或變數)。 這時需要有一個語法讓我們可以稱呼「A 類別的方法 f」 以便和「B 類別的方法 f」區分。 在 c++ 當中, 用 :: 表示「(某類別) 的」, 在 perl 當中, 同樣用 :: 表示「(某模組) 的」, 這個運算子叫做 scope resolution operator。 通常一個 模組/類別/檔案/程式庫 (究竟是何者, 要視語言而定) 就定義了一個 namespace, 所以不同的 模組/類別/檔案/程式庫 當中可以出現名稱相同, 但其實完全不相干的變數或函數; 而如果有需要的話, 程式設計師仍舊可以在同一場合將它們拿來使用, 只要把 namespace 與變數的名稱一起寫出來就可以了。 ("各班的總務請將錢交給系學會的總務") 你也可以把 A::f 視為一個函數的全名, A 是它的姓, f 是它的名。 寫小程式時, 所有的變數都在同一個 namespace 當中, 所以我們習慣直呼其名; 同時使用到好幾個 namespaces 時, 就必須要連姓帶名喊出來了。

支援 namespace 觀念的語言, 通常也支援多層次的 namespace -- 也就是說 namespace 裡面又有好幾個 namespaces。

Lexical (Static) Scoping 與 Dynamic Scoping

先前我們提到 shadow 的觀念; 這節我們要談的是 shadow 沒有發生的狀況: 在程式原始碼的某一點 (例如在副程式 h 當中), 用到一個變數 x, 但 local variables 當中並沒有 x。 先前我們的答案很簡單: 那麼這個 x 指的當然就是 global 的 x 嘍!

問題是: 那一個 global 的 x? 在 pascal 與 lisp 語系 等等允許副程式定義裡面又有其他副程式定義的語言裡, 單單講 global 與 local 並不清楚。 如果最內層的副程式裡面沒有宣告 x 但卻用到了 x, 那麼究竟要去那裡找其他 (比較 global) 的 x? 像這個 例子

最直覺的答案是: 看程式碼, 找最內層 (最 local) 的 x。 這種「根據程式碼當中, 變數出現的位置, 來決定未在本地宣告的變數究竟來自何處」的規則, 叫做 lexical scoping 也叫做 static scoping。 大部分的語言 (例如 pascal) 都採取這個規則。

還可能有別種規則嗎? Lisp 語系的眾多方言 (dialects) 當中, 有許多 (例如 emacs lisp) 採取的是 dynamic scoping: 光看程式碼還不夠, 必須真的知道程式執行的過程, 畫出程式執行到使用 x 那句話當時的 系統堆疊, 由內 (最近放上去的 A.V.) 向外找到何處有宣告 x。 在 lisp 的某些方言 (例如 librep) 當中, 平常採取的是 lexical scoping; 但如果你用 defvar 宣告變數, 則這個名稱的變數就變成了 dynamic scoping。 在 perl 當中, 比較常用 lexical 變數 (用 my 宣告); 如果要用 dynamic scoping, 必須用 use vars 宣告全域變數, 用 local 宣告局部變數。

用術語來講, lexical scoping 循的是 static ancestors 的族譜向上找 (h -> f -> 最外層); 而 dynamic scoping 循的是 dynamic ancestors 的族譜向上找 (h -> g1 -> f -> 最外層 或 h -> g2 -> f -> 最外層)。