3. C++存儲方式(存儲持續性、作用域和鏈接性)與名稱空間


變量的存儲方式對函數、模板、類等同樣有意義。

翻譯單元(translation unit):According to standard C++ (wayback machine link) : A translation unit is the basic unit of compilation in C++. It consists of the contents of a single source file, plus the contents of any header files directly or indirectly included by it, minus those lines that were ignored using conditional preprocessing statements.

A single translation unit can be compiled into an object file, library, or executable program.

The notion of a translation unit is most often mentioned in the contexts of the One Definition Rule, and templates.

在同一個翻譯單元中的內容可視爲是同一個文件中的。

3.1 存儲持續性

  • 自動存儲持續性:在函數中聲明的變量的存儲持續性爲自動的。它們佔用的內存會在執行完函數或代碼塊時被自動釋放。C++中有兩種存儲持續性爲自動的變量。

  • 靜態存儲持續性:在函數外定義或使用關鍵字static定義的變量的存儲持續性都爲靜態的。它們在整個程序運行過程中都存在。C++中有三種存儲持續性爲靜態的函數。

  • 動態存儲持續性:使用new運算符分配的內存將一直存在,直到使用delete運算符將內存釋放或程序結束爲止。這種內存的存儲持續性爲動態,有時被稱爲自由存儲(free store)或堆(heap)。

  • 線程存儲持續性(C++):使用關鍵字thread_local生命的變量。

3.1.1 自動存儲持續性

  • 自動變量與棧

C++中的自動變量儲存在棧中。程序使用一個指向棧頂(最後添加的變量)和一個指向棧底(第一個變量)的指針來跟蹤棧。
棧是一種LIFO(後入先出)的數據結構。在釋放變量時並不會寫入數據,而僅僅只是向下移動指針。

傳統的K&R C不允許自動初始化自動存儲的數組與結構,只允許初始化靜態結構與變量。

  • 寄存器變量(register關鍵字)

在C語言中,register表示建議使用CPU寄存器來儲存自動變量,以提高訪問變量的速度。
在早期的C++,隨着編譯器與硬件越來越複雜,register表示提示某一自動變量使用頻繁,編譯器可以對其進行特殊處理。
到了C++11,register只是顯式地指出變量是自動的(這個含義與C語言和早期的C++中的auto一樣)。保留register的原因只是避免使用了register的早期代碼非法。

3.1.2 靜態存儲持續性

靜態存儲變量有三種鏈接性,取決於變量的聲明位置。

  • 外部鏈接性:名稱在其他文件中可見。
  • 內部鏈接性:名稱只在當前文件中可見。
  • 無連接性:名稱只在當前代碼塊中可見。

靜態變量的數目在程序運行過程中是不變的,所以不需要特殊的裝置(如棧)來管理它們。編譯器直接分配固定的內存塊來儲存靜態變量。
另外,如果沒有顯式地初始化靜態變量(包括數組和結構),編譯器會將所有數據位都設置爲0。

在全局作用域(不在任何函數和代碼塊中)聲明的變量默認爲內部鏈接性的靜態變量。
要聲明爲外部鏈接性的靜態變量,要在在全局作用域中聲明的同時在聲明前加上關鍵字static

以下爲三種靜態變量的聲明示例。

int global = 1000;    // 靜態變量, 內部鏈接性
static int one_file = 500;    // 靜態變量, 外部鏈接性

int main()
{
    // ...
}

void func(int n)
{
    static const int count = 0;    // 靜態變量, 無鏈接性
}

靜態變量的初始化

  • 靜態初始化:在編譯期間就給變量分配內存。
    • 零初始化:見上文。
    • 常量表達式初始化:聲明靜態變量時用常量的表達式給給靜態變量賦值。
  • 動態初始化:用非常量的表達式初始化,在程序運行期間進行。

3.1.2.1 靜態存儲持續性&外部鏈接性,extern關鍵字

C++中提供了兩種變量聲明。

  1. 定義聲明(簡稱定義):在聲明時給變量分配存儲空間。
  2. 引用聲明:引用已有的變量,不給變量分配存儲空間。

若要使用在其他文件中的變量,需要在本文件的全局作用域中使用引用聲明。

// file1.cpp
#include <iostream>

extern int i = 10;    // 這裏的extern關鍵字不是必須的

int main()
{
    // ...
    return 0;
}
#include <iostream>

extern int i;    // 引用聲明

int find(const char* str, const char ch)
{
    // ...
    return pos;
}

3.1.2.2 靜態存儲持續性&內部鏈接性,static關鍵字

在要被鏈接(link)在一起的文件中簡單地聲明同名變量違反單定義規則(One Definition Rule,ODR)。會導致編譯錯誤。
使用static關鍵字可以指明變量的鏈接性爲內部,而非要提供全局定義。

// file1.cpp
#include <iostream>

extern int i = 10;

