C++ Primer(第4版)-學習筆記-第3部分:類和數據抽象

第12章 類
     
  1. 每一個類能夠沒有成員,也能夠定義多個成員,成員能夠是數據、函數或類型別名。

  2. 成員函數必須在類內部聲明,能夠在類內部定義,也能夠在類外部定義。若是在類內部定義,就默認是內聯函數。
    內聯函數有三種:
    (1)直接在類內部定義。
    (2)在類內部聲明,加上inline關鍵字,在類外部定義。
    (3)在類內部聲明,在類外部定義,同時加上inline關鍵字。
    注意:此種狀況下,內聯函數的定義一般應該放在類定義的同一頭文件中,而不是在源文件中。這是爲了保證內聯函數的定義在調用該函數的每一個源文件中是可見的。
  3. const 成員不能改變其所操做的對象的數據成員。const 成員函數的const關鍵字必須同時出如今聲明和定義中,若只出如今其中一處,就會出現一個編譯時錯誤。
    double avg_price() const;

  4. 爲何將類定義在頭文件中?
    遇到右花括號,類的定義結束。一旦定義了類,就知道了類的成員,存儲空間。在一個給定的源文件中,一個類只能被定義一次。若是在多個文件中定義一個類,那麼每一個文件中的定義必須是徹底相同的。
    將類定義在頭文件中,能夠保證每一個使用該類(要使用該類,必須#include該類的頭文件)的文件中以一樣的方式定義類。
     
  5. 避免同一個文件被包含屢次,保證即便頭文件在同一個文件中被包含屢次,其定義只出現一次。
    (1) 使用頭文件保護符 (#ifndef …… #endif)
    #ifndef __SOMEFILE_H__
    #define __SOMEFILE_H__
    ... ... // 聲明、定義語句
    #endif
        優勢:#ifndef的方式受C/C++語言標準支持。它不光能夠保證同一個文件不會被包含屢次,也能保證內容徹底相同的兩個文件(或者代碼片斷)不會被不當心同時包含。
        缺點:若是不一樣頭文件中的宏名不當心「撞車」,可能就會致使你看到頭文件明明存在,編譯器卻硬說找不到聲明的情況。
    (2) 使用#pragma once
    #pragma once
    ... ... // 聲明、定義語句
        #pragma once通常由編譯器提供保證:同一個文件不會被包含屢次。注意這裏所說的「同一個文件」是指物理上的一個文件,而不是指內容相同的兩個文件。你沒法對一個頭文件中的一段代碼做pragma once聲明,而只能針對文件。
        優勢:你沒必要再費勁想個宏名了,固然也就不會出現宏名碰撞引起的奇怪問題,大型項目的編譯速度也所以提升了一些。
        缺點:是若是某個頭文件有多份拷貝,本方法不能保證他們不被重複包含。固然,相比宏名碰撞引起的「找不到聲明」的問題,這種重複包含很容易被發現並修正。
  6. 類聲明(declare)
    class Screen;
    聲明瞭一個類,但沒有定義它。在聲明以後,定義以前,只知道Screen是一個類名,但不知道包含哪些成員。只能以有限方式使用它,例如:是一個類名,但不知道包含哪些成員。只能以有限方式使用它,例如:定義指向該類型的指針或引用,聲明(不是定義)使用該類型做爲形參類型或返回類型的函數。例如:
    void Test1(Screen& a){};
    void Test1( Screen* a){};-
  7. 類定義(define)
    在建立類的對象以前,必須完整的定義該類,而不僅是聲明類。因此,類不能具備自身類型的數據成員,但能夠包含指向本類的指針或引用。
    class LinkScreen
    {
              Screen window;
              LinkScreen* next;
              LinkScreen* prev;
    }; //注意,分號不能丟

  8. 類對象
    定義類時,不進行存儲分配。
    定義類對象時,才分配存儲空間。如: LinkScreen scr;

  9. 爲何類的定義以分號結束?
    由於在類定義以後能夠接一個對象定義列表。定義必須以分號結束:
    class Sales_item { /* ... */ };
    class Sales_item { /* ... */ } accum, trans; 

  10. mutable可變數據成員
    mutable可變數據成員永遠都不能爲const,因此,const成員函數能夠改變mutable成員。

  11. 類做用域
    每一個類都定義本身的做用域和惟一的類型。
    類的做用域包括類的內部(花括號以內), 定義在類外部的成員函數的參數表(小括號以內)和函數體(花括號以內)。
    注意:成員函數的返回類型不必定在類做用域中。可經過 類名::來判斷是不是類的做用域,::以前不屬於類的做用域,::以後屬於類的做用域。例如
    char Screen::get(index r, index c) const
    {
         index row = r * width;      // compute the row location
         return contents[row + c];   // offset by c to fetch specified character

    Screen:: 以前的返回類型就不在類的做用域,Screen:: 以後的函數名開始到函數體都是類的做用域。
  12. 構造函數
    構造函數是特殊的成員函數,用來保證每一個對象的數據成員具備合適的初始值。
    構造函數名字與類名相同,不能指定返回類型(也不能定義返回類型爲void),能夠有0-n個形參。
    在建立類的對象時,編譯器就運行一個構造函數。

  13. 構造函數初始化式
    Sales_item::Sales_item(const string &book):isbn(book), units_sold(0), revenue(0.0) { } 
    構造函數初始化列表以一個冒號開始,接着是一個以逗號分隔的數據成員列表,每一個數據成員後面跟一個放在圓括號中的初始化式。構造函數初始化只在構造函數的定義中而不是聲明中指定。
    能夠認爲構造函數分兩個階段執行:(1)初始化階段;(2)普通的計算階段。初始化列表屬於初始化階段(1),構造函數函數體中的全部語句屬於計算階段(2)。初始化列表比構造函數體先執行。無論成員是否在構造函數初始化列表中顯式初始化,類類型的數據成員老是在初始化階段初始化。
      
  14. 類對象的數據成員的初始化      
    在類A的構造函數初始化列表中沒有顯式說起的每一個成員,使用與初始化變量相同的規則來進行初始化。例如,類類型的數據成員,運行該類型的默認構造函數來初始化。內置或複合類型的成員的初始值依賴於該類對象的做用域:在局部做用域中不被初始化,在全局做用域中被初始化爲0。假設有一個類A,
    class A
    {
        public:
            int ia;
            B b;
    };
    A類對象A a;無論a在局部做用域仍是全局做用域,b使用B類的默認構造函數來初始化,ia的初始化取決於a的做用域,a在局部做用域,ia不被初始化,a在全局做用域,ia初始化0。
        能夠初始化 const 對象或引用類型的對象,但不能對它們賦值。在開始執行構造函數的函數體以前,要完成初始化。初始化 const 或引用類型數據成員的惟一機會是構造函數初始化列表中,在構造函數函數體中對它們賦值不起做用。沒有默認構造函數的類類型的成員,以及 const 或引用類型的成員,必須在初始化列表中完成初始化。

  15. 成員初始化的次序
    每一個成員在構造函數初始化列表中只能指定一次。
    成員被初始化的次序就是定義成員的次序,跟初始化列表中的順序無關。

  16. 初始化式能夠是任意表達式
    Sales_item(const std::string &book, int cnt, double price): isbn(book), units_sold(cnt), revenue(cnt * price) { }

  17. 類類型的數據成員的初始化式
    初始化類類型的成員時,要指定實參並傳遞給成員類型的一個構造函數,可使用該類型的任意構造函數。
    Sales_item(): isbn(10, '9'), units_sold(0), revenue(0.0) {}

  18. 默認構造函數
    定義一個對象時沒有提供初始化式,就使用默認構造函數。爲全部形參提供默認實參的構造函數也定義了默認構造函數。
    只有當一個類沒有定義構造函數時,編譯器纔會自動生成一個默認構造函數。一個類哪怕只定義了一個構造函數,編譯器也不會再生成默認構造函數。
    建議:若是定義了其餘構造函數,也提供一個默認構造函數。
    若是類包含內置或複合類型(如 int& 或 string*)的成員,它應該定義本身的構造函數來初始化這些成員。每一個構造函數應該爲每一個內置或複合類型的成員提供初始化式。

  19. 隱式類類型轉換
    只含單個形參的構造函數可以實現從形參類型到該類類型的一個隱式轉換。
    class Sales_item {
    public:
             Sales_item(const std::string &book = ""): isbn(book), units_sold(0), revenue(0.0) { }
    };
    string null_book = "9-999-99999-9"; 
    item.same_isbn(null_book);  本行就實現了從null_book 到Sales_item 類型的隱式轉換。

  20. 抑制由構造函數定義的隱式轉換
    經過將構造函數聲明爲 explicit,來防止在須要隱式轉換的上下文中使用構造函數: 
    class Sales_item {
         public:
             // default argument for book is the empty string
             explicit Sales_item(const std::string &book = ""):
                       isbn(book), units_sold(0), revenue(0.0) { }
             explicit Sales_item(std::istream &is);
             // as before
         }; 
    一般,除非有明顯的理由想要定義隱式轉換,不然,單形參構造函數應該爲 explicit。將構造函數設置爲 explicit 能夠避免錯誤,而且當轉換有用時,用戶能夠顯式地構造對象。

  21. 友元
    友元機制容許一個類將對其非公有成員的訪問權授予指定的函數或類。友元只能出如今類定義的內部的任何地方。友元不是授予友元關係的那個類的成員,因此它們不受聲明出現部分的訪問控制影響。建議將友元聲明成組地放在類定義的開始或結尾

    使其餘類的成員函數成爲友元
    例如,在類A中要使類B中的成員函數C成爲友元,必須先定義類B(在B中聲明函數C),而後定義類A,在類A中聲明B的成員函數C爲友元,最後再定義C。

  22. static 成員變量
    static 數據成員是與類關聯的對象,並不與該類的對象相關聯。
    static 成員遵循正常的公有/私有訪問規則。
     
  23. 使用 static 成員而不是全局對象有三個優勢。
    (1)  static 成員的名字是在類的做用域中,所以能夠避免與其餘類的成員或全局對象名字衝突。
    (2)  能夠實施封裝。static 成員能夠是私有成員,而全局對象不能夠。
    (3)  經過閱讀程序容易看出 static 成員是與特定類關聯的。這種可見性可清晰地顯示程序員的意圖。 


  24. static 成員函數
    在類的內部聲明函數時須要添加static關鍵字,可是在類外部定義函數時就不須要了。
    由於static 成員是類的組成部分但不是任何對象的組成部分,因此有如下幾個特色:
    1) static 函數沒有 this 指針
    2) static 成員函數不能被聲明爲 const 
    3) static 成員函數也不能被聲明爲虛函數

  25. static 數據成員 
    static 數據成員能夠聲明爲任意類型,能夠是常量、引用、數組、類類型,等等。
    static 數據成員必須在類定義體的外部定義(正好一次),而且應該在定義時進行初始化。建議定義在類的源文件中名,即與類的非內聯函數的定義同一個文件中。注意,定義時也要帶上類類型+"::"
    double Account::interestRate = 0.035; 

  26. 特殊的整型 const static 成員 
    整型 const static 數據成員能夠直接在類的定義體中進行初始化,例如:
    static const int period = 30; 

  27. static 數據成員的類型能夠是該成員所屬的類類型。非 static 成員只能是自身類對象的指針或引用: 
    class Bar {
         public:
             // ...
         private:
             static Bar mem1; // ok
             Bar *mem2;       // ok
             Bar mem3;        // error
    }; 

  28. 非 static 數據成員不能用做默認實參,static 數據成員可用做默認實參。
    class Screen {
         public:
             // bkground refers to the static member
             // declared later in the class definition
             Screen& clear(char =  bkground);
         private:
              static const char  bkground = '#';
    };
 

