天天看點

c++ primer 第五版 翻譯 第七章第七章 類

第七章 類

在u盤中找回部分翻譯,接着上翻譯

内容:

7.1 定義抽象資料類型

7.2 通路控制和封裝

7.3 類的其他特性

7.4 類作用域

7.5 構造器再探

7.6 static類成員

本章小結

專業術語

在c++中,使用類來自定義資料類型。通過定義新類型來反映待解決問題中的各種概念,并且也使得我們的程式更加容易編寫,調試和修改。

本章繼續第二章中的類的介紹。本章重點關注資料抽象的重要性,因為資料抽象可以讓一個對象的實作與一個對象的操作相分離。在第十三章中将會介紹,對象的複制,移動,指派和銷毀。在第十四章将會介紹怎麼定義自己的運算符

類的基本思想是資料抽象和封裝。資料抽象是一種依賴于接口和實作分離的技術。一個類的接口由這個類的使用者能夠執行的操作組成。一個類的實作則包括類的資料成員,構成接口的函數的函數體,以及定義類需要的各種函數。

封裝實作了類的接口和實作分離。一個類被封裝,它就隐藏了他的實作。即這個類的使用者隻能使用接口,而不能通路實作。

類要實作資料抽象和封裝,就要定義一個抽象資料類型。在抽象資料類型中,類的設計者負責類的實作。使用這個類的程式員不需要知道這個類是怎麼工作的,他們隻需要抽象的思考這個類型是做什麼的即可。

7.1 定義抽象資料類型

在第一章使用的Sales_item類就是一個抽象資料類型。通過使用他的接口來使用Sales_item類。我們不能存取在Sales_item對象中的資料成員。事實上,我們不知道這個類有哪些資料成員。

我們的Sales_data類(2.6.1小節)不是一個抽象的資料類型。因為類的使用者可以存取它的資料成員,并且強制使用者編寫自己的操作.為了使Sales_data成為一個抽象的資料類型,我們需要定義Sales_data使用者能使用的操作。一旦Sales_data定義了它自己的操作,那麼就可以封裝他的資料成員了。

7.1.1 設計Sales_data類

最終,我們想Sales_data類跟Sales_item類有相同的操作。Sales_item類有一個成員函數叫做,isbn,并且支援+,=,+=,<<,和>>運算符。

對于怎樣定義自己的運算符,将在第十四章介紹。現在,我們隻定義跟這些操作相同的普通函數。由于在14.1小節将會介紹的原因,執行加法和IO操作的函數不作為Sales_data的成員函數。而,将這些函數定義為普通的函數。複合指派操作的函數将作為成員函數。并且這個類不需要定義指派函數,這樣做的原因将會在7.1.5小節中介紹。

是以,Sales_data的接口由下面的操作組成:

  1. 一個isbn函數,傳回這個對象的ISBN号
  2. 一個combine函數,将一個Sales_data對象加到另外一個對象上
  3. 一個add函數,将兩個Sales_data對象相加
  4. 一個read函數,從istream中讀取資料到Sales_data對象中
  5. 一個print函數,列印Sales_data對象中的資料到ostrem中

關鍵概念:不同的程式設計角色

程式員将運作他們程式的人稱為使用者。相似的,類的設計者設計并且實作了一個類,就是為了類的使用者。此時,使用者是程式員,而不是應用的最終使用者。

當我們提及使用者一詞時,不同的語境決定了其不同的意思。如果我們說使用者代碼或者Sales_data類的使用者,指的是程式員。如果我們說書店應用的使用者,指的是運作這個書店程式的管理者。

注意:c++程式員無須刻意區分應用程式的使用者以及類的使用者。

在一個簡單的應用中,一個類的使用者和這個類的設計者,可能是同一個人。盡管如此,還是應該将角色區分開來。當設計類的接口時,應該思考怎麼使用這個類更容易。當使用類時,就不應該思考這個類是怎麼工作的。

成功的應用程式的作者必須了解并且實作使用者的需求。同樣,一個好的類設計者也應該關心使用這個類的程式員的需求。一個設計良好的類必須要有直覺且便于使用的接口,還要有高效的實作。

使用改進的Sales_data類

在思考怎麼實作我們的類之前,讓我們先看看怎麼使用這些接口函數。例如,可以使用這些接口函數,寫一個類似于1.6小節的書店程式,他們使用Sales_data,而不是使用Sales_item.

Sales_data total;		//儲存和的變量
if(read (cin,total)){		//讀取第一條交易記錄
	Sales_data trans;	//儲存下一條交易記錄的變量
while(read(cin,trans)){
	if(total.isbn() == trans.isbn())
		total.combin(trans);
	else{
		print(cout,total) << end;
		total = trans;
	}
}
print(cout,total) << endl;
}else{
	cerr << “No data?!” <<endl;
}

           

最開始定義了一個Sales_data變量用于存放運作的總和。在if的條件表達式裡面,調用read讀取第一個交易記錄到total裡面。這個條件表達式的工作,跟其他的循環裡面使用>>運算符一樣,read函數也會傳回他的stream形參,他被用着條件判斷(4.11.2小節)。如果read失敗,将跳轉到else分支,然後列印錯誤資訊。

如果有資料被讀到,我們将定義一個trans變量,這個變量儲存下一條交易記錄。在while中的條件表達式也會檢查read傳回的stream形參。隻要在read裡面的輸入運算符讀取成功,這個條件表達式就為true,就表示有一條交易記錄需要處理。

在while的循環體内部,調用total的isbn函數和trans的isbn函數,分别擷取對應的ISBN号,如果total和trans都是相同的書籍,就調用combine将trans加到total裡面。如果trans代表了新書,就列印前一種書的total。因為print傳回的是他的stream形參的引用,是以可以使用print的結果作為<<運算符的左操作數。使用這種方法,輸出print函數的結果,然後轉到下一行。接下來将trans指派給total,進而處理檔案中下一本書的交易記錄。

處理完所有的資料之後,一定要記住列印一下最後的交易記錄。是以在while的循環體之後再次調用了print函數。

7.1.2 定義改進的Sales_data 類

改進類跟2.6.1小節定義的類有相同的資料成員:bookNo,字元串代表ISBN;units_sold,unsigned 類型,代表有多少書被賣出去了;revenue,double類型代表注本書的總銷售額。

前面可以看見,我們的類有兩個成員函數,一個combine,一個isbn。另外再加一個成員函數,傳回售出書籍的平均價格。這個函數的名字叫做avg_price,因為這個函數不是通用的,是以這個函數是實作的一部分,不是接口的一部分。

定義和聲明成員函數跟普通函數一樣。成員函數必須被聲明在類内部。成員函數可以定義在類的内部,也可以定義在類的外部。接口中的非成員函數,如add,read,print需要聲明在類的外部。

根據這些資訊,寫如下的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 add(const Sales_data&,const Sales_data&);
std::ostream &print(std::ostream&,const Sales_data&);
std::istream &read(std::istream&,Sales_data&);

           
注意:定義在類内的函數隐式的為inline函數

定義成員函數

盡管成員函數必須聲明在類内部,但是成員函數的實作既可以在類内部,也可以在類外部。在Sales_data中,isbn定義在了類的内部;combine和avg_print定義在其他地方。

首先解釋一下isbn函數,他傳回一個字元串,并且形參為空

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

跟其他的函數一樣,成員函數的函數體是一個塊。此處,這個塊隻有一個return語句,這個return語句傳回Sales_data對象的bookNo成員資料。比較有意思的是:這個函數如何擷取到目前對象,然後依此來取得bookNo的呢?

引入this

再來看一下isbn的調用:

total.isbn();

此處,使用了點号運算符來獲得total對象的isbn函數,然後再調用這個isbn函數。

除了在7.6小節介紹的例外以外,當我們調用成員函數,實際上是替某個對象調用。當isbn指向Sales_data的成員時,它也隐式的指向了被調函數的對象的成員。在這個調用中,當調用isbn傳回bookNo時,隐式的傳回total.bookNo;

成員函數通路調用它的對象,通過一個隐式的this形參。當我們調用一個成員函數時,this被初始化為調用對象的指針。例如,當我們調用

total.isbn();

編譯器傳遞total的位址給,isbn的隐藏形參this。就好像編譯器重寫如下的調用一樣:

他将調用Sales_data的isbn成員函數,并且實參為total的位址。

在成員函數内部,可以直接通路調用對象的成員。不必使用this和成員運算符來通路這些成員。任何對類成員的直接通路,都被認為是隐式的使用了this。即,當isbn使用bookNo時,他隐式的使用了this指針,就好像this->bookNo一樣。

這個this形參是隐式定義的。如果定義一個形參名字為this,則是非法的。在一個成員函數的内部,可以直接使用this,如下的定義,盡管是合法的,但是沒必要:

因為this總是指向目前對象,this是一個const指針。我們沒法改變this儲存的指針。

引入const成員函數

isbn函數另外一個比較重要的部分是跟在形參清單後面的cont關鍵字。這個const隐式地修改this指針的類型。

