主頁 > 後端開發 > 第9章 記憶體模型和名稱空間

第9章 記憶體模型和名稱空間

2022-11-07 07:01:54 後端開發

看《C++ Primer Plus》時整理的學習筆記,部分內容完全摘抄自《C++ Primer Plus》(第6版)中文版,Stephen Prata 著,張海龍 袁國忠譯,人民郵電出版社,只做學習記錄用途,

目錄
  • 9.1 單獨編譯
    • 9.1.1 程式組織策略
    • 9.1.2 頭檔案
    • 9.1.3 源代碼檔案
  • 9.2 存盤持續性、作用域和鏈接性
    • 9.2.1 存盤持續性種類
    • 9.2.2 作用域種類
    • 9.2.3 鏈接性種類
    • 9.2.4 自動存盤持續性變數
    • 9.2.5 靜態存盤持續性變數
    • 9.2.6 外部鏈接性的靜態變數
    • 9.2.7 內部鏈接性的靜態變數
    • 9.2.8 無鏈接性的靜態變數
    • 9.2.9 存盤說明符和 cv 限定符
    • 9.2.10 函式鏈接性
    • 9.2.11 語言鏈接性
  • 9.3 定位 new 運算子
    • 9.3.1 動態存盤持續性
    • 9.3.2 常規 new 運算子的使用
    • 9.3.3 定位 new 運算子的使用
  • 9.4 名稱空間
    • 9.4.1 傳統的 C++ 名稱空間
    • 9.4.2 新增的 C++ 名稱空間
    • 9.4.3 using 宣告和 using 編譯指令
    • 9.4.4 嵌套的名稱空間
    • 9.4.5 未命名的名稱空間
    • 9.4.6 名稱空間的使用方法

本章介紹 C++ 的記憶體模型和名稱空間,包括資料的存盤持續性、作用域和鏈接性,以及定位 new 運算子,

9.1 單獨編譯

C++ 鼓勵程式員將組件函式放在獨立的檔案中,可以單獨編譯這些檔案,然后將它們鏈接成可執行的程式,(通常,C++ 編譯器既編譯程式,也管理聯結器,)如果只修改了一個檔案,則可以只重新編譯該檔案,然后將它與其他檔案的編譯版本鏈接,大多數集成開發環境(如 Microsoft Visual C++Apple Xcode)都提供了這一功能,減少了人為管理的作業量,

9.1.1 程式組織策略

以下是一種非常有效且常用的程式組織策略,它將整個程式分為三個部分:

  • 頭檔案:包含結構宣告和使用這些結構的函式的原型
  • 源代碼檔案:包含定義與結構有關的函式的代碼,
  • 源代碼檔案:包含呼叫與結構有關的函式的代碼,

在編譯時,C++ 前處理器會將源代碼檔案中的 #include 指令替換成頭檔案的內容,源代碼檔案和它所包含的所有頭檔案被編譯器看成一個包含以上所有資訊的單獨檔案,該檔案被稱為翻譯單元(translation unit),描述一個具有檔案作用域的變數時,它的實際可見范圍是整個翻譯單元,如果程式由多個源代碼檔案組成,那么該程式也將由多個翻譯單元組成,每個翻譯單元均對應一個源代碼檔案和它所包含的頭檔案,下圖簡要地說明了在 UNIX 系統中,將含 1 個頭檔案 coordin.h 與 2 個源代碼檔案 file1.cppfile2.cpp 的程式編譯成一個 out 可執行程式的程序,

image-20221030212704018

由于不同 C++ 編譯器對函式的名稱修飾方式不同,因此由不同編譯器創建的二進制模塊(物件代碼檔案,如上圖中的 file1.ofile2.o)很可能無法正確地鏈接,因為兩個編譯器將為同一個函式生成不同的名稱修飾,這時,可使用同一個編譯器重新編譯所有源代碼檔案,來消除鏈接錯誤,

9.1.2 頭檔案

在同一個檔案中只能將同一個頭檔案包含一次,否則可能會出現重復定義的問題,一般在頭檔案中使用前處理器編譯指令 #ifndef(即 if not defined)來避免多次包含同一個頭檔案,編譯器首次遇到該檔案時,名稱 COORDIN_H_ 沒有定義(加上下劃線以獲得一個在其他地方不太可能被定義的名稱),這時編譯器將查看 #ifndef#endif 之間的內容,并通過 #define 定義名稱 COORDIN_H_,如果在同一個檔案中遇到其他包含 coordin.h 的代碼,編譯器將知道 COORDIN_H_ 已經被定義了,從而跳到 #endif 后面的一行,但這種方法并不能防止編譯器將檔案包含兩次,而只是讓它忽略除第一次包含之外的所有內容,

