1. 程式人生 > >C++記憶體與名稱空間

C++記憶體與名稱空間

C++ Primer Plus讀書筆記

記憶體與名稱空間

1.標頭檔案

  • 一種組織程式的策略,就是:一個檔案(標頭檔案)包含了使用者定義型別的定義,另一個檔案包含操縱使用者定義型別的函式的程式碼。這兩個檔案組成一個軟體包,可以應用在各種程式中。

  • 不把函式定義放在標頭檔案中,原因是:如果在標頭檔案中包含了一個函式的定義,然後在其他兩個檔案中include這個檔案,則同一個程式中將包含同一個函式的兩個定義,除非函式是內聯的,否則將會出錯。

  • 下面是標頭檔案中常包含的內容:

    函式原型
    使用#define或const定義的符號變數
    結構宣告
    類宣告
    模板宣告
    行內函數
  • 標頭檔案管理
//假設標頭檔案名稱為zmyyq.h
#ifndef ZMYYQ_H_
......
#endif

2.儲存持續性

  • 儲存持續性(用來確定資料在記憶體中留存的時間)

    • 1.自動儲存持續性:在函式定義中宣告的變數(包含函式引數)的儲存持續性為自動的,他們在程式開始執行其所屬的函式或程式碼塊時被建立,在執行完函式或程式碼塊時,他們使用的記憶體就被釋放

    • 2.靜態儲存持續性:在函式定義外定義的變數和使用關鍵字static定義的變數的儲存持續性都為靜態,他們在整個程式執行過程中都存在。

      __static修飾的內容__
      1. 如果是區域性變數,則在用static修飾區域性變數後,該變數只在初次執行時進行初始化工作,且只進行一次。
      
          區域性變數是存放在棧區的,並且區域性變數的生命週期在該語句塊執行結束時便結束了。但是如果用static
      進行修飾的話,該變數便存放在__靜態資料區__ ,其生命週期一直持續到整個程式執行結束。但是在這裡要注意的是,雖然用static對區域性變數進行修飾過後,其生命週期以及儲存空間發生了變化,但是其作用域並沒有改變,其仍然是一個區域性變數,作用域僅限於該語句塊。 2. 通常情況下對於一個全域性變數,它既可以在本原始檔中被訪問到,也可以在同一個工程的其它原始檔中被訪問(只需用extern進行宣告即可)。例如: file1.c int a=1; file2.c #include<stdio.h> extern int a; int main(void) { printf("%d\",a); return 0; } 則執行結果為 1 但是如果在file1.c中把int a=1改為static int a=1; 那麼在file2.c是無法訪問到變數a的。原因在於用static對全域性變數進行修飾改變了其作用域的範圍,由原來的整個工程可見變為本原始檔可見。 3. 用static修飾函式的話,情況與修飾全域性變數大同小異,就是改變了函式的作用域。
      extern
      在C語言中,修飾符extern用在變數或者函式的宣告前,用來說明“此變數/函式是在別處定義的,要在此處引用”。
      
      注意extern宣告的位置對其作用域也有關係,如果是在main函式中進行宣告的,則只能在main函式中呼叫,在其它函式中不能呼叫。其實要呼叫其它檔案中的函式和變數,只需把該檔案用#include包含進來即可,為啥要用extern?因為用extern會加速程式的編譯過程,這樣能節省時間。
    • 3.執行緒儲存持續性:使用關鍵字thread_local宣告的變數,則其生命週期與所屬的執行緒是一樣的,這部分屬於並行程式設計。

    • 4.動態儲存持續性:用new運算子分配的記憶體將一直存在,知道使用delete運算子將其釋放或程式結束為止,這種記憶體的儲存持續性為動態的,有時候被稱為自由儲存或堆

  • 作用域和連結

    • 作用域描述了名稱在檔案的多大範圍可見
      • C++函式的作用域可以是整個類或者整個名稱空間,但不可能是區域性的,也就是說程式碼塊中不可以定義函式
    • 連結性描述了名稱如何在不同單元間共享。
      • 1.外部連結性:可在其他檔案中訪問
      • 2.內部連結性:只能在當前檔案中訪問
      • 3.無連結性:只能在當前程式碼塊中訪問
  • 自動儲存持續性:
    自動指的是在函式宣告的函式引數和變數的儲存性為自動,作用域為區域性,沒有連結性

    1. 自動變數的初始化:可以使用任何在宣告時其值為已知的表示式來初始化自動變數。由於自動變數的數目隨函式的開始和結束而增減,因此程式必須在執行時對自動變數進行管理。

      • 解決自動變數管理的常用方法是留出一段記憶體,將其視為棧,管理變數的增減。之所以被稱為棧是因為新資料被象徵性地放在原有資料的上面,當程式使用完之後從棧中刪除.
      • 跟蹤棧的實現方法:程式中通常使用2個指標來跟蹤棧,一個指向棧底(棧開始的位置),另一個指標指向堆頂——下一個可用的記憶體單元。當函式被呼叫時,該函式的自動變數將加入到棧中,棧頂指標指向變數後面的可用記憶體單元,當函式呼叫結束之後棧頂指標被重置為函式被呼叫之前的位置。
      • 棧空間的大小,在linux中是使用ulimit來指定的,編譯時確定。在windows中,棧的大小是在連結是確定的。一般都是十幾兆到幾十兆之間不會太大。
  • 總結各個區

    動態儲存區(堆):(動態分配)

    • malloc,new動態分配在heap堆區。
    • 動態儲存區(堆),程式設計師自己分配自己釋放。
    • 使用delete 指標釋放完記憶體後,指標還是存在的,並且還是之前的位置,只是它指向的內容已經變了。

    動態儲存區(棧):(動態分配)

    • 自動變數、const變數在stack棧區。
    • 動態儲存區(棧),系統自動分配釋放。

    靜態儲存區:(靜態分配)

    • extern,全域性變數,在static靜態儲存區。
    • 靜態儲存區,一旦分配,不會被回收,可讀可寫

    程式程式碼區:(靜態分配)

    • main函式、其他函式在code程式程式碼區。
    • 程式程式碼區,一旦分配,可讀不可寫,不可改變
  • 說明符與限定符

    • 下面的屬於儲存說明符
      1. auto
      2. register 暫存器
      3. static 靜態
      4. extern
      5. thread_local C++11新增的
      6. mutable
        • 這個說明符是const的一個延伸,在使用const對結構進行修飾時,該結構不可被改變,但是他的成員還是可以被修改的,用這個修飾結構中的成員,則該成員不可被修改

          struct data
          {
          char a[30];
          mutable int access;//該成員變數不可被修改
          }
    • cv限定符

      1. c表示const,它表明記憶體被初始化後,程式便不能再對它進行修改。它對於預設儲存型別稍有影響。在預設情況下,全域性變數的連結性為外部的,但const全域性變數的連結性是內部的,也就是用了const就相當於加上了static。

      2. v表示volatile,該關鍵字表明:即使程式程式碼沒有對記憶體單元進行修改,其值也可能會發生變化(大多是硬體改變某些取值,也有可能是兩個程式共享一塊記憶體)這個關鍵字的作用是為了改善編譯器的優化能力,假設編譯器發現程式在幾條語句中兩次使用了某個變數的值,則編譯器可能不是讓程式查詢這個量兩次,而是將這個值快取到暫存器中,假設變數的值在這兩次使用之間都不會變化,如果不設定這個volatile則告訴編譯器不進行這種優化。

  • 函式的連結性:

    • 函式在儲存的持續性上都自動為靜態的,因為函式定義的時候不允許在一個函式中定義另一個函式。
    • 函式加上一個static表示只允許在當前程式碼檔案中執行,一般來說,如果一個函式只在某個檔案中執行,則要把他定義成static。
    • 使用extern來指出函式是在另一個檔案中定義的。如果使用extern則該函式只能在一個地方定義。
  • 查詢函式的方式

    • 假設在程式的某個檔案中呼叫一個函式,則按照一下順序進行查詢
      1. 如果該檔案中的函式原型指出該函式是static,則編譯器只在該檔案中查詢函式定義,否則將在所有檔案中查詢。
      2. 如果找到兩個定義,編譯器將會發出錯誤訊息,每個外部函式只能有一個定義。如果在檔案中找不到,則會到庫中查詢。這就意味著如果定義了一個與庫函式同名的函式,編譯器將使用自己寫的函式,如果沒有才去找庫函式。