預設情況下,this的類型是一個const指針,指向一個非const的對象。例如,預設情況下,this的類型為Sales_data *const.盡管this是隐式的,但是他也需要支援常見的初始化操作,這就意味着我們不能将this綁定到一個const對象上(2.4.2小節)。這就意味着,沒法在一個const對象上,調用一個普通的成員函數。

如果isbn是一個普通的函數,并且this也是一個普通的指針形參,那麼可以聲明this為const Sales_data *const類型。畢竟,isbn不會改變this所指對象的,是以當this指向一個const對象時,我們的函數更加靈活(6.2.3小節)。

但是,this是隐式的形參,不能出現在形參清單中,那麼就沒有地方讓我們聲明this為指向const的對象。c++為了解決這個問題,可以在形參清單後面跟一個const來解決。一個跟在形參清單後面的const表明,this是一個指向const對象的指針。這種函數稱為:const成員函數。

可以将isbn的函數當作如下的形式:

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

           

這就意味着,const成員函數無法改變調用對象。是以isbn隻能讀不能寫調用對象的成員資料。

注意:const對象,指向const對象的指針,以及綁定到const對象的引用,隻能調用const成員函數。

類作用域和成員函數

回憶2.6.1小節所講:一個類就是一個作用域。一個類的成員函數的定義被嵌入了這個類的作用域内。是以,在isbn裡面的使用bookNo是定義在Sales_data裡面的成員資料。

值得注意的是:就算bookNo定義在isbn之後,isbn可以也使用bookNo。在7.4.1小節将會介紹,編譯器處理類分為兩步:首先編譯成員的聲明,然後才是編譯函數體(如果有的話)。是以,成員函數,可以使用成員資料,而不用管成員函數出現在什麼地方。

定義在類外部的成員函數

跟其他的函數一樣,當定義類外部的函數時,必須和函數的聲明一樣。即,傳回類型,形參清單以及函數名,都必須和類内部的聲明一樣。如果成員函數被聲明為const成員函數,那麼這個定義也必須在形參清單後面寫上const。定義在類外部的成員必須包含類名:

double Sales_data::avg_price() const{
if(units_sold)
	return revenue / units_sold;
else
	return 0;
}

           

函數名Sales_data::avg_price,使用了作用域運算符,這個表明,我們定義的avg_price函數是在Sales_data作用域内的。一旦編譯器看見這個函數名,就會明白剩下的代碼是位于類的作用域内。是以,當avg_price使用revenue和units_sold時,他也隐式的表示使用了Sales_data的成員。

定義傳回this對象的函數

combine想要跟複合指派運算符一樣的行為。調用這個函數的對象代表了指派運算符的左操作數。右操作數通過顯示的傳參:

Sales_data & Sales_data::combine(const Sales_data &rhs){
	units_sold += rhs.units_sold;
	revenue += rhs.revenue;
	return *this;
}
           

當交易處理程式調用如下函數時:

total的位址被隐式的綁定到了this形參上。并且rhs也被綁定到了trans上。是以當combin執行時:

實際上是total.units_sold和trans.units_sold相加,然後将結果存放在total_units_sold.

這個函數有趣的地方在于傳回類型和return語句。通常,當我們想定義一個跟内置運算相同行為的函數時,應該模仿這個運算符的行為。内置的指派運算符将他的左操作數當作左值傳回。因為左操作數是Sales_data對象,是以傳回類型也應該是Sales_data&.

正如上面所見,我們不需要使用this指針通路調用對象的成員,但是我們需要使用this指針通路整個對象。

return *this;

此處,解引用this之後,獲得調用對象,然後傳回。即上面的調用傳回的是total對象的引用。

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

類的作者經常定義輔助函數,例如add,read,print等。盡管這些函數是類的接口的一部分,但是他們卻不是類的部分。

定義非成員函數跟其他函數一樣。通常,将函數的聲明和實作分離。這些函數概念上跟類是一部分,但是不會定義在類内部,經典的聲明是:定義在同一個頭檔案中。這樣使用者就隻需要包含一個檔案,然後就可以使用這個接口的任何部分了。

注意:通常,一個類接口的非成員函數應該聲明在這個類相同的頭檔案中。

定義read和print函數

read,print函數跟2.6.1小節中對應代碼的工作相同。并且函數體也類似:

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

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

           

read函數從給定的流中将資料讀入到給定的對象。print函數将給定對象列印到給定的流上。

但是,這裡有兩點值得注意:首先,read和print函數都有一個IO類型的引用。因為IO類型不能複制,是以隻能通過引用來傳參。并且,讀和寫都改變了這個流,是以這兩個函數的引用形參都是普通形參,不是const形參。

第二點需要注意的是:print不會輸出一個新行。通常,做輸出的函數,應該盡量減少格式化輸出。這樣,使用者就可以自行決定是否需要一個新行。

定義add函數

add函數帶有兩個Sales_data對象,并且傳回一個新的代表和的Sales_data對象。

Sales_data add(const Sales_data &lhs,const Sales_data &rhs){
	Sales_data sum = lhs;
	sum.combine(rhs);
	return sum;
}
           

在這個函數體内,定義了一個新的叫做sum的Sales_data對象,這個對象用于儲存兩個交易的和。使用lhs來初始化sum。預設情況下,複制類類型的對象,将會複制這個對象的成員。複制之後,sum的bookNo,units_sold和revenue跟lhs的成員一樣。接下來調用combine将rhs的units_sold和revenue加到sum上面。一切就緒之後,傳回sum的副本。

7.1.4 構造器

每一個類都需要定義這個類類型的對象如何被初始化。類通過一個或者多個成員函數來控制對象的初始化,這樣的函數成為構造器(構造函數)。構造器的工作就是初始化類對象的資料成員。隻要一個類對象被建立,這個構造器就會被運作。

本小節将會介紹如何定義構造器。構造器是一個非常複雜的主題,是以在7.5小節,15.7小節,18.1.3小節以及第十三章,都還有更加詳細的介紹。

構造起跟類名相同。跟其他函數不同的是,構造器沒有傳回類型。跟其他函數一樣的是,構造器也有形參清單(可能為空)和函數體(可能為空)。一個類可以有多個構造器。跟重載函數一樣,多個構造器之間必須不同,即形參個數,或者形參類型互不相同。

不像其他函數,構造器不可能聲明為const(7.1.2小節)。當建立一個const對象的時候,直到構造器完成對象的初始化之後,這個對象才呈現出const屬性。是以,構造器可以在const對象構造期間,向其寫值。

合成的預設構造器

我們的Sales_data類沒有定義任何的構造器。但是我們寫的程式也正常地編譯Sales_data對象并且正确運作。舉個例子:在255頁(原書)程式定義了兩個對象:

Sales_data total;
Sales_data trans;
           

自然産生的問題是:total和trans是如何初始化的?

此處沒有提供初始值,是以是預設初始化。通過在類内部定義預設的構造器來控制類的預設初始化。預設構造器就是沒有形參的構造器。

預設構造器有幾個特殊,其中之一就是:如果類沒有顯式的定義任何構造器,那麼編譯器将會定義一個預設的構造器。

編譯器生成的構造器稱為:合成預設構造器。對于大多數的類來說,這個合成構造器按下面的方式初始化類中的每一個成員:

如果有類内的初始值(2.6.1),就用他來初始化成員。

否則,就執行成員的預設初始化(2.2.1)。

因為,Sales_data提供了units_sold和revenue的初始值,是以預設構造器,就使用這個值來初始化這些成員。預設初始化bookNo為空字元串。

一些類不能依賴于合成的預設構造器

簡單類可以使用合成的預設構造器,例如Sales_data類。但是大多數的類都應該定義自己的預設構造器,而不是讓編譯器生成預設構造器。原因有三:其一,如果有其他任何的構造器,這個類就不會産生預設構造器,除非我們自己定義預設構造器。這個規則的思想是,如果類需要控制某種情況下的初始化,那麼就應該控制所有情況的初始化。

注意:隻有當類沒有任何構造器的時候,編譯器才産生預設構造器

其二,對于某些類來說,合成的預設構造器會執行錯誤的操作。回憶之前所講,在塊内的内置類型或者複合類型(例如數組和指針)的對象被預設初始化時,其值是未定義的。這也适用于類内的内置類型成員。是以,當類有内置類型或者複合類型的成員時,應該在類内初始化這些成員,或者定義預設構造器。否則使用者可能建立帶有未知值的對象。

警告:

一個類,有内置類型或者複合類型成員時,隻有這些成員有類内初始值時,才能依賴于合成預設構造器

第三個原因是:有時編譯器不能合成預設構造器,此時需要類自己定義預設構造器。例如,類有一個類類型的成員,并且這個成員沒有預設構造器, 是以編譯器不能初始化這個成員。對于這種情況,必須自己定義預設構造器。否則,類就不會有一個可用的預設構造器。在13.1.6小節将會看到另外的一些情況,也會讓編譯器沒法生成合适的預設構造器.

定義Sales_data的預設構造器

對于Sales_data類,我們将定義如下的參數,定義四個構造器:

  • 一個istream& ,從這個istream&中讀取交易
  • 一個const string& ,它代表了ISBN,一個unsigned 代表了銷售數量,一個double代表了銷售額
  • 一個const string& 代表ISBN。這個構造器将使用其他成員的預設值。
  • 一個空的參數清單,這個是我們必須定義的,因為我們定義了其他的構造器