第13章 複製控制    
  1. 複製構造函數
    複製構造函數是一種特殊構造函數,只有1個形參,該形參(經常使用 const &修飾)是對該類類型的引用。當定義一個新對象並用一個同類型的對象對它進行初始化時,將顯式使用複製構造函數。當將該類型的對象傳遞給函數或函數返回該類型的對象時,將隱式使用複製構造函數。

  2. 析構函數
    析構函數是構造函數的互補:當對象超出做用域或動態分配的對象被刪除時,將自動應用析構函數。析構函數可用於釋放構造對象時或在對象的生命期中所獲取的資源。無論類是否認義了本身的析構函數,編譯器都自動執行類中非 static 數據成員的析構函數。

  3. 複製構造函數、賦值操做符和析構函數總稱爲複製控制。編譯器自動實現這些操做,但類也能夠定義本身的版本。

  4. C++ 支持兩種初始化形式:直接初始化和複製初始化。直接初始化將初始化式放在圓括號中,複製初始化使用 = 符號。
    對於內置類型,例如int, double等,直接初始化和複製初始化沒有區別。
    對於類類型:直接初始化直接調用與實參匹配的構造函數;複製初始化先使用指定構造函數建立一個臨時對象,而後用複製構造函數將那個臨時對象複製到正在建立的對象。直接初始化比複製初始化更快。


  5. 形參和返回值
    當形參或返回值爲類類型時,由複製構造函數進行復制。

  6. 初始化容器元素
    複製構造函數可用於初始化順序容器中的元素。例如:
    vector<string> svec(5);
    編譯器首先使用 string 默認構造函數建立一個臨時值,而後使用複製構造函數將臨時值複製到 svec 的每一個元素。 

  7. 構造函數與數組元素
    若是沒有爲類類型數組提供元素初始化式,則將用默認構造函數初始化每一個元素。
    若是使用常規的花括號括住的數組初始化列表來提供顯式元素初始化式,則使用複製初始化來初始化每一個元素。根據指定值建立適當類型的元素,而後用複製構造函數將該值複製到相應元素:
    Sales_item primer_eds[] = { string("0-201-16487-6"),
                                     string("0-201-54848-8"),
                                     string("0-201-82470-1"),
                                     Sales_item()
                                   };

  8.  合成的複製構造函數
    若是咱們沒有定義複製構造函數,編譯器就會爲咱們合成一個。
    合成複製構造函數的行爲是,執行逐個成員初始化,將新對象初始化爲原對象的副本。
    逐個成員初始化:合成複製構造函數直接複製內置類型成員的值,類類型成員使用該類的複製構造函數進行復制。
    例外:若是一個類具備數組成員,則合成複製構造函數將複製數組。複製數組時合成複製構造函數將複製數組的每個元素。

  9. 定義本身的複製構造函數
    (1) 只包含類類型成員或內置類型(但不是指針類型)成員的類,無須顯式地定義複製構造函數,也能夠複製。 
    (2) 有些類必須對複製對象時發生的事情加以控制。例如,類有一個數據成員是指針,或者有成員表示在構造函數中分配的其餘資源。而另外一些類在建立新對象時必須作一些特定工做。這兩種狀況下,都必須定義本身的複製構造函數。


  10. 禁止複製
    有些類須要徹底禁止複製。例如,iostream 類就不容許複製。
    爲了防止複製,類必須顯式聲明其複製構造函數爲 private。

  11. 大多數類應定義複製構造函數和默認構造函數
    通常來講,最好顯式或隱式定義默認構造函數和複製構造函數。只有不存在其餘構造函數時才合成默認構造函數。
    若是定義了複製構造函數,也必須定義默認構造函數。

  12.  賦值操做符
    與複製構造函數同樣,若是類沒有定義本身的賦值操做符,則編譯器會合成一個。
     Sales_item& operator=(const Sales_item &);

  13. 合成賦值操做符
    合成賦值操做符會逐個成員賦值:右操做數對象的每一個成員賦值給左操做數對象的對應成員。除數組以外,每一個成員用所屬類型的常規方式進行賦值。對於數組,給每一個數組元素賦值。

  14. 複製和賦值常一塊兒使用 
    通常而言,若是類須要複製構造函數,它也會須要賦值操做符。 

  15. 析構函數
    構造函數的用途之一是自動獲取資源;與之相對的是,析構函數的用途之一是回收資源。除此以外,析構函數能夠執行任意操做,該操做是類設計者但願在該類對象的使用完畢以後執行的。

  16. 什麼時候調用析構函數?
    撤銷(銷燬)類對象時會自動調用析構函數。
    變量(類對象)在超出做用域時應該自動撤銷(銷燬)。
    動態分配的對象(new A)只有在指向該對象的指針被刪除時才撤銷(銷燬)。
    撤銷(銷燬)一個容器(無論是標準庫容器仍是內置數組)時,也會運行容器中的類類型元素的析構函數(容器中的元素老是從後往前撤銷)。

  17.  什麼時候編寫顯式析構函數?
    若是類須要定義析構函數,則它也須要定義賦值操做符和複製構造函數,這個規則常稱爲三法則:若是類須要析構函數,則須要全部這三個複製控制成員。

  18. 合成析構函數
    合成析構函數按對象建立時的逆序撤銷每一個非 static 成員,所以,它按成員在類中聲明次序的逆序撤銷成員。對於每一個類類型的成員,合成析構函數調用該成員的析構函數來撤銷對象。合成析構函數並不刪除指針成員所指向的對象。 因此,若是有指針成員,必定要定義本身的析構函數來刪除指針。
    析構函數與複製構造函數或賦值操做符之間的一個重要區別:即便咱們編寫了本身的析構函數,合成析構函數仍然運行。

  19. 大多數 C++ 類採用如下三種方法之一管理指針成員:
    1)指針成員採起常規指針型行爲。這樣的類具備指針的全部缺陷但無需特殊的複製控制。
    2)類能夠實現所謂的「智能指針」行爲。指針所指向的對象是共享的,但類可以防止懸垂指針。
    3)類採起值型行爲。指針所指向的對象是惟一的,由每一個類對象獨立管理。

  20. 具備指針成員的對象通常須要定義複製控制成員。
    爲了管理具備指針成員的類,必須定義三個複製控制成員:複製構造函數、賦值操做符和析構函數。這些成員能夠定義指針成員的指針型行爲或值型行爲。 

  21. 智能指針
    智能指針類與實現普通指針行爲的類的區別在於:智能指針一般接受指向動態分配對象的指針並負責刪除該對象,用戶分配對象,但智能指針類刪除它,所以智能指針類須要實現複製控制成員來管理指向其共享對象的指針,只有在銷燬了指向共享對象的最後一個智能指針後,才能刪除該共享對象,使用計數是實現智能指針類的最經常使用的方式。

  22. 值型類
    所謂值型類,實質具備值語義的類,其特徵爲:對該類對象進行復制時,會獲得一個不一樣的新副本,對新副本所作的改變不會影響原有對象。

