《C++ Primer》筆記 第7章 類


定義抽象數據類型

定義成員函數

  • 成員函數的聲明必須在類的內部,它的定義則既可以在類的內部也可以在類的外部。作為接口組成部分的非成員函數,它們的定義和聲明都在類的外部。
  • 定義在類內部的函數是隱式的inline函數。

引入this

  • 成員函數通過一個名為this的額外的隱式參數來訪問調用它的那個對象。當我們調用一個成員函數時,用請求該函數的對象地址初始化this(相當於Python中的self形參?)。偽代碼示意:Sales_data::isbn(&total),任何對類成員的直接訪問都被看做this的隱式引用,也就是說,當isbn使用bookNo時,它隱式地使用this指向的成員,就像我們書寫了this->bookNo一樣。
  • 對於我們來說,this形參是隱式定義的。實際上,任何自定義名為this的參數或變量的行為都是非法的。我們可以在成員函數體內部使用this。例:std::string isbn() const { return this->bookNo; }
  • 因為this的目的總是指向“這個”對象,所以this是一個常量指針,我們不允許改變this中保存的地址。

引入const成員函數

  • 默認情況下,this的類型是指向類類型非常量版本的常量指針。盡管this是隱式的,但它仍然需要遵循初始化規則,意味着(在默認情況下)我們不能把this綁定到一個常量對象上。這一情況也就使得我們不能在一個常量對象上調用普通的成員函數。
  • 緊跟在參數列表后面的const表示this是一個指向常量的指針。像這樣使用const的成員函數被稱作常量成員函數
  • 在常量成員函數中,因為this是指向常量的指針,所以常量成員函數不能改變調用它的對象的內容。
  • 常量對象,以及常量對象的引用或指針都只能調用常量成員函數。

類作用域和成員函數

  • 編譯器分兩步處理類:首先編譯成員的聲明,然后才輪到成員函數體(如果有的話)。因此,成員函數體可以隨意使用類中的其他成員而無須在意這些成員出現的次序。

在類的外部定義成員函數

  • 當我們在類的外部定義成員函數時,成員函數的定義必須與它的聲明匹配。例:double Sales_data::avg_price() const {...}。使用作用域運算符(::),告知編譯器剩余的代碼是位於類的作用域內的。(參數列表和函數體內不用再加作用域運算符)

定義一個返回this對象的函數

  • 內置的賦值運算符把它的左側運算對象當成左值返回。下面的示例模擬了加法行為,並返回對象的引用:
      Sales_data& Sales_data::combine(const Sales_data &rhs)
      {
            units_sold += rhs.units_sold; // 把rhs的成員加到this對象的成員上
            revenue += rhs.revenue;
            return *this; // 返回調用該函數的對象
      }
    

定義類相關的非成員函數

  • 一般來說,如果非成員函數是類接口的組成部分,則這些函數的聲明應該與類在同一個頭文件中。
  • 默認情況下,拷貝類的對象其實拷貝的是對象的數據成員。

構造函數

  • 構造函數的名字和類名相同。
  • 構造函數沒有返回類型。
  • 類可以包含多個構造函數,和其他重載函數差不多,不同的構造函數之間必須在參數數量或參數類型上有所區別。
  • 構造函數不能被聲明成const的。當我們創建類的一個const對象時,直到構造函數完成初始化過程后,對象才能真正取得其“常量”屬性。因此,構造函數在const對象的構造過程中可以向其寫值。
  • 類通過一個特殊的構造函數來控制默認初始化過程,這個函數叫做默認構造函數,默認構造函數無須任何實參。如果我們的類沒有顯式地定義構造函數,那么編譯器就會為我們隱式地定義一個默認構造函數。
  • 編譯器創建的構造函數又被稱為合成的默認構造函數。對於大多數類來說,這個合成的默認構造函數將按照如下規則初始化類的數據成員:
    • 如果存在類內的初始值,用它來初始化成員。
    • 否則,默認初始化該成員。

不能依賴合成的默認構造函數

  • 只有當類沒有聲明任何構造函數時,編譯器才會自動地生成默認構造函數。
  • 如果類包含有內置類型或者復合類型(比如數組和指針)的成員,則只有當這些成員全都被賦予了類內的初始值時(否則它們的值將是未定義的),這個類才適合於使用合成的默認構造函數。
  • 有的時候編譯器不能為某些類合成默認的構造函數。例如:如果類中包含一個其他類類型的成員且這個成員的類型沒有默認構造函數,那么編譯器將無法初始化該成員。對於這樣的類來說,我們必須自定義默認構造函數,否則該類將沒有可用的默認構造函數。

= default的含義

  • 如果我們需要默認的行為,那么可以通過在參數列表后面寫上= default來要求編譯器生成構造函數。
  • 其中,= default既可以和聲明一起出現在類的內部,也可以作為定義出現在類的外部。
  • 和其他函數一樣,如果= default在類的內部,則默認構造函數是內聯的;如果它在類的外部,則該成員默認情況下不是內聯的。例:Sales_data() = default;

構造函數初始值列表

  • 構造函數初始值是成員名字的一個列表,每個名字后面緊跟括號括起來的(或者在花括號內的)成員初始值。不同成員的初始化通過逗號分隔開來。例:Sales_data(const std::string &s): bookNo(s) {}
  • 當某個數據成員被構造函數初始值列表忽略時,它將以與合成默認構造函數相同的方式隱式初始化。如果你的編譯器不支持類內初始值,則所有構造函數都應該顯式地初始化每個內置類型的成員。
  • 沒有出現在構造函數初始值列表中的成員將通過相應的類內初始值(如果存在的話)初始化,或者執行默認初始化。

拷貝、賦值和析構

  • 對象在幾種情況下會被拷貝,如我們初始化變量以及以值的方式傳遞或返回一個對象等。當我們使用了賦值運算符時會發生對象的賦值操作。當對象不再存在時執行銷毀的操作。如果我們不主動定義這些操作,則編譯器將替我們合成它們。一般來說,編譯器生成的版本將對對象的每個成員執行拷貝、賦值和銷毀操作。
      total = trans; // 處理下一本書的信息
      
      // 它的行為與下面的代碼相同
      total.bookNo = trans.bookNo;
      total.units_sold = trans.units_sold;
      total.revenue = trans.revenue;
    

訪問控制與封裝

使用class和struct的區別

  • 使用class和struct定義類唯一的區別就是默認的訪問權限。如果我們使用struct關鍵字,則定義在第一個訪問說明符之前的成員是public的;相反,如果過我們使用class關鍵字,則這些成員是private的。

友元

友元的聲明

  • 友元聲明只能出現在類定義的內部,但是在類內出現的具體位置不限。友元不是類的成員也不受它所在區域訪問控制級別的約束。一般來說,最好在類定義開始或結束前的位置集中聲明友元。
  • 友元的聲明僅僅指定了訪問的權限,而非一個通常意義上的函數聲明。如果我們希望類的用戶能夠調用某個友元函數,那么我們就必須在友元聲明之外再專門對函數進行一次聲明。為了使友元對類的用戶可見,我們通常把友元的聲明與類本身放置在同一個頭文件中(類的外部)。
  • 許多編譯器並未強制限定友元函數必須在使用之前在類的外部聲明。

類的其他特性

類成員再探

定義一個類型成員

  • 除了定義數據和函數成員之外,類還可以自定義某種類型在類中的別名。由類定義的類型名字和其他成員一樣存在訪問權限,可以是public或者private中的一種。用來定義類型的成員必須先定義后使用,這一點與普通成員有所區別。因此,類型成員通常出現在類開始的地方。
      class Screen
      {
      public:
            typedef std::string::size_type pos; // using pos = std::string::size_type;
      private:
            pos cursor = 0;
            pos height = 0, width = 0;
            std::string contents;
      };
    

令成員作為內聯函數

  • 定義在類內部的成員函數是自動inline的。我們可以在類的內部把inline作為聲明的一部分顯式地聲明成員函數,同樣的,也能在類的外部用inline關鍵字修飾函數的定義。
  • 雖然我們無須在聲明和定義的地方同時說明inline,但這么做其實是合法的。不過,最好只在類外部定義的地方說明inline,這樣可以使類更容易理解。和我們在頭文件中定義inline函數的原因一樣(函數的展開不僅需要函數的聲明,還需要函數的定義),inline成員函數也應該與相應的類定義在同一個頭文件中。

可變數據成員

  • 一個可變數據成員永遠不會是const,即使它是const對象的成員。因此,一個const成員函數可以改變一個可變成員的值。

      class Screen
      {
      public:
            void some_member() const;
      private:
            // 該成員是個可變成員,因此任何成員函數,包括const函數在內都能改變它的值
            mutable size_t access_ctr; // 即使在一個const對象內也能被修改
            // 其他成員與之前的版本一致
      };
      void Screen::some_member() const
      {
            ++access_ctr; // 保存一個計數值,用於記錄成員函數被調用的次數
            // 該成員需要完成的其他工作
      }
    

類數據成員的初始值

  • 當我們為數據成員提供一個類內初始值時,必須以符號=或者花括號表示(不能用圓括號,會被當成函數)。

返回*this的成員函數

  • 一個const成員函數如果以引用的形式返回*this,那么它的返回類型將是常量引用。

類類型

  • 即使兩個類的成員列表完全一致,它們也是不同的類型。對於一個類來說,它的成員和其他任何類(或者任何其他作用域)的成員都不是一回事兒。

  • 我們可以把類名作為類型的名字使用,從而直接指向類類型。或者,我們也可以把類名跟在關鍵字class或struct后面。

      Sales_data item1; // 默認初始化Sales_data類型的對象
      class Sales_data item1; // 一條等價的聲明
    

類的聲明

  • 就像可以把函數的聲明和定義分離開來一樣,我們也能僅僅聲明類而暫時不定義它:class Screen;這種聲明有時被稱為前向聲明,它向程序中引入了名字Screen並且指明Screen是一種類類型。在它聲明之后定義之前是一個不完全類型(不清楚它到底包含哪些成員)。

  • 對於不完全類型:可以定義指向這種類型的指針或引用,也可以聲明(但是不能定義)以不完全類型作為參數或者返回類型的函數。

  • 對於一個類來說,在我們創建它的對象之前該類必須被定義過,而不能僅僅被聲明。否則編譯器就無法了解這樣的對象需要多少存儲空間。類似的,類也必須首先被定義,然后才能用引用或指針訪問其成員。畢竟,如果類尚未定義,編譯器也就不清楚該類到底有哪些成員。

  • 因為只有當類全部完成后類才算被定義,所以一個類的成員類型不能是該類自己。然而,一旦一個類的名字出現后,它就被認為是聲明過了(但尚未定義),因此類允許包括指向它自身類型的引用或指針:

      class Link_screen
      {
            Screen window;
            Link_screen *next;
            Link_screen *prev;
      };
    

友元再探

  • 在類中,可以把普通的非成員函數是定義成友元,也可以把其他類定義成友元,也可以把其他類(之前已定義過的)的成員函數定義成友元。此外,友元函數能定義在類的內部,這樣的函數是隱式內聯的。

      class Screen
      {
            // Window_mgr::clear必須在Screen類之前被聲明
            friend void Window_mgr::clear(ScreenIndex);
            // Screen類的剩余部分
      };
    
  • 如果一個類指定了友元類,則友元類的成員函數可以訪問此類包括非公有成員在內的所有成員。

      class Screen
      {
            // Window_mgr的成員可以訪問Screen類的私有部分
            friend class Window_mgr;
            // Screen類的剩余部分
      };
    
  • 友元關系不存在傳遞性。每個類負責控制自己的友元或友元函數。

令成員函數作為友元

  • 要想令某個成員函數作為友元,我們必須仔細組織程序的結構以滿足聲明和定義的彼此依賴關系。

函數重載和友元

  • 如果一個類型想把一組重載函數聲明成它的友元,它需要對這組函數中的每一個分別聲明。

友元聲明和作用域

  • 類和非成員函數的聲明不是必須在它們的友元聲明之前。當一個名字第一次出現在一個友元聲明中時,我們隱式地假定該名字在當前作用域中是可見的。然而,友元本身不一定真的聲明在當前作用域中。甚至就算在類的內部定義該函數,我們也必須在類的外部提供相應的聲明從而使得函數可見。換句話說,即使我們僅僅是用聲明友元的類的成員調用該友元函數,它也必須是被聲明過的。

      struct X
      {
            friend void f() { /*友元函數可以定義在類的內部*/ }
            X() { f(); } // 錯誤:f還沒有被聲明
            void g();
            void h();
      };
      void X::g() { return f(); } // 錯誤:f還沒有被聲明
      void f(); // 聲明那個定義在X中的函數
      void X::h() { return f(); } // 正確:現在f的聲明在作用域中了
    
  • 上面這段代碼最重要的是理解友元聲明的作用是影響訪問權限,它本身並非普通意義上的聲明

  • 注意,有的編譯器並不強制執行上述關於友元的限定規則。

