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


下面是我看《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。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM