DEV Community

Cover image for 變數的有效範圍 (scope) 與生命週期 (life time)
codemee
codemee

Posted on

變數的有效範圍 (scope) 與生命週期 (life time)

變數的有效範圍與生命週期聽起來似乎很像, 不過實質的意義不同:

  • 有效範圍是指該變數在程式碼的哪些地方有效, 你也可以把它當成哪些地方寫出該變數的名稱, 編譯器是認得的, 表示在那些地方這個名稱是有效的, 可以存取變數的內容。
  • 生命週期指的則是變數在執行時期的那一個時間點才會配置記憶體空間, 又持續到哪一個時間點才會把配置的記憶體空間歸還?

顯而易見, 只要執行到有效範圍內的程式碼, 一定就位於生命週期內, 否則就無法存取該變數。但是反過來說, 就有可能在執行時發生變數仍在其生命週期內, 但因為執行的位置不在該變數的有效範圍內, 而無法存取該變數的狀況。例如, 以下是一個 Python 的例子:

>>> def outer():
...     x = 10
...     def inner():
...         print(x)
...     return inner
>>> inner = outer()
>>> x
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'x' is not defined

>>> inner()
10
>>>
Enter fullscreen mode Exit fullscreen mode

當我們叫用 outer 取得 inner 時, outer 內的 x 已經配置了, 但是因為閉包 (closure) 的作用, x 並沒有因為從 outer 返回而消失, 仍處於生命週期內, 但是因為我們離開了 outer 函式, 所以不在 x 的有效範圍內, 因此無法取用 x。只要叫用我們剛剛取得的 inner, 就可以確認 x 依然存在, 而可以正確印出 x 的值 10。

其實只要簡單的從函式中叫用另一個函式, 就可以造成這種變數仍在生命週期內, 但卻不在有效範圍內的狀況, 例如:

>>> def func1():
...     x = 10
...     func2()
...     print(x)
>>> def func2():
...     print(x)
>>> func1()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in func1
  File "<stdin>", line 2, in func2
NameError: name 'x' is not defined
Enter fullscreen mode Exit fullscreen mode

當我們叫用 func1 時, 它會再去叫用 func2, 這時 func1 內的 x 仍在其生命週期內, 但是有效範圍僅限於 func1 內部, func2 不在它的有效範圍, 所以在 func2 中取用 x 就會出錯。

透過這兩個範例, 應該可以清楚區別有效範圍跟生命週期的差別。

對於編譯式的程式語言, 例如 C/C++, 有效範圍的問題在編譯時期就會發現, 但是變數的生命週期有時候就不是那麼容易辨別, 例如函式中的靜態變數雖然是宣告在函式內, 但其實在程式一開始執行時就會配置, 因此生命週期涵蓋整個程式執行的期間, 而不只是該函式被叫用執行的期間。以底下這個 C 程式碼為例:

#include<stdio.h>

int foo(void) {
    static int i = 10;
    return i;
}
void main(void) {
    printf("hello\n");
}
Enter fullscreen mode Exit fullscreen mode

在 x86 的 gcc 14.1 實際編譯出來的 (Intel) 組合語言如下:

foo:
        push    rbp
        mov     rbp, rsp
        mov     eax, DWORD PTR i.0[rip]
        pop     rbp
        ret
.LC0:
        .string "hello"
main:
        push    rbp
        mov     rbp, rsp
        mov     edi, OFFSET FLAT:.LC0
        call    puts
        nop
        pop     rbp
        ret
i.0:
        .long   10
Enter fullscreen mode Exit fullscreen mode

你可以看到 foo 函式內的變數 i 是在函式之外配置的, 程式一執行時就會佔用記憶體, 並且在程式結束後隨之歸還給作業系統, 也就是它的生命週期與程式的執行期間一樣。雖然如此, i 的有效範圍卻只限於 foo 函式內, 如果你在 main 中想要取用這個已經存在的變數 i, 例如:

#include<stdio.h>

int foo(void) {
    static int i = 10;
    return i;
}
void main(void) {
    printf("%d\n", i);
}
Enter fullscreen mode Exit fullscreen mode

編譯時就會告知錯誤:

<source>: In function 'main':
<source>:8:20: error: 'i' undeclared (first use in this function)
    8 |     printf("%d\n", i);
      |                    ^
<source>:8:20: note: each undeclared identifier is reported only once for each function it appears in
Compiler returned: 1
Enter fullscreen mode Exit fullscreen mode

你看, 編譯器認為這個一個尚未宣告的名稱。

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more