類的作用域

  • 每個類都會定義它自己的作用域。在類的作用域之外,普通的數據和函數成員只能由對象、引用或者指針使用成員訪問運算符來訪問。對於類類型成員則使用作用域運算符訪問。不論哪種情況,跟在運算符之后的名字都必須是對應類的成員。

作用域和定義在類外部的成員

  • 一旦遇到了類名,定義的剩余部分就在類的作用域之內了,這里的剩余部分包括參數列表和函數體。

      void Window_mgr::clear(ScreenIndex i)
      {
            Screen &s = screens[i];
            s.contents = string(s.height * s.width, ' ');
      }
    
  • 函數的返回類型通常出現在函數名之前,因此當成員函數定義在類的外部時,返回類型中使用的名字都位於類的作用域之外。這時,返回類型必須指明它是哪個類的成員(尾置返回類型在當前類的定義域中)。

    class Window_mgr{
    public:
        // 向窗口添加一個Screen,返回它的編號
        ScreenIndex addScreen(const Screen&);
        // 其他成員與之前的版本一致
    };
    // 首先處理返回類型,之后我們才進入Window_mgr的作用域
    Window_mgr::ScreenIndex
    Window_mgr::addScreen(const Screen &s)
    {
    	Screens.push_back(s);
        return screens.size() - 1;
    }
    

名字查找與類的作用域

  • 一般的名字查找(尋找與所用名字最匹配的聲明的過程):
    • 首先,在名字所在的塊中尋找其聲明語句,只考慮在名字的使用之前出現的聲明。
    • 如果沒找到,繼續查找外層作用域。
    • 如果最終沒有找到匹配的聲明,則程序報錯。
  • 對於定義在類內部的成員函數來說,解析其中名字的方式與上述的查找規則有所區別:
    • 首先,編譯成員的聲明
    • 直到類全部可見后才編譯函數體
  • 編譯器處理完類中的全部聲明后才會處理成員函數的定義。
  • 按照這種兩段的方式處理類可以簡化類代碼的組織方式。因為成員函數體直到整個類可見后才會被處理,所以它能使用類中定義的任何名字。相反,如果函數的定義和成員的聲明被同時處理,那么我們將不得不在成員函數中只使用那些已經存在的名字。

用於類成員聲明的名字查找

  • 這種兩段的處理方式只適用於成員函數體中使用的名字。聲明中使用的名字,包括返回類型或者參數列表中使用的名字,都必須在使用前確保可見。如果某個成員的聲明使用了類中尚未出現的名字,則編譯器將會在定義該類的作用域中繼續查找(即進行一般的名字查找過程)。

      // 理解下面這段程序代碼
      typedef double Money;
      string bal;
      class Account
      {
      public:
            Money balance() { return bal; } // 返回的是成員bal,而非外層作用域的string對象
      private:
            Money bal; // balance函數體在整個類可見后才被處理
            // ...
      };
    
    /*
    只有成員函數體內部使用的名字使用的是二段式的處理方式,其余均遵從一般的名字查找。比如上面的兩個Money都會向前查找聲明,為typedef處的double類型。而函數體中的bal會在類體內查找,返回的是成員bal,而非外層作用域的string對象。
    */
    

類型名要特殊處理

  • 一般來說,內層作用域可以重新定義外層作用域中的名字,即使該名字已經在內層作用域中使用過。然而在類中,如果成員使用了外層作用域中的某個名字,而該名字代表一種類型,則類不能在之后重新定義該名字(因為兩段式的名字查找)。

      typedef double Money;
      string bal;
      class Account
      {
      public:
            Money balance() { return bal; } // 使用外層作用域的Money
      private:
            typedef double Money; // 錯誤:不能重新定義Money,即使與外層作用域中的定義完全一致
            Money bal;
            // ...
      };
    
  • 類型名的定義通常出現在類的開始處,這樣就能確保所有使用該類型的成員都出現在類名的定義之后。

成員定義中的普通塊作用域的名字查找

  • 成員函數中使用的名字按照如下方式解析:

    • 首先,在成員函數內查找該名字的聲明。和前面一樣,只有在函數使用之前出現的聲明才被考慮。(成員函數作用域)
    • 如果在成員函數內沒有找到,則在類內繼續查找,這時類的所有成員都可以被考慮。(類作用域)
    • 如果類內也沒找到該名字的聲明,在成員函數定義之前(成員函數可以定義在類外)的作用域內繼續查找。(類的外層作用域)
  • 盡管類的成員被隱藏了,但我們仍然可以通過加上類的名字或顯式地使用this指針來強制訪問成員。

      void Screen::dummy_fcn(pos height) // 不建議隱藏類中同名的成員
      {
            cursor = width * this->height;
            // 另一種表示該成員的方式
            cursor = width * Screen::height;
      }
    

