C++ 煉氣期之變數的生命週期和作用域

2022-06-17 15:00:37

1. 前言

什麼是變數的生命週期?

從變數被分配空間到空間被收回的這一個時間段,稱為變數的生命週期

什麼是變數的作用域?

在變數的生命週期內,其儲存的資料並不是在任何地方都能使用,變數能使用的範圍,稱為變數的作用域。

廣義而言,可以根據變數的宣告位置,把變數分為全域性(全域性作用域)變數區域性(區域性作用域)變數

  • 全域性變數: 在一個較大的範圍之內宣告的變數。如在原始碼檔案中宣告的變數能在整個檔案中使用(檔案級別作用域),在類中宣告的變數能在類中使用(類級別作用域)、名稱空間中宣告的變數可以在整個名稱空間內使用。除此之外,還有程式級別作用域,變數能在整個程式中使用。
  • 區域性變數: 如函數體內宣告的變數(作用域函數級別)、程式碼塊內宣告的變數(程式碼塊級別的作用域)。

變數的宣告位置也決定了變數在記憶體中的儲存位置,如函數體內宣告的區域性變數一般會儲存在中,如類中宣告的變數儲存在中,檔案中宣告的全域性變數儲存在全域性\靜態儲存區。

程式執行時,會向OS申請一塊記憶體區域用來儲存程式執行時的指令和資料。C++執行系統會對分配到的記憶體區域進行管理。相當於OS給的是毛坯房,自己還需要裝修一下,專業叫記憶體管理。其中有 2 個很重要的隔間:

  • 棧: 這裡的棧有 2 層意思,一是對一個特定記憶體區域的命名,另一層含義是儲存資料時遵守資料結構理論,按先進後出原則。可以認為此隔間只有一個門:資料的進與出都是走這個門。

    函數的引數、函數體內宣告的變數都會儲存在棧中,棧的特點是由執行時系統自動分配與釋放,另棧分配空間是向高地址向低地址擴張。

  • 堆: 堆是一個自由、開放式儲存空間。開發者可以根據邏輯需要隨時申請,但開發者需要根據實際情況手動釋放。堆的使用是由低地址向高地址擴張。

下面繼續深入聊聊變數的儲存型別對生命週期和作用域的影響。

2. 儲存型別

生命週期指資料在記憶體中保留的時間,也可稱為儲存持續性

變數的生命週期和變數的作用域是有區別的。就如同你家裡養的花開了 1 個月,但只有你的家裡人才能聞到花香,花園裡的花只開了 1 天,但是,公園裡的所有人都能聞到花香。

生命週期相當於你在某一個公司工作了近 10 年,作用域則相當於你一直服務於開發部。

可以說變數的生命週期較長,其能使用的範圍可能很廣,但不能說資料在記憶體中儲存的時間越久,其能使用的範圍就一定很廣。

作用域一定要在變數的生命週期之內討論才有意義。

C++有如下幾種儲存方案,儲存方案不同,其變數生命週期也不一樣。

  • 自動儲存:如函數定義時宣告的變數就屬於自動儲存類別。生命週期較短,僅在函數被呼叫到函數執行結束後其記憶體就會被釋放。

  • 靜態儲存:在函數定義外宣告的變數、使用關鍵字static宣告的變數都為靜態儲存類別。它們在整個程式執行過程中都存在。

  • 執行緒儲存:在並行、並行環境中,如果變數使用關鍵字 thread_local宣告,則生命週期和所依附的執行緒生命週期同步。

    本文不會對此儲存類別展開細聊。

  • 動態儲存:使用 new運運算元宣告的變數,其儲存塊一般在堆中,如果開發者不顯示釋放(delete)會一直存在,直到程式結束。

    本文不會對此儲存類別展開細聊。

2.1 自動儲存

函數體內宣告的變數屬於自動儲存類別。變數在函被呼叫時生命開始(分配空間),函數執行完畢後,變數的生命結束(回收空間)。此型別的變數的特點:

  • 區域性的。

  • 沒有共用性。