将這些成員加到類中之後,我們現在為:

struct Sales_data {
	//增加的構造器
	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::istream &);
	
	//其他的成員
	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新标準下,如果我們想要預設的行為,可以在形參清單寫上=default,讓編譯器生成構造器。=default可以出現在類内部的聲明中,也可以出現在類外部的定義中。跟其他函數一樣,如果=default出現在類内部的聲明中,預設構造器就是内聯的;如果出現在類外部的定義中,就不是内聯的。

警告

Sales_data的預設構造器可以工作,是因為我們為類内部的内置類型成員提供了初始值。如果編譯器不支援類内初始值,那麼你必須使用構造器初始清單來初始化類内的每個成員。

構造器初始清單

接下來看類内部的其他兩個構造器:

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){}

           

新增的部分是冒号和大括号之間新增的代碼。這個新增的部分就是構造器初始清單,它為類的一個或者多個資料成員指定初始值。構造器初始清單是一些列的成員名,這些成員後面用小括号括起來他們的初始值,多個初始值之間使用逗号分隔。

有三個形參的構造器,使用它的前面兩個形參分别初始化bookNo和units_sold成員。而revenue的初始值則通過将p和n相乘得到。

有單個striing形參的構造器,使用這個string初始化bookNo,但是它沒有顯示的初始化units_sold和revenue成員。如果一個成員在構造器初始化清單裡面省略時,那麼它的初始化就和合成的預設構造器的初始化一樣。此時,被類内初始值初始化。是以,這個構造器等價于:

對于構造器來說,使用類内初始值是最好的,因為隻要這個值存在,那麼就能保證成員函數有正确的初始值。另一方面,如果編譯器還沒有支援類内初始值,那麼構造器就必須顯示的初始化類内的成員。

經驗之談

構造器不應該複寫類内初始值,除非需要使用一個不同的值來初始化。如果沒有使用類内初始值,那麼就應該顯示的初始化類内的内置成員。

值得注意的是,每一個構造器都有一個空的函數體。因為構造器唯一要做的工作是,給這些成員正确的值。如果沒有其他工作,函數體可以為空。

定義在類外的構造器

跟其他構造器不同,帶有一個istream的構造有其他的工作需要做。在這個函數體内,這個構造器調用read,擷取新值:

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

構造器沒有傳回類型,是以,以函數名開始。跟其他成員函數一樣,當在類外定義構造器時,需要指定這個構造器是屬于那個類。是以,Sales_data::Sales_data表明:我們定義了Sales_data的Sales_data成員。這個成員是一個構造器,因為他和類名相同。

這個構造器沒有構造器初始化清單,盡管如此,從技術上來說,構造器初始化清單為空也是正确的。盡管初始化清單為空,但是這個對象的類還是在構造器函數體運作之前就被初始化好了。

沒有出現在構造器初始化清單中的成員,使用類内初始值初始化,或者預設初始化。對于Sales_data來說,當構造器的函數體開始執行時,bookNo已經為一個空字元串,units_sold和revenue為0.

為了了解read的調用,切記,read的第二個形參是Sales_data對象的引用。在7.1.2小節中,我們講到,使用this來通路整個對象。此處,使用的是*this,表明将本對象作為實參傳遞給read函數。

7.1.5 複制,指派和析構

除了定義類對象如何被初始化,還可以定義複制,指派和析構類對象的行為。在某些情況下類對象執行複制操作,例如,通過值初始化一個對象,或者通過值傳遞/傳回一個對象。當使用指派運算符(4.4小節)的時候,對象執行指派操作。當對象不存在時,對象執行銷毀操作,例如,當退出這個對象建立的塊時,會執行銷毀操作。當vector銷毀時,存儲在vector中的對象也會執行銷毀操作。

如果沒有定義這些操作,編譯器将自動合成。通常,編譯器合成的這些操作,将會複制,指派,銷毀對象中的每個成員。例如,在我們的書店程式中,當編譯器執行下面的指派時:

就等同于下面的執行:

total.bookNo = trans.bookNo;
total.units_sold = trans.units_sold;
total.revenue = trans.revenue;
           

在第十三章将會介紹,如何定義這些行為。

某些類不能依賴于合成的函數

盡管編譯器會合成複制,指派,析構操作,但是對于某些類來說,合成的這些操作無法正确執行。尤其是,當對象需要配置設定對象之外的資源時,這些合成版本根本不可能正常工作。在第十二章将會看到一個例子,c++如何動态的配置設定和管理記憶體。在13.1.4小節也會看到,管理動态記憶體的類,不會依賴于合成的版本。

但是,對于大多數類來說,如果需要動态記憶體,基本上可以使用vector和string。使用vector和string的類避免了複雜的記憶體配置設定和釋放的操作。

是以,對于有vector和string成員的類來說,他們的複制,指派,析構的合成版本能夠正确工作。當複制或者指派一個含有vector成員的類時,将會對這個vector裡面的成員進行複制或者指派。當這個對象被銷毀時,vector成員也被銷毀,vector裡面的元素也被銷毀。string跟這個類似。

警告:除非你已經知道了第十三章要介紹的知識,否則對于類需要配置設定的資源,應該直接作為類的資料成員來存儲。

7.2 通路控制和封裝

到現在,我們已經定義了一個接口.但是并沒有任何機制強制使用者使用這些接口.我們的類還沒有封裝,使用者可以直接通路Sales_data對象中的成員,并且影響他的實作。在c++中可以使用通路訓示符強制進行封裝:

定義在public訓示符後面的成員,可以被程式的任何部分通路。public成員用來定義類的接口

定義在private訓示符後面的成員,隻能被類的成員函數通路,不能被類的使用者通路。private段用于封裝具體的實作。

重新定義Sales_data,如下:

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){}
	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:
	double avg_price() const{
		return units_sold ? revenue / units_sold : 0;
	}
	std::string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0 ;
}
           

跟在public訓示符後面的構造器和成員函數(isbn,combine)是接口的一部分。跟在private訓示符後面的資料成員和其他的函數是具體的實作。

一個類可能有零個或者多個通路訓示符。c++沒有限制每個通路訓示符出現多少次。每一個通路訓示符指定了跟着其後的成員的通路級别。通路級别的範圍為:直到遇到下一個通路訓示符,或者類的結尾。

使用class或者struc關鍵字

我們也可以做更加微妙的改變:使用class關鍵字,而不是struc來定義類。這種改變僅僅是形式上面的改變。可以使用任何一個關鍵字來定義類類型,而唯一的不同就是預設的通路級别不一樣。

類可以在第一個通路訓示符之前定義成員。而這些成員的通路級别,依賴于類是怎麼定義的。如果使用struct關鍵字定義,這些成員的通路級别為public。如果使用的是class關鍵定義的,這些成員的通路級别為private。

作為一種程式設計風格:當所有的成員都是public時,使用struct定義類;當有private成員時,使用class關鍵字。

7.2.1 友元

既然Sales_data的資料成員是private的,那麼我們的read,print和add函數就不能編譯。那麼出現這個問題的原始是,盡管Sales_data是接口的一部分,但是不是這個類的成員。

通過将A類或者A函數,标記為友元,就可以讓這個A類或者這個A函數通路另外一個B類的非public成員。要想使A函數成為B類的友元,則B類包含A函數的聲明,并且這個聲明跟在關鍵字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(std::istream&);
	std::string isbn() cons {return bookNo;}
	Sales_data &combine(const Sales_data&);
private:
	std::striing bookNo;
	unigned 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&);
           

友元聲明隻能是在類的内部,它可以在類内的任何地方。友元不是類的成員,不會受通路訓示符的限制。在7.3.4小節有更多關于友元的問題。

提示:在類定義的開頭或者結尾,将所有的友元寫在一起是非常好的注意

關鍵概念:封裝的好處

封裝提供兩個非常重要的優勢:

使用者代碼不會無意破壞被封裝對象的狀态;

封裝好了的代碼實作可以任意改變而不用改變使用者代碼

通過将資料成員定義為private,這個類的作者就可以自由的改變這些資料。此時,僅僅隻有這個類的代碼需要做一下測試,看看是否會有影響。而使用者代碼,隻要接口不變,就不會有任何影響。如果資料是public,這些使用舊版本的代碼,就可能會被破壞。是以必須找到,并且重寫基于舊版本的代碼。

把資料成員做成private的另外一個好處是,可以防止使用者對資料的破壞。如果有一個bug,破壞了對象的狀态,可以在局部地區進行debug:因為隻有實作部分才可能産生錯誤。是以,debug就在有限範圍内,極大減輕了程式問題的維護和修正。

注意:盡管當類的定義改變時,使用者代碼不需要改變,但是使用這個類的所有使用者代碼都需要重新編譯。

友元的聲明

友元的聲明僅僅指定了通路的權限。他不是函數的常用聲明。如果我們想要某個類的使用者代碼能夠調用這個類的友元函數,我們也必須另外再次聲明這個函數。

為了使類的使用者對于友元函數可見,通常将類本身和友元函數的聲明放在同一個頭檔案中(類之外)。是以,我們的Sales_data應該為read,print,add提供分開的聲明。