#ifndef COORDIN_H_
#define COORDIN_H_

//頭檔案內容
...

#endif

在頭檔案中,可以包含以下內容:

  • 使用 #defineconst 定義的符號常量
  • 結構宣告,它們并不創建變數,只是告訴編譯器當需要創建它們時應該如何創建,
  • 類宣告,同結構宣告一樣,它們并不創建類,只是告訴編譯器當需要創建它們時應該如何創建,
  • 模板定義,它們不是將被編譯的代碼,只是被用來指示編譯器如何生成與源代碼中的函式呼叫相匹配的函式定義,
  • 常規函式原型
  • 行內函式定義

不要將常規函式定義(非函式模板、非行內函式)或常規變數宣告(非 const 變數、非 static 變數)放到頭檔案中,否則當同一個程式的兩個源檔案都包含該頭檔案時,可能會出現重復定義的問題,

9.1.3 源代碼檔案

在源代碼檔案開頭處,通常會使用 #include 預編譯指令包含所需的頭檔案,有以下兩種包含方式:

  • 使用尖括號 <> 包含,例如 #include <iostream>,如果檔案名包含在尖括號中,則 C++ 編譯器將在存盤標準頭檔案的主機系統的檔案系統中查找,一般用來包含系統自帶的頭檔案或標準頭檔案
  • 使用雙引號 "" 包含,例如 #include "coordin.h",如果檔案名包含在雙引號中,則編譯器將首先查找當前的作業目錄或源代碼目錄(或其它目錄,這取決于編譯器以及用戶設定),如果沒有在那里找到頭檔案,則將在標準位置查找,一般用來包含用戶自定義的頭檔案

不要在源代碼檔案中包含其它源代碼檔案,這可能出現重復定義的問題,在源代碼檔案中,一般包含頭檔案中常規函式原型所對應的函式定義(宣告與定義相分離的策略,宣告位于頭檔案中,定義位于源代碼檔案中)、類宣告中成員函式的定義、全域變數宣告等,

9.2 存盤持續性、作用域和鏈接性

不同的 C++ 存盤方式是通過存盤持續性作用域鏈接性來描述的,下表總結了引入名稱空間之前使用的存盤特性,

存盤描述 持續性 作用域 鏈接性 宣告方式
常規自動變數 自動存盤持續性 代碼塊 在代碼塊中
暫存器自動變數 自動存盤持續性 代碼塊 在代碼塊中,使用關鍵字 register
外部鏈接性的靜態變數 靜態存盤持續性 翻譯單元 外部 不在任何函式內,分為定義宣告和參考宣告
內部鏈接性的靜態變數 靜態存盤持續性 翻譯單元 內部 不在任何函式內,使用關鍵字 static
無鏈接性的靜態變數 靜態存盤持續性 代碼塊 在代碼塊中,使用關鍵字 static

下面對這些存盤特性進行逐一介紹,

9.2.1 存盤持續性種類

C++ 使用三種(C++11 中是四種)不同的方案來存盤資料,這些方案的區別就在于資料保留在記憶體中的時間,即存盤持續性,

  • 自動存盤持續性:在函式定義中宣告的變數(包括函式引數)的存盤持續性為自動的,它們在程式開始執行其所屬的函式或代碼塊時被創建,在執行完函式或代碼塊時,它們使用的記憶體被釋放,
  • 靜態存盤持續性:在函式定義外部定義的變數和使用關鍵字 static 定義的變數的存盤持續性都為靜態,它們在程式整個運行程序中都存在,
  • 動態存盤持續性:用 new 運算子分配的記憶體將一直存在,直到使用 delete 運算子將其釋放或程式結束為止,這種記憶體的存盤持續性為動態,有時被稱為自由存盤(free store)或堆(heap),
  • 執行緒存盤持續性(C++11):當前,多核處理器很常見,這些 CPU 可同時處理多個執行任務,這讓程式能夠將計算放在可并行處理的不同執行緒中,如果變數是使用關鍵字 thread_local 宣告的,則其生命周期與所屬的執行緒一樣長,

9.2.2 作用域種類