類作用域之后,在外圍的作用域中查找

  • 盡管外層的對象被隱藏掉了,但我們仍然可以用作用域運算符訪問它。

      void Screen::dummy_fcn(pos height) // 不建議隱藏外層作用域中可能被用到的名字
      {
            cursor = width * ::height; // 全局的那個height
      }
    

在文件中名字的出現處對其進行解析

  • 當成員定義在類的外部時,名字查找的第三步不僅要考慮類定義之前的全局作用域中的聲明,還需要考慮在成員函數定義之前的全局作用域中的聲明。

      // 代碼可以被正常使用
      int height; // 定義了一個名字,稍后將在Screen中使用
      class Screen
      {
      public:
            typedef std::string::size_type pos;
            void setHeight(pos);
            pos height = 0; // 隱藏了外層作用域中的height
      };
      Screen::pos verify(Screen::pos);
      void Screen::setHeight(pos var)
      {
            // var:參數
            // height:類的成員
            // verify:全局函數
            height = verify(var);
      }
    

構造函數再探

構造函數初始值列表

  • 如果沒有在構造函數的初始值列表中顯式地初始化成員,則該成員將在構造函數體之前執行默認初始化。

構造函數的初始值有時必不可少

  • 如果成員是const、引用,或者屬於某種未提供默認構造函數的類類型,我們必須通過構造函數初始值列表為這些成員提供初值。

  • 隨着構造函數體一開始執行,初始化就完成了。我們初始化const或者引用類型的數據成員的唯一機會就是通過構造函數初始化。

      class ConstRef
      {
      public:
            ConstRef(int ii);
      private:
            int i;
            const int ci;
            int &ri;
      };
    
      // 錯誤:ci和ri必須被初始化
      ConstRef::ConstRef(int ii)
      {
            // 賦值
            i = ii; // 正確
            ci = ii; // 錯誤:不能給const賦值
            ri = i; // 錯誤:不能給const賦值
      }
    
      // 正確形式:顯式地初始化引用和const成員
      ConstRef::ConstRef(int ii): i(ii), ci(ii), ri(i) { }
    

成員初始化的順序

  • 成員的初始化順序與它們在類定義中的出現順序一致。構造函數初始值列表中初始值的前后位置關系不會影響實際的初始化順序。

      class X
      {
            int i;
            int j;
      public:
            // 未定義的:i在j之前被初始化
            X(int val): j(val), i(j) { } // 錯誤:試圖使用未定義的值j初始化i
      };
    
  • 最好令構造函數初始值的順序與成員聲明的順序保持一致。而且如果可能的話,盡量避免使用某些成員初始化其他成員。

  • 如果可能的話,最好用構造函數的參數作為成員的初始值,而盡量避免使用同一個對象的其他成員。這樣的好處是我們可以不必考慮成員的初始化順序。

默認實參和構造函數

  • 如果一個構造函數為所有參數都提供了默認實參,則它實際上也定義了默認構造函數。

委托構造函數

  • 一個委托構造函數使用它所屬類的其他構造函數執行它自己的初始化過程,或者說它把它自己的一些(或者全部)職責委托給了其他構造函數。

      class Sales_data
      {
      public:
            // 非委托構造函數使用對應的實參初始化成員
            Sales_data(std::string s, unsigned cnt, double price):
                  bookNo(s), units_sold(cnt), revenue(cnt*price) { }
            // 其余構造函數全都委托給另一個構造函數
            Sales_data(): Sales_data("", 0, 0) { }
            Sales_data(std::string s): Sales_data(s, 0, 0,) { }
            Sales_data(std::istream &is): Sales_data() { read(is, *this); }
            // 其他成員與之前的版本一致
      }
    
  • 當一個構造函數委托給另一個構造函數時,受委托的構造函數的初始值列表和函數體被依次執行(然后才會執行委托者的函數體)。

