變數的作用域和儲存方式,C語言變數作用域和儲存方式完全攻略

2020-07-16 10:04:24
變數按作用域可分為“區域性變數”和“全域性變數”。按儲存方式又可分為“自動變數(auto)”、“靜態變數(static)”、“暫存器變數(register)”和“外部變數(extern)”。注意,這裡的“自動變數”不是指的“動態變數”。

什麼叫“暫存器”?我們知道,記憶體條是用來儲存資料的,硬碟也是儲存資料的,而在 CPU 內部也有一些用來儲存資料的區域,即暫存器。暫存器是 CPU 的組成部分,是 CPU 內部用來存放資料的一些小型儲存區域,用來暫時存放參與運算的資料和運算結果。它同記憶體一樣,只不過它能儲存的資料要少得多。

區域性變數

區域性變數是定義在函數內部的變數,全域性變數是定義在函數外部的變數。

區域性變數只在本函數內有效,即只有在本函數內才能使用,在本函數外不能使用。如果區域性變數定義在子函數中,那麼只能在該子函數中使用。該子函數呼叫完後,系統為該子函數中的區域性變數分配的記憶體空間就會被釋放掉。

如果區域性變數定義在主函數 main 中,那麼只能在 main 函數中使用,main 函數執行結束後,系統為其中的區域性變數分配的記憶體空間就會被釋放掉。主函數也不能使用其他函數中定義的變數。所以不同函數中可以定義同名的變數,但它們所表示的是不同的物件,互不干擾。

在一個函數內部,可以在複合語句中定義變數,這些變數只在本複合語句中有效,離開本複合語句就無效,且記憶體單元隨即被釋放。所謂複合語句就是用大括號“{}”括起來的多個語句。下面給大家舉一個例子:
# include <stdio.h>
int main(void)
{
    int a = 1, b = 2;
    {
        int c;
        c = a + b;
    }
    printf("c = %dn", c);
    return 0;
}
編譯結果:
error C2065: 'c' : undeclared identifier

這個程式有一個錯誤,編譯結果顯示:“變數 c 身份不明”,也就是說系統不認識 c 是什麼。變數 a 和 b 在整個 main 函數中都有效,但變數 c 是在複合語句中定義的,所以只有在複合語句中才能使用。printf 在複合語句外,所以不能使用在複合語句中定義的變數 c。如果把 printf 放到複合語句中程式就編譯成功了。
# include <stdio.h>
int main(void)
{
    int a = 1, b = 2;
    {
        int c;
        c = a + b;
        printf("c = %dn", c);
    }
    return 0;
}
輸出結果:
c = 3

我們在程式設計的時候,if 語句、for 迴圈、while 迴圈下面都有大括號。這就意味著如果區域性變數是定義在這些大括號中的,那麼就只能在這些大括號中使用。當這些大括號執行完之後,在其中定義的區域性變數隨即就會被釋放。函數的形參也是區域性變數,呼叫完該函數後,形參也會隨之被釋放。

所以區域性變數的作用範圍準確地說不是以函數來限定的,而是以大括號“{}”來限定的。在一個大括號中定義的區域性變數就只能在這個大括號中使用,但因為每個函數體都是用大括號括起來的,而且我們一般也不會在函數體中的其他大括號中定義變數,所以習慣上就說“區域性變數是定義在‘函數’內部的變數,只在本‘函數’內有效”。

全域性變數

定義在函數內部的變數叫作區域性變數,定義在函數外部的變數叫作全域性變數。區域性變數只能在該函數內才能使用;而全域性變數可以被整個 C 程式中所有的函數所共用。它的作用範圍是從定義的位置開始一直到整個 C 程式結束。所以根據定義位置的不同,全域性變數的作用範圍不同。

在一個函數中既可以使用本函數中的區域性變數,也可以使用有效的全域性變數。但需要注意的是,全域性變數和區域性變數命名決不能衝突。