作用域(scope)描述了名稱在檔案(翻譯單元)的多大范圍內可見,C++ 變數的作用域有多種:

  • 區域作用域:作用域為區域的變數只能在宣告它的代碼塊(由一對花括號括起來的多條陳述句)中使用,不能在其它地方使用,所有自動變數的作用域都是區域的,靜態變數的作用域是全域還是區域取決于它是如何被宣告的,例如:函式體內宣告的常規變數、函式形參、無鏈接性的靜態變數,
  • 全域作用域:作用域為全域的變數在其宣告位置到檔案結尾之間都可以用,全域作用域也稱為檔案作用域,例如在檔案中函式定義之前定義的變數(外部鏈接性的靜態變數、內部鏈接性的靜態變數),
  • 函式原型作用域:在函式原型作用域中使用的名稱只在包含引數串列的括號內可用,C++11 中可在原型括號后面使用 decltype 關鍵字推斷回傳型別,但這實際上并沒有使用引數的值,只用它們來做了型別推斷,
  • 類作用域:在類中宣告的成員的作用域為整個類,它們又有三種不同的屬性:公有、私有和繼承,這將在后續章節介紹,
  • 名稱空間作用域:在名稱空間中宣告的變數的作用域為整個名稱空間,全域作用域是名稱空間作用域的特例,

C++ 函式的作用域可以是類作用域或名稱空間作用域(包括全域作用域),但不能是區域作用域,

9.2.3 鏈接性種類

鏈接性(linkage)描述了名稱如何在不同單元間共享,有以下三種鏈接性:

  • 外部鏈接性:鏈接性為外部的名稱可在檔案間共享,
  • 內部鏈接性:鏈接性為內部的名稱只能由一個檔案中的函式共享,
  • 無鏈接性:自動變數的名稱沒有鏈接性,因為它們不能共享,

9.2.4 自動存盤持續性變數

自動變數的初始化:在默認情況下,在函式或代碼塊中宣告的函式引數和變數的存盤持續性為自動,作用域為區域,沒有鏈接性,只有在定義它們的函式中才能使用它們,當函式結束時,這些變數都將消失,可以使用任何在宣告時其值為已知的運算式來初始化自動變數,若在宣告時未進行初始化,則其值是未知的,

int w;               //未被初始化,其值未知
int x = 5;           //被數字字面常量初始化
int y = 2*x;         //被可計算值的運算式初始化
int z = INT_MAX - 1; //被常量運算式初始化

自動變數的記憶體管理:自動變數的數目隨函式的開始和結束而增減,程式常用的方法是留出一段記憶體,并將其視為堆疊,以管理變數的增減,

  • 堆疊的默認長度取決于實作,但編譯器通常提供改變堆疊長度的選項,Microsoft Visual Studio 默認大小為 1 MB,
  • 堆疊的虛擬記憶體是連續的,但物理記憶體不一定連續,程式使用兩個指標來跟蹤堆疊,一個指標指向堆疊底(堆疊的開始位置),另一個指標指向堆疊頂(堆疊的下一個可用記憶體單元),
  • 當函式被呼叫時,其中的自動變數將被加入到堆疊中,堆疊頂指標指向變數后面的下一個可用的記憶體單元,當函式結束時,堆疊頂指標被重置為函式被呼叫前的值,從而釋放新變數使用的記憶體,
  • 堆疊是 LIFO 的(后進先出),即最后加入到堆疊中的變數首先被彈出,這種設計簡化了引數傳遞,函式呼叫時將其引數的值放在堆疊頂,然后重新設定堆疊頂指標,被呼叫的函式根據其形參描述來確定每個引數的地址,

image-20221103222645660

函式 fib() 被呼叫時,傳遞一個 2 位元組的 int 和一個 4 位元組的 long,這些值被加入到堆疊中,當 fib() 開始執行時,它將名稱 realtell 同這兩個值關聯起來,當 fib() 結束時,堆疊頂指標重新指向以前的位置,新值沒有被洗掉,但不再被標記,它們所占據的空間將被下一個將值加入到堆疊中的函式呼叫所使用,(上圖做了簡化,實際上函式呼叫可能傳遞其它資訊,比如回傳地址,深入學習可查看函式呼叫時的匯編代碼)

自動變數的隱藏:如下例子所示,在函式內的代碼塊中,新的同名自動變數 value 隱藏了代碼塊外部的 value變數,當程式離開該代碼塊時,原來的 value 變數又重新可見,

int main()
{
    //自動變數1
    int value = https://www.cnblogs.com/young520/archive/2022/11/06/1;
    
    //輸出結果為0x0080FDC8
    cout << &value << endl;
    
    //用花括號括起來的代碼塊
    {
        //自動變數2
        int value = 2;
        
        //輸出結果為0x0080FDBC
        cout << &value << endl;
    }
    
    //輸出結果為0x0080FDC8
    cout << &value << endl;
    
    return 0;
}

auto 關鍵字:在 C++11 之前,關鍵字 auto 被用來顯式地指出變數為區域自動存盤,且只能被用于默認為自動存盤的變數;在 C++11 中,關鍵字 auto 被用來做自動型別推斷,

//C++11之前,顯式指明x為區域自動存盤
auto double x = 53.0;

//C++11中,用于自動型別推斷
auto x = 53.0;