共用性:指變數中的資料是否能讓其它的程式碼可見、可用。

區域性變數的區域性的含義可以理解為不共用,作用域範圍只供自己使用,。

如下程式碼:

#include <iostream>
void test(){
	int tmp=10;
}
int main(int argc, char** argv) {
	int tmp=20;
    test();
	return 0;
}

在函數 test中宣告的 tmp變數只有在test函數被呼叫時才會分配空間,當函數呼叫結束後自動釋放。

同時maintmp變數也區域性變數。雖然 testmain函數中有同名的 tmp變數,兩者是互不可見的,或者說兩者存在於 2 個不同的時空中。

為什麼會互不可見?

原因可用函數的底層呼叫機制解釋:

  • C++呼叫函數時,會在棧中為函數分配一個區域用來儲存此函數有關的資料,稱這個區域叫棧幀
  • 每一個函數所分配到的棧幀是隔離的,且按先呼叫先分配的棧原則。

上述的情形相當於 2 個家裡都有一個叫 temp 的家人。即使同名,但存在不同的空間中,彼此之間是無法可見的。

再聊一下變數間的隱藏性。

如下程式碼,兩次輸出的結果分別是多少?

#include <iostream>
using namespace std;
int main(int argc, char** argv) {
	int temp=20;
	{
	 int temp=10;
	 cout<<"程式碼塊中輸出:"<<temp<<endl; 
	} 
	cout<<"程式碼塊外輸出:"<<temp<<endl; 
	return 0;
}

輸出結果是:

程式碼塊中輸出:10
程式碼塊外輸出:20

什麼是隱藏性?

main函數中的第一次宣告的 temp變數實際作用域是整個 main函數中,但是,當執行到內部程式碼塊時,發現程式碼塊中的 temp變數和程式碼塊外的變數 temp同名。此時C++如何處理這種情況?

C++會採用就近原則,進入程式碼塊後使用程式碼塊中定義的 temp變數,外部的 temp 變數被暫時隱藏起來。離開程式碼塊後,重回 main函數的主體,回收程式碼塊使用的記憶體資源。此時main函數中的 temp又變得可見。

當執行流從高階別的作用域進入低階別作用域後,如果有同名變數,則會隱藏高階別變數的可見性。

當再次從低階別作用域返回高階別作用域後,高階別作用域中的同名變數會變得可見。

在同一個作用域內是不能有同名變數的,如下程式碼,會報錯。

int main(int argc, char** argv) {
    //函數體內這一範圍內不能出現同名變數
	int guoKe; 
	int guoKe; 
	return 0;
}
int main(int argc, char** argv) {
    {
    //同一程式碼塊中不能出現同名變數
	int guoKe; 
	int guoKe; 
    }
     return 0;
}

理解變數的隱藏性後,就不會為下面程式碼的輸出結果感到吃驚了。

#include <iostream>
using namespace std;
int main(int argc, char** argv) {
	//主函數中可見
    int temp=20;
	{
        //程式碼塊外的不可見
		int temp=10;  
		{
			//自己可見,程式碼塊外的都不可見
             int temp=5;
            //輸出 5
			cout<<"輸出一:"<<temp<<endl;
		}
        //輸出 10
		cout<<"輸出二:"<<temp<<endl;
	}
    //輸出 20 
	cout<<"輸出三:"<<temp<<endl;
	return 0;
}
//輸出結果
輸出一: 5
輸出二:10
輸出三:20

C++ 中有 2 個與自動儲存變數相關的關鍵字:

  • auto: auto關鍵字在C++ 11以前的版本和 C語言中,用來顯示指定變數為自動儲存。 C++ 11中表示自動型別推斷。
  • register:此關鍵字由C語言引入,如果有 register關鍵字的變數宣告為暫存器變數,目的是為加快資料的存取速度。而在C++ 11中的語意是顯示指定此變數為自動儲存,和以前的 auto 功能相同。

2.2 靜態儲存

C++對記憶體進行管理劃分時,除了分有之外,還分有全域性\靜態區域(還有常數區域自由儲存區域),具有靜態儲存類別的變數被儲存在此區域。