前面說過,不同函數中可以定義同名的變數,但它們代表的是不同的物件,所以互不干擾,原因是區域性變數的作用範圍不同。但是全域性變數和區域性變數的作用範圍可能有重疊,這時區域性變數和全域性變數的名字就不能定義成相同的了,否則就會互相干擾。如果在同一個作用範圍內區域性變數和全域性變數重名,則區域性變數起作用,全域性變數不起作用。這樣區域性變數就會遮蔽全域性變數。下面寫一個程式驗證一下:
# include <stdio.h>
int a = 10;  //定義一個全域性變數
int main(void)
{   
    int a = 5;  //定義一個同名的區域性變數
    printf("a = %dn", a);
    return 0;
}
輸出結果是:
a = 5

所以定義變數的時候區域性變數和全域性變數最好不要重名。此外,如果區域性變數未初始化,那麼系統會自動將“–858993460”放進去(VC++6如此)。但如果全域性變數未初始化,那麼系統會自動將其初始化為 0。它們的這個區別主要源自於它們儲存空間的不同。區域性變數是在棧中分配的,而全域性變數是在靜態儲存區中分配的。只要是在靜態儲存區中分配的,如果未初始化則系統都會自動將其初始化為 0。下面來寫一個程式驗證一下:
# include <stdio.h>
int a;  //定義一個全域性變數
int main(void)
{   
    printf("a = %dn", a);
    return 0;
}
輸出結果是:
a = 0

設定全域性變數的作用是增加函數間資料聯絡的渠道。因為全域性變數可以被多個函數所共用,這樣就可以不通過實參和形參向被調函數傳遞資料。所以利用全域性變數可以減少函數中實參和形參的個數,從而減少記憶體空間傳遞資料時的時間消耗。但是除非是迫不得已,否則不建議使用全域性變數。

為什麼不建議使用全域性變數

1) 全域性變數在程式的整個執行過程中都佔用儲存單元,而區域性變數僅在需要時才開闢儲存單元。

2) 全域性變數降低了函數的通用性,因為函數在執行時要依賴那些全域性變數。我們將一個功能編寫成函數,往往希望下次遇到同樣功能的時候直接複製過去就能使用。但是如果使用全域性變數的話,這個函數和全域性變數有聯絡,就不好直接複製過去了,還要考慮這個全域性變數的作用。即使把全域性變數也複製過去,但是如果該全域性變數和其他檔案中的變數重名的話,程式就會出問題。而且如果該全域性變數和其他函數也有關聯的話,程式也會出問題。所以全域性變數降低了程式的可靠性和通用性。

在程式設計中,劃分模組時要求模組的“內聚性”強、與其他模組的“關聯性”弱。即模組的功能要單一,不要把許多互不相干的功能放到一個模組中,與其他模組間的相互影響要盡量小。而使用全域性變數是不符合這個原則的。一般要求把C程式中的函數做成一個封閉體,除了可以通過“實參–形參”的渠道與外界發生聯絡外,沒有其他渠道。這樣的程式可移植性好,可讀性強,而使用全域性變數會使程式各函數之間的關係變得極其複雜。

3) 過多的全域性變數會降低程式的清晰性。人們往往難以清楚地判斷出每個時刻各個全域性變數的值,原因是每個函數在執行的時候都有可能改變全域性變數的值。這樣程式就很容易出錯,而且邏輯上很亂。因此要限制全域性變數的使用,不到萬不得已,不要使用全域性變數。

4) 如果在同一個程式中,全域性變數與區域性變數同名,則在區域性變數的作用範圍內,全域性變數就會被“遮蔽”,即它不起作用。

自動變數(auto)

前面定義的區域性變數其實都是 auto 型,只不過 auto 可以省略,因為省略就預設是 auto 型。

靜態變數(static)

static 還是比較重要的,static 可以用於修飾區域性變數,也可以用於修飾全域性變數。下面分別介紹一下。

用 static 修飾過的區域性變數稱為靜態區域性變數。區域性變數如果不用 static 進行修飾,那麼預設都是 auto 型的,即儲存在棧區。而定義成 static 之後就儲存在靜態儲存區了。