第14章 重載操做符與轉換
  1.  重載操做符與內置操做符有什麼異同?
    相同:操做符的優先級,結合性或操做數數目均相同
    不一樣:重載操做符必須具備至少一個類類型或枚舉類型的操做數;重載操做符並不保證操做數的求值順序,尤爲是,不會保證內置邏輯 AND、邏輯 OR和逗號操做符的操做數求值。


  2. 類成員與非成員重載操做符
    重載一元操做符若是做爲成員函數就沒有(顯式)形參,若是做爲非成員函數就有一個形參。相似地,重載二元操做符定義爲成員時有一個形參,定義爲非成員函數時有兩個形參。

  3. 將操做符設置爲類成員仍是普通非成員函數的原則: •  賦值(=)、下標([])、調用(())和成員訪問箭頭(->)等操做符必須定義爲成員,將這些操做符定義爲非成員函數將在編譯時標記爲錯誤。 •  像賦值同樣,複合賦值操做符一般應定義爲類的成員,與賦值不一樣的是,不必定非得這樣作,若是定義非成員複合賦值操做符,不會出現編譯錯誤。 •  改變對象狀態或與給定類型緊密聯繫的其餘一些操做符,如自增、自減和解引用,一般就定義爲類成員。 •  對稱的操做符,如算術操做符、相等操做符、關係操做符和位操做符,最好定義爲普通非成員函數。
相關文章
相關標籤/搜索