第7章:類

  • #1.定義抽象數(shù)據(jù)類型
    • 1.1 設(shè)計Sales_data類
    • 1.2 定義改進(jìn)的Sales_data類
    • 1.3 定義類相關(guān)的成員函數(shù)
    • 1.4 構(gòu)造函數(shù)
    • 1.5 拷貝、賦值和析構(gòu)
  • #2.訪問控制和封裝
    • 2.1 友元
  • #3.類的其他特性
    • 3.1 類成員再探
    • 3.2 返回*this的成員函數(shù)
    • 3.3 類類型
    • 3.4 友元再探
  • #4.類的作用域
    • 4.1 名字查找和類的作用域
  • #5.構(gòu)造函數(shù)再探
    • 5.1 構(gòu)造函數(shù)初始值列表
    • 5.2 委托構(gòu)造函數(shù)
    • 5.3 默認(rèn)構(gòu)造函數(shù)的作用
    • 5.4 隱式的類類型轉(zhuǎn)換
    • 5.5 聚合類
    • 5.6 字面值常量類
  • #6.類的靜態(tài)成員

類的基本思想是數(shù)據(jù)抽象封裝,數(shù)據(jù)抽象是一種依賴于接口實現(xiàn)分離的編程技術(shù)。

封裝實現(xiàn)了類的接口和實現(xiàn)分離。封裝后的類隱藏了實現(xiàn)細(xì)節(jié),類的用戶只能使用接口而無法訪問實現(xiàn)部分。

類要想實現(xiàn)數(shù)據(jù)抽象和封裝,需要首先定義一個抽象數(shù)據(jù)類型

#1. 定義抽象數(shù)據(jù)類型

要想定義抽象數(shù)據(jù)類型,我們需要定義一些操作以供類的用戶使用。一旦類定義了自己的操作,我們就可以封裝它的數(shù)據(jù)成員。

1.1 設(shè)計Sales_data類

