C++ 學習筆記之(7)-類

C++ 學習筆記之(7)-類

類的基本思想是數據抽象和封裝。封裝實現了類的接口和實現的分離。數據抽象是依賴於接口和實現分離的編程技術。

定義抽象數據類型

定義改進的Sales_data

struct Sales_data{
    std::string isbn() const { return bookNo; }
    Sales_data& combine(const Sales_data&);
    double avg_price() const;
    // 數據成員
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
}
// Sales_data 的非成員接口函數
Sales_data add(const Sales_data&, const Sales_data&);
std::ostream &print(std::ostream&, const Sales_data&);
std::istream &read(std::istream&, Sales_data&);
  • 定義在類內部的函數是隱式的inline函數

  • 所有成員必須在類內部聲明,但成員函數體定義可在類內或類外

  • this爲成員函數的隱式參數,由函數對象地址初始化,爲常量指針,指向對象本身,不可改變。

  • 常量成員函數(const member function):成員函數參數列表後加const關鍵字,作用是修改隱式this指針的類型,使其成爲指向常量對象的常量指針const Sales_data *const。因爲默認this類型爲指向類類型非常量版本的常量指針Sales_data *const,故this無法綁定到常量對象。常量成員函數不能修改對象內容。

    std::string isbn() const { return bookNo; }
  • 常量對象以及常量對象的引用或指針都只能調用常量成員函數

  • 編譯器分兩步處理類,首先編譯類成員聲明,然後到成員函數體。故成員函數體可隨意使用類其他成員。

定義類相關的非成員函數

類需要一些輔助函數比如上述add, read等,這些函數屬於類的接口組成部分,實際不屬於類本身

  • 如果非成員函數是類接口的組成部分,則這些函數的聲明應該與類在同一個頭文件中

  • IO類屬於不能被拷貝的類型,所以只能通過引用傳遞他們

    istream &read(istream &is, Sales_data &item)
    {
      double price = 0;
        is >> item.bookNo >> item.units_sold >> price;
        item.revenue = price * item.units_sold;
        return is;
    }

構造函數

構造函數是用來控制其對象的初始化過程。

  • 構造函數名字和類名相同,沒有返回類型

  • 構造函數不能被聲明成const,當創建類的const對象時,知道構造函數完成初始化過程,對象才真正取得常量屬性

  • 默認構造函數:類通過一個特殊的構造函數控制默認初始化過程,無需任何實參。當類沒有顯示定義構造函數時,編譯器會隱式定義默認構造函數,又被稱爲合成的默認構造函數

  • 如果類包含內置類型或複合類型的成員,則應賦予其類內初始值,這樣類才適合使用合成的默認構造函數,否則默認初始化,其值未定義。

  • C++11新標準定義在參數列表後使用= default要求編譯器生成構造函數。

    struct Sales_data{
      Sales_data() = default;
    }
  • 構造函數初始值列表:負責爲新創建對象的一個或幾個數據成員賦初值

    Sales_data(const std::string &s, unsigned n, double p): bookNo(s), units_sold(n), revenus(p*n) {}

訪問控制與封裝

  • C++使用訪問說明符(access specifiers)加強累的封裝性

    • 定義在public說明符之後的成員在整個程序內可被訪問
    • 定義在private說明符後的成員可被類成員函數訪問,但不能被使用該類的代碼訪問
  • structclass的默認訪問權限不一樣。class關鍵字的默認爲private

  • 友元:類可以使其他類或函數成爲該類友元,就可以使其他類或函數訪問它的非公有成員

    friend Sales_data add(const Sales_data&, const Sales_data&);
  • 友元聲明僅指定了範文權限,而非通常意義的函數聲明,最好在友元聲明外再對函數進行一次聲明

類的其他特性

類成員再探

  • inline可用在類內部函數聲明,也可用在類外部函數定義處

  • 可變數據成員(mutable):永遠不會是const, 即使它是const對象的成員,即一個const成員函數可以改變一個可變成員的值

    class A
    {
        public:
            void add() const
          {
              ++b;  // 成立,因爲是可變成員 mutable
          }
      private:
            mutable int b;  // 即使在一個`const`對象內也能被修改
    }

返回 *this的成員函數

  • 一個const成員函數如果以引用形式返回*this, 那麼它的返回類型將是常量引用
  • 通過區分成員函數是否爲const,可以對其進行重載,原因類似於函數參數中指針是否爲const

類類型

  • 前向聲明:函數聲明和定義分離,在聲明之後定義之前是一個不完全類型,即只知類類型,卻不清楚包含那些成員
  • 不完全類型的使用非常有限:可以定義指向不完全類型的指針或引用個也可以聲明(但不能定義)以不完全類型作爲參數或返回類型的函數。

友元再探

類之間的友元關係

class Screen{
    friend class Window_mgr;  // Window_mgr 的成員可以訪問 Screen 類的私有部分
};
  • 若類指定了友元類,則友元類的成員函數可以訪問此類包括非公有成員在內的所有成員
  • 友元關係不存在傳遞性,即window_mgr的友元類不具有訪問Screen的特權
  • 每個類負責控制自己的友元類或友元函數

令成員函數作爲友元

class Screen{
    friend void Window_mgr::clear(ScreenIndex);  // Window_mgr::clear必須在Screen類之前被聲明
}

