天天看點

繁書簡讀之C++ Primer Day7: 類(class)

作者:Linux後端MrWu

類的基本思想是資料抽象(Abstraction)和封裝(Encapsulation)。資料抽象是一種依賴于接口(interface)和實作(implementation)分離的程式設計及設計技術。類的接口包括使用者所能執行的操作;類的實作包括類的資料成員、負責接口實作的函數體以及其他私有函數

7.1 定義抽象類型

7.1.1 設計sales_data類

1. 類的使用者是程式員,而不是應用的使用者。設計類接口時,要考慮如何使類易于使用;當使用者使用類時,不需要考慮類的實作原理

7.1.2 定義改進的sales_data類

1. 類的成員函數(member function)聲明必須在類内,定義則既可以在類内也可以在類外。定義在類内部的函數是隐式的inline函數

2. 成員函數通過名為this的隐式參數來通路調用它的對象,this是一個常指針,被初始化成調用該函數的類對象的位址,在函數體内可以顯示使用this指針

total.isbn();
Sales_data::isbn(&total); //僞代碼顯示說明了total.isbn()的調用過程,即取total的位址賦給this指針

std::string isbn() const { return this->bookNo; } //顯示使用this
std::string isbn() const { return bookNo; }           

3. this預設是指向類類型非常量版本的常指針,是以預設不把this綁定到常量對象上,也就是說不能使用常量對象調用普通成員函數

4. 成員函數參數清單後添加const關鍵字代表常量成員函數,編譯器處理常量成員函數調用時傳入的是const this,此時,this指針同時持有指針常量和常量指針的雙重屬性

//isbn是const成員函數,如下僞代碼展示了編譯器的處理過程
std::string Sales_data::isbn(const Sales_data *const this)
{
    return this->isbn;
}           

5. 常量對象和指向常量對象的引用或指針都隻能調用常量成員函數,常量成員函數不論聲明或者定義都不要忘了參數清單之後的const關鍵字

6. 傳回this對象的成員函數

Sales_data& Sales_data::combine(const Sales_data &rhs)
{
    units_sold += rhs.units_sold;
    revenue += rhs.revenue;
    return *this; //傳回調用此成員函數的對象
}           

7.1.3 定義類相關的非成員函數

1. 程式員通常會定義一些輔助函數,但這些函數并不屬于類本身

std::ostream &print(std::ostream &os, const Sales_data &item)
{
    os << item.isbn() << " " << item.units_sold << " "
        << item.revenue << " " << item.avg_price();
    return os;
}           

7.1.4 構造函數

1. 構造函數用來初始化類對象的資料成員,當類的對象被建立時,就會執行構造函數

2. 構造函數特點:

a) 名字與類名相同

b) 無傳回類型

c) 不能是const函數

d) 可重載

3. 如果類沒有顯式地定義構造函數,則編譯器會為類隐式地定義一個預設構造函數,該構造函數也被稱為合成的預設構造函數

4. 合成預設構造函數初始化資料成員的規則:

a) 如果存在類内初始值,則用它來初始化成員(類内初始值必須以=或者{}的方式初始化

b) 否則預設初始化該成員

5. 無合成預設構造函數的場景:

a) 隻有類沒有聲明任何構造函數時,編譯器才會自動生成預設構造函數,一旦類定義了其他構造函數,除非再顯式定義一個預設構造函數,否則類将沒有預設構造函數

b) 如果類包含内置類型或複合類型(複合類型:數組/字元串/struct/union/enum/指針/類型組合等)成員,則僅當這些成員都存在類内初始值時,該類才适合使用合成的預設構造函數

c) 若類A中包含其他類類型成員,且該成員類沒有預設構造函數,那麼編譯器就不會為這種類A合成預設構造函數

6. C++11及以後,可以在函數清單後添加=default來要求編譯器生成構造函數,如果=default出現在類内部,則預設構造函數是内聯的

7. 構造函數初始值清單:

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

8. 當某些資料成員被構造函數初始值清單忽略時,它們會以與合成預設構造函數相同的方式隐式初始化

Sales_data(const std::string &s):
			bookNo(s), units_sold(0), revenue(0) { }           