注意:許多編譯器并不會強制規定,在使用友元函數之前,必須在類的外部聲明

一些編譯器也允許,在沒有函數聲明的前提下,調用友元函數。即使你的編譯器支援這種調用,但是也最好為友元函數提供單獨的聲明。這樣,當你切換編譯器的時候,就不用更改代碼了。

7.3 類的其他特性

盡管Sales_data類非常簡單,但是,他也讓我們探索了許多c++支援的特性。本段,将介紹Sales_data沒有展現的其他的c++特性。這些特性包括:類型成員,類類型成員的類内初始值,可變資料成員,内聯成員函數,傳回*this的成員函數,以及更多關于如何定義和使用類類型和友元類。

7.3.1 類成員再談

為了探索這些特性,我們将定義一對類,這兩個類的名字為Screen和Window_mgr.

定義類型成員

Screen代表顯示上面的一個視窗。每個Screen都有一個string成員,這個string成員儲存有視窗的内容,還有三個String::size_type成員,分别代表光标的位置,以及視窗的寬和高。

除了定義資料和函數成員以外,類也可以定義它自己的本地類型名(即某個類型的别名)。由類定義的類型名跟其他的成員一樣有通路限制,public或者private。

class Screen{
public:
	typedef std::string::size_type pos;
private:
	pos cursor = 0;
	pos height = 0, width = 0;
	std::string contents;
};
           

我們在Screen的public部分定義了pos,因為我們希望使用者代碼也能使用這個類型。Screen的使用者不應該知道Screen是使用string來儲存的資料,是以定義pos為public成員,這可以隐藏screen是如何實作的。

此處關于pos聲明有兩點需要注意,第一,盡管我們使用了typedef,但是我們也可以使用等價的類型聲明:

class Screen{
public:
	//聲明類型别名的另外一種方式
	using pos = std::string::size_type;
};

           

第二,不想普通的成員,類型别名的聲明必須出現在使用之前,具體原因将在7.4.1小節中介紹。是以,類型成員出現在類的開始。

Screen類的成員函數

為了使類更加有用,增加一個可以讓使用者定義大小和内容的構造器,還有移動光标的成員和擷取給定位置字元的成員:

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 {return contents[cursor];}
	inline char get(pos ht,pos wd) const;
	Screen &move(pos r,pos c);
private:
	pos cursor = 0;
	pos height = 0,width = 0;
	std::string contents;
};

           

因為我們提供了一個構造器,是以編譯器不會自動合成預設構造器。如果我們需要預設構造器的話,就必須顯示的聲明他。此處,使用了=default讓編譯器合成預設構造的定義。

值得注意的使:第二個構造器使用了三個參數,對于cursor成員來說,隐式的使用了類内初始值。如果cursor沒有類内初始值,那麼我們應該顯示的初始cursor。

使成員成為内聯函數

類内的小函數可以做成内聯的。正如所見,定義在類内部的成員函數就是自動變成内聯的(6.5.2)。是以,Screen的構造和get()函數是内聯的。

我們還可以在類内,可以把inline作為聲明的一部分,來聲明成員函數為内聯函數。另外也可以在類外的函數定義中指定為内聯。

inline Screen & Screen::move(pos r,pos c){
	pos row = r*width;
	cursor = row+c;
	return *this;
}

char Screen::get(pos r,pos c) const{
	pos  row = r*width;
	return contents[row + c];
}
           

盡管沒必要在聲明和定義兩端都指定為inline,但是在兩端都指定inline是合法的。是以,在類的定義處指定inline更有助于閱讀。

注意:類的内聯函數應該定義在類的聲明的同一個頭檔案中,這根内聯函數應該定義在頭檔案中的原因一樣。

重載成員函數

跟非成員函數一樣,成員函數也可以重載。隻要函數的形參個數,或者類型不同即為重載。而對于成員函數的調用,進行的函數比對處理,與非成員函數的比對處理一樣。

例如,Screen類定義了兩個get。一個get傳回目前光标的字元;另外一個get放回給定行列所表示的字元。編譯器通過參數個數,來決定調用哪一個函數:

Screen myscreen;
char ch = myscreen.get();//調用Screen::get()
ch = myscreen.get(0,0);//調用screen::get(pos,pos)
           

可變資料成員

有時(不經常)我們想修改const成員函數裡面的類成員。通過在成員的聲明中包含mutable關鍵字來表明這個成員可以被修改。

一個可變資料的成員就不再是const,就算他是const對象的成員,他還是可以改變。是以一個const成員函數可以改變可變成員的值。舉個例子,給Screen 一個可變的成員,叫做access_ctr,使用這個變量來跟蹤每個成員函數被調用了多少次:

class Screen{
public:
	void some_member() const;
private:
	mutable size_t access_ctr;
};

void Screen::some_member() const{
	++access_ctr
}
           

盡管some_member是const函數,但是,他還是可以修改access_ctr的值。這個成員是可變成員,是以任何成員函數,包括const成員函數,都可以改變他的值。

類類型資料成員初始值

除了定義Screen類以外,還要定義一個視窗管理類他代表了目前顯示屏上面的Screen集合。這個管理類将會有一個Vector,它的每一個元素都是一個特定的Screen.我們想Window_mgr類一開始就有一個,預設的Screen。在c++11新标準下,最好的方式就是類内初始值(2.6.1小節):

class Window_mgr{
	private:
		std::vector<Screen> screens(Screen(24,80,’ ’));
};
           

當我們初始化類類型成員時,需要給一個符合構造函數的實參。此處,用了單個來清單初始化vector成員。這個初始值包含一個Screen對象,這個對象被傳遞到vector的構造器,然後建立含有一個Screen元素的vector。這個Screen對象有構造器建立,這個構造器需要傳遞兩個表示大小的形參和一個字元。

正如所見,内類初始值必須使用=号初始化,或者大括号初始化。

注意:當提供類内初始值時,必須跟在等号,或者大括号内部。

7.3.2 傳回*this的函數

下面增加一個函數,這個函數在給定位置處的光标設定一個字元串:

class Screen{
public:
	Screen &set(char);
	Screen & set(pos,pos,char);
};

inline Screen &Screen:set(char c){
	contents[cursor] =c;
	return *this;
}

inline Screen & Screen:sett(pos r,pos col,char ch){
	contens[r*width + col] =ch;
	return *this;
}

           

跟move函數一樣,這個函數傳回調用對象的引用。傳回引用的函數是左值,這就意味着傳回的是對象本身,而不是對象的副本。

如果将這些操作連接配接成一個表達式:

這些操作将在同一個對象上面執行。這個表達式中,首先myScreen内部的移動光标,然後将myScreen的内容設定為給定的字元。等價于下面的表達式:

myScreen.move(4,0);
myScreen.set(‘#’);
           

如果move和set的傳回值為Screen而不是Screen&.那麼上面連結在一起的表達式完全不同,此時,等價于:

Screen temp = myScreen.move(4,0);//傳回值将被複制
temp.set(‘#’);	//在myScreen的内容依然沒有改變
           

如果move的傳回值為非引用,那麼move的傳回值就是*this的副本。調用set改變的是臨時的副本,而不是myScreen。

從const成員函數傳回*this

下面将新增一個操作,這個操作用來列印Screen裡面的内容,它叫做display,我們還想這個操作能夠跟在set和move的後面,是以這個display傳回調用對象的引用。

邏輯上講,顯示screen裡面的内容不會改變對象,是以應該定義display為const成員。如果display是cont成員,那麼*this就是一個const對象。是以,display的傳回類型必須是const Sales_data&.而一旦display是傳回的const引用,那麼就不能嵌入一系列的操作中:

Screen myScreen;
myScreen.display(cout).set(‘*’);
           

盡管myScreen不是一個const對象,但是調用set是錯誤的。因為display傳回的是一個const引用,在這個const引用上面不能調用set函數。

注意:一個傳回*this的const成員函數,如果他是引用傳回類型那麼必定是const引用。

基于const的函數重載

基于是否為const可以對一個成員函數進行重載,就跟基于指針是否指向const對象的重載原因一樣。對于const對象,他的非const成員是不可見的,僅能調用const的成員函數。而對于非const對象,他既可以調用const成員,也可以調用非const成員,但是非const成員才是最佳比對。

此處将舉個例子,這個例子将定義一個叫做do_display的函數,這個函數才是實際列印Screen裡面的内容。每一個display都将調用這個函數,然後傳回調用對象的引用:

class Screen{
public:
	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;}
};

           

在任何情況下,某個類的成員函數調用另外一個成員函數,this指針是隐式的被傳遞。是以當display調用do_display時,它自己的this指針被隐式的傳遞給了do_display函數。當display的非const版本調用do_display時,它的this指針由非const轉換成了const。

當do_display執行完成,display函數通過解引用,傳回調用對象的引用。在非const版本中,this指向的是一個非const的對象,是以這個display傳回普通的引用;而const成員函數則傳回const引用。

當在一個對象上調用display時,跟着這個對象是否為const來決定按一個版本被調用。

Screen myScreen(5,3);
const Screen blank(5,3);
myScreen.set(‘#’).display(cout);//調用非const版本
blank.display(cout); //調用const版本

           

建議:對于公共代碼使用私有的功能函數

有些讀者可能非常驚訝,這裡居然分出了do_display函數。畢竟,調用do_display比将do_display寫在函數内部更複雜。我們如此做是基于下面幾個原因:

  1. 一個基本思想是避免在多個地方編寫相同的代碼
  2. 當display操作變得更加複雜時,将代碼寫在一處比寫在兩處就更加明顯了
  3. 很可能在開發階段想要增加調試資訊,而在釋出階段,需要去掉調試資訊。如果do_display隻有一處定義,那麼增加和去掉這些調試資訊就更加容易。
  4. 調用這個額外的函數并不會産生額外的負擔。因為将do_display定義在了類内部,他是隐式的内聯函數。

    事實上,一個良好設計得c++代碼,有很多形如do_display的小函數,這些函數才是真正做工作的函數。

7.3.3 類類型

每一個類定義了唯一的一種類型。兩個不同的類,就定義了兩種不同的類型,即使他們有相同的成員,例如:

struct First{
	int memi;
	int getMem();
};
struct Second{
	int memi;
	int getMem();
};

First obj1;
Second obj2 = obj1; //錯誤obj1和obj2的類型不同
           
注意:即使兩個類有完全一樣的成員,他們也是不同的類型。兩個類的成員是互不相同的。

将類名當做類型名,我們可以直接使用類類型,另外還可以在class或者struct關鍵字後面使用類名,當做類型名。

Sales_data item1;
class Sales_data item1;
           

上面兩個是等價的。第二方式源自于c,并且在c++中也是有效的。

類定義

函數的聲明和定義可以分開,同樣的類的定義和聲明也可以分開:

這種聲明有時被稱為前向聲明,引入Screen的名字到程式中,然後表明Screen是類類型。聲明之後定義之前,這個類是可見的,但是類類型Screen不是完整的類型,是以不知道這個類不含那些成員。

隻有在有限的條件下才能使用非完整類型:定義指向這個類型的指針或者引用;或者聲明(不是定義)帶有這個類型或者傳回這個類型的函數。

當編寫建立某個類對象的代碼之前,這個類必須被定義,但不一定被聲明。否則,編譯器不知道這個對象到底需要多少的存儲。同樣的,通過這個類的引用或者指針,通路成員之前,也必須定義這個類。如果累不定義,編譯器則不知道這個類有哪些成員。

隻有當這個類類型被定義之後,才能成為資料成員。唯一的一個例外在7.6小節講解。類類型必須定義完成,因為編譯器需要知道這個類的資料成員到底需要多少存儲。在類的定義體沒有完成之前,這個類是沒有定義的,是以這個類内部不能有自身類型的資料成員。但是,隻要類名可見,就可以作為聲明。是以,類内部可以有指向自身類型的成員指針或者引用。

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

7.3.4 友元在探

Sales_data定義了三個常用的非成員函數作為友元(7.2.1小節)。一個類也可以将另外一個類聲明為友元,也可以将另外一個類的成員函數聲明為友元。此外,友元函數也可以被定義在類内部,這種函數也是inline函數。

在類之間的友元

來個例子,在Window_mgt類内部有成員需要通路Screen對象的内部資料。例如,嘉定Window_mgr有個clear函數,它将一個Screen的類容清空。為了做此工作,clear需要通路Screen的私有資料資料和成員。為了允許這種通路,Screen可以将Window_mgr作為他的友元。

class Screen{
	//Windwo_mgr的成員可以通路Screen的私有資料
	friend class Window_mgr;
};
           

一個友元類的成員函數可以通路授予友元類的所有的成員,包括非公有成員。現在Window_mgr是Screen的友元了,可以寫clear成員函數了,如下:

class Window_mgr{
	public:
		using ScreenIndex = std::vector<Screen>::size_type;
		void clear(ScreenIndex);
	private:
		std::vector<Screen> screens{Screen(24,80,’ ’)};
};

void Window_mgr::clear(ScreenIndex i){
	Screen &s = screens[i];
	s.contents = string(s.height * s.width,’ ’);
}
           

定義i位置的Screen引用s。然後使用了這個Screen對象的widht和height成員來計算一個新的string,這個string有正确個數的空格字元。讓後将這個空格字元串指派給contents成員。

如果clear函數不是Screen函數的友元,,那麼這個代碼是編譯不過的。clear函數不能通路Screen對象的width,height,和contents成員。因為Screen授予了Window_mgr友元的關系,是以,Screen的所有成員對于window_mgr的成員函數來說,都是可以通路的。

注意友元不能傳遞,即,如果Window_mgr有自己的友元,那麼這些友元不能通路Screen的成員。

注意:每個類決定哪些類或者函數是它自己的友元

成員函數作為友元

Screen可以将clear成員函數指定為友元,而不是整個Window_mgr.當聲明某個成員函數為友元是,必須指定這個類的類名:

class Screen{
	frend void Window::clear(ScreenIndex);
};
           

将成員函數作為友元需要留意成員的結構,以容納聲明和定義之間的依賴。在這個例子中,必須按如下的順序進行定義:

  1. 首先,定義Window_mgr類,聲明clear,但不定義它。在clear使用Screen的成員函數之前,Screen必須被聲明。
  2. 接着,定義Screen類,包含clear的友元聲明
  3. 最後,定義clear,此時它才可以通路Screen的成員。

重載函數和友元

盡管重載函數之間名字相同,但是他們仍然是不同的函數。是以,類必須為每一個重載函數都聲明為友元:

extern std::ostream & storeOn(std::ostream &,Screen &);
extern BitMap& storeOn(BitMap &,Screen &);
class Screen{
	friend std::ostream& storeOn(std::ostream &,Screen &);
};
           

Screen類讓帶有一個ostream&的storeOn成為了友元。帶有BitMap&的storeOn則不是友元。

友元聲明和作用域

在将類和函數變成友元之前,這個類和非成員函數不需要聲明。當名字第一次出現在友元聲明中,就假定這個名字是在這個作用域内可見。然而,友元不一定真的聲明在這個作用之中。

即使定義函數在類的内部,我們也必須提供一個類外部的聲明,以保證以其可見。盡管從授權友元的類的成員函數中調用這個友元,也必須提供一個聲明。

struct X{
	friend void f() {/*友元函數可以定義在類内部*/}
	X() {f();}	//錯誤:f還沒有被聲明
	void g();
	void h();
}

void X::g(){return f();}//錯誤:f還沒有聲明
void f();
void X::h(){return f();}//正确:f的聲明在作用域中了

           
注意:記住,某些編譯器不會對友元的規則進行強制檢查(7.2.1小節)。

7.4 類作用域

每一個類都有他自己的作用域。在類作用域外,類中的普通資料和函數成員可以通過類對象,類指針,類引用和成員通路運算符來通路。通路類型成員,則是通過作用域運算符。在任何情況下,跟在運算符後面的名字必須是相應類的成員。

Screen::pos ht  = 24,wd = 80;	//使用定義在Screen内部的pos
Screen scr(ht,wd,’ ’);
Screen *p &scr;
char c= scr.get();
c = p->get();
           

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

一個類就是一個作用域,這就是為什麼在類的外部定義函數時,必須寫上類名。因為在類的外部成員函數時被隐藏的。

一旦類名可見,剩下的定義就在這個類的作用域内了,包括形參清單,函數體。是以,可以直接使用類的其他成員而不用再次寫上類名。

例如,回憶clear函數的定義。這個函數使用了Winddow_mgr裡面的類型:

void Window_mgt::clear(ScreenIndex i){
	Screen &s = screens[i];
	s.contents = string(s.height * s.width,’ ’);
}
           

因為編譯器看見形參清單在後面,是以就認為這是在Window_mgr的作用域内。此時就不用再ScreenIndex前面加上Window_mgr::了。同樣,在函數體内就直接使用了Window_mgr裡面的名字了。

另外,傳回類型通常出現在函數名之前。當成員函數定義在類外部時,任何在傳回類型使用的名字,都是在作用域之外的。是以傳回類型必須指出他是那個類的成員。例如,可以給Window_mgr一個函數,叫做addScreen,這個函數增加其他的Screen到顯式中。這個函數将傳回ScreenIndex值。

class Window_mgr{
public:
	ScreenIndex addScreen(const Screen&);
};
Window_mgr::ScreenIndex 
Window_mgr::addScreen(const Screen &s)
{
	screens.push_back(s);
	return screens.size()-1;
}
           

因為傳回類型出現在類的名字被看見之前,是以,它在類的作用域之外。為了使用ScreenIndex在傳回類型上,必選在前面寫上Window_mgr::

7.4.1 名字查找和類作用域

到目前為止,我們所寫程式的名字查找(尋找使用的名字與哪一個聲明比對的過程)都比較直截了當。

  1. 首先,在塊内尋找被使用名字的聲明,隻考慮在使用之前出現的聲明
  2. 如果沒有找到,就查找外層作用域。
  3. 如果還是沒有找到,就出錯。

對于定義在類内部的成員函數查找名字的過程跟上訴規則有點差別。但是,在這個例子中,不太明顯。類的定義分為兩部分:

  1. 首先,成員聲明被編譯
  2. 然後,整個類可見之後,函數體被編譯
注意:成員函數的編譯過程在整個成員的聲明處理過程之後。

用這種兩段方式來處理類,更加便于對類代碼進行組織。在整個類可見之前,成員函數的函數體不會被編譯,是以他們可以使用定義在類内部的任何名字。如果函數的定義和成員的聲明同時被處理,我們就不得不對成員函數進行排序,以便于能夠調用到可見的名字。

類成員聲明的名字查找

這兩階段隻應用在成員函數體中的名字。而對于聲明中的名字,包括傳回類型的名字,以及形參中的名字,都必須在使用之前可見。如果在成員聲明中使用了,還未在類内可見的名字,編譯器則在這個類定義的作用域中尋找這個名字,例如:

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

當編譯器看見balance函數的聲明時,他将在Account裡面尋找Money的聲明,并且隻會考慮在類内部且出現在Money使用之前的聲明。因為沒有找到,編譯器繼續尋找外層作用域中的聲明。在這個例子中,編譯器将找到Money的typedef。是以這個類型将被用在balance的傳回類型和成員bal的聲明上。另一方面balance函數體的處理在整個類可見之後。是以在函數體内的傳回值是叫做bal的成員函數,而不是外部作用域的string變量。

類型名要特殊處理

通常,内部作用域可以重新定義來自于外部作用域的名字,就算這個名字已經在内部作用域内被使用了。但是,在類内,如果成員使用了來自于外部作用域的名字,并且這個名字是某種類型,那麼這個類在後面就不能重新定義這個名字:

typedef double Money;
class Account{
public:
	Money balance(){return bal;}
private:
	typedef double Money;//錯誤:不能重新定義Money
	Money bal;
};
           

值得注意的是:盡管在Account内部對Money的定義與外層一緻,但是這個代碼也是錯誤的。

盡管重新定義名字是錯誤的,編譯器也不強制要求診斷這種錯誤。即使程式是錯誤的,一些編譯器仍然接受這種代碼。

提示:類型名的定義通常應該出現在類的開始。通過這種方式,任何使用這種類型的成員都是在類型名的定義之後。

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

在成員函數體内部的名字,将按照如下進行處理:

•首先,在成員函數内尋找這個名字的聲明。通常隻有在函數内部并且在使用之前的聲明才會被考慮。

•如果在函數成員内部沒有找到聲明,就在類的作用域内尋找。類中的所有成員都會被考慮。

•如果在類内還是沒有找到,則在函數成員定義之前的作用域内尋找。

通常,成員函數的形參名與另外一個資料成員名字相同,是非常不好的習慣。但是,為了展示名字如何被查找的,我們将在dummy_fcn函數中違法這個約定;

//注意:這個代碼僅僅是為了說明,這是一種非常不好的程式設計習慣

//将形參名與成員名,命名相同是非常不好的習慣

int height;
class Screen{
public:
	typedef std::string::size_type pos;
	void dummy_fcn(pos height){
		cursor = width * height;
}
private:
	pos cursor = 0;
	pos height = 0,width = 0;
};
           

當編譯器處理dummy_fcn裡面的乘法時,它首先尋找這個表達式所在作用域中的名字。函數的形參名也在這個作用域内。是以,height,被認為是形參。

此種情況,height形參隐藏了height成員。如果想要改變這個隐藏,可以如下操作:

void Screen::dummy_fcn(pos height){
	cursor = width * this->height;
	//另外一種寫法
	cursor = width * Screen::height;
}
           
注意:盡管類成員被隐藏了,但是通過使用this指針,或者類名,他仍然可以是被使用

一個更好的方式是,保證成員height跟形參的名字不一樣:

//好的程式設計習慣:不要将形參或者局部變量的名字跟成員名字相同

void Screen::dummy_fcn(){
	cursor = width * height;
}
           

此種情況下,當編譯器尋找height時,不會在dummy_fcn中找到。編譯器将會在Screen中進行尋找。盡管height的聲明出現在dummy_fcn中的使用之後,但是編譯器仍然能正确的找到正确的height,即Screen中的height資料成員。

類作用域之後,尋找外層作用域

如果編譯器在函數或者類作用域中沒有找到,它将繼續在外層作用域中尋找。在我們例子中,height被定義在外層作用域中,并且出現在Screen的定義之前。如果我們想通路外層作用域中的名字,可以通過使用作用域運算符:

//不推薦使用:不要隐藏外層作用域中需要用到的名字
void Screen::dummy_fcn(pos height){
	cursor = width * ::height;	//哪一個height?最外層那個
}
           
注意:盡管外層對象被隐藏了,仍然可以使用作用域運算符來通路。

在檔案中名字的出現初進行處理

當成員定義在類的外部時,名字尋找的第三步包括:成員定義作用域中的尋找和類定義作用域中的尋找。例如:

int height;//定義了一個在後續Screen類中會被使用的變量
class Screen{
public:
	typedef std::string::size pos;
	void setHeight(pos);
	pos height =0;	//隐藏了外部作用域中的聲明
};
Screen::pos verify(Screen::pos);
void Screen::setHeight(pos var){
	//var:指向形參
	//height:指向類成員
	//verify:指向全局函數
	height = veriry(var);
}
           

注意在Screen類定義之前,verify函數不可見。但是名字尋找的第三步包含成員定義作用域中尋找。此例中,對于verify的聲明出現在setHeight的定義之前,是以可以被找到,病被使用。

7.5 構造器再探

構造器是任何類的重要部分。在7.1.4小節中介紹了構造器的基本知識。本段将介紹構造器的另外的能力,并且對以前的内容更進一步的了解。

7.5.1 構造器初始化清單

當定義變量時,通常馬上就會對其進行初始化,而不是先定義然後再指派:

string  foo = “Hello World”;//定義并且初始化
string bar;		//預設初始化一個空的字元串
bar = “Hello World”;//指派一個新的值給bar
           

初始化和指派的差別,也同樣适用于對象成員的初始化和指派。如果沒有在構造器初始化清單中顯示的初始化成員,這個成員就會在構造器函數體執行之前,執行預設初始化。例如:

//合法,但是比較草率,因為沒有使用構造器初始清單
Sales_data::Sales_data(const string &s,unsigned cnt,double price){
	bookNo = s;
	units_sold = cnt;
	revenue = cnt * price;
}
           

這個版本和以前的定義效果相同:當構造器結束時,資料成員儲存有相同的值。唯一的不同就是:原始版本對成員進行初始化,這個版本對成員進行指派。這兩種操作到底有多麼大的影響,完全取決于具體的類型。

構造函數初始值有時是必須的

我們有時常常(不是總是)忽視成員是該初始化還是該指派。const或者引用成員必須被初始化。同樣的,當類類型成員沒有預設構造函數時,也必須初始化。例如:

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

跟其他conost對象和引用一樣,ci和ri必須被初始化。是以,對這些成員省略構造器初始值是錯誤的

ConstRef::ConstRef(int ii){
	I == ii;	//指派:正确
	ci = ii;	//指派:錯誤,不能指派給const對象
	ri = I;	//錯誤:ri沒被初始化
}
           

在構造函數函數體執行之前,初始化已經完成。唯一能夠初始化const對象和引用資料成員的方式是構造函數初始值。這個構造函數正确的寫法是:

//正确:顯式的初始化引用和const成員

ConstRef::ConstRef(int ii):i(ii),ci(ii),ri(i){}
           
注意:必須對const成員,引用成員,以及沒有預設構造器的類類型成員,用構造器初始值清單進行初始化。

建議:使用構造器初始值

在許多類中,初始化和指派之間的差別是一個非常關乎底層效率的事情:前者直接初始化,後者先初始化再指派。

比效率更重要的是:有些資料隻能初始化,不能指派。使用構造初始值,可以避免有些編譯錯誤,尤其是在某些類成員必須要構造初始值的情況。

成員初始化的順序

不要驚訝,在構造器初始值清單中每個名字隻能出現一次。否則,就意味着要給要給一個成員初始化多個值。

讓人更感驚訝的是:構造器初始值清單隻指定了初始值,不會指定初始化的順序。

成員初始化的順序依賴于成員定義的順序:第一個成員首先被初始化,接着是第二個,以此類推。在構造函數初始值中出現的順序不會改變成員初始化的順序。

初始化的順序通常沒有什麼大礙。但是,如果一個成員由另外一個初始化,那麼成員初始化的順序就非常重要了。

舉個例子,思考下面的代碼:

class X{
	int i;
	int j;
pulic	:
	//未定義:i在j之前初始化
	X(int val):j(val):i(j){}
};
           

此例中,構造函數的寫法,好像就是j由val初始化,然後j再去初始化i。但是,事實上是,i先被初始化。這種初始化的影響是:初始化i的初始值,即j的值,此時是未定義的。

有些編譯器足夠友好,如果構造器初始值的順序和聲明的順序不一樣,則會給出警告。

經驗之談

構造函數初始值清單建議跟成員的聲明順序一樣。同時,避免使用一個成員去初始化另外一個成員

如果有可能,使用構造函數形參初始化成員,而不是另外一個成員來初始化成員。這樣就可以避免成員初始化的順序了。例如,對于X的構造器,下面的寫法可能更好:

在這個版本中,i和j被初始化的順序根本不用關心。

預設實參和構造器

Sales_data的預設構造的行為與帶有一個string實參的構造器行為類似。唯一的差別是:帶有string實參的構造器使用這個string初始化bookNo。而預設構造器使用string的預設初始化去初始化bookNo.通過帶有預設實參,可以重寫這些構造器:

class Sales_data{
public:
	//定義預設構造器,也是一個帶有string實參的構造器
	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)}
	//剩下的成員跟以前一樣
};
           