默認構造函數的作用

  • 當對象被默認初始化或值初始化時自動執行默認構造函數。

  • 默認初始化在以下情況下發生:

    • 當我們在塊作用域內不使用任何初始值定義一個非靜態變量或者數組時。
    • 當一個類本身含有類類型的成員且使用合成的默認構造函數時(那么這個成員執行默認構造函數)。
    • 當類類型的成員沒有在構造函數初始值列表中顯式地初始化時(那么這個成員執行默認構造函數)。
  • 值初始化在以下情況下發生:

    • 在數組初始化的過程中如果我們提供的初始值數量少於數組的大小時。
    • 當我們不使用初始值定義一個局部靜態變量時。
    • 當我們通過書寫形如T()的表達式顯式地請求值初始化時,其中T是類型名(例如vector)。
  • 類必須包含一個默認構造函數以便在上述情況下使用,其中的大多數情況非常容易判斷。不那么明顯的一種情況是類的某些數據成員缺少默認構造函數:

      class NoDefault
      {
      public:
            NoDefault(const std::string&);
            // 還有其他成員,但是沒有其他構造函數了
      };
      struct A
      { 
            // 默認情況下my_mem是public的
            NoDefault my_mem;
      };
      A a; // 錯誤:不能為A合成構造函數
      struct B
      {
            B() { } // 錯誤:b_member沒有初始值
            NoDefault b_member;
      }
    
  • 在實際中,如果定義了其他構造函數,那么最好也提供一個默認構造函數。

使用默認構造函數

  • 對於C++新手來說有一種常犯的錯誤:

      Sales_data obj(); // 錯誤:聲明了一個函數而非對象
      Sales_data obj2; // 正確:obj2是一個對象而非函數
    

隱式的類類型轉換

  • 如果構造函數只接受一個實參,則它實際上定義了轉換為此類類型的隱式轉換機制,有時我們把這種構造函數稱作轉換構造函數

  • 能通過一個實參調用的構造函數定義了一條從構造函數的參數類型向類類型隱式轉換的規則。

  • 編譯器只會自動地執行一步類型轉換。例如,因為下面的代碼隱式地使用了兩種轉換規則,所以它是錯誤的:

      // 錯誤:需要用戶定義的兩種轉換:
      // (1)把"9-999-99999-9"轉換成string
      // (2)再把這個(臨時的)string轉換成Sales_data
      item.combine("9-999-99999-9");
    
      // 正確:顯式地轉換成string,隱式地轉換成Sales_data
      item.combine(string("9-999-99999-9"));
      // 正確:隱式地轉換成string,顯式地轉換成Sales_data
      item.combine(Sales_data("9-999-99999-9"));
    
      // 通過讀取標准輸入創建了一個(臨時的)Sales_data對象,隨后將得到的對象傳遞給combine。
      item.combine(cin);
    
      // Sales_data對象是個臨時量,一旦combine完成我們就不能再訪問它了。實際上,我們構建了一個對象,先將它的值加到item中,隨后將其丟棄。
    

抑制構造函數定義的隱式轉換

  • 在要求隱式轉換的程序上下文中,我們可以通過將構造函數聲明為explicit加以阻止。此時,沒有任何構造函數能用於隱式地創建Sales_data對象,之前的兩種用法都無法通過編譯:

      item.combine(null_book); // 錯誤:string構造函數是explicit的
      item.combine(cin); // 錯誤:istream構造函數時explicit的
    
  • 關鍵字explicit只對一個實參的構造函數有效。需要多個實參的構造函數不能用於執行隱式轉換,所以無需將這些構造函數指定為explicit的。只能在類內聲明構造函數時使用explicit關鍵字,在類外部定義時不應重復。

    • inline是用於實現的關鍵字(放在定義處)
    • static是用於聲明的關鍵字(放在聲明處)
    • explicit是用於聲明的關鍵字(放在聲明處)
    • friend是用於聲明的關鍵字(放在聲明處)

explicit構造函數只能用於直接初始化

  • 發生隱式轉換的一種情況是當我們執行拷貝形式的初始化時(使用=)。此時,我們只能使用直接初始化而不能使用explicit構造函數。

      Sales_data item1(null_book); // 正確:直接初始化
      // 錯誤:不能將explicit構造函數用於拷貝形式的初始化過程
      Sales_data item2 = null_book;
    
  • 當我們用explicit關鍵字聲明構造函數時,它將只能以直接初始化的形式使用。而且,編譯器將不會在自動轉換過程中使用該構造函數。

為轉換顯式地使用構造函數

  • 盡管編譯器不會將explicit的構造函數用於隱式轉換過程,但是我們可以使用這樣的構造函數顯式地強制進行轉換。
      // 正確:實參是一個顯式構造的Sales_data對象
      item.combine(Sales_data(null_book));
      // 正確:static_cast可以使用explicit的構造函數
      item.combine(static_cast<Sales_data>(cin));
    

