首頁 > 軟體

C++記憶體模型與名稱空間概念講解

2023-01-04 14:00:18

程式可分為三部分:

標頭檔案:包含結構宣告和使用這些結構的函數的原型

原始碼檔案:包含與結構有關的函數的程式碼

原始碼檔案:包含呼叫與結構相關的函數程式碼。

標頭檔案中常包含的內容:

函數原型、使用#define或const定義的符號常數、結構宣告、類宣告、模板宣告、行內函式。

檔案在編譯時可以解釋為翻譯單元。

1、儲存持續性與作用域及連結性

儲存類別如何影響資訊在檔案間的共用呢?C++使用三種不同的方案來儲存資料,這些方案的區別在於資料保留在記憶體中的時間:

自動儲存特性:在函數定義中宣告的變數(包括函數引數)的儲存持續性為自動的,他們在程式開始執行其所屬的函數或程式碼塊時被建立,在執行完函數或程式碼塊時,他們使用的記憶體被釋放

靜態儲存特性:在函數定義外定義的變數和使用關鍵字static定義的變數的儲存持續性都為靜態,他們在程式整個執行過程中都存在

執行緒儲存持續性:如果變數使用關鍵字thread_local宣告,其宣告週期和所屬執行緒一樣長

動態儲存特性:用new運運算元分配的記憶體將一直存在,直到使用delete運運算元釋放或者程式結束。

2、作用域和連結

作用域描述了名稱在檔案(翻譯單元)的多大範圍內可見。連結性描述了名稱如何在不同單元間共用,連結性為外部的名稱可在檔案間共用,連結性為內部的名稱只能由一個檔案中的函數共用,自動變數的名稱沒有連結性,因為他們不能共用。

3、靜態持續變數

靜態儲存持續性變數有三種連結性:外部、內部和無連結性,這三種連結性都在整個程式執行期間存在,它們的壽命更長,編譯器將分配固定的記憶體。另外如果沒有顯式初始化靜態變數,編譯器將把它設定為0。

int global = 100;			// 外部連結
static int one_file = 10;	// 內部連結
extern double up = 0;		// 外部連結
int func(int n)
{
	static int cnt = 0;		// 無連結,只在程式碼塊內使用
	return 0;
}

4、靜態持續性和外部連結性

連結性為外部的變數稱為外部變數,他們的儲存性為靜態,作用域為整個檔案。

C++提供有兩種變數宣告,一種是定義宣告,它給變數分配儲存空間;另一種是參照宣告,它不給變數分配儲存空間,而是參照已有的變數。

double d;        // 定義
extern int a;    // 參照宣告
extern char c = 'a';    定義宣告

如果要在多個檔案中使用外部變數,只需要在一個檔案中包含該變數的定義,在使用該變數的其他所有檔案中,都必須使用關鍵字extern宣告它。

// a.h
#pragma once
int getGlobalNum()
// a.cpp
#include "a.h"
extern int global;
int getGlobalNum()
{
	return global;
}
// b.cpp
#include "a.h";
int global = 100;
int main()
{
	cout << getGlobalNum() << endl;		// 100
	getchar();
}

定義與全域性變數同名的區域性變數後,區域性變數將隱藏全域性變數。C++中提供了作用域解析運運算元(::),放在變數名前面是,該運運算元表示使用變數的全域性版本。

// a.cpp
#include "a.h"
extern int global;
int getGlobalNum()
{
	int global = 10;
	return ::global;
}
// b.cpp
#include "a.h";
int global = 100;
int main()
{
	cout << getGlobalNum() << endl;		// 100
	getchar();
}

5、靜態持續性與內部連結性

將static限定符用於作用域為整個檔案的變數時,該變數的連結性為內部的,連結性為內部的變數只能在其所屬的檔案中使用。

常規外部變數具有外部連結性,即可以在其他檔案中使用,如果要在其他檔案中使用相同的名稱來表示其他變數,需要使用static。(如果在兩個檔案中有兩個相同名稱的外部變數,那麼第三個檔案參照時就不能確定參照哪一個)

// a.cpp
extern int a = 10;
// b.cpp
extern int a = 30;    // error
static int a = 30;    // OK

6、靜態儲存性與無連結性

在程式碼塊中使用static時,將導致區域性變數的儲存持續性為靜態的,該變數在程式碼塊不在活動時仍存在。兩次函數呼叫之間,靜態區域性變數的值將保持不變。初始化了靜態區域性變數,程式只在啟動時進行一次初始化,以後再呼叫時將不會再被初始化。

void add2()
{
	static int value = 0;
	cout << value++ << " ";		// 0 1 2
}
	for (size_t i = 0; i < 3; i++)
		add2();

7、const

預設情況下全域性變數的連結性為外部的,但是const全域性變數的連結性為內部的,也就是說C++中全域性const定義就像使用了static說明符一樣。因為有該特性,const修飾的常數可以放在標頭檔案中,並且可以在多個檔案中使用該標頭檔案(如果const宣告是外部的,根據單定義規則將出錯,即只有一個檔案可以使用const宣告,其他需要用extern來提供參照宣告)。

extern const int a = 10;    // 外部宣告的常數
       const int b = 10;    // 內部宣告的常數

如果希望某個常數的連結性是外部的,可以使用extern來覆蓋預設的內部連結性。

8、函數和連結性

C++不允許在一個函數中定義另一個函數,所以所有的函數的儲存持續性都自動為靜態的,即整個程式執行期間都存在。

