Scope & Lifetime 前言 Local Scope Global Functions & Objects Local Objects Dynamically Allocated Objects Namespace
前言 在 C++ 中,每一個變數、函式、自定的資料型態、和樣板等等都有一個名稱。這些名稱的適用範圍稱之為 scope(名域)。我們必須瞭解 scope 的規則,才能夠區別出名稱相同的物件所指涉的對象究竟是哪一個。 程式執行時,物件從生到死的期間稱之為 lifetime(生命期)。我們必須瞭解 lifetime 的規則,才能夠避免使用已經不存在的物件所導致的程式執行錯誤。
物件導向程式語言講義(Scope & Lifetime) C++ 定義了以下四種 scope: Local Scope 由函式或區塊敘述所引介的 scope。 Class Scope 由 class 定義所引介的 scope。 Namespace Scope namespace 宣告所引介的 scope。 Global Scope 上述三種之外的 scope。 靜宜大學資訊管理學系 蔡奇偉 副教授編製
// global scope int x; void g () { // local scope int y; } void f (int x) namespace N // namespace scope class C { void f (); void C::f () // class scope int y = x + 1;
在 C++ 中,任何名稱必須先宣告才能使用於算式之中。 void foo () { x = 1; // error: not declared } 名稱的可見度(visibility)始於宣告點,終於宣告所處 scope 的結尾。 int x; x = 3; // ok y = 4; // error int y; x = y + 1; // ok x 的 可見度 範圍 y 的可見度範圍
在同一層 scope 之中,名稱不可重複宣告。 void foo () void bar () { int x; … } x = 1; // error 在同一層 scope 之中,名稱不可重複宣告。 void foo () int x = 3; int x; // error x 的可見度範圍
在不同層的 scope 中,名稱可以重複宣告。 int x; void foo () { int x = 3; // ok … double x; // ok }
Name Resolution 由於名稱可以重複使用於程式之中,C++ 編譯器因而必須決定算式中的名稱到底指涉那一個宣告。這個步驟稱為 name resolution(名稱解析)。我們在這一章中先討論 local scope 的 name resolution,以後再討論 function template 定義之中和 class 定義之中的 name resolution。
void foo () { int x; x = 1; // } x = 2; // ::x = 1; // N::x = 3; // // global scope int x; 1 2 3 4 namespace N { int x; } 4 3 void bar () { x = 1; // } 1 1 2
Local Scope 函式的定義形成一個 local scope。不同的函式定義所形成的 local scope 各自獨立。比方說,在以下的兩個函式之中,變數 x 的兩個定義是獨立互不干擾。 void foo () { int x; } void bar () { int x; } 定義在 local scope 的變數稱為 local object(局部物件)。
函式之中的區塊敘述也形成一個內層的 local block scope。 local block scope 之中可以有另一個 local block scope 而形成巢狀的結構。 void foo () { int x; x = 3; } x = 2; x = 1;
定義在 if 敘述左右括號之中的物件,其有效範圍及於整個 if 敘述。 if (int x = get_value()) { x++; } x 的有效範圍 if (int x = get_value()) { x++; } else { x--; x 的有效範圍
定義在 for 敘述或 while 敘述左右括號之中的物件,其有效範圍及於整個敘述。 for (int k = 0; k < N; k++) { // ... } k 的有效範圍 while (int x = get_value()) { // ... } x 的有效範圍
測驗題: 底下的程式碼掃描輸入行以取得其中的數字串。然而其中有點錯誤,使得它無法編譯成功。請問錯誤在何處? #include <ctype.h> /* for isspace() and isdigit() */ char buf[MAX_BUFER], digits[MAX_DIGIT]; // code for getting input line into “buf” is omitted here. for (char *cp = buf; isspace(*cp); cp++) ; for (char *dp = digits; isdigit(*cp); *dp++ = *cp++) *dp = ‘\0’;
解答: C++ 編譯器會抱怨第二個 for 敘述中的 cp 變數和最後一行的 dp 變數沒有定義。正確的寫法如下: #include <ctype.h> /* for isspace() and isdigit() */ char buf[MAX_BUFER], digits[MAX_DIGIT]; // code for getting input line into “buf” is omitted here. char *cp; for (cp = buf; isspace(*cp); cp++) ; char *dp; for (dp = digits; isdigit(*cp); *dp++ = *cp++) *dp = ‘\0’;
函式參數的有效範圍涵蓋整個函式。 void foo (int x) { … } x 的有效範圍
在同一層 scope 之中,名稱不可重複定義。 void foo (int x) { int x; // error } for (int k = 1; k < N; k++) { int k; // error } void bar (int x) { int x; // ok } if (int x = get_value()) { ... } else { int x = 0; // error
當遇到一個名稱時,C++ 編譯器會由該名稱所在的 scope 開始,由內而外地搜尋對應的宣告,直到 global scope 為止。若全部找不到,則會標示為編譯錯誤。 int y; void foo () { int x; x = 2; y = 0; } x = 1; z = -1; // error: not declared
Global Functions & Objects 定義在 global scope 的函式稱為 global (全域)函式。定義在 global scope 的變數稱為 global 物件。 global 函式(除了 overloaded 函式之外)和 global 物件的定義,在整個程式中必須是惟一的,這個規則稱之為「定義惟一性」。舉例來說,假定某個程式包含 foo.cpp 和 bar.cpp 兩個檔案: // foo.cpp int x, y; void dup () { … } // bar.cpp int x; double y; void dup () { … } 它們將造成連結上的錯誤(link error)。
沒有設定初值的 global 物件在程式執行之前,其所佔據的記憶體會全部被清除成 0。但是沒有設定初值的 local 物件和 class 資料成員所佔據的記憶體則不會被清除成 0。 int x; int y = 0; ... int main () { int z; if (x == y) { cout << “x == y ” << endl; cout << “z = ” << z << endl; } else cout << “x != y” << endl; 輸出結果: x == y z = 23529970
extern 宣告 C/C++ 程式通常是由多個程式檔所組成。假定 global 函式或 global 物件並不是定義在目前的程式檔之中。如果我們想使用它們,就必須用 extern 關鍵字來宣告它們。譬如: // foo.cpp int x; int foo () { … } // bar.cpp extern int x; extern int foo (); void bar () { x = foo(); }
為了維持一致性,我們通常把 extern 宣告擺在 為了維持一致性,我們通常把 extern 宣告擺在 .h 標頭檔之中,讓需要使用的程式檔用 include 的方法加入檔中。譬如上一頁的程式可寫成以下的方式: // foo.cpp #include “foo.h” int x; int foo () { … } // bar.cpp #include “foo.h” void bar () { x = foo(); } // foo.h extern int x; extern int foo ();
雖然 global 函式和 global 物件的定義只能有一個,但是宣告卻可以重複出現,當然這些宣告必須保持一致。比方說,你不能把一個 global 物件在某一處宣告成 int,但在另外一處又宣告成 double。 // bar.cpp #include “foo.h” extern double x; extern int foo (); // ok void bar () { x = foo(); } // foo.cpp int x; int foo () { … } // foo.h extern int x; extern int foo (); 編譯器會抱怨 bar.cpp 中變數 x 的宣告不一致。
如果 global 物件的定義和 extern 宣告不一致的話,會造成以下兩種結果:(1)程式連結的錯誤、或(2)程式執行上的錯誤。比方說,編譯底下的 bar.cpp 時,變數 x 被視為一個 double 型態的外部變數。如果程式中並沒有定義這樣的 global 物件,在連結時可能會產生「變數 x 沒有定義」的錯誤訊息。即使連結時沒有發現錯誤,當程式執行時, foo.cpp 中的函式把變數 x 解釋成 int,但 bar.cpp 中的函式又把變數 x 解釋成 double。這種解釋不一致所造成的程式 bug 往往不容易追查出來 。 // bar.cpp #include “foo.h” void bar () { x = foo(); } // foo.cpp int x; int foo () { … } // foo.h extern double x; extern int foo ();
顯然地,global 函式的宣告和 extern 宣告不一致的話,也會產生一些問題。比方說, 函式 foo 的參數是單位元組的字元,但在 bar.cpp 呼叫 foo 時,卻傳遞了4 位元組的 int。 // foo.cpp void foo (char c) { … } // bar.cpp extern void foo (int) … int x; foo(x); 幸好 C++ 提供「型態安全的連結(type-safe linkage)」的機制,可用來避免這一類的問題。以上例來說,連結器(linker)會 把 bar.cpp 中的 void foo (int) 標示成一個沒有定義的函式。
所謂「型態安全的連結」是指編譯器會把函式的參數型態與個數等資訊傳送給連結器(linker),讓連結器得以檢查呼叫函式時的引數型態是否符合對應參數的宣告。這個機制主要是為了處理超載函式(overloaded functions)。
static 宣告 如果想把 global 物件或 global 函式的可見度侷限在定義的程式檔之中,我們可以在它的定義之前加上關鍵字 static。以底下的兩個程式檔為例,連結器會抱怨 bar.cpp 中的外部變數 x 和外部函式 foo() 沒有定義。 // bar.cpp extern int x; extern void foo () ; extern void setx(); void bar () { x = 1; // error foo(); // error setx(); // ok } // foo.cpp static int x; static void foo () { … } void setx () { x = 1; }
Local Objects 定義在 local scope 中的物件稱為 local object(局部物件)。根據儲存方式和生命期的不同, local objects 可區分為以下三種: automatic objects register objects local static objects
automatic objects local objects 若沒有特別地宣告,將視為 automatic objects。 這類的物件會被配置在 run-time stack 之中。譬如:底下 foo 函式中的 local objects 在 run-time stack 的配置方式類似右下圖所示。 run-time stack return address x s c f void foo (int x, short s) { char c; float f; ... }
#include <iostream> int main () { int *ip = foo(); 由於 automatic objects 是配置在 run-time stack 之中,因此只有進入函式之後,它們才真正存在,離開函式,它們就消失了。由於這樣的性質,函式的傳回值不應該是 automatic objects 的位址或參照,否則會造成程式執行錯誤。 int * foo () { int x = 10; return &x; } #include <iostream> int main () { int *ip = foo(); cout << *ip << endl; } 會產生執行錯誤
register objects 從硬體的觀點來看,CPU 內部 registers 的存取速度要比記憶體快了許多。計算所需的資料如果能夠長駐在 registers,往往能提升計算的效率。在 C/C++ 中,你可以在變數的定義之前加上關鍵字 register,要求 C++ 編譯器盡量把變數保留在 register 中,藉此來提高程式的效率。譬如迴路變數就很適合宣告成 register 。 for (register int k; k < MAX_LOOP; k++ { … }
register 宣告只是一種對 C++ 編譯器的建議,而不具有強制性。此外, C++ 編譯器的最佳化(optimization)步驟,透過對程式流程的分析,常常可以獲得 registers 配置的最好方式。基於這個緣故,目前大部份的程式設計師傾向不使用 register 宣告,而全部交由 C++ 編譯器代為決定 registers的配置方式。
local static objects 如果在一個 local object 的宣告之前加上關鍵字 static,則該物件稱為 local static object。譬如在以下的函式中,變數 count 被宣告成一個 local static object : void foo () { static int count = 0; … }
local static objects 與前兩種 local 物件不同的地方在於: 它是以 global 物件的方式來儲存,而不是存在 run-time stack之中 它的生命期並不止於 local scope 的結束,而是 延續至程式的結束。 然而 local static objects 仍是 local scope 中的物件,它的可見度還是僅涵蓋 local scope。譬如: void foo () { static int count = 0; … } void bar () { count++; // error: undefined }
local static objects 的初值設定只執行一次,而不是每一次進入函式都重新設定一遍。譬如: #include <iostream> int foo () { static int count = 0; return ++count; } void main () for (int k = 0; k < 10; k++) cout << foo(); << endl; 輸出結果: 1 2 3 . 10
Dynamically Allocated Objects 如前所述地,local 物件和 global 物件的生命期都有嚴格的規定,而無法更改。然而用動態配置的方法所產生的物件,允許我們根據程式流程的需要來控制它們的生與死。 我們可以使用 new 運算子來產生動態配置的物件,將 new 運算子傳回來的物件位址存放在指標中,然後用間接的方式來存取其中的內容。當動態配置的物件不再需要時,我們必須用 delete 運算子把它所佔據的記憶體還回給系統,才不會造成記憶體流失(memory leak)的現象。
範例: #include <iostream> int * alloc_int () { int *ip = new int(10); return ip; } int main () int *xp = alloc_int(); (*xp)++; cout << *xp << endl; delete xp; return 0; 輸出結果: 11
local 指標變數的生命期當離開所在的 scope 之後就結束了,但是它所指的動態物件若沒有被刪除掉,則會一直存在著。譬如底下 local 指標變數 ip 離開函式 alloc_int() 之後就消失,但是它所指的記憶體仍可在 main() 函式中繼續使用。 #include <iostream> int * alloc_int () { int *ip = new int(10); return ip; } int main () { int *xp = foo(); (*xp)++; cout << *xp << endl; delete xp; return 0; }
記憶體流失 當一個動態配置的物件無法用任何方法來存取其中的資料,也無法釋放它所佔據的記憶體,我們稱這個物件造成記憶體流失( memory leak )的問題。譬如在以下的例子: void foo () { int *ip = new int; } 指標 ip 所指的記憶體在離開函式 foo 之後,就無法再利用,也無法被釋放。
auto_ptr(自動指標) auto_ptr 是 C++ 標準函式庫中的一個樣板類別。它可用來自動刪除動態物件。 auto_ptr 只能用於動態配置的單一物件,而不能用於動態配置的陣列。 一個 auto_ptr 物件在宣告時,初值常被設定成一個指標(如: new 運算子所傳回來的動態物件位址)。 auto_ptr 物件可以像指標般地使用。然而,當 auto_ptr 物件的生命期結束時,它所指的動態物件會自動地刪除,而不須要使用 delete 運算子。 使用 auto_ptr 時,我們必須加入系統標頭檔:memory,如: #include <memory> using std::auto_ptr;
我們可以用以下三種方式來定義 auto_ptr 物件: auto_ptr<type_pointed_to> identifier (ptr_allocated_by_new); auto_ptr<type_pointed_to> identifier (auto_ptr_of_same_type); auto_ptr<type_pointed_to> identifier; 譬如: auto_ptr<int> api (new int(1024)); api 預設成一個整數自動指標,且 *api 的值等於 1024 auto_ptr<int> api2 (api); api2 預設成整數自動指標 api auto_ptr<int> api3 ; api3 是一個無預設值的整數自動指標
auto_ptr<int> api2 (api); int x; auto_ptr<int> api4 (&x); // error 自動指標不可用來儲存靜態變數的位址,因為靜態變數所佔據的記憶體是無法被釋放的。 auto_ptr<int> api4(new int[100]) ; // error 自動指標不可用來儲存陣列的位址
auto_ptr 物件的生命期結束時,它所指的動態物件亦自動被刪除。譬如以下的函式並不會造成記憶體流失: void foo () { auto_ptr<int> api(new int); } 原因是:當函式 foo 結束時,自動指標 api 的生命期隨之結束,經由 api 所配置的記憶體會被自動刪除。如此一來,就不會發生記憶體流失。
auto_ptr 物件會刪除它所「擁有」的動態物件,而不會刪除它所「不擁有」的動態物件。此擁有權會隨著 auto_ptr 物件間的初始化設定而移轉。當多個 auto_ptr 物件擁有同一個動態物件時,常常會因為重複刪除而造成程式執行的錯誤。 void g (T *p) { auto_ptr<T>p2(p); // p2 負責刪除 p 所指的動態物件 auto_ptr<T>p3(p2); // 擁有權從 p2 移轉給 p3,變成 // p3 負責刪除 p 所指的動態物件 auto_ptr<T>p4(p); // 程式錯誤,因為 p3 和 p4 擁有 // 相同的動態物件 }
當指定 auto_ptr 物件給另一個 auto_ptr 物件時,譬如: auto_ptr<int> p1(new int(0)); auto_ptr<int> p2(new int(1)); p1 = p2; p1 會先刪除它所擁有的動態物件,然後設定其指標指向 p2 所指的動態物件,最後把 p2 的擁有權轉移給 p1。 1 p1 p2 指定前 指定後
若 auto_ptr 物件並未設定初值的話,你可以用 type casting 的方式在稍後設定其值。譬如: auto_ptr<int> p; … p = auto_ptr<int>(new int(0)); 然而,你不可以用以下的方式: p = new int(0); // The C++ Programming Language p.reset(new int(0)); // C++ Primer p = static_cast<auto_ptr<int>>(new int(0));
auto_ptr 類別提供以下兩個成員函式: get() 此成員函式傳回 auto_ptr 物件內部的指標值。 release() 此成員函式除了傳回 auto_ptr 物件內部的指標值之外,也解除擁有權。 auto_ptr<int> p1(new int(0)); auto_ptr<int> p2(p1.get()); // error:多重擁有 auto_ptr<int> p3(p1.release()); // ok
若要測試 auto_ptr 物件內部的指標是否為空值,必須使用 get() 成員函式。譬如: auto_ptr<int> p(new int); if (p.get() != 0) { /* 指標不等於空值 */ } else { /* 指標等於空值 */ 你不能使用下列的方法: if (p != 0) { … } else { … }
namespace Namespace 的用途 Namespace 的定義與宣告 Scope operator :: Unnamed namespace 使用 namespace 中的成員 alias using 宣告 using 指令 標準 namespace: std
Namespace 的用途 若程式庫中定義了 global 物件與函式,由於它們在程式之中必須是惟一的,因此程式設計師必須避免使用到相同的名稱。當運用到的程式庫或程式檔案很多時,可能會造成程式設計師在選取名稱上的困擾。這個現象稱之為 global name space pollution。 為了解決上述的問題,C++ 提供了 namespace(名稱空間)的機制,在 global scope 加上另一層的 scope,讓程式庫設計師把它們所定義的 global 物件與函式,依據程式的邏輯架構放在不同的 namespace 之中,以避免名稱發生衝突。
// global scope int x; namespace lib { // namespace scope } int main () x = 1; lib::x = 2;
Namespace 的定義 namespace 的定義語法如下: namespace namespace_name { // global 物件與函式的定義 } 譬如: namespace lib { int x; void inverse (matrix &m) { … } double area (double radius) { … } ...
Namespace 的宣告 namespace 的宣告語法如下: namespace namespace_name { // global 物件與函式的宣告 } 譬如: namespace lib { extern int x; void inverse (matrix &); double area (double); ...
namespace 的定義(或宣告)可以分散在不同的地方。在這種情況下,這些同名的 namespace 等同於合併為一的 namespace。譬如: int x; } int y; 由於上述的合併性質以及 global 名稱必須惟一等原因,在同名的 namespace 定義之中,物件名稱和函式名稱不允許重複使用。
通常我們把 namespace 的宣告寫在. h 標頭檔內,把 namespace 的定義寫在 通常我們把 namespace 的宣告寫在 .h 標頭檔內,把 namespace 的定義寫在 .cpp 程式檔內。當須要使用此 namespace 時,則把 .h 標頭檔用 include 的方式加入程式檔之中。譬如: // lib.h namespace lib { extern int x; const double pi = 3.14; double foo (int); } // lib.cpp #include “lib.h” int x; double foo (int k) { … } // prog.cpp #include “lib.h” int x; double bar (int k) { double a = lib::pi * lib::foo(k); return x * a; }
巢狀的 namespace int x; 我們可以在一個 namespace 中再定義或宣告內層的 namespace,而形成巢狀的結構。 namespace outer { namespace inner { } int main () { x = 1; outer::x = 2; outer::inner::x = 3; 我們可以在一個 namespace 中再定義或宣告內層的 namespace,而形成巢狀的結構。
namespace_name::member_name Scope operator :: 我們可以用 scope 運算子 :: 以下列的格式: namespace_name::member_name 來存取 namespace namespace_name 中的成員 member_name。譬如: namespace lib { extern int x; // lib::x const double pi = 3.14; // lib::pi double foo (int); // lib::foo }
我們可以用 ::name 的寫法來存取 global scope 的名稱 name 。 // global scope int x; namespace lib { // namespace scope } int main () int x = 1; ::x = 2; lib::x = 3;
Unnamed namespace 若想把 global 物件或函式的可見度限制於所在的程式檔之中的話,我們可以把它們定義在所謂的「無名」namespace 之中。比方說,假定函式 swap() 只用於 sort.cpp 檔之中,而且也不希望它在其它的程式檔中被誤用。我們可以在 sort.cpp 檔之中做如下的定義: // sort.cpp namespace { // unnamed namespace void swap (int &x, int &y) { int t = x; x = y; y = t; } ...
定義在無名 namespace 之中的物件和函式,它們的可見度僅及於所在的程式檔之中。此外,我們不必使用 scope 運算子來存取它們。 int main () { int y = 1; x = 2; swap(x,y); } namespace { void swap (int &x, int &y) int t = x; x = y; y = t; int x;
C 語言原來的 static 宣告可以用無名 namespace 取代 。譬如前一頁的 C++程式範例等同於右邊所示的 C 程式。不過,寫 C++ 程式時,你應該儘量採用 C++ 的寫法。 int main () { int y = 1; x = 2; swap(x,y); } // C version static void swap (int &x, int &y) int t = x; x = y; y = t; static int x;
使用 namespace 中的成員 由於使用 scope 運算子來存取 namespace 的成員,寫起來往往非常地繁頊,因此 C++ 提供下列三種簡化的方式: alias 用一個比較短的別名來取代比較長的 namespace 名稱。 using 宣告 宣告 namespace 的成員。 using namespace 指令 宣告所使用的 namespace。
namespace short_name = long_name; 設定別名 Namespace 可以用以下的指令來設定一個比較短的別名: namespace short_name = long_name; 譬如: namespace International_Business_Machines { int x; } namespace IBM = International_Business_Machines; void foo () { IBM::x++;
巢狀內部的 namespace 也可取別名,如: namespace mylib { … namespace matrix { void foo (); } namespace libM = mylib::matrix; void bar () { libM::foo();
using namespace_name::namespace_member; 宣告之後,我們就可以直接使用 namespace_member 而不須要在其前加上 namespace_name::。 譬如: namespace mylib { void foo (); } using mylib::foo; void bar () { foo();
using 宣告也和其它名稱一樣,有其 scope 的限制。 namespace blip { int bi = 16, bj = 15, bk = 23; } int bj = 0; void mainp () { using blip::bi; ++bi; // set blip::bi to 17 using blip::bj; ++bj; // set blip::bj to 16 int bk; using blip::bk; // error: redeclaration of bk. int wrongInit = bk; // error: bk is undefined here
using namespace namespace_name; using namespce 指令 using namespace 指令的格式如下: using namespace namespace_name; 宣告之後,我們就可以直接使用名稱空間 namespace_name 中的任何成員而不須要在前面加上 namespace_name::。 譬如: namespace mylib { void foo (); } using namespace mylib; void bar () { foo();
使用 using namespace指令相當於把其中的成員置於 global scope 一樣。 namespace blip { int bi = 16, bj = 15, bk = 23; } int bj = 0; void mainp () { using namespace blip; ++bi; // set blip::bi to 17 ++bj; // error: ambiguous ++::bj; // ok, global bj ++blip::bj; // ok, blip::bj int bk = 99; ++bk; // ok: local bk. int wrongInit = bk; // error: bk is undefined here
物件導向程式語言講義(Scope & Lifetime) std namespace C++ 把系統標準函式庫的宣告和定義都擺在名為 std 的名稱空間之中。如果你在程式中 include 無 .h 副檔名的標頭檔的話,就必須使用前面所述的規則來存取 std 名稱空間的成員,否則會造成編譯上的錯誤。換句話說,你可以採取以下的方式: 使用 using namespace std 讓 std 名稱空間的成員全部暴 露在 global scope 之中。 用 using std::ios 之類的 using 宣告來挑選特定的成員。 直接用 scope 運算子來指明,如 std::cin。 靜宜大學資訊管理學系 蔡奇偉 副教授編製