類的這個版本提供了跟以前版本一樣的接口。當不給參數,或者給定一個string參數時,建立的對象都一樣。因為可以不帶參數調用這個構造器,是以相當于定義了一個預設的構造器。

注意:一個構造器為所有的形參都定義了預設實參,也相當于定義了預設構造器。

值得注意的是:不應為Sales_data帶有三個形參的構造器提供預設實參。因為如果用于提供了一個非零的售賣數,那麼就應該提供這本書被賣的價格。

7.5.2 委托構造函數

c++11新标準擴充了構造函數初始值的功能,可以讓我們定義所謂的預設構造函數。委托構造器使用它自己類的其他構造函數執行它自己的初始化。之是以成為委托是因為他将工作的一些交給了其他構造器。

跟其他構造器一樣,委托構造器也有成員初始值清單以及函數體。在委托構造器中,成員初始值清單,有唯一的一個入口,就是類名本身。跟其他的成員初始值一樣,類名後面跟着小括号括起的實參。這個實參必須比對類中的某個構造器。

例如,使用委托構造函數重寫Sales_data類:

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);}
	//其他成員跟以前一樣
};
           

這個版本中,除了一個,其他都委托他們自己的工作。第一個構造器帶有三個形參,使用者三個形參初始化了資料成員,并且沒有做進一步的工作。在這個版本中,定義了預設構造器,預設構造器使用帶有三個實參的構造器來進行初始化。正如空函數表明的那樣,它也沒有做進一步的工作。帶有一個string的構造器也委托給了三個參數的構造器。