預設情況下,函數的連結性為外部的,可以使用extern來指出函數實在另一個檔案中定義的(而不用去加標頭檔案);

extern int pub();

可以使用static將函數的連結性設定為內部的,使之只能在一個檔案中使用,必須同時在原型和定義中使用該關鍵字。

static int private();
static int private()
{
}

使用static修飾函數並不僅意味著該函數只在當前檔案中可見,還意味著可以在其他檔案中定義同名的函數。

// a.cpp
int pub()
{
	return 0;
}
// b.cpp
int pub()        // error
{
	return 0;
}
static int pub()    // OK
{
    return 0;
}

在定義靜態函數的檔案中,靜態函數將覆蓋外部定義。

對於每個非行內函式,程式只能包含一個定義(如果兩個檔案中包含相同名稱的外部函數,那麼第三個檔案使用外部函數時將不能確定使用哪個定義)

行內函式不會受到單定義規則約束,所以可以放在標頭檔案中,這樣包含有標頭檔案的每個檔案都有行內函式的定義。C++要求同一個函數的所有內聯定義都必須相同。

9、語言的連結性

連結程式要求每個不同的函數都有不同的符號名。C語言中一個名稱只能對應一個函數,這很容易實現,C語言編譯器可能將spiff翻譯為_spiff。但是在C++中,同一個名稱可能對應多個函數,必須將這些函數翻譯為不同的符號名稱,可能將spiff(int)翻譯為_spiff_i,將spiff(double)翻譯為_spiff_d。

連結程式尋找與C++函數呼叫匹配的函數時,使用的方法與C語言不同,要在C++程式中使用C庫中預編譯的函數可以在宣告時指定連結性說明符,比如下面第一種指定用C語言的連結方式去連結spiff方法

extern "C" void spiff(int);		// 使用C語言連結性
extern void spoff(int);			// 預設使用C++連結性
extern "C++" void spaff(int);	// 顯式指定C++連結性

假設有一個C庫libC,裡面有一個函數spiff,如果我們在C++程式中直接參照標頭檔案,並且呼叫函數,那麼會出現找不到函數定義的情況。這時候我們可以用extern "C"將標頭檔案包裹起來,表示用C語言的連線方式去連結方法:

extern "C" {
#include "a.h"
}

10、名稱空間

一個名稱空間中的名稱不會與另一個名稱空間中的相同名稱發生衝突,名稱空間可以是全域性的,也可以位於另一個名稱空間中,但不能位於程式碼中。預設情況下,名稱空間中宣告的名稱的連結性為外部的。

除了使用者定義的名稱空間外,還有一個全域性名稱空間,它對應於檔案及宣告區域,因此前面所說的全域性變數現在被描述為位於全域性名稱中間中。

namespace A
{
	int a = 10;
	void printk()
	{
	}
}
namespace B {
	int a = 20;
	void printk()
	{
	}
}

名稱空間是開放的,可以把名稱加入到已有的名稱空間中:

namespace B
{
	void printkk();
}

C++提供using宣告和using編譯兩種機制來簡化對名稱空間中名稱的使用:

using宣告使特定的識別符號可用,在函數外使用using宣告,可以把名稱新增到全域性名稱空間中

using B::printkk;
int main()
{
	using B::printk;
	printk();
}

using編譯指令使整個名稱空間可用,在全域性宣告區域中使用using編譯指令,使得該名稱空間中的名稱全域性可用:

using namespace B;
int main()
{
	// using namespace B;
	printk();
}

如果名稱空間和宣告區域定義了相同的名稱,如果使用using宣告來匯入,則兩個名稱會發生衝突:

namespace B {
	int a = 20;
}
int main()
{
	using B::a;
	// int a = 10;		// error
	cout << a;
	getchar();
	return 0;
}

如果用using編譯指令將名稱空間的名稱匯入,則區域性版本將隱藏名稱空間版本:

namespace B {
	int a = 20;
}
int a = 100;
int main()
{
	using namespace B;
	int a = 10;
	cout << a << " " << ::a << " " << B::a << endl;		// 10 100 20
}

一般來說,使用using宣告比使用using編譯指令更安全,如果名稱和區域性名稱發生衝突,編譯器將發出指示。using編譯匯入所有名稱可能包括不需要的名稱。

名稱空間的宣告可以進行巢狀:

namespace element {
	namespace fire {
	}
}
using namespace element::fire;

可以在名稱空間中使用using編譯指令和using宣告:

namespace spaceA {
	int a = 10;
}
namespace spaceB {
	using namespace A;
}

可以給名稱空間建立別名,來簡化巢狀名稱空間的使用:

namespace spaceA {
    namespace B {
	    int a = 10;
    }
}
namespace spaceX = spaceA::spaceB;
spaceX::a = 100;

名稱空間的使用指導原則:

使用在已命名的名稱空間中宣告的變數,而不是使用外部全域性變數;

使用在已命名的名稱空間中宣告的變數,而不是使用靜態全域性變數;

如果開發了一個函數庫或者一個類庫,將其放在一個名稱空間中;

不要在標頭檔案中使用using編譯指令,如果非要使用將其放在所有預處理指令之後;

匯入名稱時,首選使用作用域解析運運算元或using宣告的方法;

對於using宣告,首選將其作用域設定為區域性而不是全域性。

到此這篇關於C++記憶體模型與名稱空間概念講解的文章就介紹到這了,更多相關C++記憶體模型內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com