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

来源:https://www.cnblogs.com/young520/archive/2022/11/06/16864440.html
-Advertisement-
Play Games

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


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

目錄

本章介紹 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 = 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) 版權協議。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 具體需求 在我的疫情可視化項目中有一個功能需要導出word文檔,在頁面點擊按鈕後處理數據生成word文件,然後自動下載文檔。 實現步驟 多番查詢後發現前端導出word,使用docxtemplater較為方便。具體使用步驟如下: 安裝docxtemplater:npm i docxtemplater ...
  • 導讀:對於錯誤碼的設計,不同的開發團隊有不同的風格習慣。本文分享阿裡文娛技術專家長統對於錯誤碼的看法,希望從錯誤碼使用的不同場景討論得到一個合理的錯誤碼規約,得到一個面嚮日志錯誤碼標準和一個面向外部傳遞的錯誤碼標準。 ...
  • 1、UML統一建模語言 定義:用於軟體系統設計與分析的語言工具 目的:幫助開發人員更好的梳理邏輯、思路 學習地址:UML概述_w3cschool 官網:https://www.omg.org/spec/UML 1.1、UML組成結構 flowchart TD; UML圖 --> 結構圖 & 行為圖; ...
  • 一、人狗大戰 1、需求 用代碼模擬人、狗打架的小游戲 人和狗種類不同,因此雙方的屬性各不相同 推導一: 人和狗各有不同屬性 使用字典方式儲存屬性較為方便,並可儲存多種屬性 # 1、在字典內儲存‘人’屬性 person = { 'name': '阿拉蕾', 'age': 18, 'gender': ' ...
  • 您好,我是湘王,這是我的博客園,歡迎您來,歡迎您再來~ Lambda表達式雖然將介面作為代碼塊賦值給了變數,但如果僅僅只是Lambda表達式,還無法讓Java由量變引起質變。真正讓Lambda能夠發揮出巨大威力的,就是流式計算。 所謂流式計算,就是讓數據像在流水線上一樣,從一道工序流轉到下一道工序。 ...
  • 簡介: 橋接模式又叫橋梁模式,屬於結構型模式。目的是將抽象與實現分離,使它們都可以獨立的變化,解耦。繼承有很多好處,但是會增加耦合,而橋接模式偏向組合和聚合的方式來共用。 適用場景: 不希望或不適用使用多繼承的場景。 一個類存在2個或更多的 獨立變化維度 , 並且這些維度都需要 獨立擴展 優點: 解 ...
  • 日誌概念 1. 日誌文件 日誌文件是用於記錄系統操作事件的文件集合 1.1 調試日誌 1.2 系統日誌 系統日誌是記錄系統中硬體、軟體和系統問題的信息,同時還可以監視系統中發生的事件。用戶可以通過它來檢查錯誤發生的原因,或者尋找受到攻擊時攻擊者留下的痕跡 日誌門面 當我們的系統變的更加複雜的時候,我 ...
  • Fsm1 這裡需要實現一個簡單的摩爾狀態機,即輸出只與狀態有關的狀態機。 我這裡代碼看上去比長一點,答案用的case和三目運算符,結果是一樣的。 module top_module( input clk, input areset, // Asynchronous reset to state B ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...