register 關鍵字:在 C++11 之前,關鍵字 register 被用來建議編譯器使用 CPU 暫存器來存盤自動變數,提示編譯器這種變數用得很多,可對其做特殊處理(暫存器變數);在 C++11 中,關鍵字 register 被用來顯式地指出變數是區域自動存盤,且只能被用于原本就是自動存盤的變數,這與 auto 以前的用法完全相同,使用它的唯一原因是,指出一個自動變數,這個自動變數可能與外部變數同名,

//C++11之前,建議編譯器用暫存器存盤x
register int x = 53;

//C++11中,顯式指明x為區域自動存盤
register int x = 53;

9.2.5 靜態存盤持續性變數

靜態變數的種類:C++ 為靜態存盤持續性變數提供了 3 種鏈接性:外部鏈接性(可在其他檔案中訪問)、內部鏈接性(只能在當前檔案中訪問)和無鏈接性(只能在當前函式或代碼塊中訪問),

  • 要想創建外部鏈接性的靜態變數,必須在代碼塊的外面宣告它,如下代碼片段中的 global_all_file 變數,可以在程式的其他檔案中使用它;
  • 要想創建內部鏈接性的靜態變數,必須在代碼塊的外面宣告它并使用 static 關鍵字,如下代碼片段中的 global_one_file 變數,只能在包含 static int global_one_file = 50; 陳述句的檔案中使用它,
  • 要想創建沒有鏈接性的靜態變數,必須在代碼塊的內部宣告它并使用 static 關鍵字,如下代碼片段中的 local_one_function 變數,它的作用域為區域,只能在 func() 函式中使用它,與自動變數不同的是,即使在 func() 函式沒有被執行時,它也留在記憶體中,
int global_all_file = 1000;               //外部鏈接性的靜態變數
static int global_one_file = 50;          //內部鏈接性的靜態變數
int main()
{
    ...
}
void func()
{
    static int local_one_function = 10;   //無鏈接性的靜態變數
    ...
}

靜態變數的記憶體管理:靜態變數在整個程式執行期間一直存在,靜態變量的數目在程式運行期間是不變的,程式不需要使用特殊的裝置(如堆疊)來管理它們,編譯器將分配固定的記憶體塊來存盤所有的靜態變數,這些變數在整個程式執行期間一直存在,因此,與自動變數相比,它們的壽命更長,

靜態變數的初始化:所有靜態變數都有如下初始化特征:未被初始化的靜態變數的所有位都被設定為 0,這種變數被稱為零初始化的(zero-initialized),包括靜態陣列和結構,對于標量型別,零將被強制轉換為合適的型別,例如空指標用 0 表示,但內部可能采用非零表示,除默認的零初始化外,還可對靜態標量進行常量運算式初始化動態初始化,零初始化和常量運算式初始化被統稱為靜態初始化,這意味著在編譯器處理檔案(翻譯單元)時初始化變數,動態初始化意味著變數將在編譯后初始化,

#include <cmath>
int x;                       //零初始化
int y = 5;                   //常量運算式初始化
int z = 13 * 13;             //常量運算式初始化
int u = 2 *sizeof(long) + 1; //常量運算式初始化
double pi = 4.0 * atan(1.0); //動態初始化

首先,所有靜態變數都被零初始化,而不管程式員是否顯式地初始化了它,接下來,如果使用常量運算式初始化了變數,且編譯器僅根據當前翻譯單元就可計算運算式,編譯器將執行常量運算式初始化,必要時,編譯器將執行簡單計算,C++11 新增了關鍵字 constexpr,這增加了創建常量運算式的方式,最后,在程式執行時將進行動態初始化,上述程式中,xyzupi 首先被零初始化,然后編譯器計算常量運算式的值對 yzu 進行常量運算式初始化,但要初始化pi,必須呼叫函式 atan(),這需要等到該函式鏈接且程式執行時,

9.2.6 外部鏈接性的靜態變數

外部變數的使用:鏈接性為外部的變數通常簡稱為外部變數,它們的存盤持續性為靜態,作用域為整個檔案,但也可以在同一專案的其他檔案中使用它,外部變數的使用條件有兩個:

  • 一方面,在每個使用外部變數的檔案中,都必須宣告它,
  • 另一方面,C++ 有單定義規則 "One Definition Rule",簡稱 ODR,該規則指出,變數只能有一次定義,

C++ 提供了兩種變數宣告方式,來滿足這兩個條件:

  • 定義宣告(defining declaration)或簡稱為定義(definition),它給變數分配存盤空間,定義宣告不使用關鍵字 extern,或者在使用關鍵字 extern 的同時對變數進行了人為初始化(可用此法來修改 const 全域常量默認的內部鏈接性為外部鏈接性,見后面的 cv 限定符小節),
  • 參考宣告(referencing declaration)或簡稱為宣告(declaration),它參考已有的變數,不給變數分配存盤空間,需要使用關鍵字 extern不能進行初始化,否則該宣告將變為定義宣告