靜態儲存變數的特點:

  • 生命週期長。其生命週期從變數宣告開始,可以直到程式結束 。
  • 如前文所說,生命週期長,並不意味著誰都可以看得見它,誰都可以使用它。其作用域有外部可見、內部可見、區域性可見 3 種情形。

2.2.1 外部可見

外部可見作用域,可認為在整個程式中可用。此型別變數為廣義上的全域性變數

一個有一定規模的程式往往會有多個原始碼檔案。

如下程式碼:

#include <iostream>
int guoKe; 
using namespace std;
int main(int argc, char** argv) {
	cout<<guoKe;
    return 0;
}
//輸出值為 `0`

變數 guoKe在檔案中宣告,預設為靜態儲存型別變數。變數guoKe可以在本檔案中使用,也可以在外部檔案中使用。如果宣告時沒有為其賦值,C++會對其初始化,賦值為 0

Tip: 本檔案可使用的範圍指從變數宣告位置開始一直到檔案結束的任一位置都能使用。外部檔案可使用指在另一個檔案中也可以使用。

如果要在檔案的外部使用,需要使用 extern變數說明符。如下圖,保證 main.cppextern.cpp 2 個檔案在同一個專案中。且在 extern.cpp 中宣告如下變數:

main.cpp中如果需要使用 extern.cpp檔案中的變數 guoKe_。則需要使用關鍵字extern加以說明。

輸出結果:

如果在 main.cpp中使用 guoKe_時沒有新增extern關鍵字,則會出錯。會認為在程式作用域內宣告了 2 個同名的變數。

如果在整個程式執行期間,需要一個在整個程式中大家都能存取到的全域性可用的變數時,則可以使用外部可見的儲存方案。

2.2.2 內部可見

在檔案內當使用 static關鍵字宣告的變數其作用域為本檔案可見,也就是內部可見。變數只能在宣告的檔案內使用,不能在外部檔案中使用,也是廣義上的全域性變數

如下程式碼,在檔案 extern.cpp中宣告了一個使用 static關鍵字說明的變數 guoKe_

其使用範圍只能是在 extern.cpp檔案中。如果在 main.cpp中用如下方式使用,則會出錯。

如果省略 main.cpp的變數 guoKe_前的extern 關鍵字。則相當於在 main.cpp檔案中重新宣告了一個新的變數(程式級別),只是與 extern.cpp 檔案中的變數同名(檔案級別),且作用域比其要高。

2.2.3 區域性可見

在函數體內使用 static宣告的變數, 如下宣告語句,則認為變數的作用域是區域性可見,變數只能在宣告它的函數體內使用。也是廣義上的區域性變數

#include <iostream>
using namespace std;
void test(){
    //靜態區域性變數
	static int temp=20;
	temp++;
	cout<<temp<<endl;
} 

int main(int argc, char** argv) {
   test();
   return 0;
}

輸出結果:

和前文沒有使用 static關鍵字宣告的自動儲存型別的區域性變數有本質的不同。

  • 使用 static關鍵字宣告的區域性變數其生命週期是程式級別的。即使函數呼叫結束,變數依然還在,資料也還在。
  • 變數只能在宣告它的函數內使用,其作用域是函數級別的。這也驗證了前文所說的生命週期長並意味著變數的作用域範圍就一定廣。

如下程式碼反覆呼叫函數,在輸出結果時會發現變數 temp 中的資料在不停增加。

#include <iostream>

using namespace std;
void test(){
	static int temp=20;
	temp++;
	cout<<temp<<endl;
} 

int main(int argc, char** argv) {
   test();
   test();
   return 0;
}

輸出結果:

21
22

3. 總結

宣告變數時,儲存類別決定了變數的生命週期。

生命週期指變數的存活時間,作用域指變數能在一個什麼範圍之內被使用。兩者之間有很明顯的區別,本文聊到了自動儲存型別和靜態儲存類別的變數。另,如動態儲存和執行緒儲存可以自行了解。