帶有一個istream&的構造器也委托了。它委托給了預設構造器,最終委托到了三個實參的構造器。一旦這些構造器完成了他們的工作,istream&版本的構造器的函數體就開始執行。它的函數體調用read從給定的istream中讀資料。

當一個構造器委托給另外一個構造器時,受委托構造器的初始值清單和函數都會被執行。在Sales_data中,受委托的構造器函數體恰巧為空。如果函數體包含代碼,這些代碼會先執行,然後才是将控制權傳回給委托者的函數體。

7.5.3 預設構造函數的作用

對一個對象是預設初始化或者值初始化時,預設構造函數自動的被使用。預設初始化發生在如下情況:

•在塊作用域中定義非靜态變量或者數組,而不帶初始值時

•在帶有類類型成員的來中使用了合成的預設構造器時

•在類類型成員沒有在構造器初始值清單中顯式初始化時

值初始化發生在如下情況

•在定義一個靜态對象而沒有帶初始值時

•在書寫形如T()這樣的表達式,來請求值初始化時。其中T是類型名(vector的帶有一個實參的構造器,用于說明這個vector的大小。此時就是使用的這種方式對其元素進行值初始化)

類要有一個預設構造器就是為了在這種情況中使用,大多數情況都非常好判斷。

不好判斷的是,類的某些資料成員缺少預設構造函數:

class NoDefault{
public:
	NoDefault(const std::string);
};

struct A{
	NoDefault my_mem;
};

A a;	//錯誤:不能為A合成一個預設的構造器
struct B{
	B(){}		//錯誤:b_member沒有初始值
	NoDefault b_member;
};

           

經驗之談:

事實上,如果有其他構造器,提供一個預設構造器總沒有錯。

使用預設構造器

下面的對于obj的聲明不會問題,但是當嘗試使用obj時:

Sales_data obj();
if(obj.isbn() == Primer_5th_en.isbn()) //錯誤obj是一個函數。
           

編譯器出錯,報:不能對函數進行成員通路。問題就來了:我們其實想聲明一個預設初始化的對象obj,但是實際上聲明了一個函數,這個函數沒有形參,并且傳回一個Sales_data類型的對象。

使用預設構造器定義一個對象的正确方式是,丢掉後面空的小括号:

//正确:obj是預設初始化的對象
Sales_data obj;
           
警告:對于新手c++程式員來說,使用如下的預設構造聲明對象,常常是錯誤的:
Sales_data obj();	//oops!  聲明了一個函數,不是對象
Sales_data oboj2;	//正确: obj2是一個對象,不是函數
           

7.5.4 隐式的類型轉換

正如在4.11小節中介紹的那樣,c++對于内置類型定義了幾種自動轉換規則。我們也注意到類也有類似的隐式轉換。如果一個構造器帶有一個實參,那麼這個構造器就定義了一種轉換到此類類型的隐式規則,這種構造函數有時也稱為轉換構造器。我們将在14.9小節,介紹如何定義從一個類到另外一個類的轉換規則。

注意:可以通過單個實參調用的構造器定義了一個隐含的從構造器形參類型轉換成類類型的轉換規則。

Sales_data的構造器帶有一個string,還有一個構造器帶有一個istream。他們都定義了從這些類型到Sales_data類型的轉換規則。即,在使用Sales_data的地方都可以使用string和istream。

string  null_book = “9-999-9999-9”;
//構造了一個臨時的Sales_data對象
//這個對象的units_sold和revenue等于0,并且bookNo等于null_book
item.combine(null_book);
           

此處調用Sales_data 的combine函數,傳遞了一個string實參。這個調用完全是合法的。編譯器根據string自動建立Sales_data對象。這個新建立的Sales_data對象,被傳遞到coimbine函數。因為combine的形參是const,是以我們可以傳遞這個臨時對象給形參。

僅僅隻有一步類類型可以被轉換

在4.11.2小節中,我們注意到編譯器會自動的進行類類型轉換。例如,下面的代碼是錯誤的,因為他們隐式的轉換了

//錯誤:需要進行兩次轉換
//(1) 将 “9-999-99999-9” 變成字元串
//(2) 将這個臨時的string對象轉換成Sales_data
item.combine(“9-999-99999-9”);
           

如果我們想這種調用,我們必須顯式的将字元串轉變成string,或者Sales_data.

兩次:

//正确:顯式轉換為string,隐式的轉換為Sales_data
item.combine(string(“9-999-99999-9”));
//正确:隐式轉換為string,顯式轉換為Sales_data
item.combine(Sales_data(“9-999-99999-9”));
           

類類型轉換不總是有效的

是否期望從string到Sales_data的轉換,依賴于我們對使用者使用這種轉換的看法。在此例中,它可能是正确的。在null_book的string可能就代表一個不存在的ISBN号。

更加有問題的是從istream到Sales_data的轉換:

//使用istream的構造器,建立一個對象,然後傳遞給combine
item.combin(cin);
           

這段代碼隐式将cin轉換成Sales_data.這個轉換執行了帶有一個istream的構造器。這個構造器從标準輸入中讀入資料,然後建立一個臨時的Sales_data對象。這個對象然後傳遞給combine函數。

這個Sales_data是臨時,無法再combine函數結束之後通路他。事實上,我們建立了一個對象,然後在這個對象被加到item之後,就抛棄了這個對象。

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

通過在構造函數中聲明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 &);
	//剩下的跟以前一樣
};
           

現在,構造器不能被用在隐式的建立Sales_data對象中了。那麼前面的使用就是錯誤的:

item.combine(null_book);//錯誤:形參為string的構造器時explicit的
item.combine(cin);//錯誤:形參為istream的構造是explicit的
           

explicit隻有在構造器有單個形參的時候才有意義。因為有多個形參的構造器不能用作用作隐式轉換,是以沒有必要為這種構造器聲明為explicit.這個關鍵字僅被用在類内部的構造器聲明中,不能再類外部的構造器定義中,再次書寫:

//錯誤:explicit僅允許在類内部的構造器聲明處
explicit Sales_data::Sales_data(istream & is){
	read(is, *this);
}
           

explicit構造器僅可以被用來直接初始化

隐式轉換的發生的場景是:使用複制形式的初始化(使用=)(3.2.1小節)。這種形式的初始化不能使用explicit構造器,但是我們可以使用這種構造器進行直接初始化:

Sales_data item(null_book);//正确,直接初始化
//錯誤:不能使用複制形式的初始化,因為構造器時explicit的
Sales_data item2 = null_book;
           
注意:當一個構造器聲明為explicit時,它僅能用作直接初始化(3.2.1)。而且編譯器不會使用這個構造器進行自動轉換。

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

盡管編譯器不能使用explicit構造器進行隐式轉換,但是我們可以使用這個構造器進行顯式的強制轉換:

//正确:實參被形式強制轉換為Sales_data對象
item.combine(Sales_data(null_book));
//正确:static_cast 可以使用explicit構造器
item.combine(static_cast<Sales_data>(cin));
           