int x;            //定義宣告
extern int y = 0; //定義宣告
extern int z;     //參考宣告,必須在其他檔案中進行定義

在多個檔案中使用外部變數時,必須且只能在一個檔案中包含該變數的定義宣告(滿足第二個使用條件),在使用該變數的其他所有檔案中,都必須使用關鍵字 extern 宣告它,即包含該變數的參考宣告(滿足第一個使用條件),

//檔案file01.cpp
int dogs = 22;        //定義宣告
extern int cats = 40; //定義宣告

//檔案file02.cpp
extern int dogs;      //參考宣告
extern int cats;      //參考宣告

外部變數的隱藏:區域變數可能隱藏同名的全域變數,這并不違反單定義規則,雖然程式中可包含多個同名的變數的定義,但每個變數的實際作用域不同,作用域相同的變數沒有違反單定義規則,定義與外部變數同名的區域變數后,區域變數將隱藏外部全域變數,但 C++ 提供了作用域決議運算子雙冒號(::),將它放在變數名前面,可使用該變數的全域版本,

//檔案file01.cpp
int dogs = 22;        //定義宣告

//檔案file02.cpp
extern int dogs;      //參考宣告
void local()
{
    int dogs = 88;
    cout << dogs << endl;   //輸出88
    cout << ::dogs << endl; //輸出22
}
int main()
{
    ...
}

9.2.7 內部鏈接性的靜態變數

static 關鍵字用于作用域為整個檔案的變數時,該變數的鏈接性將為內部的,鏈接性為內部的變數只能在其所屬的檔案中使用,無法在其他檔案中使用,但外部變數都具有外部鏈接性,可以在其他檔案中使用,

//檔案file02.cpp
static int errors = 2; //內部鏈接性的靜態變數,只能在其所屬檔案中使用

可使用外部變數在多檔案程式的不同部分之間共享資料;可使用鏈接性為內部的靜態變數在同一個檔案中的多個函式之間共享資料(名稱空間提供了另一種共享資料的方法),另外,如果將作用域為整個檔案的變數變為內部鏈接性的,就不必擔心其名稱與其他檔案中的作用域為整個檔案的變數發生沖突,因為此時若存在同名的外部變數,具有內部鏈接性的變數將完全隱藏同名外部變數,且無法通過 extern 關鍵字以及 :: 作用域決議運算子訪問到同名外部變數,

//檔案file01.cpp
int errors = 1;          //外部鏈接性靜態變數

//檔案file02.cpp
static int errors = 2;   //內部鏈接性靜態變數
void func()
{
    int errors = 3;
    cout << errors << endl;   //結果為3
    cout << ::errors << endl; //結果為2
}
void fund()
{
    extern int errors;
    cout << errors << endl;   //結果為2
    cout << ::errors << endl; //結果為2
}
int main()
{
    ...
}

9.2.8 無鏈接性的靜態變數

static 關鍵字用于在代碼塊中定義的區域變數時,該變數沒有鏈接性,且將導致區域變數的存盤持續性為靜態的,這意味著雖然該變數只在該代碼塊中可用,但它在該代碼塊不處于活動狀態時仍然存在,因此在兩次函式呼叫之間,靜態區域變數的值將保持不變,另外,如果初始化了靜態區域變數,則程式只在啟動時進行一次初始化,以后再次呼叫函式時,將不會像自動變數那樣再次被初始化,

void func()
{
    //初始化只進行一次
    static int count = 0;
    
    //每次呼叫時改變其值
    count++;
    
    //輸出
    cout << count << endl;
}

int main()
{
    func();  //輸出1
    func();  //輸出2
    func();  //輸出3
    func();  //輸出4
    
    return 0;
}

9.2.9 存盤說明符和 cv 限定符

C++ 關鍵字中包含以下六個存盤說明符(storage class specifer),它們提供了有關存盤的資訊,除了 thread_local 可與 staticextern 結合使用,其他五個說明符不能同時用于同一個宣告

  • auto 關鍵字:在 C++11之前,可以在宣告中使用關鍵字 auto 指出變數為自動變數;在 C++11 中,auto 用于自動型別推斷,已不再是存盤說明符,
  • register 關鍵字:在 C++11 之前,關鍵字 register 用于在宣告中指示暫存器變數;在 C++11中,它只是顯式地指出變數是區域自動存盤,
  • static 關鍵字:關鍵字 static 被用在作用域為整個檔案的宣告中時,表示內部鏈接性;被用于區域宣告中時,表示區域變數的存盤持續性是靜態的,有人稱之為關鍵字多載,
  • extern 關鍵字:關鍵字 extern 表明是參考宣告,即宣告參考在其他地方定義的變數,
  • thread_local 關鍵字:關鍵字 thread_local 指出變數的持續性與其所屬執行緒的持續性相同,thread_local 變數之于執行緒,猶如常規靜態變數之于整個程式,
  • mutable 關鍵字:關鍵字 mutable 被用來指出,即使結構(或類)變數為 const,其某個成員也可以被修改