前面說過,儲存在靜態儲存區中的變數如果未初始化,系統會自動將其初始化為 0。用 static 修飾過的區域性變數也不例外,下面寫一個程式驗證一下:
# include <stdio.h>
int main(void)
{   
    static int a;
    printf("a = %dn", a);
    return 0;
}
輸出結果是:
a = 0

靜態儲存區主要用於存放靜態資料和全域性資料。儲存在靜態儲存區中的資料存在於程式執行的整個過程中。所以靜態區域性變數不同於普通區域性變數,靜態區域性變數是存在於程式執行的整個過程中的。下面來寫一個程式看一下:
# include <stdio.h>
void Increment(void);  //函數宣告
int main(void)
{
    Increment();
    Increment();
    Increment();
    return 0;
}
void Increment(void)
{
    static int x = 0;
    int y = 0;
    ++x;
    ++y;
    printf("x = %d, y = %d", x, y);
    printf("n");
    return;
}
輸出結果是:
x = 1, y = 1
x = 2, y = 1
x = 3, y = 1

我們看到,變數 x 是靜態區域性變數,而變數 y 是普通的區域性變數。從輸出結果可以看出,y 的值一直都是 1,而 x 的值是變化的。這是因為變數 y 是普通的區域性變數,每次函數呼叫結束後就會被釋放;而變數 x 是用 static 修飾的,所以它是靜態區域性變數,即使函數呼叫結束它也不會被釋放,而是一直存在於程式執行的整個過程中,直到整個程式執行結束後才會被釋放。這就相當於全域性變數,但它不是全域性變數,靜態區域性變數仍然是區域性變數,仍然不能在它的作用範圍之外使用。

有人可能會問,如果靜態區域性變數在函數呼叫結束後也不釋放,那函數每呼叫一次變數 x 不就重新定義一次,並重新賦值一次了嗎?

靜態區域性變數僅在第一次函數呼叫時定義並初始化,以後再次呼叫時不再重新定義和初始化,而是保留上一次函數呼叫結束後的值。也就是說“static int x=0;”這條語句只會執行一次。

下面再來看全域性變數。全域性變數預設都是靜態的,都是存放在靜態儲存區中的,所以它們的生存週期固定,都是存在於程式執行的整個過程中。所以一個變數生命週期的長短本質上是看它儲存在什麼地方。儲存在棧區中的變數,在函數呼叫結束後記憶體空間就會被釋放;而儲存在靜態儲存區中的變數會一直存在於程式的整個執行過程中。這就是區域性變數和全域性變數生命週期不同的原因。

雖然全域性變數本身就是儲存在靜態儲存區的,但它仍然可以用 static 進行修飾。而且修飾和不修飾是有區別的。用 static 修飾全域性變數時,會限定全域性變數的作用範圍,使它的作用域僅限於本檔案中。這個是使用 static 修飾全域性變數的主要目的。

那麼,不用 static 修飾,全域性變數不也是只能在本檔案中使用嗎?這麼說不完全對,因為雖然全域性變數的作用範圍不會自己主動作用到其他檔案中,但不代表其他檔案不會使用它。如果不用 static 進行修飾,那麼其他檔案只需要用 extern 對該全域性變數進行一下宣告,就可以將該全域性變數的作用範圍擴充套件到該檔案中。但是當該全域性變數在定義時用static進行修飾後,其他檔案不論通過什麼方式都不能存取該全域性變數。

而且如果一個專案的多個 .c 檔案中存在同名的全域性變數,那麼在編譯的時候就會報錯,報錯的內容是“同一個變數被多次定義”。但是如果在這些全域性變數前面都加上 static,那麼編譯的時候就不會報錯。因為用 static 修飾後,這些全域性變數就只屬於各自的 .c 檔案了,它們是相互獨立的,所以編譯的時候就不會發生衝突而產生“多次定義”的錯誤。所以使用 static 定義全域性變數是非常有用的,因為當一個專案中有很多檔案的時候,重名不可避免。這時候只要在所有的全域性變數前面都加上 static 就能很好地解決這個問題。定義全域性變數時用 static 進行修飾可以大大提高程式碼的品質。