7.1.5 拷貝/指派和析構

編譯器能合成拷貝、指派和析構函數,但是對于某些類來說合成的版本無法正常工作。特别是當類需要配置設定類對象之外的動态記憶體,合成的版本通常會失效

7.2 通路控制與封裝

1. 使用通路說明符可以加強類的封裝性:

a) public:關鍵字之後的成員在整個程式生命周期内都可以通路

b) private:關鍵字之後的成員隻能被類的成員函數通路,private隐藏了類的實作細節

2. 使用struct定義類時,預設所有成員都時public屬性;使用class時,預設所有成員都是private

7.2.1 友元

1. 類可以允許其他類或函數通路它的非公有成員,方法是使用關鍵字friend将其他類或函數聲明為它的友元

class Sales_data
{
    //類的友元函數聲明
    friend Sales_data add(const Sales_data&, const Sales_data&);
    friend std::istream &read(std::istream&, Sales_data&);
    friend std::ostream &print(std::ostream&, const Sales_data&);

public:
    Sales_data() = default;
    Sales_data(const std::string &s, unsigned n, double p):
        bookNo(s), units_sold(n), revenue(p*n) { }
    Sales_data(const std::string &s): bookNo(s) { }
    Sales_data(std::istream&);
    std::string isbn() const { return bookNo; }
    Sales_data &combine(const Sales_data&);

private:
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

// 類外友元函數聲明
Sales_data add(const Sales_data&, const Sales_data&);
std::istream &read(std::istream&, Sales_data&);
std::ostream &print(std::ostream&, const Sales_data&);           

2. 友元必須定義在類内,友元不是類的成員,不受區域通路級别限制,通常在類定義開始或結束前集中聲明友元

3. 為了使友元對類的使用者可見,通常會把友元的聲明(類的外部)與類本身放在同一個頭檔案中(注意:為了使用友元函數,類内類外的聲明都必須存在,類内隻是聲明了這些函數的權限,而類外不帶friend關鍵字的函數才是真正聲明)

7.3 類的其他特性

7.3.1 類成員再探

1. 類可以自定義某種類型在類内的别名,類型成員一樣有通路級别限制

class Screen
{
public:
    using pos = std::string::size_type;
};           

2. 與普通成員不同,用來定義類型的成員必須先定義後使用

3. 内聯成員函數:

a) 在類内定義函數,為隐式内聯

b) 在類内用inline顯示聲明成員函數

c) 在類外用inline定義成員函數

d) 同時在類内類外修飾

4. 使用關鍵字mutable可以聲明可變資料成員,可變資料成員永遠不會是const的,即使它在const對象内。是以const成員函數可以修改可變成員的值

class Screen
{
public:
    void func() const;
private:
    mutable size_t mut_val; //即使在const對象内也可能改變
};

void Screen::func() const
{
    ++mut_val;
}           

7.3.2 傳回*this的成員函數

1. const成員函數如果傳回*this,則傳回類型為常量引用

2. 通過區分成員函數是否是const,可以對其重載;對于常量對象,隻能調用const版本函數,非常量對象則優先調用非常量版本

class Screen
{
public:
    //根據是否為const實作重載
    Screen &display(std::ostream &os)
    { do_display(os); return *this; }
    const Screen &display(std::ostream &os) const
    { do_display(os); return *this; }

private:
    void do_display(std::ostream &os) const
    { os << contents; }
};

Screen myScreen(5,3);
const Screen blank(5, 3);
myScreen.display(cout); //調用非常量版本
blank.display(cout); //調用常量版本           

7.3.3 類類型

1. 可以僅僅聲明一個類而暫時不定義它,這種聲明被稱作前向聲明(forward declaration),在類聲明之後定義之前它都是一個不完全類型(incomplete type)

2. 可以定義指向不完全類型的指針或引用,也可以聲明(不能定義)以不完全類型作為參數或傳回類型的函數

3. 隻有當類全部完成後才算被定義,是以一個類的成員類型不能是該類本身。但是一旦類的名字出現,就可以被認為是聲明過了,是以類可以包含指向它自身類型的引用或指針