//mutable變數不受const限制
struct mdata
{
    int x;
    mutable int y;
};
const mdata veep = {0, 0};
veep.x = 5;  //不被允許
veep.y = 5;  //可以正常運行

C++ 中常說的 cv 限定符是指 const 關鍵字和 volatile 關鍵字,關鍵字 volatile 表明,即使程式代碼沒有對記憶體單元進行修改,其值也可能發生變化,例如:可以將一個指標指向某個硬體位置,其中包含了來自串行埠的時間或資訊,在這種情況下,硬體(而不是程式)可能修改其中的內容,或者兩個程式可能互相影響,共享資料,該關鍵字的作用是為了防止編譯器進行相關的優化(若編譯器發現程式在相鄰的幾條陳述句中兩次使用了某個變數的值,則編譯器可能不是讓程式查找這個值兩次,而是將這個值快取到暫存器中,這種優化假設變數的值在這兩次使用之間不會變化),

關鍵字 const 表明,記憶體被初始化后,程式便不能再對它進行修改,除此之外,在 C++ 中,const 限定符對默認存盤型別也稍有影響,在默認情況下全域變數的鏈接性為外部的,但 const 全域變數的鏈接性為內部的,因此,在 C++ 看來,全域定義 const 常量就像使用了 static 說明符一樣:

//內部鏈接性的靜態const常量,以下兩種方式等效
const int x = 10;
static const int x = 10;

const 全域變數的這種特性意味著,可以將 const 常量的定義宣告放在頭檔案中,只要在源代碼檔案中包含這個頭檔案,它們就可以獲得同一組常量,此時每個定義宣告都是其檔案(翻譯單元)所私有的,而不是所有檔案共享同一組常量,若程式員希望某個 const 全域變數的鏈接性為外部的,可以在定義宣告中增加 extern 關鍵字,來覆寫默認的內部鏈接性,此時就只能有一個檔案包含定義宣告,其他使用到該 const 常量的檔案必須包含相應的 extern 參考宣告,這個 const 常量將在多個檔案之間共享,

//外部鏈接性的靜態const常量
extern const int y = 10;

9.2.10 函式鏈接性

C++ 不允許在一個函式中定義另外一個函式,因此所有函式的存盤持續性都是靜態的,即在整個程式執行期間都一直存在,在默認情況下,函式的鏈接性為外部的,即可以在檔案間共享,還可以使用關鍵字 static 將函式的鏈接性設定為內部的,使之只能在一個檔案(翻譯單元)中使用,必須同時在原型和定義中使用 static 關鍵字:

//鏈接性為內部的函式,只能在所在檔案中使用
static int privateFunction();  //函式原型

//函式定義
static int privateFunction()
{
    ...
}

和變數一樣,在定義內部鏈接性的函式的檔案中,內部鏈接性函式定義將完全覆寫外部同名函式定義,單定義規則也適用于非行內函式,對于鏈接性為外部的函式來說,這意味著在多檔案程式中,只能有一個檔案(該檔案可能是庫檔案)包含該函式的定義,但使用該函式的每個檔案都應包含其函式原型(和外部變數不同的是,函式原型前可省略使用關鍵字 extern),行內函式則不受單定義規則的約束,可將行內函式定義寫在頭檔案中,但 C++ 要求同一個函式的所有行內定義都必須相同,內部鏈接性的 static 函式定義也可寫在頭檔案中,這樣每個包含該頭檔案的翻譯單元都將有各自的 static 函式,而不是共享同一個函式,

//檔案file.cpp
#include <iostream>
#include <cmath>
double sqrt(double x) { return 0.0; }
int main()
{
    using namespace std;
	cout << sqrt(4.0) << endl;   //結果為0
	cout << ::sqrt(4.0) << endl; //結果為0

	return 0;
}