總結:
1) 用 static 修飾後的區域性變數稱為靜態區域性變數。因為區域性變數在函數呼叫結束後就會被釋放,所以如果不希望它被釋放的話,就在定義時用 static 對它進行修飾。如:
static int a;
2) 定義成 static 型的變數是存放在靜態儲存區中的,在程式的整個執行過程中都不會被釋放。這跟全域性變數很像,但它不是全域性變數。靜態區域性變數仍然是區域性變數,只不過它的生命週期改變了,但作用範圍並沒有改變,所以其他函數仍然不能使用它。它之所以跟全域性變數很像是因為它們都是存放在靜態儲存區中,所以它們都存在於程式執行的整個過程中。

3) 如果把區域性變數定義成 static 型,那麼在程式的整個執行過程中,它只在編譯時賦初值一次,以後每次呼叫函數時不會再重新給它賦值,而是保留上一次函數呼叫結束時的值。如果定義靜態區域性變數時沒有初始化的話,那麼在編譯時系統會自動給它賦初值 0(對數值型變數)或空字元 ''(對字元變數)。

4) 但是靜態區域性變數跟全域性變數一樣,長期佔用記憶體不放,而且降低了程式的可讀性,當呼叫次數過多時往往弄不清楚其當前值是什麼。所以若非必要,不要過多使用靜態區域性變數。

5) 對全域性變數使用 static 進行修飾是 static 非常重要的用法。如果不用 static 對全域性變數進行修飾,那麼其他檔案只需要使用 extern 對該全域性變數進行宣告就可以直接使用該全域性變數。但是當用 static 進行修飾後,即使其他檔案用 extern 對它進行宣告也不能使用它。這種用法可以有效防止並解決不同檔案中全域性變數重名的問題。

暫存器變數(register)

無論是儲存在靜態儲存區中還是儲存在棧區中,變數都是儲存在記憶體中的。當程式用到哪個變數的時候,CPU 的控制器就會發出指令將該變數的值從記憶體讀到 CPU 裡面。然後 CPU 再對它進行處理,處理完了再把結果寫回記憶體。

但是除了記憶體可以儲存資料之外,CPU 內部也有一些用來儲存資料的區域,這就是暫存器。暫存器是 CPU 的組成部分,是 CPU 內部用來存放資料的小型儲存區域,用來暫時存放參與運算的資料和運算結果。與記憶體相比,暫存器所能儲存的資料要小得多,但是它的存取速度要比記憶體快很多。

那為什麼暫存器的存取速度比記憶體快呢?最主要的原因是因為它們的硬體設計不同。計算機中硬體執行速度由快到慢的順序是:

暫存器>快取>記憶體>固態硬碟>機械硬碟

為了提高程式碼執行的效率,可以考慮將經常使用的變數儲存到暫存器中。比如迴圈變數和迴圈體內每次回圈都要使用的區域性變數。這種變數叫作暫存器變數,用關鍵字 register 宣告。如
register  int  a;
但是需要注意的是,register 關鍵字只是請求編譯器盡可能地將變數儲存在 CPU 內部的暫存器中,但並不一定。因為我們說過,暫存器所能儲存的資料是很少的,所以如果暫存器已經滿了,那麼即使你用 register 進行宣告,資料仍然會被儲存到記憶體中。而且需要注意的是,並不是所有的變數都能定義成暫存器變數,只有區域性變數才可以。或者說暫存器變數也是一個區域性變數,在函數呼叫結束後就會被釋放。

register 看起來很有用,但是要跟大家說的是,這個修飾符現在已經不用了。現在的編譯器都比較智慧化,它會自動分析,即使變數定義的是 auto 型(預設的),但如果它發現這個變數經常使用,那麼它也會把這個變數放到暫存器中。同樣,即使定義成 register 型,但如果暫存器中沒地方存放,那麼它也會把這個變數放到記憶體中。

