C++ 鍊氣期之變量的生命周期和作用域

1. 前言

什麼是變量的生命周期?

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

什麼是變量的作用域?

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

廣義而言,可以根據變量的聲明位置,把變量分為全局(全局作用域)變量局部(局部作用域)變量

  • 全局變量: 在一個較大的範圍之內聲明的變量。如在源代碼文件中聲明的變量能在整個文件中使用(文件級別作用域),在類中聲明的變量能在類中使用(類級別作用域)、名稱空間中聲明的變量可以在整個名稱空間內使用。除此之外,還有程序級別作用域,變量能在整個程序中使用。
  • 局部變量: 如函數體內聲明的變量(作用域函數級別)、代碼塊內聲明的變量(代碼塊級別的作用域)。

變量的聲明位置也決定了變量在內存中的存儲位置,如函數體內聲明的局部變量一般會存儲在中,如類中聲明的變量存儲在中,文件中聲明的全局變量存儲在全局\靜態存儲區。

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

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

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

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

1.png

下面繼續深入聊聊變量的存儲類型對生命周期和作用域的影響。

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 的家人。即使同名,但存在不同的空間中,彼此之間是無法可見的。

2.png

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

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

#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又變得可見。

3.png

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

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

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

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 中聲明如下變量:

5.png

main.cpp中如果需要使用 extern.cpp文件中的變量 guoKe_。則需要使用關鍵字extern加以說明。

6.png

輸出結果:

7.png

如果在 main.cpp中使用 guoKe_時沒有添加extern關鍵字,則會出錯。會認為在程序作用域內聲明了 2 個同名的變量。

如果在整個程序運行期間,需要一個在整個程序中大家都能訪問到的全局可用的變量時,則可以使用外部可見的存儲方案。

2.2.2 內部可見

在文件內當使用 static關鍵字聲明的變量其作用域為本文件可見,也就是內部可見。變量只能在聲明的文件內使用,不能在外部文件中使用,也是廣義上的全局變量

如下代碼,在文件 extern.cpp中聲明了一個使用 static關鍵字說明的變量 guoKe_

8.png

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

6.png

9.png

如果省略 main.cpp的變量 guoKe_前的extern 關鍵字。則相當於在 main.cpp文件中重新聲明了一個新的變量(程序級別),只是與 extern.cpp 文件中的變量同名(文件級別),且作用域比其要高。

10.png

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;
}

輸出結果:

12.png

和前文沒有使用 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. 總結

聲明變量時,存儲類別決定了變量的生命周期。

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