2.名稱空間

  • 名稱空間可以是全域性的,也可以位於另一個名稱空間中,但是不能位於程式碼塊中,因此他的連結性為外部的
  • using宣告和using編譯指令

    • using宣告使得特定的識別符號可用;(區域性性)
    • using編譯指令使得整個名稱空間可用。(所有)
    • 對比:

      1. 使用using編譯指令匯入一個名稱空間中的所有名稱與使用多個using宣告是不一樣的。如果某個名稱已經在函式中被宣告,則不能使用using宣告匯入相同的名稱,
      2. 使用using編譯指令時,如果匯入了一個已經在函式中宣告的名稱,則區域性名稱將隱藏名稱空間名,就像隱藏同名的全域性變數一樣,不過仍可以用作用域解析運算子來呼叫。
      3. 一般來說,使用using宣告比使用using編譯指令要安全,它只匯入指定的名稱,如果與區域性的名稱發生衝突,則編譯器會發出指示。
        
        #include<iostream>
        
        using namespace std;
        
        namespace ZhuMengYanYiQuan
        {
            double fetch = 3.14;
        }
        
        double fetch = 5.16;
        
        int main()
        {
            double fetch = 4.15;
            using namespace ZhuMengYanYiQuan;
            cout << fetch << endl;    //4.15
            cout << ::fetch << endl;  //5.16
            cout << ZhuMengYanYiQuan::fetch << endl; //3.14
            return 0;
        }
  • 名稱空間的其他特性
    • 可以將名稱空間宣告進行巢狀,呼叫的時候就按著巢狀的順序使用::進行解析即可。

      namespace ZhuMengYanYiQuan
      {
      namespace QuanQuanQuan
      {
      float ChunJie = 5.5;
      }
      double fetch = 3.14;
      }
    • 未命名的名稱空間:表示在該名稱空間中宣告的名稱的潛在作用域為:從宣告點到該宣告區域末尾,與全域性變數類似。因為它沒有名字,所以不能讓他在其他的地方使用。但是它提供了連結性為內部的靜態變數的替代品。

      namespace
      {
      int ice;
      int hot;
      }
  • 名稱空間及其用途:下面是一些指導性的準則

    • 使用在已命名的名稱空間中宣告的變數,而不是使用外部全域性變數。
    • 使用在已命名的名稱空間中宣告的變數,而不是使用靜態全域性變數。
    • 如果開發了一個函式庫或類庫,將其放在一個名稱空間中。
    • 僅將編譯指令作為一種將舊程式碼轉換為使用名稱空間的權宜之計。
    • 在標頭檔案中不要使用using編譯指令,首先這樣會掩蓋要讓那些名稱可用哪些不可用,另外,包含標頭檔案的順序可能會影響程式的行為。如果非要加using編譯指令,則應將其放在所有預處理編譯指令#include之後。
    • 匯入名稱時,首選使用作用域解析運算或者using宣告的方法。

    • 對於using宣告,首選將其作用域設定為區域性而不是全域性。

3.總結

  • 一種程式碼的組織策略:

    • 使用標頭檔案來定義使用者型別、函式原型;

    • 並將函式定義放在一個獨立的原始碼中;

    • 標頭檔案和原始碼檔案一起用來定義和實現使用者定義的型別和方法;
    • 呼叫這些函式的函式放在第三個檔案之中。
    • 也就是一個.h和.cpp用來定義和實現功能,對他們的呼叫放在另一個.cpp中
  • C++的儲存方案

    • 靜態變數存在於整個程式執行期間,對於在函式外面定義的變數,其所屬的檔案中位於該變數定義之後的所有函式都可以使用,並且可以在程式的其他檔案中使用(外部連結性),另一個檔案要使用它,必須使用extern關鍵字來宣告。
    • 對於檔案之間共享的變數,應在一個檔案包含其定義宣告,也可進行初始化。在其他檔案中包含宣告(使用extern且不初始化)
    • 使用static修飾的在函式之外宣告的變數,只能在當前的檔案中使用,不能在其他檔案中使用。