在程式的某個檔案中呼叫一個函式時,如果該檔案中的函式原型指出該函式是靜態的,則編譯器將只在該檔案中查找函式定義;否則,編譯器(包括鏈接程式)將在所有的程式檔案中查找,如果找到兩個定義,編譯器將發出錯誤訊息,如果在程式檔案中未找到,編譯器將在庫中搜索,這意味著如果定義了一個與庫函式同名的函式,編譯器將使用程式員定義的版本,而不是庫函式,為養成良好的編程習慣,應盡量避免使用與標準庫函式相同的函式名,上述程式在 Microsoft Visual Studio 2019 中的輸出結果都為 0,但編譯器會輸出 C28251 的警告資訊,如下圖所示,

image-20221106011124473

9.2.11 語言鏈接性

另一種形式的鏈接性——稱為語言鏈接性(language linking)也對函式有影響,鏈接程式要求每個不同的函式都有不同的符號名,在 C 語言中,一個名稱只對應一個函式,編譯器可能將 spiff 這樣的函式名翻譯為 _spiff,這種方法被稱為 C 語言鏈接性(C language linking),但在 C++ 中,由于函式多載,一個名稱可能對應多個函式,編譯器將執行名稱修飾,可能將 spiff(int) 轉換為 _spiff_i,將 spiff(double, double) 轉換為 _spiff_d_d,這種方法被稱為 C++ 語言鏈接性(C++ language linking),因此,鏈接程式尋找與 C++ 函式呼叫匹配的函式時,使用的查詢約定與 C 語言不同,若要在 C++ 程式中使用 C 庫(靜態庫、動態庫)中預編譯的函式 spiff(int),應該使用如下函式原型來指出要使用的函式符號查詢約定:

//使用C庫中的預編譯好的函式
extern "C" void spiff(int); //方式一
extern "C"                  //方式二
{
    void spiff(int);
}

上面的兩種方式都指出了使用 C 語言鏈接性來查找相應的函式,若要使用 C++ 語言鏈接性,可按如下方式指出:

//使用C++庫中的預編譯好的函式
void spiff(int);              //方式一
extern void spiff(int);       //方式二
extern "C++" void spiff(int); //方式三
extern "C++"                  //方式四
{
    void spiff(int);
}

C 和 C++ 鏈接性是 C++ 標準指定的說明符,但實作可提供其他語言鏈接性說明符,

9.3 定位 new 運算子

9.3.1 動態存盤持續性

使用 C++ 運算子 new(或 C 函式 malloc())分配的記憶體,被稱為動態記憶體,動態記憶體由運算子 newdelete 控制,而不是由作用域和鏈接性規則控制,動態記憶體的分配和釋放順序取決于 newdelete 在何時以何種方式被使用,因此,可以在一個函式中分配動態記憶體,而在另一個函式中將其釋放,通常,編譯器使用三塊獨立的記憶體:一塊用于靜態變數(可能再細分),一塊用于自動變數,另外一塊用于動態存盤

//檔案file01.cpp
float * p_fees = new float[20];

//檔案file02.cpp
extern float * p_fees;

雖然存盤方案概念不適用于動態記憶體,但適用于用來跟蹤動態記憶體的自動和靜態指標變數,例如上述程式中由 new 分配的 80 個位元組(假設 float 為 4 個位元組)的記憶體將一直保留在記憶體中,直到使用 delete 運算子將其釋放,但指標 p_fees 的存盤持續性與其宣告方式有關,若 p_fees 是自動變數,則當包含該申明的陳述句塊執行完畢時,指標 p_fees 將消失,如果希望另一個函式能夠使用這 80 個位元組中的內容,則必須將其地址傳遞出去,若將 p_fees 宣告為外部變數,則檔案中位于該宣告后面的所有函式都可以使用它,通過在另一個檔案中使用它的參考宣告,便可在其中使用該指標,

在程式結束時,由 new 分配的記憶體通常都將被系統釋放,但在不那么健壯的作業系統中,在某些情況下,請求大型記憶體塊將導致該代碼塊在程式結束不會被自動釋放,最佳習慣是:使用 delete 來釋放 new 分配的記憶體

9.3.2 常規 new 運算子的使用

使用常規 new 運算子初始化動態分配的記憶體時,有以下幾種方式:

//C++98風格,小括號初始化
int *pint = new int(6);

//C++11風格,大括號初始化
int *pint = new int{6};

//C++11大括號初始化可用于結構和陣列
struct points {
    double x;
    double y;
    double z;
};
points * ptrP = new points{1.1, 2.2, 3.3};
int * arr = new int[4]{2, 4, 6, 7};

常規 new 負責在堆(heap)中找到一個足以能夠滿足要求的記憶體塊,當 new 找不到請求的記憶體量時,最初 C++ 會讓 new 回傳一個空指標,但現在將會拋出一個例外 std::bad_alloc,這將在后續章節介紹,當使用 new 運算子時,通常會呼叫位于全域名稱空間中的分配函式(alloction function),當使用 delete 運算子時,會呼叫對應的釋放函式(deallocation function),

