C++ Primer Plus第6版18個重點筆記

Alexia(minmin)發表於2014-11-30

下面是我看《C++ Primer Plus》第6版這本書後所做的筆記,作為備忘錄便於以後複習。

筆記部分

  1. C++的const比C語言#define更好的原因?

    首先,它能夠明確指定型別,有型別檢查功能。
    其次,可以使用C++的作用域規則將定義限制在特定的函式或檔案中。
    第三,可以將const用於更復雜的型別,比如陣列和結構。

    C語言中也有const,其與C++中const的區別是:一是作用域規則不同;另一個是,在C++中可以用const值來宣告陣列長度。

  2. 不能簡單地將整數賦給指標,如下所示:

    int *ptr;
    ptr = 0xB8000000;  // type mismatch

    在這裡,左邊是指向int的指標,因此可以把它賦給地址,但右邊是一個整數。您可能知道,0xB8000000是老式計算機系統中視訊記憶體的組合段偏移地址,但這條語句並沒有告訴程式,這個數字就是一個地址。在C99標準釋出之前,C語言允許這樣賦值。但C++在型別一致方面的要求更嚴格,編譯器將顯示一條錯誤訊息,通告型別不匹配。要將數字值作為地址來使用,應通過強制型別轉換將數字轉換為適當的地址型別:

    int *ptr;
    ptr = (int *) 0xB8000000;  // type now match

    這樣,賦值語句的兩邊都是整數的地址,因此這樣賦值有效。

    注意,pt是int值的地址並不意味著pt本身的型別是int。例如,在有些平臺中,int型別是個2位元組值,而地址是個4位元組值。

  3. 為什麼說字首++/--比字尾++/--的效率高?

    對於內建型別和當代的編譯器而言,這看似不是什麼問題。然而,C++允許您針對類定義這些運算子,在這種情況下,使用者這樣定義字首函式:將值加1,然後返回結果;但字尾版本首先複製一個副本,將其加1,然後將複製的副本返回。因此,對於類而言,字首版本的效率比字尾版本高。
    總之,對於內建型別,採用哪種格式不會有差別,但對於使用者定義的型別,如果有使用者定義的遞增和遞減運算子,則字首格式的效率更高。

  4. 逗號運算子
    到目前為止,逗號運算子最常見的用途是將兩個或更多的表示式放到一個for迴圈表示式中。逗號運算子的特性有下面幾個:

    • 它確保先計算第一個表示式,然後計算第二個表示式;
      i = 20, j = 2 * i; // i set to 20, then j set to 40
    • 逗號表示式的值是第二部分的值。例如,上面表示式的值為40。
    • 在所有運算子中,逗號運算子的優先順序是最低的。例如:
      cats = 17, 240;
      被解釋我:
      (cats = 17), 240;
      也就是說,將cats設定為17,後面的240不起作用。如果是cats = (17, 240);那麼cats就是240了。
  5. 有用的字元函式庫cctype
    從C語言繼承而來,老式格式是ctype.h,常用的有:

    1.png
     
  6. 快排中中值的選取:
    將元素每5個一組,分別取中值。在n/5箇中值裡面找到中值,作為partition的pivot。
    為什麼*不每3個一組?保證pivot左邊右邊至少3n/10個元素,這樣最差O(n)。

  7. C++儲存方案:C++三種,C++11四種
    這些方案的區別就在於資料保留在記憶體中的時間。

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

  8. 自己寫string類注意事項:

    • 關於記錄已有物件數object_count
      不要在類宣告(即標頭檔案)中初始化靜態成員變數,這是因為宣告描述瞭如何分配記憶體,但並不分配記憶體。對於靜態類成員,可以在類宣告之外使用單獨的語句來進行初始化,這是因為靜態類成員是單獨儲存的,而不是物件組成部分。請注意,初始化語句指出了型別int(不可缺少),並使用了作用域運算子,但沒有使用關鍵字static。
      初始化是在方法檔案中,而不是在類宣告檔案中進行的,這是因為類宣告位於標頭檔案中,可能被包含多次,這樣若在標頭檔案中進行初始化靜態成員,將出現多個初始化語句副本,從而引發錯誤。
      對於不能在類宣告中初始化靜態成員的一種例外情況是:靜態資料成員為整型或列舉型const。即如果靜態資料成員是整型或列舉型,則可以在類宣告中初始化。
    • 注意重寫拷貝建構函式和賦值運算子,其中賦值運算子的原型為:
      Class_name & Class_name::operator=(const Class_name &);
      它接受並返回一個指向類物件的引用,目的應該是方便串聯使用。
  9. 何時呼叫拷貝(複製)建構函式:

    StringBad ditto (motto);   
    StringBad metoo = motto; 
    StringBad also = StringBad(motto); 
    StringBad * pStringBad = new StringBad (motto);

    以上4中方式都將呼叫:StringBad(const StringBad &)

    • 其中中間兩種宣告可能會使用複製建構函式直接建立metoo和also物件,也可能使用複製建構函式生成一個臨時物件,然後將臨時物件的內容賦給metoo和also,這取決於具體的實現。最後一種宣告使用motto初始化一個匿名物件,並將新物件的地址賦給pStringBad指標。
    • 每當程式生成了物件副本時,編譯器都將使用複製建構函式。具體的說,當函式按值傳遞物件或函式返回物件時,都將使用複製建構函式。記住,按值傳遞意味著建立原始變數的一個副本。
    • 編譯器生成臨時物件時,也將使用複製建構函式。例如,將3個Vector物件相加時,編譯器可能生成臨時的Vector物件來儲存中間的結果。
    • 另外,String sailor = sports;等價於String sailor = (String)sports;因此呼叫的是拷貝建構函式
  10. 何時呼叫賦值運算子:

    • 已有的物件賦給另一個物件時,將呼叫過載的賦值運算子。
    • 初始化物件時,並不一定會使用賦值操作符:
      StringBad metoo=knot;   // use copy constructor, possibly assignment, too

      這裡,metoo是一個新建立的物件,被初始化為knot的值,因此使用賦值建構函式。不過,正如前面指出的,實現時也可能分兩步來處理這條語句:使用複製建構函式建立一個臨時物件,然後通過賦值操作符將臨時物件的值複製到新物件中。這就是說,初始化總是會呼叫複製建構函式,而使用=操作符時也可能呼叫賦值建構函式。

    與複製建構函式相似,賦值運算子的隱式實現也對成員進行逐個複製。如果成員本身就是類物件,則程式將使用為這個類定義的賦值運算子來複制該成員,但靜態資料成員不受影響。

  11. 賦值運算子和拷貝建構函式在實現上的區別:

    • 由於目標物件可能引用了以前分配的資料,所以函式應使用delete[]來釋放這些資料。
    • 函式應當避免將物件賦給自身;否則給物件重新賦值前,釋放記憶體操作可能刪除物件的內容。
    • 函式返回一個指向呼叫物件的引用(方便串聯使用),而拷貝建構函式沒有返回值。

    下面的程式碼說明了如何為StringBad類編寫賦值操作符:

    StringBad & StringBad::operator=(const StringBad & st)
    {
     if(this == & st)
        return * this;
     delete [] str;
     len = st.len;
     str = new char [len + 1];
     strcpy(str,st.str);
     return *this;
    }

    程式碼首先檢查自我複製,這是通過檢視賦值操作符右邊的地址(&s)是否與接收物件(this)的地址相同來完成的,如果相同,程式將返回*this,然後結束。

    如果不同,釋放str指向的記憶體,這是因為稍後將把一個新字串的地址賦給str。如果不首先使用delete操作符,則上述字串將保留在記憶體中。由於程式程式不再包含指向字串的指標,一次這些記憶體被浪費掉。
    接下來的操作與複製建構函式相似,即為新字串分配足夠的記憶體空間,然後複製字串。
    賦值操作並不建立新的物件,因此不需要調整靜態資料成員num_strings的值。

  12. 過載運算子最好宣告為友元
    比如將比較函式作為友元,有助於將String物件與常規的C字串進行比較。例如,假設answer是String物件,則下面的程式碼:
    if("love" == answer)
    將被轉換為:
    if(operator == ("love", answer))
    然後,編譯器將使用某個建構函式將程式碼轉換為:
    if(operator == (String("love"), answer))
    這與原型是相匹配的。

  13. 在重寫string類時使用中括號訪問字元時注意:
    (1)為什麼過載的[]返回值是個char &而不是char?
    (2)為什麼有兩個過載[]的版本,另一個是const版本?

    解答(1):
    將返回類制宣告為char &,便可以給特定元素陚值。例如,可以編寫這樣的程式碼:
    String means ("might");
    means [9] = ' r';
    第二條語句將被轉換為一個過載運算子函式呼叫:
    means.operator[][0] = 'r';
    這裡將r陚給方法的返回值,而函式返回的是指向means.str[0]的引用,因此上述程式碼等同於下面的程式碼:
    means.str[0] = 'r';
    程式碼的最後一行訪問的是私有資料,但由於operator 是類的一個方法,因此能夠修改陣列的內容。 最終的結果是“might”被改為“right”。

    解答(2):
    假設有下面的常量物件:
    const String answer("futile");
    如果只有上述operator定義,則下面的程式碼將出錯:
    cout << answer[1]; // compile-time error
    原因是answer是常量,而上述方法無法確保不修改資料(實際上,有時該方法的工作就是修改資料, 因此無法確保不修改資料)。
    但在過載時,C++將區分常量和非常量函式的特徵標,因此可以提供另一個僅供const String物件使用 的 operator版本:
    // for use with const String objects
    const char & string::operator const {
    return str[i];
    }
    有了上述定義後,就可以讀/寫常規String物件了 :而對於const Siring物件,則只能讀取其資料。

  14. 靜態成員函式在類宣告外定義實現時不能再加static關鍵字,與靜態成員變數一樣。

  15. 實現has-a關係的兩種方法:

    • 組合(或包含)方式。這是我們通常採用的方法。
    • c++還有另一種實現has-a關係的途徑—私有繼承。使用私有繼承,基類的公有成員和保護成員都將稱為派生類的私有成員。這意味著基類方法將不會稱為派生物件公有介面的一部分,但可以派生類的成員函式中使用它們。而使用公有繼承,基類的公有方法將稱為派生類的公有方法。簡言之,派生類將繼承基類的介面:這是is-a關係的一部分。使用私有繼承,基類的公有方法將稱為派生類的私有方法,即派生類不繼承基類的介面。正如從被包含物件中看到的,這種不完全繼承是has-a關係的一部分。
      使用私有繼承,類將繼承實現。例如,如果從String類派生出Student類,後者將有一個String類元件,可用於儲存字串。另外,Student方法可以使用String方法類訪問String元件。

      包含將物件作為一個命名的成員物件新增到類中,而私有繼承將物件作為一個未被命名的繼承物件新增到類中。我們使用術語子物件來表示同繼承或包含新增的物件。
      因此,私有繼承提供的特性與包含相同:獲得實現,但不獲得介面。所以,私有繼承也可以用來實現has-a關係

    • 使用包含還是使用私有繼承?
      由於既可以使用包含,也可以使用私有繼承來建立has-a關係,那麼應使用何種方式呢?大多數C++程式設計師傾向於使用包含。

      通常,應使用包含來建立has-a關係;如果新類需要訪問原有類的保護成員,或需要重新定義 虛擬函式,則應使用私有繼承。
      • 首先,它易於理解。類宣告中包含表示被包含類的顯式命名物件,程式碼可以通過名稱引用這些物件,而使用繼承將使關係更抽象。
      • 其次,繼承會引起很多問題,尤其從多個基類繼承時,可能必須處理很多問題,如包含同名方法的獨立的基類或共亨祖先的獨立基類。
        總之,使用包含不太可能遇到這樣的麻煩。
      • 另外,包含能夠包括多個同類的子物件。如果某個類需要3個string物件,可以使用包含宣告3個獨立的string成員。而繼承則只能使用一個這樣的物件(當物件都沒有名稱時,將難以區分)。

      然而,私有繼承所提供的特性確實比包含多。例如,假設類包含保護成員(可以是資料成員,也可以是成員函式),則這樣的成員在派生類中足可用的,但在繼承層次結構外是不可用的。如果使用組合將這樣的類包含在另一個類中,則後者將不是派生類,而是位於繼承層次結構之外,因此不能訪問保護成員。但通過繼承得到的將是派生類,因此它能夠訪問保護成員。
      另—種需要使用私有繼承的情況是需要重新定義虛擬函式。派生類可以重新定義虛擬函式,但包含類不能。使用私有繼承,重新定義的函式將只能在類中使用,而不是公有的。

  16. 關於保護繼承
    保護繼承是私有繼承的變體,保護繼承在列出基類時使用關鍵字protected;

    class Student : protected std::string,
                 protected std::valarray<double>
    {
    ...
    }

    使用保護繼承時,基類的公有成員和保護成員都將成為派生類的保護成員,和私有繼承一樣,基類的介面在派生類中也是可用的,但在繼承層次結構之外是不可用的。當從派生類派生出另一個類的時,私有繼承和保護繼承

    之間的主要區別便呈現出來了。使用私有繼承時,第三代將不能使用基類的介面,這是因為基類的共有方法在派生類中將變成私有方法;使用保護繼承時,基類的公有方法在第二代中將程式設計呢個受保護的,因此第三代派生類可以使用它們。

    下表總結了公有、私有和保護繼承。隱式向上轉換意味著無需進行顯式型別轉換,就可以將基類指標或引用指向派生類物件。

    2.png
     
  17. 智慧指標相關
    請參考:C++智慧指標簡單剖析,推薦必看。

  18. C++中的容器種類:
    • 序列容器(7個)
      • vector:提供了自動記憶體管理功能(採用了STL普遍的記憶體管理器allocator),可以動態改變物件長度,提供隨機訪問。在尾部新增和刪除元素的時間是常數的,但在頭部或中間就是線性時間。
      • deque:雙端佇列(double-ended queue),支援隨機訪問,與vector類似,主要區別在於,從deque物件的開始位置插入和刪除元素的時間也是常數的,所以若多數操作發生在序列的起始和結尾處,則應考慮使用deque資料結構。為實現在deque兩端執行插入和刪除操作的時間為常數時間這一目的,deque物件的設計比vector更為複雜,因此,儘管二者都提供對元素的隨機訪問和在序列中部執行線性時間的插入和刪除操作,但vector容器執行這些操作時速度更快些。
      • list:雙向連結串列(是迴圈的)。目的是實現快速插入和刪除。
      • forward_list(C++11):實現了單連結串列,不可反轉。相比於list,forward_list更簡單,更緊湊,但功能也更少。
      • queue:是一個介面卡類。queue模板讓底層類(預設是deque)展示典型的佇列介面。queue模板的限制比deque更多,它不僅不允許隨機訪問佇列元素,甚至不允許遍歷佇列。與佇列相同,只能將元素新增到隊尾、從隊首刪除元素、檢視隊首和隊尾的值、檢查元素數目和測試佇列是否為空。
      • priority_queue:是另一個介面卡類,支援的操作與queue相同。
        priority_queue模板類是另一個介面卡類,它支援的操作與queue相同。兩者之間的主要區別在於,在priority_queue中,最大的元素被移到對首。內部區別在於,預設的底層類是vector。可以修改用於確定哪個元素放到隊首的比較方式,方法是提供一個可選的建構函式引數:
        priority_queue<int> pq1;                     // default version
        priority_queue<int> pg2(greater<int>);       // use greater<int> to order
        greater<>函式是一個預定義的函式物件。

        stack:與queue相似,stack也是一個介面卡類,它給底層類(預設情況下為vector)提供了典型的棧介面。

    • 關聯容器
      • 4種有序關聯容器:set、multiset、map和multimap,底層基於樹結構
      • C++11又增加了4種無序關聯容器:unordered_set、unordered_multiset、unordered_map和unordered_multimap,底層基於hash。

相關文章