標准庫中含有顯式構造函數的類

  • 我們用過的一些標准庫中的類含有單參數的構造函數:
    • 接受一個單參數的const char*的string構造函數不是explicit的。
    • 接受一個容量參數的vector構造函數是explicit的。

聚合類

  • 聚合類使得用戶可以直接訪問其成員,並且具有特殊的初始化語法形式。當一個類滿足如下條件時,我們說它是聚合的:

    • 所有成員都是public的
    • 沒有定義任何構造函數
    • 沒有類內初始值
    • 沒有基類,也沒有virtual函數
      struct Data
      {
            int ival;
            string s;
      };
    
  • 我們可以提供一個花括號括起來的成員初始值列表,並用它初始化聚合類的數據成員:Data val1 = {0, "Anna"};。初始值的順序必須與聲明的順序一致Data val2 = {"Anna", 1024};錯誤。如果初始值列表中的元素個數少於類的成員數量,則靠后的成員被值初始化。初始值列表的元素個數絕對不能超過類的成員數量。

  • 顯式地初始化類的對象存在三個明顯的缺點:

    • 要求類的所有成員都是public的
    • 將正確初始化每個對象的每個成員的重任交給了類的用戶(而非類的作者)。因為用戶很容易忘掉某個初始值,或者提供一個不恰當的初始值,所以這樣的初始化過程冗長乏味且容易出錯。
    • 添加或刪除一個成員之后,所有的初始化語句都需要更新。

字面值常量類

  • constexpr函數的參數和返回值必須是字面值類型。除了算術類型、引用和指針外,某些類也是字面值類型。和其它類不同,字面值類型的類可能含有constexpr函數成員。這樣的成員必須符合constexpr函數的所有要求,它們是隱式const的。
  • 數據成員都是字面值類型的聚合類是字面值常量類。如果一個類不是聚合類,但它符合下述要求,則它也是一個字面值常量類:
    • 數據成員都必須是字面值類型
    • 類必須至少含有一個constexpr構造函數
    • 如果一個數據成員含有類內初始值,則內置類型成員的初始值必須是一條常量表達式;或者如果成員屬於某種類類型,則初始值必須使用成員自己的constexpr構造函數。
    • 類必須使用析構函數的默認定義,該成員負責銷毀類的對象。

constexpr構造函數

  • constexpr構造函數必須既符合構造函數的要求(意味着不能包含返回語句),又符合constexpr函數的要求(意味着它能擁有的唯一可執行語句就是返回語句)。綜合這兩點可知,constexpr構造函數體一般來說應該是空的。

      class Debug
      {
      public:
            constexpr Debug(bool b = true): hw(b), io(b), other(b) { }
            constexpr Debug(bool h, bool i, bool o): hw(h), io(i), other(o) { }
            constexpr bool any() { return hw || io || other; }
            void set_io(bool b) { io = b; }
            void set_hw(bool b) { hw = b; }
            void set_other(bool b) { other = b; }
      private:
            bool hw; // 硬件錯誤,而非IO錯誤
            bool io; // IO錯誤
            bool other; // 其他錯誤
      };
    
      constexpr Debug io_sub(false, true, false); // 調試IO
      if (io_sub.any()) // 等價於if(true)
            cerr << "print appropriate error messages" << endl;
      constexpr Debug prod(false); // 無調試
      if (prod.any()) // 等價於if(false)
            cerr << "print an error message" << endl;
    
  • constexpr構造函數必須初始化所有數據成員。初始值(或者)使用constexpr構造函數或者是一條常量表達式。

  • constexpr構造函數用於生成constexpr對象以及constexpr函數的參數或返回類型。

類的靜態成員

聲明靜態成員

  • 我們通過在成員的聲明之前加上關鍵字static使得其與類關聯在一起。和其他成員一樣,靜態成員可以是public的或private的。靜態數據成員的類型可以是常量、引用、指針、類類型等。
  • 類的靜態成員存在於任何對象之外,對象中不包含任何與靜態數據成員有關的數據。
  • 類似的,靜態成員函數也不與任何對象綁定在一起,它們不包含this指針。作為結果,靜態成員函數不能聲明成const的,而且我們也不能在static函數體內使用this指針。這一限制既適用於this的顯式使用,也對調用非靜態成員的隱式使用有效。