struct Sales_data {
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

1.2 定義改進(jìn)的Sales_data類

定義和聲明成員函數(shù)的方式和普通函數(shù)類似差不多。成員函數(shù)的聲明必須在類的內(nèi)部,它的定義則既可以在類的內(nèi)部也可以在類的外部。作為接口組成部分的非成員函數(shù),它們的定義和聲明都在類的外部。

struct Sales_data {
    //新成員:關(guān)于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的非成員接口函數(shù)
Sales_data add(const Sales_data &,const Sales_data &);
std::ostream &print(std::ostream &,const Sales_data &);
std::istream &read(std::istream &,Sales_data &);

==定義在類內(nèi)部的函數(shù)都是隱式的inline函數(shù)。==

定義成員函數(shù)

所有的成員都必須在類的內(nèi)部聲明,但是成員函數(shù)體可以定義在類內(nèi)也可以定義在類外。以Sales_data的成員函數(shù)isbn為例來引出this指針:

std::string isbn() const {
    return bookNo;
}

isbn函數(shù)返回Sales_data的數(shù)據(jù)成員bookBo。

引入this

成員函數(shù)通過一個名為this的額外的隱式參數(shù)來訪問調(diào)用它的那個對象。當(dāng)我們調(diào)用一個成員函數(shù)時,用請求該函數(shù)的對象地址初始化this。例如,如果調(diào)用:

total.isbn();

則編譯器負(fù)責(zé)把total的地址傳遞給isbn的隱式形參this??梢缘葍r地認(rèn)為編譯器將該調(diào)用重寫成了如下形式:

Sales_data::isbn(&total);

其中,調(diào)用Sales_data的isbn成員時傳入了total地址。在成員函數(shù)內(nèi)部,我們可以直接使用調(diào)用該函數(shù)的對象的成員,而不須通過成員訪問運(yùn)算符來做到這一點,因為this所指向的正是這個對象。任何對類的成員的直接訪問都被看作this的隱式引用。對于我們來說,this形參是隱式定義的。我們可以把isbn定義成如下形式:

std::string isbn() const {
    return this->bookNo;
}

因為this的目的總是指向這個對象,所以this是一個常量指針。

引入const成員函數(shù)

isbn函數(shù)的另一個關(guān)鍵之處是緊隨參數(shù)列表之后的const關(guān)鍵字,這里,const關(guān)鍵字的作用是修改隱式this指針的類型。默認(rèn)情況下,this的類型是指向類類型非常量版本的常量指針。C++語言允許我們把const關(guān)鍵字放在成員函數(shù)的參數(shù)列表之后,此時,緊跟在參數(shù)列表后面的const表示this是一個指向常量的指針。像這樣使用const的成員函數(shù)被稱作常量成員函數(shù)

//偽代碼。說明隱式的this指針如何使用的
//下面代碼是非法的,因為我們不能顯式的定義自己的this指針
//謹(jǐn)記此處的this是一個指向常量的指針,因為isbn是一個常量成員
std::string Sales_data::isbn(const Sales_data *const this) const { 
    return this->bookNo; 
} 

==常量對象,以及常量對象的引用或指針都只能調(diào)用常量成員函數(shù)。==

類作用域和成員函數(shù)

編譯器分兩步處理類:首先編譯成員的聲明,然后再是函數(shù)體。因此,成員函數(shù)體可以隨意使用類的其他成員而無須在意這些成員的出現(xiàn)的次序。

在類的外部定義成員函數(shù)

當(dāng)我們在類的外部定義成員函數(shù)時,成員函數(shù)的定義必須與它的聲明匹配,同時,類外部定義的成員的名字必須包含它所屬的類名:

double Sales_data::avg_price() const {
    if (units_sold) {
        return revenue / units_sold;
    }else {
        return 0;
    }
}
定義一個返回this對象的函數(shù)
Sales_data &Sales_data::combine(const Sales_data &rhs) {
    units_sold += rhs.units_sold; //把rhs的成員加到this對象的成員上
    revenue += rhs.revenue;
    return *this; //返回調(diào)用該函數(shù)的對象
}

1.3 定義類相關(guān)的非成員函數(shù)

我們定義非成員函數(shù)的方式和其他函數(shù)一樣,通常把函數(shù)的聲明和定義分離開來。如有函數(shù)在概念是屬于類但是不定義在類中,則它一般應(yīng)與類聲明在同一個頭文件內(nèi)。

定義read和print函數(shù)
std::istream &read(std::istream &is, Sales_data &item) {
    double price = 0.0;
    is >> item.bookNo >> item.units_sold >> price;
    item.revenue = price * item.units_sold;
    return is;
}
std::ostream &print(std::ostream &os, const Sales_data &item) {
    os << item.isbn() << " " << item.units_sold 
        << " " << item.revenue << " " << item.avg_price();
    return os;
}
定義add函數(shù)
Sales_data add(const Sales_data &lhs, const Sales_data &rhs) {
    Sales_data sum = lhs;
    sum.combine(rhs);
    return sum;
}

1.4 構(gòu)造函數(shù)

每個類都分別定義了它的對象被初始化的方式,類通過一個或幾個特殊的成員函數(shù)來控制其對象的初始化過程,這些函數(shù)叫做構(gòu)造函數(shù)。構(gòu)造函數(shù)的任務(wù)是初始化類對象的數(shù)據(jù)成員,無論何時只要類的對象被創(chuàng)建,就會執(zhí)行構(gòu)造函數(shù)。

構(gòu)造函數(shù)的名字與類名相同。和其他函數(shù)不一樣的是,構(gòu)造函數(shù)沒有返回類型;構(gòu)造函數(shù)不能被聲明成const。

合成的默認(rèn)構(gòu)造函數(shù)

類通過一個一個特殊的構(gòu)造函數(shù)來控制默認(rèn)初始化過程,這個函數(shù)叫做默認(rèn)構(gòu)造函數(shù)。默認(rèn)構(gòu)造函數(shù)無須任何實參。編譯器創(chuàng)建的構(gòu)造函數(shù)又被稱為合成的默認(rèn)構(gòu)造函數(shù)。

==只有當(dāng)類沒有聲明任何構(gòu)造函數(shù)時,編譯器才會自動生成默認(rèn)構(gòu)造函數(shù)。==

某些類不能依賴于合成的默認(rèn)構(gòu)造函數(shù)

對于一個普通的類來說,必須定義它自己的默認(rèn)構(gòu)造函數(shù),原因有三:

  1. 只有當(dāng)類沒有聲明任何構(gòu)造函數(shù)時,編譯器才會自動地生成默認(rèn)構(gòu)造函數(shù)。一旦在類中定義了其他構(gòu)造函數(shù),除非我們再定義默認(rèn)構(gòu)造函數(shù),否則類沒有默認(rèn)構(gòu)造。
  2. 對于某些類而言,合成的默認(rèn)構(gòu)造可能執(zhí)行錯誤的操作。如果類中的內(nèi)置類型或復(fù)合類型的對象被默認(rèn)初始化,它們的值將是未定義的。
  3. 編譯器有時候不能為類合成默認(rèn)的構(gòu)造函數(shù)。例如:類中包含了一個其他類型的成員,而這個成員沒有默認(rèn)構(gòu)造函數(shù)。
定義Sales_data的構(gòu)造函數(shù)
struct Sales_data {
    //C++11新標(biāo)準(zhǔn)中,如果我們需要默認(rèn)的行為,可以在參數(shù)列表后寫上=default來要求編譯器生成默認(rèn)構(gòu)造
    Sales_data() = default;
    Sales_data(const std::string &s) :bookNo(s) {}
    Sales_data(const std::string &s, unsigned n, double p) :bookNo(s), units_sold(n), revenue(p*n) {}
    Sales_data(std::iostream &);
    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;
};
=default的含義

在C++11新標(biāo)準(zhǔn)中,如果我們需要默認(rèn)的行為,可以通過在參數(shù)列表后寫上=default來要求編譯器生成構(gòu)造函數(shù)。

構(gòu)造函數(shù)初始值列表
Sales_data(const std::string &s) :bookNo(s) {}
Sales_data(const std::string &s, unsigned n, double p) :bookNo(s), units_sold(n), revenue(p*n) {}

這兩個定義中出現(xiàn)了新的部分,即冒號以及冒號和花括號之間的代碼,其中花括號定義了函數(shù)體。我們把新出現(xiàn)的部分稱為構(gòu)造函數(shù)初始值列表。它負(fù)責(zé)為新創(chuàng)建的對象的一個或幾個數(shù)據(jù)成員賦初值。構(gòu)造函數(shù)初始值是成員名字的一個列表,每個名字后面緊跟括號括起來的(或者在花括號內(nèi)的)成員初始值。不同成員初始化通過逗號分隔開來。

在類的外部定義構(gòu)造函數(shù)

與其他幾個構(gòu)造函數(shù)不同,以istream為參數(shù)的構(gòu)造函數(shù)需要執(zhí)行一些實際操作。在它的函數(shù)體內(nèi),調(diào)用了read函數(shù)以給數(shù)據(jù)成員賦以初值:

Sales_data::Sales_data(std::iostream &is) {
    read(is, *this);
}

當(dāng)我們在類的外部定義構(gòu)造函數(shù)時,必須指明該構(gòu)造函數(shù)是哪個類的成員。因此,需要指定具體作用域。

1.5 拷貝、賦值和析構(gòu)

除了定義類的對象如何初始化之外,類還需要控制拷貝、賦值和銷毀對象時發(fā)生的行為。

某些類不能依賴于合成的版本

盡管編譯器能替我們合成拷貝、賦值和銷毀的操作,但是必須清楚的一點是,對于某些類來說合成的版本無法正常工作。


#2. 訪問控制與封裝

c++語言中,使用訪問說明符加強(qiáng)類的封裝性:

  • 定義在public說明符之后的成員在整個程序內(nèi)可被訪問,public成員定義類的接口。
  • 定義在private說明符之后的成員可以被類的成員函數(shù)訪問,但是不能被使用該類的代碼訪問,private部分封裝了類的實現(xiàn)細(xì)節(jié)。
使用class或struct關(guān)鍵字

使用class和struct定義類唯一的區(qū)別就是默認(rèn)的訪問權(quán)限。使用struct關(guān)鍵字,則定義在第一個訪問說明符之前的成員是public;如果使用class關(guān)鍵字,則成員是private的。

==使用class和struct定義類唯一的區(qū)別就是默認(rèn)的訪問權(quán)限。==

2.1 友元

類可以允許其他類或者函數(shù)訪問它的非公有成員,方法是令其他類或者函數(shù)成為它的友元。如果類想把一個函數(shù)作為它的友元,只需要增加一條以friend關(guān)鍵字開始的函數(shù)聲明即可:

class Sales_data {
//為Sales_data的非成員函數(shù)所做的友元聲明
friend Sales_data add(const Sales_data &, const Sales_data &);
friend std::ostream &print(std::ostream &, const Sales_data &);
friend std::istream &read(std::istream &, Sales_data &);
public:
    Sales_data() = default;
    Sales_data(const std::string &s, unsigned n, double p) :bookNo(s), units_sold(n), revenue(n*p) {}
    Sales_data(const std::string &s) :bookNo(s) {}
    Sales_data(std::iostream &);
    std::string isbn() const {
        return bookNo;
    }
    Sales_data &combine(const Sales_data &);
private:
    double avg_price() const {
        return units_sold ? revenue / units_sold : 0;
    }
    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 &); 

==一般來說,最好在類定義開始或結(jié)束前的位置集中聲明友元。==

友元的聲明

友元的聲明僅僅指定了訪問的權(quán)限,而非一個通常意義上的函數(shù)聲明。如果我們希望用戶能夠調(diào)用某個友元函數(shù),那么我們必須在友元聲明之外再專門對函數(shù)進(jìn)行一次聲明。


#3. 類的其他特性

3.1 類成員再探

定義一個類型成員
class Screen {
public:
    typedef std::string::size_type pos; //pos為類型成員
private:
    pos cursor = 0;
    pos height = 0, width = 0;
    std::string contents;
};

用來定義類型的成員必須先定義后使用。

Screen類的成員函數(shù)
class Screen {
 public:
    typedef std::string::size_type pos;
    Screen() = default;
    Screen(pos ht,pos wd,char c):height(ht),width(wd),
    contents(ht * wd,c){}
    char get() const { //讀取光標(biāo)處的字符
        return contents[cursor]; //隱式內(nèi)聯(lián)
    }
    inline char get(pos ht,pos wd) const; //顯示內(nèi)聯(lián)
    Screen &move(pos r,pos c);

private:
    pos cursor = 0;
    pos height = 0,width = 0;
    std::string contents;
};
令成員作為內(nèi)聯(lián)函數(shù)

在類中,常有一些規(guī)模較小的函數(shù)適合于被聲明成內(nèi)聯(lián)函數(shù)。定義在類內(nèi)部的函數(shù)是自動inline的。我們也可以在類的外部用inline關(guān)鍵字修飾函數(shù)的定義:

inline Screen &Screen::move(pos r,pos c) {
    pos row = r * width; //計算行的位置
    cursor = row + c; //在行內(nèi)將光標(biāo)移動到指定的列
    return *this; //以左值的形式返回對象
}

char Screen::get(pos r,pos c) const { //在類的內(nèi)不聲明成inline
    pos row = r * width; //計算行的位置
    return contents[row + c]; //返回給定列的字符
}
重載成員函數(shù)

和非成員函數(shù)一樣,成員函數(shù)也可以被重載。只要函數(shù)之間在參數(shù)的數(shù)量和\或類型上有區(qū)別就行。

可變數(shù)據(jù)成員

有時會發(fā)生這樣一種情況,我們希望修改類的某個數(shù)據(jù)成員,即使是在一個const成員函數(shù)內(nèi)。可以通過在變量的聲明中加入mutable關(guān)鍵字做到這一點。

一個可變數(shù)據(jù)成員永遠(yuǎn)不會是const,即使它是const對象的成員。因此,一個const成員可以改變一個可變成員的值。

class Screen {
public:
    void some_member() const;
private:
    mutable size_t access_ptr; //即使在一個const對象內(nèi)也能被修改
};
void Screen::some_member() const {
    ++access_ptr;
}

3.2 返回*this的成員函數(shù)

class Screen {
public:
    typedef std::string::size_type pos;
    Screen &set(char);
    Screen &set(pos,pos,char);
private:
    pos cursor;
    pos height = 0, width = 0;
    std::string contents;
};
inline Screen &Screen::set(char c) {
    contents[cursor] = c;
    return *this;
}
inline Screen &Screen::set(pos r, pos col, char ch) {
    contents[r*width + col] = ch;
    return *this;
}

和move操作一樣,我們的set成員的返回值是調(diào)用set的對象的引用。返回引用的函數(shù)是左值的,意味著這些函數(shù)返回的是對象本身而非對象的副本。

從const成員函數(shù)返回*this

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

基于const的重載

通過區(qū)分成員函數(shù)是否是const的,我們可以對其進(jìn)行重載。

class Screen {
public:
    //根據(jù)對象是否是const重載了display函數(shù)
    Screen &display(std::ostream &os){
        do_display(os);
        return *this;
    }
    const Screen &display(std::ostream &os) {
        do_display(os);
        return *this;
    }

private:
    //該函數(shù)負(fù)責(zé)顯示Screen的內(nèi)容
    void do_display(std::ostream &os) const {
        os << contents;
    }
};

3.3 類類型

每個類定義了唯一的類型。對于兩個類來說,即使它們的成員完全一樣,這兩個類也是兩個不同的類型。例如:

struct First {
    int memi;
    int getMem();
};
struct Second {
    int memi;
    int getMem();
};
First obj1;
Second obj2 = obj1; //錯誤:obj1和obj2的類型不同

==即使兩個類的成員列表完全一致,它們也是不同的類型。對于一個類來說,它的成員和其他任何類的成員都不是一回事兒。==

類的聲明

就像可以把函數(shù)的聲明和定義分離開來一樣,我們也能僅僅聲明類而暫時不定義它:

class Screen; //Screen類的聲明

這種聲明有時被稱為前向聲明,對于類型Screen來說,在它聲明之后定義之前是一個不完全類型。

3.4 友元再探

類還可以把其他類定義成友元,也可以把其他類的成員函數(shù)定義成友元。此外,友元函數(shù)能定義在類的內(nèi)部,這樣的函數(shù)是隱式內(nèi)聯(lián)的。

類之間的友元關(guān)系
class Screen {
    //Window_mgr的成員可以訪問Screen類的私有部分
    friend class Window_mgr;
};

如果一個類指定了友元類,則友元類的成員函數(shù)可以訪問此類包括非公有成員在內(nèi)的所有成員。
每個類負(fù)責(zé)控制自己的友元類和友元函數(shù)。

令成員函數(shù)作為友元

除了令整個類作為友元之外,還可以只為類的某個成員函數(shù)提供訪問權(quán)限。

class Screen {
    //Window_mgr::clear必須在Screen類之前被聲明
    friend void Window_mgr::clear(ScreenIndex);  
};
函數(shù)重載和友元

盡管重載函數(shù)的名字相同,但它們?nèi)匀皇遣煌暮瘮?shù)。因此,如果一個類想把一組重載函數(shù)聲明成它的友元,它需要對這組函數(shù)中的每一個分別聲明:

