基礎程式設計(13)-變數範圍

當我們寫程式要使用變數時,必須要先宣告變數(鬆散型態程式語言除外),這時候電腦記憶體會產生一個空間來儲存我們指定給變數的內容,也就是說程式會”佔用記憶體位置”(物件也是一種變數)。相信大家都能夠明白如果記憶體被程式吃光了會有什麼結果,電腦變慢、當機都是很正常的。所以身為一個程式設計師,一定要注意變數的生命週期,也就是它出生與死亡的時間,避免常駐在記憶體中影響電腦的效能。

雖然前面說得很可怕,不過其實也沒那麼嚴重,因為大部份的人不會寫到那麼龐大的程式,只是我們應該要知道它的重要性。大部份的程式語言會根據設計師”宣告變數(物件)的位置”來決定此變數何時死亡(釋放記憶體),少數特殊的變數(物件)會由程式設計師手動撰寫釋放記憶體的程式碼。手動撰寫的部份是程式設計師自己決定的,所以本篇要介紹的就是自動產生變數生命週期的範圍。

區域/全域變數

每一種程式語言可以宣告的變數範圍略有不同,不過大致上可分成「區域變數(Local Variable)」與「全域變數(Global Variable)」兩類。在前面的文章中有提到程式區塊(Blocks)函數副程式的概念,在這些區域內所宣告的變數都稱為「區域變數」。區域變數作用的範圍是在宣告以後,直到區域結束時系統自動清除。相反的,全域變數是整支程式甚至整個專案都可以使用的變數,當程式結束執行時系統才會清除記憶體。下面使用一個Java範例來說明:

public class test
{
    static int a = 1;   //全域變數,Java又稱「成員變數(Member Variable)」
                //變數範圍: 2-35行的方法,或3-35行的全域變數

    public static void main(String[] args)
    {
        int b = 5;          //區域變數範圍: 8-22行

        System.out.println("10: 呼叫fun前,a=" + a + " b=" + b + " c=" + c);
        System.out.println("******");
        fun(b);
        System.out.println("******");

        System.out.println("15: 執行while前,a=" + a + " b=" + b + " c=" + c);
        while(c == 1) {
            int a = 3;      //區域變數範圍: 17-20行
            c = a;
            System.out.println("19: 在while內,a=" + a + " b=" + b + " c=" + c);
        }
        System.out.println("21: 結束while後,a=" + a + " b=" + b + " c=" + c);
    }
    private static void fun(int c) {    //區域變數,Java又稱「方法參數(Method Parameter)」
                        //變數範圍: 23-32行
        System.out.println("25: 區域變數c剛傳入fun時,c=" + c);
        c = 2;
        System.out.println("27: 修改區域變數c後,c=" + c);

        System.out.println("29: 宣告區域變數a前,a=" + a);
        int a = 2;          //區域變數範圍: 30-32行
        System.out.println("31: 宣告區域變數a後,a=" + a);
    }

    static int c = a;   //全域變數範圍: 2-35行的方法
}

首先看到第 3 行與第 34 行的全域變數,它的作用範圍是 2-35 行整支程式(Java稱”類別(class)“),所以在這支程式內所有的Java方法(函數副程式)都可以存取。但若是宣告其它全域變數時,則要在變數宣告之後才能讀取該變數,所以 34 行全域變數 c 一定要在全域變數 a 的下面。

接著我們從第 6 行程式入口 main() 方法隨著程式執行來看。第 8 行宣告了區域變數 b,所以變數範圍是第 8 行以後到 main 方法結束的 22 行。第 10 行在程式進行前印出變數 abc,這時 a 和 c 都是全域變數 =1,b 為區域變數 =5。輸出結果如下:

example_VariableScope

在第 12 行將 b 傳給 fun(),來到 23 行 fun 用區域變數 c 來接收(參考「函數與副程式」及「傳值與傳址」)。這個時候由於區域變數 c 和全域變數 c 的名稱一樣,所以 25 行印出 c 時會印出區域變數 c=5,全域變數 c 被”隱藏”。26 行所修改的也是區域變數 c。

這代表什麼意思呢?我們看到 29 行印出變數 a,這個時候在 fun 裡從來沒有宣告過 a,所以這個時候印出的是全域變數 a=1。當我們在 30 行宣告 a 之後,31 行印出的就是區域變數 a=2,而原本的全域變數 a 被隱藏起來了,但它並沒有消失。

結束 fun 後回到第 12 行,15 行再輸出一次 abc,我們可以看到它的數值跟第 10 行輸出結果完全一模一樣,因為在 fun 裡並沒有修改到任何全域變數,僅傳值過去的 b 當然也不會變動。

進入第 16 行的 while,再一次宣告區域變數 a=3,這個時候因為區域變數 a 所在的區域只有 while,因此它的範圍只有到 while 結束的 20 行。我們在 while 中修改全域變數 c,並在 19 行印出 abc 的數值。到了 21 行輸出後看到全域變數 c 確實被修改了(跟 15 行相比),但是全域變數 a 仍然沒有變動。

從上面的例子我們可以看得出來,相同名稱的全域變數和區域變數可以同時存在,它其實代表著不同的變數。但若是區域變數已經存在,在它死亡之前就不能再宣告一次了。

危險的全域變數

全域變數是一種很方便的變數宣告,但是如果可以用區域變數或傳值解決的事情,就盡量避免使用全域變數。這是因為全域變數有一些缺點,而且有些程式語言甚至會有安全性的問題。全域變數的缺點主要來自於它的生命週期很長,也就是會在記憶體空間中存活很久,此時就算該變數再也不會被使用到,仍然必須等到程式結束時才會被釋放,因此這個變數就會像垃圾一樣存在記憶體當中。為了使電腦有效率的工作,這種情況當然要避免,所以只有當變數在整個程式都會使用到的時候,才會設定全域變數。

至於全域變數是否會具有危險性,就要看程式語言而定,事實上全域變數也是有分好多種。有些語言的變數即使程式結束也不會被釋放,直到電腦被關機為止。所以早期有一些程式一旦執行過,電腦的效率就會變差,因為記憶體被佔用了(著明的魔獸爭霸就有這個問題)。另外,常常聽到的網頁cookies也是全域變數的一種,它的作用是讓整個網站都可以存取變數,可是大家都知道它是不安全的,cookies的存在會有個人資料外洩的風險。

單純要了解變數的作用範圍是很容易的,不過希望讀者能理解背後的意義,未來有機會再補充不同語言的特性。