C++內存模型與名稱空間概念講解

程序可分為三部分:

頭文件:包含結構聲明和使用這些結構的函數的原型

源代碼文件:包含與結構有關的函數的代碼

源代碼文件:包含調用與結構相關的函數代碼。

頭文件中常包含的內容:

函數原型、使用#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++內存模型內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: