C++】記憶體對位
C++ 11 新引入的內容我幾乎在文章中已經全部介紹過了, 但是目前還剩下兩個沒有介紹, 一個是標識符 alignas 與運算子 alignof, 另外一個是屬性. 屬性我們將在稍後介紹, 首先來介紹 alignasalignof

在介紹這兩個關鍵字之前, 我需要引入記憶體對位的內容, 這並不是 C++ 11 才引入的

對於某一個固定的型別, 硬體是按照一定的倍數對記憶體進行存取的. 例如對於 64 位元的作業系統來說, 一個 int 型別的大小為 4. 因此, 硬體是按照 4 的倍數進行存取的. 若某一個變數的記憶體位址為 0x000000000004 (簡寫為 0x4), 則這一個 int 變數是對位的

給定一個記憶體位址和型別, 要測試是否對位, 我們可以使用

記憶體位址 % 型別大小

如若上述表達式回傳的是 0, 則這個型別是對位的; 如若上述表達式回傳的結果是 0 以外的任何數, 那麼記憶體就不是對位的

對於某個 int 變數, 假設這個變數的記憶體位址為 0x6, int 型別的大小為 4, 那麼硬體首先存取 0x4, 向後取 4 個大小的位元, 再存取 0x8, 向後取 4 個大小的位元, 最後將 0x6, 0x7, 0x8, 0x9 四個記憶體位址中對應的數據拼接之後再回傳. 如果記憶體是對位的, 那麼直接在起始位址向後存取型別大小個位元即可

記憶體對位與不對位相比較, 首先是效率問題. 不對位的記憶體要存取的次數比對位的記憶體要存取的次數多, 另外不對位的記憶體資料需要進行拼接. 然後是出錯的問題, 對於現代的民用電腦來說, 很少會存在這種問題, 但是對於有些 CPU 架構, 若存取的記憶體並不是對位的, 那麼硬體可能會擲出例外情況甚至拒絕存取的請求

C++ 編碼器在預設情況之下會自動對記憶體進行對位處理, 考慮下列結構體 (64 位元的作業系統下)

struct Foo {

    char c[3];

    int i;

};

在足夠理想的情況下, sizeof(Foo) 回傳的結果為 7. 也就是說, 如果成員變數 c 被放置在 0x4 的記憶體位址處, 則成員變數 i 本應被放置在從 0x7 起的記憶體位址處

若編碼器沒有進行對位, 則記憶體中的數據是需要進行拼接的. 不但這個 Foo 型別的資料要進行拼接, 甚至位於它之後的所有資料都需要進行拼接. 如若在編碼器沒有進行對位的情況下, 那麼我們可以使用標識符 alignas 進行記憶體對位

alignas 的用法為 : alignas(expr)alignas(T) 或著 alignas(pack)

其中, T 為型別, 引數 expr 要求是一個結果大於等於 0 的常數表達式, pack 是一個包 (可以是型別也可以是常數表達式)

如果引數為一個型別, 那麼它首先要使用 alignof 運算子計算這個型別的記憶體對位. alignof 運算子和 sizeof 運算子比較類似, 在預設情況下, alignof 是佔用記憶體最大的那個成員變數的大小, 然後向 2 的乘方的結果對齊. 例如 Foo 類別中的成員變數 c, 本身來說 alignof(c) == 3, 但是編碼器對齊之後, 需要向 2 的乘方的結果對齊, 那麼 alignof(c) == 4. 和 sizeof 相同, alignof 的引數如果是一個具體的變數, 那麼可以不用加括號. alignas(T) 相當於 alignas(alignof(T))

如果引數是一個常數表達式, 那麼要求這個引數必須是 0 或著 2 的乘方的結果, 否則會產生編碼錯誤. alignas(0) 通常會被編碼器忽略

alignas 可以被用於變數或著非位元欄位的成員變數的宣告中, 或著可以被用於類別 (struct / class / union) 和列舉 (enum) 的定義, 但是不可以被用於函式參數中或著 catch 捕獲列表中

除此之外, C++ 11 還規定, 當 alignas(c) <= alignas(T) 的時候, 對位要求會被編碼器忽略. 其中, c 為原始的對位值 :

alignas(2) int a;

int 型別對位本身需要 4 個大小, 但是實際對位的大小是 2, 它要小於本身需要的大小, 因此這個 alignas 的宣告就會被編碼器忽略. 在實際的編碼過程中, 這個宣告並不存在問題, 也就是不會產生編碼錯誤

我們使用 alignas 對型別 Foo 進行記憶體對位 :

struct alignas(4) Foo {

    char c[3];

    int i;

};

或著可以這樣 (int 是型別 Foo 的成員變數中佔用記憶體最大的型別) :

struct alignas(int) Foo {

    char c[3];

    int i;

}

還可以這樣 :

struct Foo {
    alignas(4) char c[3];
    int i;
}

此時, i 的偏移量為 c + 4, 而不是 c + 3. 編碼器會自動在 c + 3 處增加一個位元用於填充

我們之前已經說過, 編碼器會自動對所有的型別進行記憶體對位, 如果我們強行要求記憶體不對位或著使用我們自己的要求進行對位, 可以通過前置處理器指令

#pragma pack(n)

n 可以是任何前處理階段常數表達式 (你暫時可以理解為字面值), 當 n < 0 的時候, 編碼器會發出警告. 如果我們希望記憶體一個一個對位, 那麼

#pragma pack(1)

此時, 如果再次宣告

struct Foo {

    char c[3];

    int i;

};

那麼 sizeof(Foo) == 7

在這樣的情況下, alignof 任何自訂型別的結果總是 1 (內建型別的 alignof 值不變)