//我們常見的連結清單聲明
class LinkList
{
    int value;
    LinkList *next;
    LinkList *prev;
};           

7.3.4 友元再探

1. 除了普通函數,還可以把其他類或其他類的成員函數聲明為友元。友元類的成員函數可以通路此類包括非公有成員在内的所有成員

2. 友元函數可以直接定義在類的内部,這種函數是隐式内聯的。但是必須在類外部提供相應聲明令函數可見

struct X
{
    friend void f() { /* friend function can be defined in the class body */ }
    X() { f(); } // error: no declaration for f
    void g();
    void h();
};

void X::g() { return f(); } // error: f此時還沒有被聲明
void f(); //聲明類X中聲明的友元函數
void X::h() { return f(); } // ok: 因為f已經被生命過           

3. 友元關系不能傳遞

4. 把其他類的成員函數聲明為友元時,必須明确指定該函數所屬的類名

class Screen
{
     // Window::clear必須已經聲明過
    friend void Window::clear();
};           

5. 如果類想把一組重載函數聲明為友元,需要對這組函數中的每一個分别聲明

7.4 類的作用域

1. 當類的成員函數的傳回類型也是類的成員時,在定義時要指明類

Student::age Student::Getage()
{
}           

7.4.1 名字查找與類的作用域

1. 類的定義過程:

a) 編譯成員的聲明

b) 直到全部類可見後才編譯函數體

2. 注意:

a) 在類内定義的類型别名要放在類的開始,放在後面其他成員是看不見的

b) 類外已定義的類型名不能在類内重新定義

3. 成員函數中名字的解析順序

a) 在成員函數内查找該名字的聲明,隻有在函數使用之前出現的聲明才會被考慮

b) 如果在成員函數内沒有找到,則會在類内繼續查找,這時會考慮類的所有成員

c) 如果類内也沒有找到,會在成員函數定義之前的作用域查找

4. 通常盡量避免将一個變量和類成員變量定義成重名,另外,可以通過作用域運算符或this指針來強制通路被隐藏的類成員

void Screen::dummy_fcn(pos height)
{
    cursor = width * this->height;//強制使用成員變量
    // 另外一種寫法
    cursor = width * Screen::height; //強制使用成員變量
    //以上兩個表達式如果不用::或this,那麼height就會使用形參接受的值
}           

7.5 構造函數再探

7.5.1 構造函數初始值清單

1. 使用初始值清單對類成員的初始化才是真正的初始化,在構造函數的函數體内指派并不是初始化

2. 如果成員是const或者引用,再或者是某種未定義預設構造函數的類類型,必須在初始值清單中将它們初始化

3. 如果一個構造函數為所有參數提供了預設實參,則它實際上相當于定義了預設構造函數

7.5.2 委托構造函數

1. 委托構造函數使用它所屬類的其他構造函數執行它自己的初始化過程

class Student
{
public:
    Student(std::string name, int age):_name(name), _age(age) { }
    Student():Student(“ ”, 18) { } //這就是委托構造函數
    Student(std::string):Student(s, 18) { }//這也是委托構造函數
};           

7.5.3 預設構造函數的作用

1. 當對象被預設初始化或值初始化時會自動執行預設構造函數

2. 預設初始化的情況:

a) 在塊作用域内不使用初始值定義非靜态變量或數組

b) 類本身含有類類型的成員且使用合成預設構造函數

c) 類類型的成員沒有在構造函數初始值清單中顯式初始化

3. 值初始化的情況:

a) 數組初始化時提供的初始值數量少于數組大小

b) 不使用初始值定義局部靜态變量

c) 通過T()形式(T為類型)的表達式顯式地請求值初始化

4. 實際使用中,即便定義了其他構造函數,最好也提供一個預設構造函數

7.5.4 隐式的類類型轉換

1. 如果構造函數隻接受一個實參,則它實際上定義了轉換為此類類型的隐式轉換機制。這種構造函數被稱為轉換構造函數

2. 一個實參的構造函數定義了一條從構造函數的參數類型向類類型隐式轉換的規則

3. 執行隐式轉換時,編譯器隻會自動執行一步類型轉換