友元函數設計規則

  • 首先定義Window_mgr類,其中聲明clear函數,但不能定義。在clear使用Screen的成員之前必須先聲明Screen
  • 接下來定義Screen, 包括對於clear的友元聲明
  • 最後定義clear,此時它纔可以使用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 Screen::dummy_fcn(pos height){
      cursor = width * ::height;   // 全局變量height
    }

構造函數再探

構造函數初始值列表

  • 若在構造函數的初始值列表中未顯示初始化成員,則成員會在構造函數體之前執行默認初始化
  • 構造函數初始值是進行初始化,構造函數體中執行的是賦值
  • 若成員是const、引用,或者某種未提供默認構造函數的類類型,必須通過構造函數初始值列表進行初始化。
  • 初始化和賦值的區別事關底層效率問題
    • 初始化是直接初始化數據成員
    • 賦值是先初始化後賦值
  • 成員初始化順序和在類定義中的出現順序一致,並且儘量避免使用某些成員初始化其他成員
  • 如果一個構造函數爲所有參數都提供了默認實參,則它實際上也定義了默認構造函數

委託構造函數

C++11定義了 委託構造函數(delegating constuctor):即可使用其所屬類的其他構造函數執行它的初始化過程

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) {}
}
  • 先執行被委託的構造函數,再執行委託者的函數體

隱式的類類型轉換

  • 轉換構造函數:若構造函數只接受一個實參,則實際上定義了轉換爲此類類型的隱式轉換機制,這種構造函數被稱爲轉換構造函數

    string null_book = '9-999=99999-9';
    // 構造臨時Sales_data對象,對象的units_sold 和 revenue 爲0, bookNo 等於 null_book
    item.combine(null_book);
  • 編譯器只會自動執行一步類型轉換

    // 錯誤:需要兩步轉換,先將字符串字面值轉爲string,再將臨時的string對象轉換成Sales_data對象
    item.combine("9-999-99999-9");
    item.combine(string("9-999-99999-9"));  // 正確:顯示轉換成 string,隱式轉換成 Sales_data
  • 可以通過將構造函數聲明爲explicit阻止隱式轉換,explicit只對一個實參的構造函數有效,需要多個實參的構造函數不能用於執行隱式轉換。

    class Salse_data{
      public:
            explicit Sales_data(const std::string &s): bookNo(s) {}
    }
  • 只能在類內聲明構造函數時使用explicit關鍵字,類外部定義時不應重複

  • explicit構造函數只能用於直接初始化,不能用於拷貝形式初始化

聚合類

聚合類是指用戶可以直接訪問其成員,並且有特殊的初始化語法形式,條件如下

  • 所有成員都是public
  • 沒有定義任何構造函數
  • 沒有類內初始值
  • 沒有基類,也沒有virtual函數

字面值常量類

字面值常量類的要求

  • 數據成員都必須是字面值類型
  • 類必須至少含有一個constexpr構造函數
  • 如果一個數據成員含有類內初始值,則內置類型成員的初始值必須是一條常量表達式;或者如果成員屬於某種類類型,則初始值必須使用成員自己的constexpr構造函數
  • 類必須使用析構函數的默認定義,該成員負責銷燬類的對象
constexpr構造函數

構造函數不能爲const,但字面值常量類的構造函數可以使constexpr函數

constexpr Debug(bool h, bool i, bool o): hw(h), io(i), other(o) {}
  • constexpr構造函數必須初始化所有數據成員,初始值使用constexpr構造函數或者是常量表達式

類的靜態成員

與類而非對象關聯的成員爲靜態成員,可在成員聲明之前加上關鍵字static, 靜態成員可以是publicprivate的,類型可以是常量、引用、指針或類類型等

  • 靜態成員函數不能聲明成const

  • static函數體內不能使用this指針,因爲靜態成員函數不與對象綁定,沒有this指針

  • 可以使用作用域運算符直接訪問靜態成員,可以使用類對象、引用或指針訪問靜態成員

    double r;
    r = Account::rate();  // 使用作用域運算符訪問靜態成員
    Account ac1, *arc = &ac1;
    // 調用靜態成員函數 rate 的等價形式
    r = ac1.rate();  // 通過 Account 的對象或引用
    r = ac2->rate();  // 通過指向 Account 對象的指針
  • 成員函數不需要通過作用域運算符就能直接使用靜態成員

  • static關鍵字只能出現在類內部的聲明語句中

  • 因爲靜態數據成員不屬於累的任何一個對象,所以不能再類內部初始化靜態成員,必須在類外部定義和初始化每個靜態成員,靜態數據成員只能定義一次

  • 若靜態成員爲字面值常量類型的constexpr,可以爲靜態成員提供const整數類型的類內初始值,初始值必須是常量表達式

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

  • 靜態成員可以做默認實參,而普通成員不行

結語

類是C++語言中最基本的特性,有兩項基本能力:數據抽象,即定義數據成員和函數成員的能力;二是封裝,即保護類的成員不被隨意訪問的能力。通過將類的實現細節設爲private,就可以實現封裝。類可以將其他類或者函數設爲友元,這樣它們就能訪問類的非公有成員

類可以定義構造函數,用來初始化對象。構造函數可以重載,切應該使用構造函數初始值列表來初始化所有數據成員

類還能定義可變或靜態成員,一個可變成員永遠不會是const, 即使在const成員函數內也能修改它的值;一個靜態成員可以使函數也可以是數據,靜態成員存在於所有對象之外。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章