int main()
{
    // ...
    return 0;
}
#include <iostream>

// int i = 50;    // error
static int i;    // valid

int find(const char* str, const char ch)
{
    // ...
    return pos;
}

3.1.2.3 靜態存儲持續性&內部鏈接性

3.1.3 動態存儲持續性(new&delete, new[]&delete[])

  • 使用new/new[]運算符初始化

可以配合使用圓括號初始化單值變量,可以用花括號(C++11)初始化結構或數組及單值變量

// 圓括號
int* pi = new int(6);
// 花括號
int* arri = new int[4]{12, 124, 534, 653};
position* pos = new position{3, 4};
char* ch = new char{'c'};
  • new/new[]失敗時

在最初的10年中,C++在這時返回空指針,但現在,將引發異常std::bad_alloc

  • 定位new(placement new)運算符

要使用定位new運算符,要包含頭文件<new>。可以指定新分配內存的地址。
定位運算符的工作原理大致上只是將傳遞給它的地址轉換爲void*類型並返回。

char buffer[50];
int* arri = new(buffer) int[20];

C++不會追蹤已分配的地址,所以可以在釋放內存前再次分配已被分配的內存,不過這樣做會覆蓋原來的數據。如果再次分配時覆蓋了已被分配的內存,那麼在釋放內存時就會重複釋放內存,這將導致錯誤。

3.1.3.1 類與動態內存分配

如果使用定位new運算符將類的對象(數組)“放在”動態分配的內存上,有兩點要注意。

  1. 不要用delete([])釋放類的對象(數組),因爲定位new運算符只是將指針的值強制轉換爲void*,並沒有分配內存。
  2. 如果這個類中使用了動態內存分配,要先顯式地調用析構函數(其中有delete([])語句)。

3.2 作用域與鏈接性

  • 作用域描述了名稱在文件或翻譯單元中的多大範圍內可見。按作用域可將變量分爲全局變量局部變量

    • 全局變量:在所有函數和代碼塊外定義的變量,也稱外部變量
    • 局部變量:在函數定義或代碼塊中(包括函數定義或代碼塊前的括號)定義的變量。
    • 函數原型作用域(function prototype scope):函數聲明中的名稱只在包含參數列表的括號內可用。
  • 鏈接性

    • 內部鏈接性:從定義的位置到定義所在的文件的結尾。
    • 外部鏈接性:從定義的位置到文件的結尾。
    • 無鏈接性:只能在聲明的代碼塊中使用,例如局部變量。
  • 存儲持續性與鏈接性

    • 自動變量的鏈接性爲局部。
    • 靜態變量的作用域是全局還是局部取決於它是怎樣被定義的。

C++中的函數的作用域可以是整個類或整個名稱空間(包括全局作用域,全局作用域是名稱空間作用域的特例),但不能是局部的。因爲函數不能在代碼塊中定義函數(內聯函數除外,內聯函數是依靠預編譯替換代碼實現的,某種意義上不是函數而更像是宏)。

3.2.1 局部變量的"覆蓋作用"

局部變量會覆蓋同名的有鏈接性的變量(包括內部鏈接性和外部鏈接性)。
C++提供了作用域解析運算符::,能做到訪問被覆蓋的這些變量。

#include <iostream>
using namespace std;

const char* str = "extern";    // 外部鏈接性
// const stetic char* str = "extern";    // 內部鏈接性

int main()
{
    const char* str = "main()";
    const char* str1 = "main()";
    {
        const char* str = "block in main()";
        cout << "-- in block in main() --" << endl;
        cout << "str:\t" << str << endl;
        cout << "::str:\t" << ::str << endl;
    }
    cout << "------ in main() ------" << endl;
    cout << "str:\t" << str << endl;
    cout << "::str:\t" << ::str << endl;
    return 0;
}

輸出:

-- in block in main() --
str:    block in main()
::str:  extern
------ in main() ------
str:    main()
::str:  extern

3.3 說明符與限定符

3.3.1 存儲說明符

  • auto(C++11中不再是說明符)
  • register
  • static
  • extern
  • thread_local(C++11中新增)
  • mutable

在同一聲明中不可以使用多個說明符,但thread_local除外,它可以與staticextern一起使用。
thread_local指出變量的生命週期與其所在的線程一樣長。thread_local之於線程,就和常規靜態變量之於整個程序。

mutable指出即使在const對象中,指定變量的值也是可以修改的。

3.3.2 cv-限定符

  • const
  • volatile

volatile指出即使程序代碼不會修改變量的值,變量的值仍然可能改變(如其他程序或硬件就可能會修改某些變量的值)。
該關鍵字的作用旨在避免編譯器做出錯誤的優化。例如,程序在幾條語句中多次使用了某個變量的值,而且其中沒有改變變量的值的代碼。那麼編譯器就可能會將變量的值儲存在寄存器中。在之後的使用中不會再此查找變量的值,而是直接使用寄存器中的值。