//分配函式原型
void * operator new(std::size_t);
void * operator new[](std::size_t);

//釋放函式原型
void operator delete(void *);
void operator delete[](void *);

其中 std::size_t 是一個typedef,對應于合適的整型,這里只做簡單的程序說明,實際上使用運算子 new 的陳述句也可包含給記憶體設定的初始值,會復雜一些,C++ 將這些函式稱為可替換的(replaceable),可根據需要對其進行定制,例如,可定義作用域為類的替換函式,對其進行定制,以滿足該類的記憶體分配需求,

int *pint = new int;   //被轉換為 int *pint = new(sizeof(int));
int * arr = new int[4];//被轉換為 int * arr = new(4 * sizeof(int));
delete pint;           //被轉換為 delete(pint);

9.3.3 定位 new 運算子的使用

new 運算子還有另一種變體,被稱為定位(placement)new 運算子,它能夠讓程式員指定要使用的位置,可使用這種特性來設定其記憶體管理規程、處理需要通過特定地址進行訪問的硬體或在特定位置創建物件,如下程式是一個使用定位 new 運算子的例子,有以下幾點需注意:

  • 使用定位 new 特性必須包含頭檔案 new,且在使用時需人為提供可用的記憶體地址,
  • 定位 new 既可以用來創建陣列,也可以用來創建結構等變數,
  • 定位 new 運算子使用傳遞給它的地址,它不跟蹤哪些記憶體單元已被使用,也不查找未使用的記憶體塊,這將一些記憶體管理的負擔交給了程式員,當在同一塊大型記憶體區域內創建不同變數時,可能需要人為計算記憶體的偏移量大小,防止出現變數記憶體區域重疊的情況,
  • delete 只能用來釋放常規 new 分配出來的堆記憶體,下面例子中的 buffer1buffer2 都屬于靜態記憶體,不能用 delete 釋放,若 buffer1buffer2 是通過常規 new 運算子分配出來的,則可以且必須用 delete 進行釋放,
#include <iostream>
#include <new>

struct person
{
    char name[20];
    int age;
};

char buffer1[50];
char buffer2[500];

int main()
{
    using namespace std;
    
    //常規new運算子,資料存盤在堆上
    person *p1 = new person;
    int *p2 = new int[20];
    
    //定位new運算子,資料存盤在指定位置,這里為靜態區
    person *pp1 = new (buffer1) person;
    int *pp2 = new (buffer2) int[20];
    
    //顯示地址(32位系統)
    cout << (void *) buffer1 << endl; //結果為0x00AEC2D0
    cout << (void *) buffer2 << endl; //結果為0x00AEC308
    cout << p1 << endl;      //結果為0x00EFF640
    cout << p2 << endl;      //結果為0x00EF6470
    cout << pp1 << endl;     //結果為0x00AEC2D0
    cout << pp2 << endl;     //結果為0x00AEC308
    
    //釋放動態堆記憶體
    delete p1;
    delete[] p2;
}

上面程式中使用 (void *)char * 進行強制轉換,以使得 buffer1buffer2 的地址能夠正常輸出,否者它們將輸出字串,定位 new 運算子的原理也與此類似,它只是回傳傳遞給它的地址,并將其強制轉換為 void *,以便能夠賦給任何指標型別,將定位 new 運算子用于類物件時,情況將更復雜,這將在第 12 章介紹,C++ 允許程式員多載定位 new 函式,它至少需要接收兩個引數,且其中第一個總是 std::size_t,指定了請求的位元組數,

int * p1 = new(buffer) int;   //被轉換為 int * p1 = new(sizeof(int),buffer);
int *arr = new(buffer) int[4];//被轉換為 int *arr = new(4*sizeof(int),buffer)

9.4 名稱空間

9.4.1 傳統的 C++ 名稱空間

且聽下回分解

9.4.2 新增的 C++ 名稱空間

且聽下回分解

9.4.3 using 宣告和 using 編譯指令

且聽下回分解

9.4.4 嵌套的名稱空間

且聽下回分解

9.4.5 未命名的名稱空間

且聽下回分解

9.4.6 名稱空間的使用方法

且聽下回分解

本文作者:木三百川

本文鏈接:https://www.cnblogs.com/young520/p/16864440.html

著作權宣告:本文系博主原創文章,著作權歸作者所有,商業轉載請聯系作者獲得授權,非商業轉載請附上出處鏈接,遵循 署名-非商業性使用-相同方式共享 4.0 國際版 (CC BY-NC-SA 4.0) 著作權協議,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/528062.html

標籤:其他

上一篇:靜態鏈接

下一篇:【HDLBits刷題筆記】13 Finite State Machines

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more