//重載的storeOn函數(shù)
extern std::ostream &storeOn(std::ostream &,Screen &);
extern BitMap &storeOn(BitMap &,Screen &);

class Screen {
    //storeOn的ostream版本能訪問Screen對象的私有部分
    friend std::ostream &storeOn(std::ostream &,Screen &);
    //...
};
友元聲明和作用域

類和非成員函數(shù)的聲明不是必須在它們的友元之前聲明。友元聲明的作用是影響訪問權(quán)限,它本身并非普通意義的聲明。

struct X {
    friend void f(); /*友元函數(shù)可以定義在函數(shù)的內(nèi)部*/
    X() { f(); }; //錯誤:f還沒有被聲明
    void g();
    void h();
};
void X::g() { return f(); } //錯誤:f還沒有被聲明
void f(); //聲明那個定義在X中的函數(shù)
void X::h() { return f();} //正確:現(xiàn)在f的聲明在作用域中了

#4. 類的作用域

每個類都會定義它自己的作用域。在類的作用域之外,普通的數(shù)據(jù)和函數(shù)成員只能由對象、引用或者指針使用成員訪問運(yùn)算符來訪問。對于類類型成員則使用作用域運(yùn)算符訪問。

4.1 名字查找與類的作用域

在目前為止,我們編寫的程序中,名字查找的過程比較直截了當(dāng):

  • 首先,在名字所在的塊中尋找其聲明的語句,只考慮在名字的使用之前出現(xiàn)的聲明。
  • 如果沒找到,繼續(xù)查找外層作用域。
  • 如果最終沒有找到匹配的聲明,則程序報錯。
    對于定義在類外部的成員函數(shù)來說,解析其中名字的方式和上述查找規(guī)則有所區(qū)別。類的定義分兩步處理:
  • 首先,編譯成員的聲明。
  • 直到類全部可見后才編譯函數(shù)體。

==編譯器處理完類中的全部聲明后才會處理成員函數(shù)的定義。==

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

這種兩階段的處理方式只使用與成員函數(shù)中使用的名字。如果某個成員的聲明使用了類中尚未出現(xiàn)的名字,則編譯器將在定義該類的作用域中繼續(xù)查找。例如:

typedef double Money;
string bal;
class Account {
public:
    Money balance() {
        return bal;
    }
private:
    Money bal;
    //...
};

當(dāng)編譯器看到balance函數(shù)的聲明語句時,它將在Account類的范圍內(nèi)尋找對Money的聲明。如果沒找到,編譯器會接著到Account的外層作用域中查找。

類型名要特殊處理

一般來說,內(nèi)層作用域可以重新定義外層作用域中的名字,即使該名字已經(jīng)在內(nèi)層作用域中使用過。然而在類中,如果成員使用了外層作用域中的某個名字,而該名字代表一種類型,則類不能在之后重新定義該名字:

typedef double Money;
class Account {
public:
    Money balance() { //使用外層作用域的Money
        return bal;
    }
private:
    typedef double Money; //錯誤:不能重新定義Money
    Money bal;
};

#5. 構(gòu)造函數(shù)再探

5.1 構(gòu)造函數(shù)初始值列表

如果沒有在構(gòu)造函數(shù)的初始值列表中顯式地初始化成員,則該成員將在函數(shù)體之前執(zhí)行默認(rèn)初始化。例如:

//Sales_data構(gòu)造函數(shù)的一種寫法,雖然合法但是比較草率:沒有使用構(gòu)造函數(shù)初始值
Sales_data::Sales_data(const string &s,unsigned cnt,double price) {
    bookNo = s;
    units_sold = cnt;
    revenue = cnt * price;
}
構(gòu)造函數(shù)的初始值有時必不可少

如果成員是const或者引用的話,必須將其初始化。類似的,當(dāng)成員屬于某種類類型且該類沒有定義默認(rèn)構(gòu)造函數(shù)時,也必須將這個成員初始化。例如:

class ConstRef {
public:
    ConstRef(int ii) : i(ii),ci(ii),ri(i) {}
private:
    int i;
    const int ci;
    int &ri;
};

