-
#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ù),原因有三:
- 只有當(dāng)類沒有聲明任何構(gòu)造函數(shù)時,編譯器才會自動地生成默認(rèn)構(gòu)造函數(shù)。一旦在類中定義了其他構(gòu)造函數(shù),除非我們再定義默認(rèn)構(gòu)造函數(shù),否則類沒有默認(rèn)構(gòu)造。
- 對于某些類而言,合成的默認(rèn)構(gòu)造可能執(zhí)行錯誤的操作。如果類中的內(nèi)置類型或復(fù)合類型的對象被默認(rèn)初始化,它們的值將是未定義的。
- 編譯器有時候不能為類合成默認(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];
};