這時有些人就提出一個疑問:“既然暫存器速度那麼快,那麼為什麼不把計算機的記憶體和硬碟都改成暫存器?我們前面說過,暫存器之所以比記憶體速度快,最主要的原因是因為它們的硬體設計不同。從硬體設計的角度來看,暫存器的製作成本要比記憶體高很多!而且暫存器數量的增加對 CPU 的效能也提出了極高的要求,而這往往是很難實現的。

外部變數(extern)

extern 變數是針對全域性變數而言的。通過前面了解到,全域性變數都是存放在靜態儲存區中的,所以它們的生存期是固定的,即存在於程式的整個執行過程中。但是對於全域性變數來說,還有一個問題尚待解決,就是它的作用域究竟從什麼位置起,到什麼位置結束。作用域是包括整個檔案範圍,還是只包括檔案中的一部分?是在一個檔案中有效,還是在程式的所有檔案中都有效?

一般來說,外部變數是在函數的外部定義的全域性變數,它的作用域是從變數的定義處開始,到本程式檔案的末尾結束。在此作用域內,全域性變數可以被程式中各個函數所參照。但是有時程式設計人員希望能擴充套件全域性變數的作用域,如以下幾種情況:

1) 在一個檔案內擴充套件全域性變數的作用域

如果全域性變數不在檔案的開頭定義,其有效的作用範圍只限於定義處到檔案結束。在定義點之前的函數不能參照該全域性變數。如果出於某種考慮,在定義點之前的函數需要參照該全域性變數,那麼就在參照之前用關鍵字extern對該變數作“外部變數宣告”,表示將該全域性變數的作用域擴充套件到此位置,比如:
extern  int  a;
有了此宣告就可以從“宣告”處起,合法地使用該全域性變數了。

2) 將外部變數的作用域擴充套件到其他檔案

一個 C 程式可以由一個或多個 .c 檔案組成。如果程式只由一個 .c 檔案組成,那麼使用全域性變數的方法前面已經介紹了。如果程式由多個 .c 檔案組成,那麼如何在一個檔案中參照另一個檔案中定義的全域性變數呢?

假如在 1.c 中定義了全域性變數“int a=10;”,如果 2.c 和 3.c 也想使用這個變數 a,而我們不能在 2.c 和 3.c 中重新定義這個 a,否則在編譯連結時就會出現“重複定義”的錯誤。正確的做法是在 2.c 和 3.c 中分別用 extern 對 a 作“外部變數宣告”,即在 2.c 和 3.c 中使用 a 之前作如下宣告:

這樣在編譯和連結時,當編譯器在 2.c 中遇到變數 a 的宣告時就會知道它有“外部連結”,就會從其他檔案中尋找a的定義,並將在另一檔案中定義的全域性變數a的作用範圍擴充套件到本檔案中。這樣在本檔案中也可以合法地使用全域性變數 a 了。但是現在問大家一個問題:“以 2.c 為例,如果在 2.c 中對 a 進行多次宣告,即寫多個“externinta;”,那麼程式會不會有錯?”答案是不會,C 語言中是允許多次宣告的,但有效的只有一個。

與全域性變數一樣,同一個專案的不同 .c 檔案中也不能定義重名的函數。如果 2.c 和 3.c 想要使用 1.c 中定義的函數,那麼也只需要在 2.c 和 3.c 中分別對該函數進行宣告就可以了。即直接把 1.c 中對函數的宣告拷貝過來就行了。

與全域性變數不同的是,它的前面不用加 extern。因為對於函數而言,預設就是 extern,所以宣告時前面的 extern 可以省略。此外在實際程式設計中,我們一般不會直接把 1.c 中某個函數的宣告拷貝到 2.c 和 3.c 中,而是把該宣告寫在一個 .h 標頭檔案中,然後分別在 2.c 和 3.c 中用 #include 包含該標頭檔案即可。如果不用標頭檔案當然也可以,但此時 2.c 和 3.c 中都要寫該函數的宣告,這樣如果要修改的話就都要修改。而如果寫在標頭檔案中,要修改的話就只需要改標頭檔案中的內容就可以了。

關於標頭檔案後面還會專門介紹。此外,同一個 .c 檔案中對同一個函數進行多次宣告也是允許的,但它們起作用的只有一個。