和其他常量對象或者引用一樣,成員ci和ri都必須被初始化。因此,如果我們沒有為它們提供構(gòu)造函數(shù)初始值的話將引發(fā)錯誤:

ConstRef::ConstRef(int ii) {
    //賦值
    i = ii; //正確
    ci = ii; //錯誤:不能給const賦值
    ri = ii; //錯誤:ri沒被初始化
}
//正確:顯示地初始化引用和const成員
ConstRef::ConstRef(int ii) :i(ii),ci(ii),ri(i) {}
成員初始化順序

成員的初始化順序與它們在類定義中的出現(xiàn)順序一致:第一個成員先被初始化,然后第二個,依次類推。構(gòu)造函數(shù)初始值列表中初始值的前后位置關(guān)系不會影響實際的初始化順序。

class X {
private:
    int i;
    int j;
public:
    //i的值為undefined,i在j之前初始化
    X(int val) :j(val), i(j) {}
};

==最好令構(gòu)造函數(shù)的初始值順序和成員聲明的順序保持一致。而且如果可能的話,盡量避免使用成員初始化其他成員。==

默認(rèn)實參和構(gòu)造函數(shù)

Sales_data默認(rèn)構(gòu)造函數(shù)的行為與只接受一個string實參的構(gòu)造函數(shù)差不多。唯一的區(qū)別是接受string實參的構(gòu)造函數(shù)使用這個實參初始化bookNo,而默認(rèn)構(gòu)造函數(shù)使用string的默認(rèn)構(gòu)造函數(shù)初始化bookNo。

class Sales_data {
public:
    //定義默認(rèn)構(gòu)造函數(shù),令其與只接受一個string實參的構(gòu)造函數(shù)功能相同
    Sales_data(std::string s = ""):bookNo(s) {}
    Sales_data(std::string s,unsigned cnt,double rev):
    bookNo(s),units_sold(cnt),revenue(rev*cnt) {}
    Sales_data(std::istream &is) {
        read(is,*this);
    }
};

==如果一個構(gòu)造函數(shù)為所有參數(shù)都提供了默認(rèn)實參,則它實際上也定義了默認(rèn)構(gòu)造函數(shù)。==

5.2 委托構(gòu)造函數(shù)

C++11新標(biāo)準(zhǔn)擴(kuò)展了構(gòu)造函數(shù)初始值的功能,使得我們可以定義所謂的委托構(gòu)造函數(shù)。
一個委托構(gòu)造函數(shù)使用它所屬類的其他構(gòu)造函數(shù)執(zhí)行它自己的初始化過程,或者說它把它自己的一些職責(zé)委托給了其他構(gòu)造函數(shù)。

class Sales_data {
public:
    //非委托構(gòu)造函數(shù)使用對應(yīng)的實參初始化成員
    Sales_data(std::string s,unsigned cnt,double price):
    bookNo(s),units_sold(cnt),revenue(cnt*price) {}
    //其余構(gòu)造函數(shù)全部委托給另一個構(gòu)造函數(shù)
    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);
    }
};

5.3 默認(rèn)構(gòu)造函數(shù)的作用

當(dāng)對象被默認(rèn)初始化或值初始化時自動執(zhí)行默認(rèn)構(gòu)造函數(shù)。默認(rèn)初始化在以下情況下發(fā)生:

  • 當(dāng)我們在塊作用域內(nèi)不使用任何初始值定義一個非靜態(tài)變量或數(shù)組時。
  • 當(dāng)一個類本身含有類類型的成員且使用合成的默認(rèn)構(gòu)造函數(shù)時。
  • 當(dāng)類類型的成員沒有在構(gòu)造函數(shù)初始值列表顯式的初始化時。

值初始化在以下情況發(fā)生:

  • 在數(shù)組初始化的過程中如果我們提供的初始值數(shù)量少于數(shù)組的大小時。
  • 當(dāng)我們不使用初始值定義一個局部靜態(tài)變量時。
  • 當(dāng)我們通過書寫表達(dá)式顯示地請求值初始化時,其中T是類型名。
class NoDefault {
public:
    NoDefault(const std::string&);
};
struct A {
    NoDefault my_mem; //默認(rèn)情況下my_mem是public的
};
A a; //錯誤:不能為A合成構(gòu)造函數(shù)
struct B {
    B(){} //錯誤:b_member沒有初始值
    NoDefault b_member;
};

==在實際中,如果定義了其他構(gòu)造函數(shù),那么最好也提供一個默認(rèn)構(gòu)造函數(shù)。==

使用默認(rèn)構(gòu)造函數(shù)
Sales_data obj(); //錯誤:聲明一個函數(shù)而非對象
Sales_data obj2; //正確:obj2是一個對象而非函數(shù)