std::string null_book = “999-9”;
item.combine(null_book); //ok, combine函數接受Sales_data類類型,但該類定義了一個接受string參數的轉換構造函數,是以這裡會執行從string到該類類型的隐式轉換

item.combine(“999-9”); //error, 隐式使用了兩次轉換,是以錯誤
item.combine(std::string(“999-9”); //ok,先顯示轉換,再執行一次隐式轉換           

4. 将轉換構造函數聲明為explicit将會阻止隐式轉換

5. 關鍵字explicit隻對一個實參的構造函數有效,因為需要多個實參的構造函數不執行隐式轉換,且explicit關鍵字隻用在類内聲明函數時

class Sales_data
{
public:
    explicit Sales_data(const std::string &s):book(s) { } //禁止隐式轉換
private:
    std::string bookNo;
};
iter.combine(null_book); //error,不能執行從std::string到Sales_data的隐式初始化           

6. 可以使用explicit的構造函數顯示地強制進行轉換

iter.combine(static_cast<Sales_data>(null_book));//ok,static_cast可以使用explicit的構造函數           

7.5.5 聚合類

1. 條件:

a) 所有成員都是public

b) 沒有定義構造函數

c) 沒有類内初始值

d) 沒有基類

e) 沒有虛函數

2. 可以使用花括号包圍的成員初始值清單初始化聚合類。初始值順序必須與聲明順序一緻。如果初始值清單中的元素個數少于類的成員個數,則靠後的成員被值初始化

7.5.6 字面值常量類

1. 資料成員都是字面值類型的聚合類是字面值常量類

2. 或者不是聚合類,但滿足下列條件,也是字面值常量類:

a) 資料成員都是字面值類型

b) 至少含有一個constexpr構造函數

c) 如果資料成員含有類内初始值,則内置類型成員的初始值必須是常量表達式;如果成員屬于類類型,則初始值必須使用成員自己的constexpr構造函數

d) 類必須使用析構函數的預設定義

3. 類的構造函數不能是const的,但是字面值常量類的構造函數可以是constexpr函數,constexpr構造函數必須初始化所有資料成員,初始值使用constexpr構造函數或常量表達式,且函數體為空(因為constexpr函數體隻能包含一條傳回語句,但是構造函數不能包含傳回語句)

4. 字面值常量類于是字面值類型

5. constexpr函數的參數和傳回值必須都是字面值類型

class Debug
{
public:
    constexpr Debug(bool b=true):a(b) { }
private:
    bool a;
};

constexpr Debug prod{false};//定義對象,實參應為常量表達式           

7.6 類的靜态成員

1. 使用關鍵字static可以聲明類的靜态成員。靜态成員存在于任何對象之外,對象中不包含與靜态成員相關的資料

2. 由于靜态成員不與任何對象綁定,是以靜态成員函數不能聲明為const的,也不能在靜态成員函數内使用this指針(是以類的成員函數不能同時用static和尾後const修飾),也是以,static函數無法通路非static的資料成員

3. 使用者代碼可以使用作用域運算符通路靜态成員,也可以通過類對象、引用或指針通路。類的成員函數可以直接通路靜态成員

4. 在類外部定義靜态成員時,不能重複static關鍵字,其隻能用于類内部的聲明語句,一個靜态成員隻能被定義一次,且存在于程式的整個生命周期

5. 建議把靜态成員定義和類的非内聯函數定義放在同一源檔案中

6. 通常情況下,不應該在類内部初始化靜态成員,但可以為靜态成員提供const整數類型的類内初始值,不過要求靜态成員必須是字面值常量類型的constexpr,初始值必須是常量表達式,而且同樣需要在類外定義

class Account
{
public:
    static double rate() { return rt; }
    static void rate(double);
private:
    static constexpr int period = 30; // 常量定義
    double tbl[period];
};           

7. 靜态成員使用的特殊場景:

a) 靜态資料成員的類型可以時它所屬的類類型

class Account
{
    static Account acc;
};           

b) 可以使用靜态成員作為函數的預設實參

class Screen
{
public:
    Screen &clear(char = background);
private:
    static const char background;
};           

繼續閱讀