在C++中(但在C中不是),const限定符對變量的存儲方式會造成影響。const全局變量的默認鏈接性爲內部的
若要定義鏈接性爲外部的conat變量,需要使用extern關鍵字。

// 以下兩行代碼等效,都是鏈接性爲內部的const變量
const int i = 10;
static const int i = 10;
// 鏈接性爲外部的conat變量
extern const int i = 10;

3.4 語言鏈接性

由於加入了函數重載,C++編譯器在對函數執行名稱修飾和名稱矯正時生成的符號名稱會包含函數的參數信息,而C語言中不會。
這兩種特殊的鏈接性分別稱爲C++語言鏈接性C語言鏈接性

由於符號名稱不同,C和C++之間在使用對方預先編譯好的庫等情況時會出現錯誤。
爲解決這個問題可以在函數原型使用形如extern "C"的說明符來指定要使用的約定。

extern "C" void spiff(int);    // C語言鏈接性, 可能將spiff(int)轉換爲_spiff。
extern void spiff(int);        // C++語言鏈接性, 可能將spiff(int)轉換爲_spiff_i。另外,extern是可選的。
extern "C++" void spiff(int);  // 與第二句等效

C和C++語言鏈接性說明符是C++標準規定的,但具體實現可以有其他語言鏈接性說明符。

3.5 名稱空間

聲明區域:聲明區域值變量聲明所在的區域,包括聲明語句前的區域。聲明區域可以是某個代碼塊,也可以是整個文件。
潛在作用域:從聲明語句開始,直到聲明區域的終點。
作用域:變量對程序而言可見的地方,範圍是潛在作用域除去被嵌套代碼塊中的同名變量隱藏的區域。

每個名稱空間都對應一個聲明區域。C++標準提供了名稱空間工具來通過定義一個新的聲明區域來創建命名的名稱空間。

3.5.1 創建名稱空間

3.5.1.1 創建語法

名稱空間可以位於另一個名稱空間中,但是不能位於代碼塊中。
除了用戶定義的名稱空間,還有全局名稱空間,它對應文件級聲明區域。
在全局名稱空間外的名稱空間內創建名稱空間稱爲嵌套式名稱空間

//jack_jill.h

namespace Jack{
    char* cake;
    float length;
    struct Student Tom;
    string object;
    int find(const char*, conat char);
}

namespace Jill{
    static int cake;
    double length;
    struct Student Tom;
    string object;
    char* str;
}

3.5.1.3 名稱空間的開放性(open)

名稱空間是開放(open)的,這意味着可以將新的名稱添加到已有的名稱空間中。

// 以下代碼將student結構體的聲明添加到名稱空間Jill中
namespace Jill{
    struct student{
        string fullname;
        int id;
        int grade_num;
        int class_unm
    };
}

3.5.2 使用名稱空間

3.5.2.1 作用域解析運算符 ::

#include "jack_jill.h"

char* str = "Still water runs deep";

Jack::length = 10.0;
Jack::find(str, 'u');
Jill::cake = 10;
Jill::length = 12.0;

3.5.2.2 using聲明與using編譯指令

using聲明會引入特定的名稱,using編譯指令則會引入整個名稱空間。
using聲明與using編譯指令都是可以在代碼塊或全局名稱空間中使用,將名稱導入到特定區域。

區別:using聲明會與同名的變量衝突,而using編譯指令不會。
目標作用域中的名稱會隱藏用using編譯指令導入的相同的名稱。不過可以用作用域解析運算符解決這一問題。

一般來說,using編譯指令比using聲明要危險。

  • 由於名稱空間的開放性,難以確定名稱空間有哪些變量。
  • 當有同名變量時,編譯器不會報錯,可能會因此產生難以察覺的錯誤。

在名稱空間中也可以使用using聲明和using編譯指令

3.5.2.3 嵌套式名稱空間

namespace Jack{
    namespace myth{
        int n;
        }
}
#include "jack_jill.h"
...

Jack::myth::n;

...

《C++ Primer Plus》第六版中指出using編譯指令是可傳遞的(using更內層的名稱空間後會將其外層名稱空間也導入),但在我測試時貌似並不是這樣。可能之後C++標準又有了變化,也可能是我理解有誤。

3.5.2.4 名稱空間的別名

使用類似賦值語句的語句可以爲名稱空間設定別名。

// 假設存在名爲my_very_favorite_things的名稱空間,下面的語句爲其設定了別名。
namespace mvft = my_very_favorite_things;

3.5.2.5 未命名的名稱空間

創建沒有名稱的名稱空間會像是在後面跟着using編譯指令一樣。這樣來看,這就像是聲明瞭全局變量。但是由於沒有名稱不能再其他文件中使用,因此該名稱空間中的名稱是內部鏈接性的。這提供了內部鏈接性的靜態變量的替代品。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章