5.4 隱式的類類型轉(zhuǎn)換

如果構(gòu)造函數(shù)只接受一個實參,則它實際上定義了轉(zhuǎn)換為此類類型的隱式轉(zhuǎn)換機(jī)制,有時我們把這種構(gòu)造函數(shù)稱為轉(zhuǎn)換構(gòu)造函數(shù)。

只允許一步類類型轉(zhuǎn)換

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

//錯誤:需要用戶定義的兩種轉(zhuǎn)換:
//(1)把“9-999-99999-9”轉(zhuǎn)換成string
//(2)再把這個臨時的string轉(zhuǎn)換成Sales_data
item.combine("9-999-99999-9");

如果想完成上述調(diào)用,可以顯示地把字符串轉(zhuǎn)換成string或者Sales_data對象:

//正確:顯示地轉(zhuǎn)換成string,隱式地轉(zhuǎn)換成Sales_data
item.combine(string("9-999-99999-9"));
//正確:隱式地轉(zhuǎn)換成string,顯示地轉(zhuǎn)換成Sales_data
item.combine(Sales_data("9-999-99999-9"));
抑制構(gòu)造函數(shù)定義的隱式轉(zhuǎn)換

在要求隱式轉(zhuǎn)換的程序上下文中,我們可以通過將構(gòu)造函數(shù)聲明為explicit加以阻止:

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

此時,沒有任何構(gòu)造函數(shù)能用于隱式地創(chuàng)建Sales_data對象,之前的兩種用法都無法通過編譯:

item.combine(null_book); //錯誤:string構(gòu)造函數(shù)是explicit的
item.combine(cin); //錯誤:istream構(gòu)造函數(shù)是explicit的

關(guān)鍵字explicit只對一個實參的構(gòu)造函數(shù)有效。需要多個實參的構(gòu)造函數(shù)不能用于執(zhí)行隱式轉(zhuǎn)換,所以無須將這些構(gòu)造函數(shù)指定為explicit的。只能在類內(nèi)聲明構(gòu)造函數(shù)時使用explicit關(guān)鍵字,在類外部定義時不應(yīng)重復(fù):

//錯誤:explicit關(guān)鍵字只允許出現(xiàn)在類內(nèi)的構(gòu)造函數(shù)聲明處
explicit Sales_data::Sales_data(istream& is) {
    read(is,*this);
}
explicit構(gòu)造函數(shù)只能用于直接初始化

發(fā)生隱式轉(zhuǎn)換的一種情況是當(dāng)我們執(zhí)行拷貝形式的初始化時。此時,我們只能使用直接初始化而不能使用explicit構(gòu)造函數(shù):

//正確:直接初始化
Sales_data item1(null_book);
//錯誤:不能將explicit構(gòu)造函數(shù)用于拷貝形式的初始化過程
Sales_data item2 = null_book;

==當(dāng)我們用explicit關(guān)鍵字聲明構(gòu)造函數(shù)時,它將只能以直接初始化的形式使用。而且,編譯器將不會在自動轉(zhuǎn)換的過程中使用該構(gòu)造函數(shù)。==

為轉(zhuǎn)換顯示地使用構(gòu)造函數(shù)

盡管編譯器不會將explicit的構(gòu)造函數(shù)用于隱式轉(zhuǎn)換過程,但是我們可以使用這樣的構(gòu)造函數(shù)顯示地強(qiáng)制進(jìn)行轉(zhuǎn)換:

//正確:實參是一個顯示構(gòu)造的Sales_data對象
item.combine(Sales_data(null_book));
//正確:static_cast可以使用explicit的構(gòu)造函數(shù)
item.combine(static_cast<Sales_data>(cin));

5.5 聚合類

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

  • 所有成員都是public的。
  • 沒有定義任何構(gòu)造函數(shù)。
  • 沒有類內(nèi)初始值。
  • 沒有基類,也沒有virtual函數(shù)。

例如,下面類是一個聚合類:

strcuct Data {
    int ival;
    string s;
};

5.6 字面值常量類

除了算術(shù)類型、引用和指針外,某些類也是字面值類型。和其他類不同,字面值類型的類可能含有constexpr函數(shù)成員。這樣的成員必須符合constexpr函數(shù)的所有要求,它們是隱式const的。

數(shù)據(jù)成員都是字面值類型的聚合類是字面值常量類。如果一個類不是聚合類,但它符合下述要求,則它也是一個字面值常量類:

  • 數(shù)據(jù)成員都必須是字面值類型。
  • 類必須含有一個constexpr構(gòu)造函數(shù)。
  • 如果一個數(shù)據(jù)成員含有類內(nèi)初始值,則內(nèi)置類型成員的初始值必須是一條常量表達(dá)式;或者如果成員屬于某種類類型,則初始值必須使用成員自己的constexpr構(gòu)造函數(shù)。
  • 類必須使用析構(gòu)函數(shù)的默認(rèn)定義,該成員負(fù)責(zé)銷毀類的對象。