使用類的靜態成員

  • 使用作用域運算符直接訪問靜態成員:r = Account::rate();
  • 雖然靜態成員不屬於類的某個對象,但是我們仍然可以使用類的對象、引用或者指針來訪問靜態成員:r = ac1.rate()r = ac2->rate()
  • 成員函數可以不用通過作用域運算符就能直接使用靜態成員:
  class Account
  {
  public:
        void calculate() { amount += amount * interestRate; }
        static double rate() { return interestRate; }
        static void rate(double);
  private:
        std::string owner;
        double amount;
        static double interestRate;
        static double initRate();
  };

定義靜態成員

  • 我們既可以在類的內部也可以在類的外部定義靜態成員函數。當在類的外部定義靜態成員時,不能重復static關鍵字,該關鍵字只出現在類內部的聲明語句:

      void Account::rate(double newRate)
      {
            interestRate = newRate;
      }
    
  • 和類的所有成員一樣,當我們指向類外部的靜態成員時,必須指明成員所屬的類名。static關鍵字只出現在類內部的聲明語句中。

  • 因為靜態數據成員不屬於類的任何一個對象,所以它們並不是在創建類的對象時被定義的。這意味着它們不是由類的構造函數初始化的。而且一般來說,我們不能在類的內部初始化靜態成員。相反的,必須在類的外部定義和初始化每個靜態成員。和其它對象一樣,一個靜態數據成員只能定義一次。

  • 類似於全局變量,靜態數據成員定義在任何函數之外。因此一旦它被定義,就將一直存在於程序的整個生命周期中。

  • 定義靜態數據成員的方式和在類的外部定義成員函數差不多。我們需要指定對象的類型名,然后是類名、作用域運算符以及成員自己的名字。

      // 定義並初始化一個靜態成員
      double Account::interestRate = initRate();
      // 從類名開始,這條定義語句的剩余部分就都位於類的作用域之內了。因此,可以直接使用initRate函數
      // 注意,雖然initRate是私有的,我們也能用它初始化interestRate
      // 和其他成員的定義一樣,interestRate的定義也可以訪問類的私有成員
    
  • 要想確保對象只定義一次,最好的辦法是把靜態數據成員的定義與其他非內聯函數的定義放在同一個文件中。

靜態成員的類內初始化

  • 通常情況下,類的靜態成員不應該在類的內部初始化。然而,我們可以為靜態成員提供const整數類型的類內初始值,不過要求靜態成員必須是字面值常量類型的constexpr。(除了靜態常量成員之外,其他靜態成員不能在類的內部初始化。)初始值必須是常量表達式,因為這些成員本身就是常量表達式,所以它們能用在所有適合於常量表達式的地方。例如:

    class Account
    {
    public:
          static double rate() { return interestRate; }
          static void rate(double);
    private:
          static constexpr int period = 30; // period是常量表達式
          double daily_tbl[period];
    };
    
  • 如果在類的內部提供了一個初始值,則成員的定義不能再指定一個初始值了。

    // 一個不帶初始值的靜態成員的定義
    constexpr int Account::period; // 初始值在類的定義內提供
    
  • 即使一個常量靜態數據成員在類內部被初始化了,通常情況下也應該在類的外部定義一下該成員。

靜態成員能用於某些場景,而普通成員不能

  • 靜態成員的優點包括:作用域位於類的范圍之內,避免與其他類的成員或者全局作用域的名字沖突;可以是私有成員,而全局對象不可以;通過閱讀程序可以非常容易地看出靜態成員與特定類關聯,使得程序的含義清晰明了。

  • 靜態數據成員可以是不完全類型。特別的,靜態數據成員的類型可以就是它所屬的類類型,非靜態數據成員只能聲明成它所屬類的指針或引用。

    class Bar {
    public:
    	// ...
    private:
    	static Bar mem1; // 正確:靜態成員可以是不完全類型
    	Bar *mem2; // 正確:指針成員可以是不完全類型
    	Bar mem3; // 錯誤:數據成員必須是完全類型
    };
    
  • 我們可以使用靜態成員作為默認實參。非靜態數據成員不能作為默認實參,因為它的值本身屬於對象的一部分,這么做的結果是無法真正提供一個對象以便從中獲取成員的值,最終將引發錯誤。(普通成員函數包含this形參,但因為函數參數解析順序是未定的,所以該默認值也是未定的,如int function(class_type *this, int n = this->a)

    class Screen {
    public:
    	// bkground表示一個在類中稍后定義的靜態成員
    	Screen& clear(char = bkground);
    private:
    	static const char bkground;
    };
    


免責聲明!

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



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