CS:APP 看到一半,覺得自己對 C 語言的一知半解似乎加深了要理解這本書的成本,因此決定先去把 K&R C 看完,前面幾個章節大多是語法就速速複習,但到了 Program structure 這章就開始卡關,尤其是 static 那與其他程式語言不同的定義,K&R C 在這一段似乎也沒有著墨太深,所以整理一下資料。

自動變數 (Automatic variable)

自動變數是指進入作用域 (scope) 後,會自動分配空間,離開作用域後會自動釋放空間的變數,像是函式裡面的局部變數。

1
2
3
4
5
6
7
8
9
int add(int numbers[], int length)
{
int i, sum = 0;
for (i = 0; i < length; i++) {
sum += numbers[i];
}

return sum;
}

在這裡面 i, sum 都是自動變數,他們的作用域從宣告開始,直到函式結束,出了 add 函式後變無法再存取這些變數。即使在不同的函式中宣告了同樣名字的變數也不會出錯,這些變數也無任何關聯。

全域變數 (Global variable)

用一般方式定義在函式外面的變數,能夠被任何函式存取,稱為全域變數。另外,由於 C 語言內無法在函式裡面定義函式,因此所有非 static 函式都是全域的。

1
2
3
4
5
6
7
8
9
10
11
int length = 20;

int add(int numbers[])
{
int i, sum = 0;
for (i = 0; i < length; i++) {
sum += numbers[i];
}

return sum;
}

::在同一個 Scope 中,C 語言的 Symbol 是不能重複的,因此若變數或函式重複定義,在編譯過程中會產生錯誤。::

1
2
3
4
5
6
7
8
9
// main.c
#include "data.h"
int length = 20;

int main()
{
printf("%d", length);
return 0;
}
1
2
// data.h
int length = 20;

由於 length 被重複定義,編譯過程會產生錯誤。

Definition vs Declaration

宣告(Declaration)是指在程式中宣告標示符名稱以及資料型態,無論是變數、函式、物件皆是。

1
2
3
// Examples of declaration
int i;
int add(int numbers[]);

定義 (Definition) 是指在做宣告標示符的同時賦予其值 (或實作它),因此你在定義時也同時做了宣告。

1
2
3
4
5
6
// Examples of definition
int i = 10;
int main()
{
return 0;
}

宣告與定義可以一起寫,也可以分開寫,所以在過去的課堂上,我們被建議要將函式與變數的宣告寫在標頭檔 (header, .h) 之中,並把實作寫在 .c 裡面。在 C 語言中,多次宣告不會出事,但同一個標示符只能被定義一次

外部變數 (External variable)

標示為 extern 的變數不能被初始賦值,他告訴程式這個變數已經在別處被定義,有可能是在同一個檔案,或是 include 進來的另一份檔案,如下面的程式,若 number 沒有加上 extern 關鍵字,則在編譯階段就會出錯。

1
2
3
4
5
6
7
8
int main()
{
extern int number;
printf("%d", number);
return 0;
}

int number = 24;

好吧,那另一個情境下,我只是要使用 include 檔案的變數呢?不加extern 也不會報錯,畢竟他們都存在於 Global Scope 底下,能夠直接存取。

加上 extern 的好處是能夠提高程式的可讀性,讓讀者知道這個變數並非在這個 scope 中定義,而是在其他檔案 / scope。

我們實際來看一個例子,當我要去存取另一個檔案裡的變數:

1
2
// number.h
const float PI = 3.14;

不使用 extern

1
2
3
4
5
6
7
8
// main.c
#include <number.h>

int main()
{
printf("%f", PI);
return 0;
}

使用 extern

1
2
3
4
5
6
7
8
9
// main.c
#include <number.h>

int main()
{
extern int PI;
printf("%f", PI)
return 0;
}

在使用 extern 的程式中,我能夠很明確地知道這個 PI 必定在其他地方被定義了,若不使用 extern,而 main 函式很長的話,我還得要瀏覽一下才能知道 PI 是否是一個 local variable。

Static variable & Static function

剛剛有提到,非 static 的函式都會變成全域函式,但在寫 OOP 時,我們希望一個模組能夠被封裝 (Encapsulation),也就是限制外部使用者存取它的資料,大量的全域函式使模組之間的介面變得模糊,這對模組化的開發模式帶來許多不便。

使用 Static 宣告的變數或函式,對於其他 .c 檔案來說是不可見的,它的作用域只存在於它被宣告出來的 source 之中。

以下我們用一個例子來說明, number.h 宣告一個 number 變數在非 static 的狀況下,number 只能被定義在 main.c 或是 data.c 其中一個 source 中。若重複定義,會在編譯過程中出現錯誤。

1
2
3
// number.h
int number;
int getNumber();
1
2
3
4
5
6
7
8
9
// main.c
int number = 10;

int main()
{
printf("%d", number);

return 0;
}
1
2
3
4
5
6
7
// data.c
int number = 20;

int getNumber()
{
return number;
}

上面的程式在 linking 階段會產生錯誤,你會看到 gcc 吐了 duplicated symbol 的錯誤。

接下來,我們希望在 main.c 直接存取 number 變數與呼叫 data.c 的 getNumber 函式能夠取回不同的值,我們來試試看 static。

1
2
3
// number.h
static int number;
int getNumber();
1
2
3
4
5
6
7
8
9
10
// main.c
static int number = 10;

int main()
{
printf("number in main.c: %d", number);
printf("number in data.c: %d", getNumber());

return 0;
}
1
2
3
4
5
6
7
// data.c
static int number = 20;

int getNumber()
{
return number;
}

觀察結果會看到 main.c 的 number 與 data.c 的 number 同時存在了,應該說,他們存在於不同的作用域中,就像兩個函式的區域變數一樣。

那如果 main.c 裡面不定義 number 的話,他會印出什麼值呢?

由於 data.c 的 number 對於 main.c 來說是不可見的,所以 number 就等於是一個「有宣告卻沒定義的變數」,因此印出來的結果會是 garbage value。

Static variable in function

在函式裡面宣告的 static 變數和自動變數不同,它不會因為作用域結束而回收,它的狀態會被保留下來。我們用一段程式來檢視 static 變數的特別之處。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>

int getNumber()
{
int x = 0;
x += 5;

return x;
}

int getStaticNumber()
{
static int x = 0;
x += 5;

return x;
}

int main()
{
printf("non-static\tstatic\n");
for (int i = 0; i < 10; i++)
{
printf("%d\t\t%d\n", getNumber(), getStaticNumber());
}

return 0;
}

執行結果可以看到, static 變數不會因為離開作用域而回收。


喜歡這篇文章嗎?

歡迎點擊按鈕分享到 Facebook 上唷!

Weightless Theme
Rocking Basscss