constexpr構(gòu)造函數(shù)

盡管構(gòu)造函數(shù)不能是const的,但是字面值類型的構(gòu)造函數(shù)可以是constexpr函數(shù)。事實上,一個字面值常量類必須至少提供一個constexpr構(gòu)造函數(shù)。

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) {
        hw = b;
    }
private:
    bool hw;
    bool io;
    bool other;
};

constexpr構(gòu)造函數(shù)必須初始化所有數(shù)據(jù)成員,初始值或者使用constexpr構(gòu)造函數(shù),或者是一條常量表達(dá)式。


#6. 類的靜態(tài)成員

有的時候類需要它的一些成員與類本身直接相關(guān),而不是與類的各個對象保持關(guān)聯(lián)。

聲明靜態(tài)成員

我們通過在成員的聲明之前加上關(guān)鍵字static使得其與類關(guān)聯(lián)在一起。和其他成員一樣,靜態(tài)成員可以是public的或private的。靜態(tài)數(shù)據(jù)成員的類型可以是常量、引用、指針、類類型等。

class Account {
public:
    void caculate() {
        amount += amount * interestRate;
    }
    static double rate() {
        return interestRate;
    }
    static void rate(double);
private:
    std::string owner;
    double amount;
    static double interestRate;
    static double initRate();
};

類的靜態(tài)成員存在于任何對象之外,對象中不包含任何與靜態(tài)數(shù)據(jù)成員有關(guān)的數(shù)據(jù)。

類似的,靜態(tài)成員函數(shù)也不與任何對象綁定在一起,它們不包含this指針。作為結(jié)果,靜態(tài)成員函數(shù)不能聲明成const的,而且我們不能在static函數(shù)體內(nèi)使用this指針。這一限制即適用于this的顯式使用,也對調(diào)用非靜態(tài)成員的隱式使用有效。

使用靜態(tài)成員

我們使用作用域運(yùn)算符直接訪問靜態(tài)成員:

double r;
r = Account::rate(); //使用作用域運(yùn)算符訪問靜態(tài)成員
定義靜態(tài)成員

和其他的成員函數(shù)一樣,我們既可以在類的內(nèi)部也可以在內(nèi)的外部定義靜態(tài)成員函數(shù)。當(dāng)在類的外部定義靜態(tài)成員時,不能重復(fù)static關(guān)鍵字,該關(guān)鍵字只出現(xiàn)在類內(nèi)部的聲明語句:

void Account::rate(double newRate) {
    interestRate = newRate;
}

==和類的所有成員一樣,當(dāng)我們指向類外部的靜態(tài)成員時,必須指明成員所屬的類名。static關(guān)鍵字則只出現(xiàn)在類內(nèi)部的聲明語句中。==

靜態(tài)成員的類內(nèi)初始化

通常情況下,類的靜態(tài)成員不應(yīng)該在類的內(nèi)部初始化。然而,我們可以為靜態(tài)成員提供const整數(shù)類型的類內(nèi)初始值,不過要求靜態(tài)成員必須是字面值常量類型的constexpr。

class Account {
public:
    static double rate() {
        return interestRate;
    }
    static void rate(double);
private:
    static constexpr int period = 30; //period是常量表達(dá)式
    double daily_tbl[period];
};
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 使用類定義自己的數(shù)據(jù)類型 本章主要關(guān)注數(shù)據(jù)抽象→將對象的具體實現(xiàn)與對象所能執(zhí)行操作分離開來 零、術(shù)語表 聚合類 類...
    菜雞也會飛閱讀 282評論 0 1
  • 數(shù)據(jù)抽象:是一種依賴于接口和實現(xiàn)分離的變成技術(shù)。 封裝:分離接口(用戶所能執(zhí)行的操作)和實現(xiàn)(數(shù)據(jù)成員、實現(xiàn)接口的...
    咸魚翻身ing閱讀 250評論 0 0
  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy閱讀 9,698評論 1 51
  • 這是16年5月份編輯的一份比較雜亂適合自己觀看的學(xué)習(xí)記錄文檔,今天18年5月份再次想寫文章,發(fā)現(xiàn)簡書還為我保存起的...
    Jenaral閱讀 3,186評論 2 9
  • 二十三兒 喝面葉兒 其實不是說特別喜歡某種食物 像二十三的面葉兒,年三十的餃子,只是特定的時間吃特定的食物已經(jīng)成了...
    歡喜_b254閱讀 284評論 0 0

友情鏈接更多精彩內(nèi)容