在第一個調用中,我們直接使用了Sales_data構造器。這個調用使用構造器建立了一個臨時的Sales_data對象。在第二個調用中,我們使用了static_cast來執行一個顯式的轉換。在這個調用中,static_const使用了帶有istream的構造器建立了一個臨時的Sales_data對象。

帶有explicit構造器的标準庫類

一些我們已經使用過的庫類,帶有一個單形參的構造器:

•帶有單個const char*的string構造器,不是explicit的

•帶有一個大小的vector構造器,是explicit的

7.5.5 聚合類

聚合類提供了直接通路成員的功能并且尤其特殊的文法。聚合類滿足如下條件:

• 所有的資料成員都是public的

• 它沒有定義任何構造器

• 它沒有類内初始值

• 它沒有基類或者虛拟函數,虛拟函數是跟類相關的特性,我們将在15章介紹。

例如,下面的類就是一個聚合類

struct Data{
	int ival;
	string s;
};
           

可以通過大括号将資料成員的初始值括起來,然後進行初始化:

//val.ival = 0; val1.s = string(“Anna”)
Data val1 = {0,”Anna”};
           

初始值順序,必須于資料成員的聲明順序一緻。即,第一個成員用第一個初始值進行初始化,第二個成員使用第二個初始值進行初始化,以此類推。下面是一個錯誤的例子:

//錯誤:不能使用“Anna”初始化ival,也不能使用1024初始化s

Data val2  ={“Anna”,1024};
           

跟數組的初始化一樣,如果初始化清單裡面的值少于類的成員,那麼剩下的成員就執行值初始化。初始值清單裡面的值個數不能多于類的成員個數。

值得注意的是:顯式的初始化類對象的成員,有三個明顯的弊端

• 它要求所有的成員都必須是public

• 将初始化的負擔交給了使用者代碼,這種初始化更容易出錯。因為使用者可能忘記初始化或者提供一個不正确的初始值。

• 如果一個成員增加或移出了,所有的初始化都必須進行更新。

7.5.6 字面量類

在6.5.2小節中,constexpr的形參和傳回類型必須是字面量。除了算數類型,引用,和指針以外,某些類也是字面量。跟其他字面量不同,字面量類可以有constexpr的函數成員。這種函數成員必須滿足所有的constexpr函數的要求,他是隐式的const成員(7.1.2小節)。

當一個聚合類的所有資料成員都是字面量類型是,這個類就是字面量類。如果一個類不是聚合類,如果他滿足下面的條件,那麼他也是字面量類:

•所有資料成員都必須是字面量類型

•類至少要有一個constexpr的構造器

•如果資料成員有類内初始值,當資料成員是内置類型時,這個成員的初始值必須是常量表達式;如果資料成員是類類型,這個成員的初始值必須是這個成員類型的constexpr構造器。

constexpr 構造函數

盡管構造器不能是const,但是對于字面量類來說,構造可以是constexpr函數。事實上,一個字面量類必須提供至少一個的constexpr構造器。

constexpr構造器可以被聲明為=default的形式(或者删除函數的形式,我們将在13.1.6節中介紹相關知識)。否則constexpr構造器必須要滿足構造器的要求——意味着他沒有傳回語句,又要滿足constexpr函數的要求——意味着僅能在return語句中執行相應的邏輯。是以,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){hw = b;}
private:
	bool hw;  //硬體錯誤,而不是io錯誤
	bool io;	//io錯誤
	bool other; //其他錯誤
};
           

constexpr構造器必須初始化所有的資料成員。這些初始值必須使用constexpr構造器或者常量表達式。

constexpr構造器被用來建立一個constexpr的對象以及用在constpexr函數的形參和傳回類型上面。

constexpr Debug io_sub(false,true,false);
if(io_sub.any())
	cerr << “print appropriate error message” << endl;
constexpr Debug prod(false);
if(prod.any())
	cerr << “print an error message” << endl;
           

7.6 static 的類成員

類有時需要跟類相關的成員,而不是跟這個類對象相關的成員。例如,銀行賬戶類可能需要一個資料成員,用來表示目前的網速。此時,我們想這個網速跟類相關,而不是跟每個每個分開的對象相關。從效率上來講,沒必要為每個對象都存儲一個網速。更重要的是,如果網速改變,每個對象都得使用這個新值。

聲明static成員

要想将一個成員與類相關,就在成員的聲明前加上關鍵字static。跟其他的成員一樣,static成員可以是public也可以是private。static成員的類型可以是const,引用,數組,類類型等。

舉個例子,定義一個代表賬戶記錄的類:

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

類的static成員存在于任何對象的外部。對象不會包含static資料成員。是以,每一個Account對象隻包含兩個資料成員owner和amount。此處隻有一個interestRate對象是被所有的Account對象共享的。

同樣的,static成員函數沒有跟任何的對象綁定;他們沒有this指針。是以,static成員函數不能聲明為const,也不能使用this指針。這個即限制了在函數體内顯式的使用this,也限制了隐式使用this去調用非static成員。

使用類的static成員

通路static成員,可直接通過作用域運算符通路:

double r;
r = Account::rate();	//通路靜态成員
           

盡管靜态成員不是對象的一部分,但是也可以通過對象的引用,指針等通路到static成員:

Account ac1;
Account *ac2 = &ac1;
r = ac1.rate();
r = ac2->rate();
           

成員函數可以直接使用static成員,不用加上作用域運算符:

class Account{
public:
	void calculate(){amount += amount * interestRate;}
private:
	static double interestRatee;
	//剩下的和以前一樣
};

           

定義static成員

跟其他的成員函數一樣,可以在類的内部或者外部定義static成員函數。當定義static函數在類的外部時,就不要重複寫static關鍵字了。這個關鍵字僅出現在類内部的聲明中。

void Account ::rate(double newRate){
	interestRate = newRate;
}
           
注意:跟其他的類成員一樣,當要定義類的static成員在類的的外部時,也必須指出這個成員所在的類。statci關鍵字,僅被用在類内的成員聲明中。

因為static資料成員不是類對象的一部分,是以當建立對象的時候,他們是為定義的。是以,他們不是在構造器中進行初始化的。并且,通常情況下,我們不能對類内的static成員進行初始化。相反,我們必須在類的外部定義和初始化static成員。跟其他的對象一樣,static資料成員隻能被定義一次。

跟全局對象一樣,static資料成員被定義在任何函數之外。是以,隻要他們已定義,他們就一直存在,直到程式結束。

定義static成員函數,跟定義類外部的成員函數一樣。首先是對象的類型,然後是類名,跟上作用域,接着是成員自己的名字:

//定義并初始化一個static成員

double Account::interestRate = initRate();
           

這個語句定義了一個叫做interestRate的對象,這個對象是Account的靜态成員,它的類型為double。一旦類名被看見,後面的定義都是在類的作用域中。是以,可以直接使用initRate,而不用前面加上Account::。還需要注意的是:盡管initRate是private的,但是我們還是可以使用這個函數去初始化interestRate. interestRate的定義跟其他的成員定義一樣,可以通路類的private的成員。

提示:保證對象被定義一次的方法是:将static資料成員的定義跟其他的非inline函數的定義放在同一個檔案中。

static成員的類内初始化

通常,static成員不能在類内進行初始化,但是,對于static成員為constexpr類型或者const類型,可以為其提供一個類内的初始值。初始值必須是常量表達式。這種成員本身就是常量表達式,他們可以用在任何需要常量表達式的地方。例如,可以使用一個static成員來表示一個數組的次元:

class Account{
public:
	static double rate(){return interestRate;}
	static void rate(double);
private:
	static constexpr int period = 30;	//period是一個常量表達式
	double daily_tbl[period];
};
           

如果某個static成員的引用場景僅僅限于編譯器可以替換他的值,那麼一個初始化的const或者constexpr的static成員不需要分開定義。但是,如果将其用于值不能替換的場景中,這些成員就必須要有一條定義。

例如,如果僅使用period來定義daily_tbl的次元,那麼沒必要在Account的外部定義period。但是,如果我們省略了定義,那麼程式微小的改變也可能引起編譯錯誤,因為沒有定義。例如,當我們将Account::period傳遞到一個函數中,這個函數的形參為const int&,此時period必須被定義。

如果在類内初始值被提供了,那麼成員的定義就不能提供一個初始值。

經驗之談:

即使const static在類内部初始化了,這個成員也應該在類的外部定義一下

static成員可以用在普通成員不能應用的場景

正如所見,static成員的存在是獨立于其他對象的。是以,static成員可以用在,普通成員不能用的地方。例如,static成員可以是不完整的類型(7.3.3)。尤其是,static成員類型可以為所在類的類型。而非static成員,隻能為所在類類型的指針,或者引用。

class Bar{
public:
	//。。。
private:
	static Bar mem1;//正确:static成員可以是不完整的類型
	Bar *mem2;//正确:指針成員可以是不完整的類型
	Bar mem3;//錯誤:資料成員必須是完整的類型
};
           

另外一個普通成員與static成員不同的是:可以使用static成員作為預設的實參。

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

一個非static資料成員,不能作為預設實參,因為他是對象的一部分。使用非static成員作為預設實參,相當于沒有提供對象,從這個對象中擷取這個成員的值,是以是錯誤的。

本章小結(譯略)

術語(譯略)

難免錯誤,望指正