天天看點

c++ primer 第五版 翻譯 第二章2.1 基本内置類型2.5 處理類型

第二章 變量和基本類型

内容

2.1 基本的内置類型

2.2 變量

2.3 複合類型

2.4 const限定符

2.5 類型處理

2.6 自定義資料結構 小結 專業術語

類型是一門語言的基礎:他表示資料的意義,以及作用在這些資料上面的操作。

C++有廣泛的類型支援。它定義了幾種基本内置類型(字元,整型,浮點數等)并且提供了自定義類型的機制。一些庫使用這種機制定義了更加複雜的類型,例如可變長度的字元串-----string,vector等等。本章主要介紹基本内置類型,并開始展示,c++是如何支援複合類型的。

類型決定了資料的意義和操作。i = i + j;這個語句的意義依賴于i和j的類型。

如果i和j都是整型,則這個語句就是常見的算數加法。如果i和j是Sales_item類型,這個語句就是将兩個對象的各個組成成員相加。

2.1 基本内置類型

c++定義了一些列的基本内置類型(本書中也叫做基本類型,或者内置類型),包括算數類型和一個特殊的void類型。算數類型有:字元型,整型,布爾型,浮點型。void類型沒有對應的值,他隻能被使用在一些少數情況下。最常見的情況就是一個函數的傳回類型,這種void傳回類型代表這個函數不傳回值。

2.1.1 算數類型

算數類型分為兩類:整數類型和浮點類型.算數類型的大小(即bit位的數量)依據平台的不同而不同.c++标準隻保證類型的最少比特位,如下表.但是編譯器也被允許使用比标準大的比特位的類型.某一種類型的bit位不同,它所表示的最大值或者最小值也不一樣.

c++ primer 第五版 翻譯 第二章2.1 基本内置類型2.5 處理類型

bool類型(又叫布爾類型),代表了true或false.

c++提供了幾種字元類型.他們中的一部分是為了支援國際化而設計的.主要的字元類型是char.char的設計是為了:有足夠的空間存放機器的基本字元集.是以.char和一個機器位元組大小相同.

剩下的字元類型有:wchar_t,char16_t,char32_t.這些字元類型用于擴充字元集.wchar_t類型的設計是為了:有足夠的空間來儲存機器的最大字元集.而char16_t和char32_t用來儲存unicode字元集(Unicode是所有自然語言的一種表示标準)

剩下的整型,代表了不同大小的整數.c++語言規定:int至少和short大小一樣.long至少和int大小一樣,long long至少和long大小一樣.long long類型是c++11新标準中引入的一種新類型.

内置類型的機器級表示 計算機存儲一些列的bit位,每個bit位隻能儲存0或者1.例如:

00011011011100010110010000111011 …

大多數的計算機,将記憶體按塊來處理,塊的大小是2的n次方.最小可尋址的記憶體塊被稱作位元組(byte).最小的存儲單元(通常為幾個位元組),被稱為字(word).在c++裡面,一個位元組至少要能儲存一個基本字元.在大多數的機器中,一個位元組包含8位.一個字為32位,或者64位,即,4個或者8個位元組.

對于大多數的電腦,在記憶體中的每個位元組都有一個數字,這個數字叫做位址.在一台8位為一位元組,32位為一字的機器中,我們可以看到下面的記憶體,這個記憶體表示的是一個字的大小:

c++ primer 第五版 翻譯 第二章2.1 基本内置類型2.5 處理類型

此處,位元組的位址在左邊.

我們可以使用位址來表示,從這個位址開始的,但是不相同的幾個資料.例如,我們可以說位址736424的那個字或者位址736424的那個位元組.為了給記憶體中的某個位址賦予一個含義.我們必須知道存儲在這個位址的類型.類型決定了有多少bit位被使用,以及怎麼來解釋這些bit位.

如果一個對象在736424位址處,并且類型為float,如果float在這個機器上以32位儲存.那麼我們就知道這個對象從736424位址開始,跨越了整整一個字的長度.float類型的值,依賴于機器如何存儲浮點數.

如果,在736424位址處的對象為unsinged

char類型.并且這台機器使用了ISO-Latine-1字元集,那麼在這個位址處的位元組就是一個分号.

浮點類型可以表示:單精度,雙精度,高精度.c++标準制定了浮點類型的最小有效數,然而大多數的編譯器都提供了比标準更高精度的浮點類型.通常,float類型大小為一個字(32位),double類型為兩個字(64位).long double 類型為三個字或者四個字(96位或者128位).float和double類型分别有7和16個有效位.long double常用在有特殊用途的浮點硬體上,它的精度依賴于它的具體實作.

有符号類型和無符号類型

除了bool類型和擴充的字元類型以外,其他類型還區分:有符号和無符号.有符号類型可以代表正數,負數,零。無符号類型隻能表示大于等于0的數。

類型int,short,long和long long都是有符号的。如果我們要得到對應的無符号的類型,可以在前面加上unsigned.例如:unsigned long.另外,對于類型unsigned int可以縮寫為unsigned.

與其他的整數類型不同的是,c++還定義了三個不同的字元類型:char,signed char和unsigned char.尤其的是:char類型。它和signed char類型不一樣。盡管此處有三種char類型,但是隻有兩種表現形式:有符号和無符号。char類型到底是有符号還是無符号,取決于編譯器的實作。

對于一個無符号的類型來說,所有的bit位都用來存儲值。例如,一個8位的unsigned char類型,它可以儲存0-255.

c++标準并沒有規定有符号類型應該怎麼表示,但是它規定了在表示範圍内的正負值應該平均配置設定。是以,一個8位的signed char類型應該保證存儲-127到127的數。大多數的現代機器都使用了一種-128到127的表示法。

建議:決定使用哪種類型

像c一樣,c++被設計得更接近硬體。算數類型被設計來滿足各種各樣的硬體,是以算數類型的種類多得讓人有點困惑。大多數的程式員可以通過限制使用的類型來忽略這種複雜性。下面的一些經驗對于決定使用哪種類型有一定的幫助:

1. 當你知道值不會為負數的時候,可以使用一個無符号的類型。

2. 對于整數的操作使用int。在實際使用中,short太小,而 long的大小一般和int一樣。如果你的值超出了int的表示範圍,那麼就使用long long

3. 不要在算數表達式中使用char類型或者bool類型。這兩種類型隻有在儲存字元和真假值的時候使用。計算機使用char類型特别容易出錯,因為char類型在一些機器上面是有符号的,而在兩外一些機器上面是無符号的。如果你需要一個最小的整數,應該明确的聲明為有符号或者無符号。

4. 對于浮點運算使用double類型,因為float類型通常精度不夠,并且double類型的浮點精度提高産生的性能消耗可以忽略不計。事實上,在某些機器上,雙精度浮點會更快一些。long

double的精度通常沒有必要,并且帶來的運作時消耗是不可忽略的。

2.1.2 類型轉換

一個對象的類型包含了這個對象的值和這個對象的可做的操作。許多類型支援将一種類型轉變成另外一種類型。

當我們使用了一種類型,而此處更希望是另外一種類型,那麼類型轉換将會自動發生。在4.11小節,我們還會讨論更多的類型轉換。本節主要是讨論:當我們給一個類型指派另外一種類型時的類型轉換。

當我們給一種算數類型指派其他算數類型時,如下:

bool b =42;		//b is true
int i = b ;			//i 得值為1
i = 3.14;			//i的值為3
double pi = i;		//pi的值為3.0
unsigned char c = -1 //假設為8位的char類型,c的值為255
signed char c2 = 256 //假設為8位的char類型,c2的值不能确定
           

對于上面的具體細節,依賴于類型能夠儲存的值的範圍:

1. 當我們給一個bool類型指派一個非bool類型的時候,結果就是:如果是0,就是false,非0,就是true。

2. 當我們将一個bool類型指派給其他的算數類型的時候,結果就是:如果bool是true,那麼就是1;如果是false,就是0。

3. 當我們将一個浮點數指派給一個整型類型的時候,浮點值将會被截斷。被存儲的值僅僅隻有小數點前面的整數部分。

4. 當我們将一個整型值指派給一個浮點類型的時候,小數部分将會是0.如果整數的bit位已經超過了浮點數的容納能力,那麼可能會造成精度丢失。

5. 如果我們給一個無符号類型,指派一個超出了它的範圍的值,結果就是:對目标類型可以儲存的個數取模之後的餘數(也将這種情況成為值環繞)。例如,一個8位的unsigned

char類型,可以儲存0-255的值。如果我們指派超出了這個範圍,那麼就是對256取模,然後用餘數。是以将-1指派給一個8位的unsigned

char類型,将會得到255

6. 如果給一個有符号的類型,指派一個超出它範圍的值,結果是未知的(又時也翻譯為未定義的),即不清楚會是什麼樣的結果。程式可能工作,也可能崩潰,或者産生垃圾值。

建議:避免使用未知結果和依賴于機器的實作

未知結果的行為源于編譯器不需要去檢查的錯誤(有時候是不能被檢查).即使代碼編譯通過,還是可能在程式執行的時候出錯.

不幸的是,包含未知結果的程式在某些編譯器下也能夠執行成功,但是卻不能保證在其他的編譯器下也能夠執行成功,甚至不能保證,在相同編譯器下再次編譯也能執行成功.此外也不能保證,程式對一組輸入有效的情況下,對另外的輸入也有效.

同樣的,程式應該避免使用依賴于實作的行為.例如假定int的大小是固定的,那麼這種程式就不能認為是可移植的.當程式移植到其他的機器上的時候,依賴于實作的代碼可能會運作失敗.從先前的代碼中跟蹤這種錯誤,是一件令人不爽的事.

當我們使用一種類型,但是此時更期望其他類型的時候,編譯器運用這些類型轉換。例如,當我們使用一個非bool類型作為條件的時候,算數類型将會轉換成bool類型,轉換的規則與指派給bool類型一樣。

int i=42;
if(i)  //條件将會轉成true
		 i=0;
           

如果值為0,那麼條件将是false,否則,就是true。

同樣的道理,當我們在算數表達式中,使用bool類型的時候,他的值總是轉換成0或者1.

是以,在算數表達式中使用bool類型是不推薦的。

涉及無符号的表達式

盡管我們不可能有意的将一個負數指派給一個無符号的對象,但是,我們寫出的一些代碼,可能隐式的進行了這種指派。例如,當我們在算數表達式中使用了unsigned int 和int的時候,通常int會被轉換成unsigned int.轉換的規則就如同将int值指派給unsigned int.

unsigned u = 10;
int i = -42;
std::cout <<  i+ i << std::endl; 	//prints -84;
std::cout << i+u<< std::endl;	// 如果int為32位,列印:4294967264
           

在第一個表達式中,我們将兩個int值相加,得到了我們期望的結果。

在第二個表達式中,相加之前,-42的int值會被轉換成unsigned int然後再相加。将一個負數值轉成一個unsigned int類型,就好像,我們将一個負數值指派給一個unsigned int類型,結果就是,如同上面講的:值環繞——即将負值與unsigned int的模相加。

如果我們從一個無符号中減去一個值,不管操作數是否為無符号,我們必須保證結果不能為負數。

unsigned u1 = 42 ,u2 = 10;
std::cout << u1 - u2 <<  std::endl ;//ok:結果為32
std::cout << u2-u1 << std::endl;//ok:但是結果将會發生值環繞——即負值,加上無符号的模
           

一個無符号不能小于零,這個事實也影響了我們寫循環的方式。例如,在1.4.1的練習中,通過遞減10到0來寫循環。可能會寫成下面這樣:

for(int i=10;i>=0;--i)
	std::cout << i << std::endl;
           

寫完之後,我們發現,我們不需要列印負值,是以使用了unsigned int來重寫這個循環。那麼,你将永遠不會結束這個循環。

for (unsigned u = 10;u>=0;--u)
	std::cout << u << std::endl;
           

思考:當u等于0的時候的情況。在此種情況下,我們會列印0然後,執行循環裡面的自減運算符。結果就是0自減,變為-1,但是-1并不适合一個unsigned int類型,此時會發生值環繞。如果int類型為32位,那麼-1将會變成4294967295.

寫這個循環的另外一種方法是使用while循環。它可以讓我們在列印之前遞減(而不是在之後)。

unsigned u  =11;//
while( u > 0){
    --u;//先遞減,目的是最後一個數為0
    std::cout << u << std::endl;
}
           

這個循環,先遞減循環控制值。在最後一次循環中,進入循環的值将為1,我們是先遞減,是以在最後一次循環中,将會列印0。接着在while的下一次條件表達式中,值為0,while循環則退出。因為最先遞減,然後才列印,是以我們不得不比第一次列印的值大1,基于此原因,初始化u為11。這樣就可以正常列印10.

注意:不要混合使用有符号和無符号類型

當有符号類型為負數的時候,有符合和無符号類型的表達式,将會産生令人驚訝的結果。因為有符号數将會轉換成無符号數。例如,在一個a*b的表達式中。如果a是-1,b是1.且a和b都是int類型,則結果為-1.但是,如果a是int,b是unsigned

int,那麼結果就依賴于int的位數。在我們的測試機中,這個表達式的結果為4294967295

2.1.3 字面量

像42這樣的值,一見便知,是以叫做字面量(有時也稱作字面值)。每一種字面量都有類型.字面量的格式和值決定了它的類型.

整數和浮點字面量

我們可以使用十進制,八進制,十六進制來寫一個整數的字面量.八進制的字面量,在前面加上數字0.十六進制的字面量在前面加上0x或者0X.我們可以寫20這個值,使用下面的三種形式:

20 /*十進制*/    024 /*八進制*/  0x14 /*十六進制*/
           

一個整數字面量的類型依賴于它的值和表現形式.預設情況下,十進制字面量是帶有符号的.但是,八進制和十六進制可以是有符号和無符号的.一個特定十進制字面量的類型是int,long,long long,三種類型中的最小的能夠存儲這個字面量的類型(此例中,是第一種類型,int型).八進制和十六進制的類型是能夠容納其數值的int,unsigned int ,long ,unsigned long ,long long ,unsigned long long中最小的類型.

當一個字面量的值太大以至于不能放在最大的相應類型中時,會報錯.這裡沒有short類型的字面量.下圖展示了我們可以使用字尾來覆寫他們的預設類型.

c++ primer 第五版 翻譯 第二章2.1 基本内置類型2.5 處理類型

盡管整型值可以被存放在有符号類型中,但是從技術上來講,十進制字面量的值從不會為負數。如果我們寫了一個看起來是負數的值,如-42,這個負号不是字面量的一部分。負号僅僅是為了取操作數的負值。

浮點字面量可以表示為:一個小數或者一個用指數表示的科學計數。使用科學記數法,指數通過使用E或者e來表明,如:

3.14159   3.14159E0  0.       .0e0    .001
           

預設情況下,浮點字面量的值為double類型。我們可以使用表2.2中所示的字尾來改變這種預設類型。

字元和字元串字面量

被單引号括起來的字元是類型char的字面量值。0個或者多個被雙引号括起來的字元序列是字元串字面量,如:

‘a’			//字元字面量
“Hello  world !” 		//字元串字面量
           

一個字元串字面量,其實是char類型的常量數組。數組類型将在3.5.4中讨論。編譯器在每個字元串字面量的末尾加上一個空字元(‘\0’)。是以實際的字元串長度,比表面看到的要大1.例如,‘A’表示的是字元A,“A”表示的是含有兩個字元的數組,一個字元為A另外一個字元為空字元(‘\0’)。

如果兩個字元串字面量緊緊相鄰,且他們由空格,tab鍵,換行符分割,那麼他們将會被連接配接成一個字元串字面量。是以,當我們需要使用字元串字面量,且這個字面量較長,不适合放在單行的時候,我們可以采取這種形式,如:

//多行字元串字面量
std::cout << “a really ,really long streing literfal ”
	 “that spans two lines” << std::endl;
           

轉義字元

一些字元,例如倒退字元,控制字元等,他們不可見,這種字元稱為不可列印字元。在c++中還有一些字元具有特殊的意義(單引号,雙引号,問号,反斜杠)。是以c++中不能直接使用這兩種字元,如果要使用這些字元,需要使用轉義字元。一個轉義字元總是以一個反斜杠開頭。c++定義了幾個轉移字元,如:

換行	\n				水準tab		\t				報警(響鈴)		\a
豎直tab	\v				倒退		\b				雙引号				\”
反斜杠	\\				問号		\?				單引号				\’
回車	\r				進紙		\f
           

使用轉義字元,就跟使用單個字元是一樣的:

std::cout << ‘\n’;			//換行
std::cout << “\tHi!\n”		//先列印一個tab,然後是Hi!,接着換行
           

我們也可以使用通用的轉義字元,格式為:\x後面跟一個或者多個十六進制,或者\後面跟一個,兩個,三個八進制。其中數字部分,為字元對應的數值。一些例子(假定字元集為Latin-1):

\7  (響鈴) 					\12 (換行)				\40(空格)
\0(空制符)					\115(‘M’)					\x4d(‘M’)
           

轉義字元的使用,跟其其他任何字元的使用一樣:

std::cout << “Hi  \x4d0\115!\n”;			//列印  Hi   MOM!,然後換行
std::cout << “\115” << ‘\n’;				//列印M然後換行
           

注意:如果反斜杠後面跟的八進制超出了三個,那麼隻有前面三個才是轉義字元。例如,”\1234”有兩個字元,一個為\123,另外一個為4.相反,\x将會使用後面四個數字,”\x1234”代表一個16位的字元。因為,大部分的機器都是8位的字元,是以這種字元可能用的場景并不多。通常情況下,超過8位的字元是随着表2.2中的字首一起被用在擴充字元集中。

指定字面量的類型

可以使用表2.2中的字首或者字尾,覆寫一個整型,浮點型,字元型字面量的預設類型。

L’a’			//類型變為wchar_t
u8”hi!”		//utf-8字元串
42ULL		//unsigned long long
1E-3F		//科學記數法表示的浮點型
3.14159L		//long double
           
經驗: 當你要寫long類型的時候,最好使用大寫的L,小寫l常常與1混淆。

對于一個整型字面量來說,我們可以分開指定符号和大小。如果字尾含有U,表示這是一個無符号的字面量,它将是,unsigned int,unsigned long ,unsigned long long 中最适合存放這個字面量的最小類型。如果字尾包含L,則這個類型至少為long。如果字尾包含LL,則類型為long long 或者unsiged long long.可以将U和L,LL放在一起使用,如,一個字面量有UL的字尾,則表示unsigned long 或者unsigned long long,具體類型則是選擇适合存放這個字面量的最小類型。

布爾和指針字面量

true和false是bool類型的字面量:

bool test = false;

nullptr 是指針類型的字面量。在2.3.2節中将會有更多關于指針的讨論。

2.2 變量

變量讓程式能夠通過名字操作存儲空間。在c++中的變量都有相應的類型。類型決定了變量空間的大小和布局。類型還決定了這個變量空間存儲值的範圍。類型還決定了可以被運用在這個變量上的操作。c++程式員一般将“變量”和“對象”互換使用。

2.2.1 變量的定義

一個簡單的變量定義包括:一個類型說明符,然後是一個,或者多個,被逗号分隔的變量名,最後是一個分号。每一個變量的類型都由類型說明符指定。定義的時候,可以給變量賦初始值。

int  sum = 0, value,//sum, value,units_sold的類型為int
units_sold = 0;//sum,units_sold的初始值為0
Sales_item item;//item類型為Sales_item
//string是一個庫類型,代表了一個可變長的字元序列。
std::string book(“0-201-78345-X”);//book變量被一個字元串字面量初始化。
           

book的定義使用了std::string庫類型。像iostream一樣,string也被定義在了std命名空間中。在第三章,将會讨論更多關于string類型的特性。現在,隻需要知道string是一個可變長的字元序列即可。string庫提供幾種方式,讓我們初始化string對象。其中一種是将字元串字面量值複制給對象(2.1.3)。是以,book被初始化儲存字元串0-201-78345-X.

術語:對象是什麼?

c++程式員喜歡使用對象這個術語。通常情況下,一個對象表示了一段記憶體,這段記憶體存儲着值和相應的類型。

一些人僅僅在類類型的情況下,才使用術語“對象”。還有一些人,将命名和未命名的對象區分開來,他們使用術語“變量”來表示命名的對象。還有一些人,将對象和值區分開來,他們使用術語“對象”來表示可以被程式改變的資料,而使用術語”值”來表示那些隻讀的資料。

在本書中,我們遵守大部分人的習慣用法。我們使用術語“對象”,而不管是否為内置類型還是類類型,也不管是否命名,也不管是否可讀寫。

初始化

在變量被建立的時候,給他值,就表示變量被初始化了。初始化變量的值可以是一個複雜的表達式。當定義了兩個或者多個變量的時,變量随着定義馬上就可用了,是以,可以用前一個變量的值,來初始化後一個變量。

//在初始化discount之前,price已經被定義和初始化了
double  price = 109.99,dicount = price * 0.16;
//調用applyDiscount 并且使用了傳回值來初始化salePrice
double salePrice = applyDiscount(price,discount);
           

初始化是c++中非常複雜的一個主題,我們将反反複複的讨論這個主題。許多程式員都對于使用=符号來初始化一個變量感到困惑,因為,他們常常将指派和初始化認為是同一種,但是在c++中,指派和初始化是兩個不同的操作。這個概念是比較困惑的,因為在其他語言中,這兩者的差別可以被忽略掉。就算在c++裡面,這兩者的差別有時也無關緊要。但是,這個概念是非常重要的,并且我們會在後面反複提及。

警告: 初始化不是指派。初始化發生在:變量建立的同時,給定一個值。指派是:使用一個新的值覆寫掉以前的舊值。

清單初始化

c++定義了幾種初始化方式,這也是初始化這個主題複雜的原因之一。例如,我們可以使用下面四種方式來定義int類型的units_sold變量。

int  units_sold = 0;
int units_sold = {0};
int units_sold(0);
int units_sold{0};
           

大括号用來初始化,是c++11新标準的一部分。這種形式在新标準以前,僅僅被用在一些受限的地方中。出于在3.3.1節中我們将學習他的原因,此處不做過多介紹。這種形式的初始化,被稱為清單初始化。現在,無論是初始化一個對象,還是指派一個對象,都可以使用這種用花括号括起來的形式。

當使用内置類型的變量的時候,清單初始化有一個非常重要的特性:如果初始化值可能導緻資訊丢失,那麼編譯器将報錯。

long double ld = 3.1415926536;
int  a{ld}, b ={ld};//錯誤:變小的類型轉換
int  c(ld),d =ld;//ok,但是值将會縮短,丢失部分資訊
           

編譯器拒絕初始化a和b,因為使用一個long double類型來初始化一個int類型,這可能導緻部分資訊丢失。至少ld的小數部分将會被丢掉,并且,ld的整數部分對于int來說也可能太大。

此處展示的差別可能看起來無關緊要,因為,我們不太可能直接用long double來初始化一個int。但是,這種轉換可能會無意的發生,如16章介紹的一樣。我們将在3.2.1,3.3.1中再次讨論更多關于這種格式的初始化。

預設初始化

當我們定義個沒有初始化的變量的時候,變量會預設被初始化。這些變量會被初始化為預設的值.預設的值依賴于變量的類型和變量定義的地方。

内置類型如果沒有被顯示的初始化,那麼它的預設值依賴于定義的位置。定義在函數外面的變量被初始化為0.然而如6.1.1節中介紹的一樣,被定義在函數内的内置類型,不會被初始化。未被初始化的變量,它的值是未知的。複制或者存取這個未被初始化的變量是一種錯誤的操作。

每一種類都控制了這種類類型被初始化時的操作。尤其,在定義對象的時候是否可以不用初始化,也被類控制。如果類允許這種行為,那麼他将決定對象初始化的值是什麼。

大多數的類都可以定義沒有顯示初始值的對象。這些類提供了一個合适的預設值。例如,上面講到的一樣,string類,如果我們沒有提供一個初始化的值,那麼這個string對象預設為一個空字元串:

std::string empty;//隐式的初始化字元串為空
Sales_item item;//預設初始化Sales_item對象
           

一些類需要為每一個對象顯式的初始化。如果對這種類,不初始化而建立對象,那麼編譯器将會報錯。

注意: 在函數内部的未初始化的内置類型,他的值是未知的。沒有被顯式初始化的類類型,他的值由定義他的類決定。

注意:未初始化的變量導緻運作時問題

一個未初始化的變量有一個不确定的值。使用這種值是錯誤的,并且這種錯誤很難調試。雖然大部分的編譯器會對部分使用了未初始化的變量給出警告,但是編譯器不強制要求檢測這種錯誤。

使用未初始化的變量将會帶來無法預估的後果。有時,我們非常幸運,在存取這個對象的時候,會馬上報錯,然後我們追蹤這個錯誤的位置,此時是非常容易發現變量沒有被相應的初始化。但是還有一些時候,程式會運作完,然後得出錯誤的結果。更壞的情況是,程式産生的結果,在某一次是正确的,在後續運作中又産生了錯誤的結果。并且将一些其他代碼添加到其他位置,此時報錯了,可能導緻我們認為:程式原來是對的,是新添加的代碼導緻了錯誤的結果。

建議: 我們建議初始化内置類型的每一個對象。雖然不是必須的,除非你能夠保證省略初始化沒有問題,否則這種方式是更簡單、更安全的。

2.2.2 變量的聲明和定義

為了把程式寫在多個邏輯段裡面,c++支援分離式編譯。分離式編譯可以讓我們将程式放在幾個檔案中,每個檔案單獨編譯。

當我們将程式放在幾個檔案中的時候,我們需要通過一種方式,來提供跨檔案的代碼通路。例如,一個檔案中代碼需要用到另外一個檔案中定義的變量。考慮一個實際的例子,std::cout和std::cin,他們被定義在标準庫中,我們的程式也可以使用這些對象。

為了支援分離式編譯,c++差別聲明和定義。聲明隻是創造了一個程式可以使用的名字。一個檔案如果想要使用定義在其他地方的名字,隻需要包括有那個聲明檔案即可。定義是建立一個相應的實體。

變量的聲明:指定了變量的類型和名字,他和變量的定義相同。另外,變量的定義還會配置設定存儲空間,并盡可能的提供一個初始值。

為了得到一個聲明,而不是定義,我們将增加關鍵字,并且不提供一個顯式的初始化。

extern  int i;//隻有聲明沒有定義 i
int j;//聲明并且定義 j
           

任何含有顯示初始化的聲明都是定義。我們可以給用了extern的變量提供一個初始值,但是這将覆寫掉extern帶來的效果,即,如果一個extern 聲明有初始化值,那麼它就是一個定義,而不是聲明。

extern double pi = 3.1416://為定義
           

在一個函數内部,給一個extern關鍵字修飾的對象初始化,是錯誤的。

注意: 變量必須被定義一次,但是可以聲明多次。

對于聲明和定義兩者的差別,可能有點晦澀難懂,但是這是非常重要的差別。為了在多個檔案中使用變量需要聲明與定義分開,我們必須定義變量在一個檔案中,并且隻能在一個檔案中,其他檔案隻能聲明這個變量,而不能定義變量。

重要概念:靜态類型

c++是一種靜态類型語言,靜态類型意味着,編譯的時候會進行類型的檢查。

正如所見,一個對象的類型限制了這個對象可以執行的操作。在c++中,編譯器檢查我們所寫的操作是否被這個類型所支援。如果我們寫了一個不支援的操作,那麼編譯器将會報錯,并且不會産生一個可執行檔案。

當我們的程式變得越來越複雜的時候,我們會發現,這種靜态類型可以幫助我們找到bug。是以,一系列的靜态檢查,必須先讓編譯器知道,例如,我們使用的每個實體的類型。是以,我們必須在使用變量之前,給每個變量聲明一種類型。

2.2.3 辨別符

c++中的标志符由字元,數字,下劃線組成。并且對于辨別符名字的長度沒有限制。辨別符必須由字元或者下劃線開始,大小寫敏感。

//定義了四個不同的int變量
int  somename ,someName,SomeName, SOMENAME;
           

c++保留了一些名字,被列在表2.3和表2.4中,這些名字不應該被作為辨別符使用。

c++ primer 第五版 翻譯 第二章2.1 基本内置類型2.5 處理類型
c++ primer 第五版 翻譯 第二章2.1 基本内置類型2.5 處理類型

c++11 标準也保留了一些名字用在标準庫中.是以,我們程式裡面自定義的辨別符不能包含連續兩個的下劃線,也不能以一個下劃線,緊跟一個大寫字元開頭.同時,定義在函數體之外的辨別符也不能以下劃線開頭.

變量名命名的一些約定俗成規則

此處有一些常用的變量名命名規則,使用下面的這些規則可以提高程式的可讀性.

1.辨別符應該具有相應的意義

2.變量名通常為小寫,如index,而不是Index或者INDEX

3.像Sales_item,這種類定義一樣,我們在定義類的時候,常常以大寫字母開始

4.多個單詞應該有明顯的區分,如student_loan 或者studentLoan,而不建議寫成studentloan.

經驗 變量命名的一些約定俗成,一旦堅持使用,将會發揮它最大的功效.

2.2.4 名字的作用域

在程式的任何位置,被使用的名字都指向一個實體,如變量,函數,類型等等.但是,一個名字在程式的不同地方,可以指向不同的實體.

作用域是程式的一部分,在作用域中,一個名字具有特定的意義.在c++中的大多數作用域被大括号限定.

同一個名字在不同的作用域可以指向不同的實體.名字從聲明處開始,到聲明所在的作用域結束.

思考下面來自于1.4.2節中的例子

#include <iostrem>
int main(){
	int sum =0;
	//将1到10相加
	for(int val = 1;val<=10;++val)
		sum += val;
	std::cout << “Sum of 1 to 10 inclusive is ”
			<< sum << std::endl;
	return 0;
}
           

這個程式定義了三個名:main,sum,val,并且使用了std命名空間中的cout和endl。main被定義在所有大括号之外。main像其他函數外部定義的名字一樣,具有全局的作用于。一旦定義在了全局作用域,那麼,它在程式的整個運作期間有效。名字sum定義在main函數的函數體内,是以他的作用域從它的聲明處開始,持續到函數的剩下部分,但是不會超出函數體。這種在某個大括号内的作用域稱為塊作用域。變量sum具有塊作用域。名字val被定義在了for語句的作用域内。他隻能在for語句中被使用,不能在main函數的其他地方使用

嵌套作用域

作用域可以包含其他的作用域。被包含的作用域稱為内部作用域,包含的作用域被稱為外部作用域。

一旦一個名字被定義在了一個作用域内,那麼這個名字就可以被這個作用域内的内部作用域通路。定義在外部作用域内的名字,也可以被内部作用域再次定義。如:

#include <iostrem>
//程式僅僅為了說明,實際中,這種寫法很low
//定義一個全局變量,和同名的本地變量
int reused = 42;//reused具有全局作用域
int main(){
    int unique = 0;//unique具有塊作用域
    //輸出(1):使用全局變量reused,輸出42  0
    std::cout << reused << “ ”<< unique << std::endl;
    int reused = 0;//一個新的本地變量,全局變量reused将被隐藏。
    //輸出(2):使用了本地變量reused ,輸出0  0
    std::cout << reused << “ ”<< unique << std::endl;
    //輸出(3):顯示的使用全局變量reused,輸出42 0
    std::cout << ::reused << “ ”<< unique << std::endl;
    return 0;
}
           

輸出(1)在本地變量reused之前,是以,使用的是全局變量的reused,輸出為:42 0.輸出(2)在本地變量reused作用域内,是以,使用的是本地變量reused,輸出為:0 0.輸出(3)使用作用域運算符,覆寫了預設的作用域。因全局作用域沒有名字,是以,作用域運算符的左側操作數是空,他表示的是:在全局作用域中擷取右邊操作數對應的那個名字。是以輸出(3)使用了全局作用域的reused,輸出為42 0。

警告 将本地變量定義成和全局變量同一個名字,通常是不好的習慣,因為程式可能更想使用全局變量,此時容易使用到本地變量。

2.3 複合類型

複合類型就是,依據其他類型來定義的一種類型。c++有幾種複合類型,其中引用和指針,将在本章中講解。

定義一個複合類型的變量比至今我們所學的所有變量的定義都複雜。在2.2節中指出:簡單的聲明語句由類型和跟在類型後面的變量名組成。更通用的描述是:一條聲明語句由:一個基類型和跟在基類型後面的一組聲明符組成。每個聲明符命名了一個變量,并且指定了這個變量的類型,這個類型與聲明語句最開始的那個基類型相關。

迄今為止,我們接觸到的聲明符就是變量名。這些變量的類型就是聲明語句的基類型。更複雜的聲明符可以是:從聲明語句的基類型中建構一個複合類型,然後再将這個類型指定給一個變量。

2.3.1 引用

注意:

c++11新标準中介紹了一種新的引用,我們稱之為“右值引用”,他們将在13.6.1節中介紹它。這種引用主要用于内置類。從技術上面來講,我們使用術語“引用”,表示的是左值引用。

引用為對象定義了一個名字。引用類型,引用其他類型。定義一個引用,隻需要寫如下形式的聲明符即可,&d,這個d就是我們聲明的名字。

int  ival = 1024;
int &refVal = ival;		//refVal 指向ival
int  &refVal2;			//錯誤:引用必須被初始化
           

通常情況下,初始化一個變量的時候,直接将初始值複制進建立的變量中。但是,初始化一個引用的時候,直接将引用和初始值綁定在一起,而不是複制初始值。一旦初始化之後,引用一直和它綁定的對象綁定在一起。不準對一個已經綁定了的引用,再次綁定其他的對象。是以c++中,引用必須初始化。

引用即别名

注意: 引用不是對象,引用隻是一個已經存在對象的别名而已。

當一個引用被定義之後,所有作用在引用上面的操作,實際上是作用在了與引用綁定的對象上。

refVal =2 ;	//将值2指派給引用,實際上是指派給變量ival
int ii = refVal;	//等價于  ii == ival
           

我們指派給引用,相當于指派給這個引用綁定的對象。我們從一個引用擷取值,相當于從這個引用綁定的對象中擷取值。同樣的,當我們使用一個引用作為初始值時,實際上是使用了這個引用綁定的對象,作為初始值。

//正确:refVal3被綁定到了ival上
int  &refVal3 = refVal;
//用refVal綁定對象的值,初始化i
int  i =  refVal;	//初始化i的值與ival相同
           

因為引用不是對象,是以我們無法定義一個引用來指向另外一個引用。

引用的定義

在一個定義中,我們可以定義多個引用。每個引用辨別符都必須以&符号開頭。

int  i = 1024, i2 = 2048; //i和i2都是int類型
int  &r = i,r2 = i2;//r是綁定到i的引用,r2是一個int類型
int  i3 = 1024, &ri = i3;//i3是一個int類型,ri是綁定i3的引用
int &r3 = i3,&r4 = i2;//r3和r4都是引用
           

除了2.4.1節和15.2.3節中介紹的例外之外,其他的引用類型必須和綁定的對象類型嚴格比對。而且,引用必須和一個對象綁定,而不能和一個字面值或者一個表達式的值進行綁定,原因将在2.4.1節中介紹。

int  &refVal4 = 10;//錯誤:初始值必須是一個對象
double  dval = 3.14;
int  &refVal5 = dval;//錯誤:初始值必須是一個int類型的對象
           

2.3.2 指針

指針是一種指向其他類型的複合類型。跟引用一樣,指針用來間接的存取對象。跟引用不一樣的是,指針是一個對象,它可以被指派和複制,在其生命周期中,可以指向幾個不同的對象。指針也不像引用,它不必再定義的時候初始化。跟其他内置類型一樣,如果指針被定義在了塊作用域,并且沒有被初始化,那麼它的值是不确定的。

警告 指針常常難以了解。有經驗的程式員常常在調試指針錯誤的時候,也備受折磨。

通過寫*d,這種格式的聲明符,我們就能夠定義一個指針類型,此處d就是被定義的名字。對于每一個指針變量來說,*必須重複書寫,如下。

int  *ip1,*ip2;//ip1和ip2都是指針,指向int類型
double  dp ,*dp2;//dp2是一個指針,指向double類型,dp是一個double類型
           

擷取一個對象的位址

一個指針儲存一個對象的位址。通過使用取位址運算符(&),擷取一個對象的位址。

int  ival  = 42;
int *p = &ival;//p儲存有ival的位址,p是一個指向ival的指針。
           

第二條語句,定義指向int的指針p,并且初始化p指向ival對象。因為引用不是對象,他們沒有位址,是以,我們不能定義一個指向引用的指針。

除了2.4.2和15.2.3節中介紹的例外,指針類型和,指向的對象類型必須嚴格比對:

double  dval;
double *pd = &dval;//正确,初始化一個double類型的位址
double  *dp2 = pd;//正确,初始化一個指向double類型的指針

int *pi = pd;//錯誤,pi和pd的類型不同
pi = *dval;//錯誤:将double類型的位址指派給一個int類型的指針
           

指針之是以要嚴格比對,是因為,通過指針的類型,來推斷指向的對象的類型。因為類型決定了對象可用的操作和記憶體空間的布局。如果一個指針指向不符合類型的對象,那麼操作這個對象将會發生錯誤。

指針的值

存儲在指針中的值,可以有四種狀态

1. 可以指向一個對象

2. 可以指向一個對象末尾緊鄰的位置。

3. 可以為一個空指針,表示沒有指定到任何對象上

4. 可以為無效狀态,超出上面3種的都是無效狀态

嘗試讀取一個無效指針的值,是一種錯誤的行為。這種錯誤編譯器不強制檢查,就像我們使用了一個未被初始化的變量一樣,編譯器也不強制檢查。是以,我們必須知道使用的指針是否是有效的。

盡管在上述2,3情況下指針是有效的,但是在這種情況下的指針是受限的。因為這種情況下的指針沒有指向任何對象,我們就不能通過這個指針來存取對象。如果我們嘗試存取這種情況下的指針,那麼結果将是未知的。

使用一個指針來存取對象

當一個指針指向一個對象的時候,我們可以使用解引用運算符(*)來存取這個對象。

int ival = 42;
int *p = &ival; //p儲存有ival的位址,p是一個指針,指向ival
cout  << *p ;//*産生p指針指向的對象;列印42
           

解引用一個指針,将會産生這個指針指向的那個對象。通過給解引用的結果指派,就相當于給那個對象指派。

*p =0;//*産生那個對象,通過p給ival賦了一個新值
cout << *p ;//列印 0
           

當我們指派給*p的時候,就相當于指派給了p指向的對象。

注意: 我們隻能解引用一個有效的指針

關鍵概念:一些符号具有多個意思 一些符号同時被用做運算符和聲明語句的一部分,例如,&和*。符号使用的上下文,決定了這個符号代表的意思:

int i = 42;
int  &r = i; 		//&跟在一個類型的後面,他是聲明的一部分,r是一個引用

int  *p;			//*跟在一個一類型的後面,它是聲明的一部分,p是一個指針
p = &i;			//& 被用在了一個表達式中,當作一個取位址運算符
*p = i;			//*被用在了一個表達式中,當作一個解引用運算符
int  &r2 = *p;		//&聲明的一部分,*是解引用運算符。
           
在聲明中,&和*用來表示一個複合類型。在表達式中,這兩個符号用來表示一個運算符。因為同一個符号在不同的上下文具有不同的意思,是以可以當作兩個不同的符号來看待。

空指針

空指針就是沒有指向任何對象的指針。在嘗試使用一個指針的時候,代碼可以檢查這個指針是否為空。下面有幾種方式,建立幾個空指針:

int  *p1 =  nullptrl;		//等價于  int  *p1  = 0;
int  *p2 = 0;			//直接初始化指針為0
//必須包含頭檔案cstdlib
int  *p3 = NULL:		//等價于  int *p3 = 0
           

最直接的方式是使用nullptr字面量來初始化一個指針,這個字面量在c++11新标準中被引入。nullptr是一種可以轉換成任何指針類型的字面量。我們也可以直接初始化指針為0,如p2的定義一樣。

老一點的程式有時也使用一個預處理變量——NULL,它在cstdlib頭檔案中被定義為0。

在2.6.3節中我們将讨論一部分預處理相關的細節。現在隻需要知道:預處理是編譯之前的一段處理過程。預處理變量由預處理器管理,并且不是std命名空間的一部分。是以我們直接使用,不用加std字首。

當我們使用一個預處理變量的時候,預處理器自動用變量的值代替變量。是以初始化一個指針NULL,就等價于初始化指針0.現代c++程式應該避免使用NULL,而用nullptr來代替。

指派一個int變量給一個指針是非法的,即使這個變量的值為0.

int  zero = 0;
pi = zero ;//錯誤:不能講int指派給一個指針
           

建議:初始化所有的指針 一個未初始化的指針常常是運作時錯誤的源頭。

正如使用未初始化的變量一樣,使用一個未初始化的指針,這種行為的結果是無法确定的。使用一個未初始化的指針,幾乎總是會産生一個運作時的崩潰。但是,調試産生崩潰的原因是非常困難的。

當我們使用一個未初始化的指針時,大多數編譯器,将指針目前的bit位内容當做位址。使用一個未初始化的指針,就相當于,在假定的位址,存取假定的對象。如果指針指向的記憶體位置有bit位,是沒有辦法分辨這個指針是有效的還是無效的。

我們建議,初始化所有的指針變量。如果有可能,應該在一個對象被定義之後,再定義指向它的指針。如果一個指針沒有對象與之綁定,應該初始化這個對象為nullptr或者0.通過這種方式,程式可以檢測這個指針是否指向一個對象。

指派和指針

指針和引用提供了一種間接存取對象的方式。但是他們兩者具有非常大的差别。最大的差别是引用不是對象。一旦我們定義一個引用,就沒辦法再讓這個引用綁定到其他的對象。

當我們使用引用的時候,總是使用引用綁定的對象。

指針和它儲存的位址,就沒有這種限制。像其他變量一樣(非引用),當指派指針的時候,指針就有了一個新值。指派讓一個指針值向了另外一個對象:

int  i = 42;
int  *pi = 0;	//pi被初始化,但是沒有指向對象
int  *pi2 = &i;	//pi2 被初始化儲存i的位址
int *pi3;	//如果pi3被定義在一個塊作用域中,pi3的值是不确定的
pi3 = pi2;		//pi3和pi2指向相同的對象
pi2 = 0;		//pi2現在不指向任何對象
           

一個指派語句到底改變的是指針還是指針指向的對象,他是難以直接觀察到的。我們應該記住最重要的原則是:指派改變的是它的左操作數。當我們寫下如下的語句時:

pi  =  &ival;		//pi的值被改變,pi現在指向ival
指派一個新值給pi,它改變了pi儲存的位址。另一方面,當我們寫下如下的語句時:
*pi = 0;		//在ival中的值被改變,pi沒有改變
此時*pi被改變(也就是pi指向的對象)
           

其他指針操作

隻要指針有效,我們就可以将指針運用在條件表達式中。就像在條件表示中使用算數值一樣,如果指針為0,則條件表達式為false:

int  ival = 1024;
int *pi = 0;		//pi 是一個有效的指針,為空指針
int  *pi2 = &ival;	//pi2是一個有效的指針,他儲存着ival的位址
if(pi) 			//pi的值為0,是以條件表達式的值為false

if(pi2)			//pi2指向ival,pi2的值不為0,是以條件表達式為true
           

任何非零的指針,都會被當作true

給定兩個類型相同的有效指針,可以使用等于運算符(==)或者不等于運算符(!=)來進行比較,比較的結果是bool類型。如果兩個指針儲存有相同的位址,那麼兩個指針就是相等的,否則不等。兩個指針儲存有相同的位址,有下面幾種情況:1.他們都為空,2.他們都指向同一個位址,3.他們都指向同一個對象的下一個位址。需要注意的是,下面這種情況是有可能發生的:一個指針指向一個對象,另外一個指針指向不同對象的下一個對象,但是他們儲存的位址是相同的,這種情況下比較指針也是相等的。

因為這種操作使用了指針的值,是以在條件表達式或者比較運算中,指針必須是有效的。如果使用了無效的指針,那麼結果将是無法預料的。

3.5.節中将會介紹另外的指針操作。

*void 指針

void *類型的指針,是一種特殊類型的指針,這種類型的指針可以儲存任何對象的位址。跟其他類型的指針一樣,void *指針儲存有一個位址,但是這個位址上面的對象的類型卻是不清楚的:

double obj = 3.14, *pd = &obj;
//正确:void *可以儲存任何類型的位址
void  *pv = &obj;//obj可以是任何類型的對象
pv  = pd;		//pv可以儲存任何類型對象的指針
           

使用void *指針能夠做的事比較有限:可以和其他指針比較,也可以向函數傳遞或者從函數傳回,還可以指派給其他void 的指針。不能使用void指針來操作上面的對象,因為我們不知道這個對象具體的類型,而類型又決定了對象所支援的操作。

通常情況下,void*指針就是把記憶體當作記憶體在處理,不能使用這個指針來存取這個記憶體上面的對象。在19.1.1小節中将介紹使用void*指針。4.11.3小節将介紹怎麼擷取void*指針所存的位址。

2.3.3 了解符合類型的聲明

正如所見,變量的定義,由一個基類型和一系列的聲明符組成。在同一條語句中,每一個聲明符,與基類型有關,但是可以于其聲明符不同。是以,單個定義語句可以定義多個不同類型的變量。

int i=1024,*p = &i,&r=i;

警告:許多程式員總是困惑于基類型和類型修飾符的關系,其實類型修飾符是聲明符的一部分。

定義多個變量

經常有人誤解:在同一個語句中的類型修飾符(*,&)會作用于這個語句中的所有變量。造成這種誤解的部分原因是:可以在類型修飾符和名字之間加空格:

int*  p ;		//這是合法的語句,但是會造成誤解
           

之是以說這種定義容易造成誤解是因為這種定義似乎是該語句中每個變量的類型。盡管如此寫,但是類型依舊是int,而不是int *。星号*修飾p的類型,他對于同一個語句下的其他對象并不起作用。

int* p1,p2;//p1是一個指向int的指針,p2是一個int對象
           

通常有兩種風格來定義指針或者引用。第一種,将類型修飾符和辨別符放在一起。

int  *p1, *p2;//pi和p2都是指向int的指針
           

這種格式強調變量具有符合類型。

第二種,将類型修飾符與類型放在一起,但是每個語句隻定義一個變量。

int* p1;//p1指向int的指針
int* p2;//p2隻想int的指針
           

這種格式強調,本次聲明了一個複合類型。

提示: 對于指針和變量的聲明沒有單一而準确的格式。最重要的原則就是堅持使用一種風格。

在本書中我們使用第一種風格,将類型修飾符和變量名放在一起。

指向指針的指針

通常情況下,對于聲明符有幾個類型修飾符是沒有限制的。當多個修飾符一起修飾的時候,我們隻需要按照邏輯來了解即可,但這也并不是那麼好了解。例如,思考如下的指針:指針是記憶體中的一個對象,像其他對象一樣他也有位址,是以,我們可以在另外一個指針中儲存這個指針的位址。

可以通過*來表明每個指針的級别。就是說,當寫下**,表示是一個指向指針的指針,***表示一個指向指針的指針的指針,依此類推:

int ival = 1024;
int *pi  = &ival;	//pi指向一個int對象
int  ** ppi = &pi;	//ppi是一個指向指針的指針
           

此處pi是一個指向int類型的指針。ppi是一個指針,這個指針指向一另外一個指針,後一個指針指向一個int對象。可以使用下面的方式來表示

c++ primer 第五版 翻譯 第二章2.1 基本内置類型2.5 處理類型

就像解引用一個指向int類型的指針,會産生一個int類型一樣,解引用一個指向指針的指針,會産生另外一個指針。為了存取下面的對象,我們必須解引用這個指針兩次:

cout  << “The value of ival \n” 
    << “direct value : ”<<ival << “\n”
    << “indirect value:” << *pi << “\n”
    << “doubly indirect value:”<<**ppi
    <<end;
           

這個程式列印ival的值三次,分别通過:1,直接列印;2,解引用pi;3,解引用兩次ppi

指向指針的引用

一個引用不是對象,是以,不可能有一個指針指向引用。但是因為一個指針是對象,我們可以定義一個引用綁定指針。

int  i  = 42;
int  *p ;
int *&r = p;	//r是一個綁定指針的引用

r = &i;		//r綁定一個指針,指派&i給r,使p指向了i
*r = 0;		//解引用r産生i,它是p指向的對象,這個語句改變i的值為0
           

了解r類型的方法是,從右往左讀定義。離變量名最近的符号直接影響變量的類型,是以,可以看到r是一個引用。剩下的聲明符決定了r綁定的類型,剩下的是一個*,在這裡r綁定的類型是一個指針類型。最後,聲明語句的基類型是int,表明:r是一個引用,這個引用綁定的是一個指針,然後這個指針指向一個int類型。

建議: 從右往左的讀聲明語句,更容易了解複合的指針和引用。

2.4 const 限定詞

有時我們想定義一個不能改變值的變量。例如,我們想定義一個變量,用作緩沖的大小。使用變量作為緩沖大小,使得更改更為容易。另一方面,我們還得防止代碼更改這個變量。可以定義一個不能改變值的變量來避免這種問題,隻需要使用const進行修飾:

const  int  bufSize = 512;	//輸入buffer大小
           

定義bufSize為一個常量,任何對它的指派都會産生錯誤:

bufSize = 512;	//錯誤:嘗試寫常量對象
           

因為一個常量對象被建立之後不能改變它的值,是以常量對象必須被初始化.通常,初始值可以是任意複雜的表達式:

const int  I = get_size();	//正确:運作時初始化
const int j= 42;			//正确:在編譯時初始化
const int k;				//錯誤:k是一個未初始化的const對象
           

初始化和const

正如我們觀察到的那樣,一個對象的類型,決定了這個對象可以執行的操作.一個const類型的對象,可以執行非const對象的大多數操作,但不是全部操作.唯一的限制就是:我們隻能使用那些不能改變對象的操作.例如,我們可以使用const int的對象進行算數運算,就跟普通的int對象是一樣;也可以将const int對象轉變成bool,也跟普通的int對象一樣;等等.

在不改變一個對象的值的操作中還有一種是初始化,當我們使用一個對象初始化另外一個對象的時候,對象是否為const無關緊要.

Int  I  =42;
const int  ci = I;	//正确:i的值被複制到ci中
int j = ci;			//正确:ci的值被複制到j中
           

盡管ci是一個const int對象,但是它的值為int類型.ci的常量屬性,僅僅與可能改變ci的值的操作有關.當我們用ci初始化j的時候,我們不關心ci是否為一個const對象,因為ci的值不會被改變.複制一個對象,不會改變這個對象的值.

預設情況下,const對象在本檔案内有效

當一個const對象在編譯的時候被确定初始化,例如bufSize的定義:

const  int  bufSize = 512;		//input的buffer大小
           

編譯器通常會使用變量的值直接代替變量.即,編譯器将直接使用512來代替bufSize的使用.

為了用值代替變量,編譯器不得不檢視變量的初始值.當程式分開在幾個檔案中的時候,每個使用了這個const對象的檔案,都必須要有這個變量初始值的通路權限.

為了檢視這個變量的初始值,變量必須被定義在每個檔案中.為了支援這種用法,也為了在避免同一個變量的多次定義。const變量預設為檔案内有效。當我們在多個檔案中定義同名的const對象的時候,就好像是我們在每個檔案中定義了一個不同的變量一樣。

有時,初始值不是一個常量表達式,而且需要跨檔案共享。在此種情況下,我們就不希望編譯器在每個檔案都生成不同的變量。相反,我們想const對象的行為,就跟非const對象一樣:定義const在一個檔案,然後在其他檔案中定義這個對象的聲明。

為了定義const變量的單個執行個體。在定義和聲明處使用extern 關鍵字

//file_1.cc定義并且初始化了一個const變量,這個變量在其他檔案也也會被通路
extern const int bufSize = fcn();
//file_1.h
extern const int bufSize;//這個變量的定義在file_1.cc檔案中
           

file_1.cc檔案中定義并初始化了bufSize,因為聲明中包含初始化值,是以這是一個定義。又因為bufSize是一個const變量,那麼為了在其他檔案中使用這個變量,我們需要使用extern關鍵字。

在file_1.h中的聲明,也使用了extern關鍵字。在這種情況下,extern表明bufSize被定義在此檔案之外的其他地方。

注意: 為了在多個檔案中共享const變量,必須在定義的時候使用extern關鍵字

2.4.1 const的引用

像其他對象一樣,我們可以綁定一個引用到一個const對象上,将此引用稱為const的引用。和其他普通的引用不同,不能通過const的引用去改變它綁定的對象。

const int ci = 1024;
const int &r1 = ci;	//正确:引用和其綁定的對象都是const
r1 = 42;			//錯誤:r1是一個const的引用
int &r2 = ci;		//錯誤:const對象的非const引用
           

因為不能直接指派給ci,是以也不能通過引用來改變ci的值。如果r2的初始化正确,那麼我們就能夠通過r2來改變綁定的對象。基于此原因,是以r2的初始化是錯誤的。

術語:cost引用就是綁定到const的引用

c++程式員常常将綁定到const的引用縮寫為const引用。如果你時刻謹記這隻是是縮寫,那麼這樣做也是可以。

從技術上來講,是不存在const引用的。因為引用不是對象,是以無法讓引用自身不變。事實上,不可能讓一個引用,綁定不同的對象,在這種情況下來說,所有的引用都是不變的。引用綁定的對象是否為一個常量,決定了這個引用可以做的操作,而不會影響引用本身的綁定。

初始化和const引用

在2.3.1小節中,提及過:引用類型必須與綁定的類型相同,但是有兩個特例.第一個特例就是:可以使用任何表達式初始化,綁定到const的引用.前提是,這個表達式可以轉換成引用的類型.尤其是,可以将const引用綁定到,非const的對象,也可以綁定到字面量,以及綁定大多數的常用表達式:

int I =42;
const int &r1 = I;		//可以綁定一個const int &到普通的int對象 正确:r1是一個const引用
const int &r2 = 42;	// 正确:r2是一個consti應用
const int &r3 = r1*2;	//正确:r3是一個const引用
int &r4 = r*2;			//錯誤:r4是一個普通,非const引用
           

要想了解這種初始化規則的最簡單的方法就是:想一想當我們綁定不同的類型時,會發生什麼

double dval = 3.14;
const int &ri = dval;
           

此處,ri綁定到一個int.在ri上面的操作将是整型所支援的操作,但是dval是一個浮點型不是整型.是以為了保證ri所綁定對象為int,編譯器将編譯成類似下面的代碼:

const int temp = dval;	//建立一個臨時的const int
const int &ri = temp;//綁定ri到這個臨時對象
           

在上例中,ri綁定到了一個臨時對象中.當編譯器需要一個位置來存放來表達式的結果時,就建立一個未命名的對象,這個對象就是臨時對象.

現在來思考一下:當ri不是const的時候,允許這種初始化,将會發生什麼情況?如果ri不是const,那麼就可以給ri指派,此時就能改變ri綁定的對象.ri綁定的對象為臨時對象,不是dval.程式員将ri綁定在了dval身上,那麼就希望,指派給ri,就會改變dval的值.因為綁定到臨時對象,幾乎是程式員沒有預料到的,是以c++語言就禁止了這種用法.

一個const引用可以綁定到一個非const的對象

明白這一點是非常重要的:一個const引用,限制的僅僅是這個引用.将一個const引用綁定到一個對象,跟這個對象是否為const無關緊要.因為被綁定的對象可以是非const的,它可以通過其他方式改變:

int i = 42;
int &r1 = i;			//r1綁定到了i
const  int  &r2  = i;	//r2也綁定到了i,并且不能通過r2來改變i
r1 = 0;				//r1不是const,是以現在i是0
r2 = 0;				//錯誤:r2是一個const引用
           

綁定r2到i是合法的.但是不能通過r2來改變i的值.盡管如此,i的值仍然能夠被改變.可以直接給i指派來改變,也可以通過其他的引用來改變,如r1.

2.4.2 指針和const

和引用一樣,我們可以定義指針,指向一個const或者非const類型.跟綁定到const的引用一樣,指向const的指針,不能用來改變它所指向的對象.存放const對象的位址,隻能使用指向const的指針.

Const  double pi = 3.14;	//pi是一個const,它的值不能被改變
double *ptr = &pi;		//錯誤:ptr是一個普通的指針
const double *cptr = &pi;	//正确:cptr指向了一個const對象
cptr = 42;				//錯誤:不能指派*cptr
           

在2.3.2節中,提及:一個指針的類型必須和它所指的對象類型嚴格比對,但是有兩種情況除外.第一種例外就是:可以使用指向const的指針,指向一個非const對象.

Double dval = 3.14;		//dval是一個double類型,值可以被改變
cptr = &dval;				//正确:不能通過cptr改變dval的值
           

跟const引用一樣,指向一個const的指針沒有規定指向的對象必須是一個const對象.指向const的指針僅僅影響這個指針可以做什麼.記住一點非常重要:一個指向const 的指針不能保證指向的對象不會被修改.

建議: 試着這樣想,或許有幫助:指向const的指針或者引用,隻不過是指針以為自己指向了一個const的對象.

const指針

跟引用不同的是,指針是對象,是以,跟其他對象一樣,他可以自己為const.跟其他const對象一樣,const指針必須被初始化,一旦初始化,就不能被改變.通過在*後面放一個const來表明這個指針是常量.這種寫法表明:這是個指針,并且不能被改變

int errNumb = 0;
int *const curErr = &errNumb;		//curErr總是指向errNumb
const double pi = 3.14159;
const double * const pip = &pi;	//pip是一個指向const的const指針
           

正如在2.3.3節所說的那樣,了解這種聲明最簡單的方法是從右往左讀.此處,離curErr最近的是符号const,表明curErr自己為一個const對象.這個對象的類型,由聲明語句的剩下部分來表示.下一個符号是*,意味着curErr是一個const的指針對象.聲明語句的基類型使curErr的聲明更加完整,它表明這是一個const的指針,指向一個int類型.同理可得,pip是一個const指針,它指向一個const的double類型.

事實上,一個const指針,并不能規定:不能通過指針來改變它指向的對象.是否有能力去改變這個對象,完全依賴于指針指向的類型而不是指針的類型.例如,pip是一個指向const的const指針.被pip所指對象的值,和pip都不能被改變.換句話說,curErr指向了一個普通的,非const的int類型,可以使用curErr去改變errNumb的值:

*pip = 2.72;		//錯誤:pip是一個指向const的指針
//是否curErr指向的對象是否為0
if(*curErr){
	errorHandler();
	*curErr = 0;			//正确:curErr所指的對象被複位
}
           

2.4.3 頂層const

正如所見:指針是一個可以指向其他對象的對象.是以,可以分開讨論,指針是否為const,以及指針指向的對象是否為const.使用術語:頂層const來表示,指針本身為const.當一個指針,指向一個const對象的時候,這個對象我們稱為:底層const

更通用的說法是:頂層const表示對象本身為const.頂層對象可以用來描述任何類型的對象,例如,任一内置算數類型,類類型,指針類型.底層const用來表示:複合類型的基類型,例如,指針和引用.注意:不像其他類型,指針的頂層const和底層const是分開的:

int i = 0;
int  * const p1 = &i;			//無法改變p1的值,它是頂層const
const int ci = 42;				//無法改變ci的值,它是頂層const
const int *p2 = &ci;			//無法改變p2的值,它是底層const
const int * const p3 = p2;		//靠近右邊的是頂層const,靠近左邊的是底層const
const int &r = ci;				//在引用中的const始終為底層const
           

當複制對象的時候,頂層const常常被忽略.

i = ci;		//正确:複制ci的值,頂層const ci被忽略
p2 = p3;		//正确:類型比對,并且p3屬于頂層const,将被忽略
           

複制一個對象,不會改變被複制對象的值.是以,不管是複制進來,還是複制出去,跟const沒有關系.

另一方面,底層const不會被忽略的情況為:複制一個對象的時候,兩個對象必須要有相同的底層const或者在他們之間能夠進行類型轉換.通常,可以将非const轉成const,反之則不行.

Int *p = p3;		//錯誤:p3有一個底層const,但是p沒有
p2 = p3;			//正确:p2,p3有相同的底層const
p2 = &i;			//正确:可以将int *轉換成const  int *
int &r = ci;		//錯誤:不能将const int綁定在int &上
const int &r2 = i;	//正确:可以将int綁定在const int & 上
           

p3有頂層const和底層const.當複制p3的時候,可以忽略頂層const,但是事實是它指向了一個const對象.是以,不能使用p3來初始化p,因為p指向了一個非const對象.另一方面,可以将p3指派給p2.因為兩個都有相同的底層const類型.這跟p3是const指針沒有關系.

2.4.4. constexpr和常量表達式

一個表達式他的值不能被改變,并且能夠在編譯的時候計算出來,那麼這個表達式就是常量表達式。一個字面量是一個常量表達式。一個被常量表達式初始化的const對象,也是常量表達式。後面會提到,在c++語言中,将會有幾個情況用到常量表達式。

一個對象或者表達式是否為常量表達式,依賴于對象的類型和初始值。例如:

const int  max_files = 20;	//max_files 是一個常量表達式
const int limit = maxfiles+1;	//limit是一個常量表達式
int  staff_size = 27;			//staff_size不是一個常量表達式
const int sz = get_size();		//sz不是一個常量表達式
           

盡管staff_size由字面量初始化,但是他也不是一個常量表達式,因為他是一個普通的int對象,不是const int。另一方面,盡管sz是const,但是他的初始化值,隻有在運作的時候才能知道。是以sz也不是常量表達式。

constexpr 變量

在一個大系統中,分辨一個初始值是否為常量表達式是困難的。我們可能定義一個const變量,然後使用一個我們認為是常量表達式的初始值。但是,當我們在真正需要一個常量表達式的上下文中,使用那個變量時,我們才發現變量的初始值不是常量表達式。通常,一個對象的定義和他的使用,在這種上下文中,可以分開來看待。

在c++11新标準中,通過聲明constexpr類型,我們可以讓編譯器去驗證,一個變量是否為常量表達式。被constexpr聲明的變量,一定是常量,且必須用常量表達式初始化。

constexpr int mf = 20;	//20是一個常量表達式
constexpr int limit = mf + 1;	//mf+1是一個常量表達式
constexpr int sz = size();		//隻有在size是一個常量函數的情況下,這個式子才成立
           

盡管不能使用正常的函數作為constexpr變量的初始值,但是,在6.5.2小節中将會介紹c++11新标準可以定義一種函數,這種函數可以作為constexpr初始值。這種函數必須足夠簡單到編譯器可以在編譯的時候計算他的值。可以使用constexpr函數來初始化constexpr變量。

建議 通常,你想将變量作為常量表達式使用的時候,使用constexpr修飾符常常是一個好的習慣。

字面值類型

因為常量表達式在編譯時就能夠被計算出來,是以在使用constexpr來聲明類型時,這些類型有一定的限制。在constexpr中使用的類型,成為“字面值類型”,因為他們簡單到可以通過字面意思得到。

迄今為止,我們使用過的類型,如,算數類型,引用,指針都是字面值類型。而Sales_item類和IO庫,以及string類型都不是字面值類型,是以不能将他們定義成constexpr.在7.5.6小節以及19.3小節,我們将介紹其他字面值類型。

盡管我們可以定義constexpr的指針和引用,但是初始化他們的值卻是嚴格受限的。可以使用nullptr字面量和0來初始化constexpr指針。也可以用固定位址的對象來初始化指針。

6.1.1小節将會介紹,定義在函數内部的變量通常沒有一個固定的位址,是以不能使用constexpr指針來指向這種變量.換句話說,定義于函數外的變量的位址是一個常量表達式,可以用來初始化constexpr指針.同樣在6.1.1小節中,将會介紹,函數體内也可以定義跨越函數體調用的變量,就跟在函數體外面定義的變量一樣,它也有固定的位址.是以也可以用constexpr引用來綁定,也可以用constexpr指針來指向這種變量.

指針和constexpr

當在constexpr中定義了一個指針,一定要明白:constexpr修飾符作用于指針,而不是指針指向的類型.

const  int  *p = nullptr;	//p是一個指向const int的指針
constexpr int *q = nullptr;	//q是一個const指針,指向int類型
           

盡管寫法看起來類似,但是p和q的類型卻是完全不同的。p是一個指向const的指針。而q是一個const指針。之是以如此,是因為:constexpr強制作用于頂層const。

跟其他常量指針一樣,一個constexpr指針可以指向一個const或者非const類型:

constexpr int *np = nullptr;	//np是一個常量指針,指向一個int類型,int值為空。
int  j = 0;
constexpr int i = 42; 	//i的類型是const  int
//i和j必須定義在函數外
constexpr const int *p = &i;	//p是一個const指針,指向一個const  int 類型i
constexpr  int *p1 = &j;		//p1是一個const指針,指向一個int類型j
           

2.5 處理類型

當我們的程式變得複雜的時候,我們将發現使用的類型也會變得複雜。類型變得複雜展現在兩方面。一是,一些類型難以拼寫;二是,有時難以确定使用哪一種類型。

2.5.1類型别名

類型别名是一個類型的代名詞。類型别名讓我們簡化了複雜類型的定義,使複雜類型更容易使用。類型别名也更容易了解這個類型的用途。

有兩種方法可以定義類型别名。傳統上,直接使用typedef關鍵字:

typedef double wages;	//wages是double的别名
typedef wages base,*p; //base是double的别名,p是double *的别名
           

關鍵字typedef可以作為一個聲明語句的基類型。包含typedef的聲明語句,是定義别名的,而不是定義變量的。正如其他聲明語句一樣,聲明符也可以包含類型修飾符,這些類型修飾符可以定義複合類型。

c++ 11新标準引入了第二種聲明别名的方式,通過使用别名聲明符來聲明:

using SI = Sales_item;	//SI是Sales_item的别名
           

一個别名的聲明語句以關鍵字using開頭,然後緊跟着别名名字和等于符号。别名聲明語句,将等号左邊的名字聲明為等号右邊類型的别名。

類型的别名,可以出現在類型出現的任何地方:

wages hourly,weekly;	//跟double hourly,weekly;一樣
SI item;		//跟Sales_item item 一樣
           

指針,const,和類型别名

一個使用了類型别名的聲明,并且這個類型别名代表一個複合類型.在這個聲明中使用了const關鍵字,那麼這個聲明語句将會産生令人意想不到的結果.例如,下面的聲明使用了pstring别名,它代表類型char *

typedef char* pstring;
const pstring cstr = 0;		//cstr是一個指向char的常量指針
const pstring *ps;		//ps是一個指向常量指針的指針.常量指針指向char
           

這個聲明語句中的基類型是const pstring.通常情況下,出現在基類型中的const修飾基類型.pstring的類型為: 一個指向char的指針.是以.const pstring是一個指向char的常量指針.而不是一個指向const char的指針.

使用類型别名時,簡單的替換别名對應的類型來解釋聲明語句是不正确的:

const char * cstr = 0;	//const  pstring  cstr錯誤的解釋
           

上述聲明是錯誤的.當聲明語句中使用pstring時,基類型是指針.而重新使用char *的時候,基類型是char.而*是聲明符的一部分.此例中,const char是基類型.cstr是一個指針,指向一個const char而不是一個const 指針指向char.

2.5.2 auto 類型訓示符

常常需要将一個表達式的值存儲在某個變量中。為了聲明這個變量,必須知道這個表達式的類型。當在程式設計的時候,判斷一個表達式的類型比較困難,有時是不可能判斷類型的。是以,在c++11新标準中,通過使用auto類型訓示符讓編譯器推導出類型。不像其他的類型訓示符,(例如double)專門指定一種類型。auto讓編譯器通過初始值來決定變量的類型。是以,一個變量使用了auto,那麼他就必須要有初始值,用于推導它的類型:

//item類型從val1和val2相加的結果進行推倒
auto item = val1+val2;		//item被val1+val2的結果初始化
           

此處,item的類型将通過val1加val2的傳回值進行推導。如果val1和val2是Sales_item類型。那麼item将是Sales_item類型。如果這兩個變量類型是double,那麼itme的類型也是double。依此類推。

跟其他類型訓示符一樣,也可以使用auto來定義多個變量。因為一個聲明語句隻有一個基類型,是以,這些變量的初始值必須具有一緻的類型:

auto i = 0, *p = &i;	//正确:i是int,p是一個指向int的指針
auto sz = 0,pi = 3.14;	//錯誤:sz和pi類型不一緻
           

複合類型,const,auto

編譯器推導出來的類型,并不總是和初始值相同。因為編譯器需要調整類型來滿足常見的初始化規則。

首先,正如所見,當我們使用引用的時候,實際上使用的是引用綁定的那個對象。編譯器使用這個對象來推導auto的類型:

int i  = 0,&r = i;
auto a = r;	//a是一個int類型(r是i的一個别名,他的類型為int)
    第二, auto通常忽略頂層const,底層const被保留:
const int ci = i,&cr = ci;
auto b = ci;	//b是一個int類型(在ci中的頂層const被忽略掉)
auto c = cr;	//c是一個int類型(cr是ci的别名,ci是一個頂層const)
auto d = &i;	//d是一個int * 類型
auto e = &ci;	//e是一個const int  *(一個const對象取位址,這個位址是一個底層const被保留)
           

如果我們想要保留頂層的const,必須顯式 的聲明:

const auto f = ci;	//ci的類型為int,f的類型為const int
           

還可以将引用用在auto推導類型上,常用的推導規則仍然适用:

auto &g =ci;	//g是一個const  int &,它綁定到ci
auto &h = 42;	//錯誤:不能将一個普通引用,綁定到一個字面量上
           

當将引用用在auto的類型推導的時候,頂層const不會被忽略。當綁定引用到初始值時,const并不算頂層const。

當在一條語句中,定義多個變量時,切記:引用和指針是聲明符的一部分,不是基類型的一部分。跟往常一樣,初始值必須具有一緻的類型:

auto k = ci,&l =i ;	//k是int;l是int&
auto &m =ci,*p = &ci;	//m是const int &,p是一個指向cont int的指針
//錯誤:來自于i的類型為int,從&ci上面推導出的類型為const int
auto &n =i,*p2 = &ci;
           

2.5.3 decltype 類型訓示符

有時,我們想讓編譯器自動推導一個表達式的類型,但是又不想使用這個表達式來初始化這個變量。基于此原因,c++11新标準引入了第二個類型訓示符,decltype,它傳回操作數的類型。編譯器分析表達式的類型,但是不會計算這個表達式:

decltype(f()) sum =x;	//sum的類型跟f傳回的類型一樣
           

此處,編譯器并不會調用f,但是使用了f傳回值的類型作為sum的類型。decltype處理頂層const和引用,跟auto的處理有點不同。當decltype應用的表達式是一個變量時,decltype傳回的是這個變量的類型,包括頂層const和引用:

const int ci = 0,&cj =ci;
decltype(ci) x = 0;	//x類型為const  int
decltype(cj) y = x;	//y的類型為const int &并且綁定到x
decltype(cj) z=;	//錯誤;z是引用,必須被初始化
           

因為cj是引用,是以decltype(cj)也是一個引用。跟其他引用一樣,z必須被初始化。

引用是唯一沒有被作為别名對待的地方就是decltype的上下文。

decltype和引用

當decltype表達式不是一個變量時,就使用的是表達式結果的類型。在4.1.1小節中,一些表達式會導緻decltype産生引用類型。通常來講,此種情況下decltype表達式傳回的引用所綁定的對象,可以作為指派語句的左值。

//decltype傳回的類型可以是引用類型
int i = 42, *p = &i,&r = i;
decltype(r+0) b;	//正确:加法産生一個int,b是一個未初始化的int類型
decltype(*p) c ;	//錯誤:c是一個int &,必須被初始化
           

此處,r是引用,是以decltype®是一個引用類型。如果想要使用r綁定的類型,直接在表達式中使用r即可,r+0,這個表達式産生的值為非引用類型。

另一方面,解引用操作符,使decltype傳回引用類型。正如所見,當解引用指針時,我們得到了這個指針指向的對象,并且可以指派給這個對象。是以,decltype(*p) 傳回的對象為int &,而不是int。

decltype和auto另一個不同的地方在于:decltype還依賴于表達式的格式。在小括号内的變量會影響decltype的傳回類型。當decltype直接應用變量,沒有小括号的時候,得到的是這個變量的類型。當使用一層或者多層小括号的時候,編譯器将這個變量當作表達式來對待。變量是可以作為指派語句的左值的特殊表達式。是以,decltype作用在這樣的表達式下會得到一個引用:

//括号包圍的變量的decltype總是一個引用
decltype((i)) d;		//錯誤:d是一個int& 必須被初始化
decltype(i) e;		//正确:e是一個未初始化的int
           
警告: decltype((變量)) 總是一個引用類型。但是decltype(變量) 隻有在變量是引用的時候,才會是引用類型。

2.6 定義我們自己的資料結構

從最基本上來說,資料結構是資料和資料操作政策的組織形式。例如,我們的Sales_item類,把ISBN,銷售額,單價組織在一起,并且提供了一系列的操作,例如isbn函數,<<,>>,+還有+=操作符。

在c++裡面,通過定義一個類來定義自己的資料類型。就跟我們自己定義的Sales_item類類似,庫string,istream,和ostream,也被定義成了類。c++對類的支援廣泛,事實上本書第3部分和4部分,将大量的介紹類相關的特性。盡管Sales_item類比較簡單,但是現在還沒有能力完整定義這個類,直到第14章介紹完如何編寫自定義運算符之後才有這個能力。

2.6.1 定義Sales_data類型

盡管我們不能夠編寫Sales_item類,但是我們可以編寫一個簡單類來組織相同的資料類型。對于這個簡單類的政策是:使用者能夠直接存取這個資料元素,并且為這些資料元素實作必要的操作函數。

因為這個資料結構不支援所有的操作符,是以命名她為Sales_data,以示Sales_item的差別。定義這個類如下:

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

這個類以關鍵字struct 開始,緊跟着類名,和類定義的實體。類的實體被大括号包圍,并且也形成了一個新的作用域。定義在類内部的名字必須是唯一的,但是可以在類的外部再次使用這個名字。

類體的末尾必須跟一個分号,這個分号是必須的,因為類體後面可以直接定義變量:

struct Sales_data{/*....*/} accum,trans,*salesptr;
//等價于上一條語句,并且比上一條語句更好
struct Sales_data{/*...*/};
Sales_data accum,trans,*salesptr;
           

分号标志着聲明符清單的結束。通常,在一個類定義後面直接定義一個對象并不算好的程式設計習慣。如果這樣做,容易引起混淆,因為兩種不同的實體,(類和對象)被放在一個單一的語句裡面。

警告 在類定義後面忘記分号是,新手程式員常犯的錯誤。

類資料成員

類體定義了類的成員。我們現在定義的類僅有資料成員。資料成員決定這個類對應的對象的内容。每一個對象都有自己的資料成員。修改一個對象的資料成員,不會改變其他對象的資料成員。

定義資料成員與定義普通的變量是相同的:給出一個基類型,然後跟着一個或者多個聲明符。Sales_data有三個資料成員:一個string類型的bookNo;一個unsigned類型的units_sold;一個double類型的revenue。每一個Sales_data對象都有這三個資料成員。

在c++11新标準下,可以為資料成員提供類内初始值來初始化他們。當建立一個對象的時候,類内初始值被用來初始化資料成員。沒有初始值的資料成員有一個預設的初始值。是以,當建立Sales_data對象的時候,units_sold和revenue被初始化為0,bookNo被初始化為空字元串。

類内初始值的限制與2.1.1節介紹的類似:他們必須被大括号括起來或者在指派符(=)後面,但不能使用圓括号。

在7.2小節中,我們将介紹c++定義資料類型的第二種關鍵字,class。也會介紹為什麼此處要使用struct。在第七章介紹類相關特性之前,應該使用struct來定義我們資料類型。

2.6.2 使用Sales_data類

跟Sales_item不一樣,Sales_data不支援任何運算符。Sales_data的使用者不得不自己編寫需要的操作運算。我們将編寫一版來自于1.5.2小節的程式,這個程式列印兩個交易記錄之和。這個程式的輸入如下:

0-201-78345-X 3 20.00

0-201-78345-X 2 25.00

每一個交易記錄都有一個ISBN号,售賣數,以及單價。

相加兩個Sales_data對象

因為Sales_data沒有提供運算符,是以,不得不手動編寫輸入,輸出和相加操作。假設Sales_data類被定義在Sales_data.h檔案中。2.6.3小節将介紹怎麼定義這個頭檔案。

因為這個程式比我們迄今為止寫的任何程式都長,是以我們分段解釋。總體來講,我們的程式有下面的一個結構:

#include <iostream>
#include <string>
#include “Sales_data.h”
int main(){
    Sales_data data1,data2;
    //将資料讀入data1和data2的代碼
    //檢查data1和data2是否用相同ISBN号
    //并列印data1和data2的和
}
           

跟以前的程式一樣,最開始是需要的頭檔案,然後定義儲存輸入的變量。注意,跟Sales_item程式不一樣,新的這個程式包含了string的頭檔案。我們需要這個頭檔案,因為需要操作bookNo成員,而他的類型就是string。

讀資料進Sales_data對象

盡管還沒有詳細介紹string類型,(這部分在第三章和第十章中介紹)但是也需要知道一點關于string類型,目的是為了定義和使用ISBN成員。string類型存儲一組字元,它的操作包括>>,<<,==運算符,分别對應,讀,寫和比較字元串。有了這個知識,我們可以開始編寫讀入第一個交易記錄的代碼了:

double price = 0;	//每本書的單價,用來計算總售價
//讀入第一個交易記錄:ISBN,售賣數,單價
std::cin >> data1.bookNo >> data1.units_sold>>price;
data1.revenue = data1.units_sold * price;
           

銷售記錄包含每本書的單價,但是使用的資料結構存儲的是總的售價。是以将單價存儲在double類型的price上,用于計算總的售價。

std::cin >> data1.bookNo >> data1.units_sold>>price;
           

使用點運算符,将資料讀入data1的bookNo和units_sold成員裡面。

代碼的最後一個語句,将data1.units_sold和price相乘放入data1的revenue成員中。

程式将重複相同的操作,将資料讀入data2中:

//讀入第二個交易記錄
std::cin >> data2.bookNo >> data2.units_sold >> price;
data2.revenue = data2.units_sold*price;
           

列印兩個Sales_data對象的和

程式其他的任務還包括:檢查交易記錄是否有相同的ISBN,如果相同,則列印他們的和,否則列印錯誤資訊:

if(data1.bookNo == data2.bookNo){
	    unsigned totalCnt = data1.units_sold + data2.units_sold;
	    double totalRevenue = data1.revenue+data2.revenue;
	    //列印,ISBN,總售價,每本書的單價
	    std:cout << data1.bookNo << “ ”<< totalCnt << “ ”<< totalRevenue << “ ”;
	    if(totalCnt != 0)
	    			std::cout << totalRevenue / totalCnt << std::endl;
	    else
	    		std::cout << “(no sales)” << std::endl;
	    return 0;//表示成功
    }else{//交易記錄沒有相同的ISBN号
	    std::cerr << “Data must refer to the same ISBN” << std::endl;
	    return -1;//表示失敗
}
           

第一條語句,比較data1和data2的bookNo。如果他們相等,那麼執行大括号内部的代碼。這部分代碼,将兩個變量的成員相加。因為需要列印平均值,是以一開始就将units_sold和revenue分别存在了totalCnt,和totalRevenue裡面。然後再列印這些值。在下一條語句檢查是否有書被賣出去。如果有,就計算書的平均價格。如果沒有賣出,就列印一條沒有賣出的消息。

2.6.3 編寫頭檔案

盡管在19.7小節會介紹,可以在函數内部定義類,但是這種類是受限制的。是以,類通常不定義在函數内部。當在函數體外定義一個類時,任何給定的源檔案可能隻有一處這個類的定義。另外,如果在幾個分開的檔案中使用這個類,那麼這個類的定義在每個檔案中都必須相同。

為了保證類的定義在每個檔案中都是相同的,類通常被定義在頭檔案中。通常,類被存儲在頭檔案,頭檔案的名字來自于類名。例如,string類型,被定義在string頭檔案中。同樣,我們也将Sales_data類定義在Sales_data.h頭檔案中

頭檔案通常包含隻能被定義一次的實體(例如類的定義,const,constexpr變量等)。頭檔案經常需要用到其他頭檔案的功能。例如,Sales_data類有string成員,Sales_data.h必須#include包含string頭檔案。是以,使用了Sales_data的程式将包含兩次string頭檔案。一次是直接在程式裡面包含,一次是在Sales_data.h中被包含。因為一個頭檔案可能被包含多次,是以,需要一種安全的方式來寫我們的頭檔案。

注意 一旦頭檔案更新,使用這個頭檔案的源檔案也必須重新編譯來使這些更新生效。

預處理器概述

對于頭檔案被包含多次,最常用的技術是依賴預處理器。預處理器,繼承于c,他是一段運作在編譯之前的程式,他可以改變程式的源文本。

我們的程式其實已經在依賴預處理器的功能了,那就是#include。當預處理器看到#include時,就使用給定的頭檔案的内容代替這個語句。

c++程式也使用預處理器來定義頭檔案保護符。頭檔案保護符依賴于預處理器變量(2.3.2小節)。預處理變量隻有兩種狀态:defined和undefined.#define指令帶有一個名字,并且将這個名字定義為一個預處理變量,使他的狀态為defined。c++有兩個其他的指令用于測試一個給定的預處理變量是否為defined狀态:#ifdef 指令,如果變量為defined則,傳回true。#ifndef指令,如果變量為undefined,則傳回true。如果傳回true,那麼#ifdef和#ifndef後面跟着的代碼将會被處理,直到遇到#endif指令。

可以使用這個功能,來保護頭檔案的多次包含:

#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <string>
struct Sales_data{
    std::string bookNo;
    unsigned units_solds = 0;
    double revenue = 0.0;
};
#endif
           

Sales_data.h第一次被包含的時候,#ifndef 傳回true。預處理器将處理#ifndef到#endif之間的所有代碼。是以,SALES_DATA_H被定義,狀态為defined,并且Sales_data.h的内容被複制到程式中。如果後面再次包含Sales_data.h頭檔案,#ifndef指令将傳回false,是以,#ifndef和#endif之間的代碼将會被忽略。

警告 預處理變量無視c++的作用域規則。

用于頭檔案保護符的預處理變量,在整個程式中必須是唯一的。通常為了保證唯一性,直接基于類的名字來給頭檔案保護符命名。為了避免和其他實體的命名沖突,預處理變量通常為大寫。

經驗 頭檔案即使不會被其他頭檔案包含,也應該有頭檔案保護符。頭檔案保護符書寫簡單,隻需要習慣性的定義他們就可以,不用判斷它們是否必須。

小結:譯(略)

專業術語:譯(略)

題外話:最近半年加班嚴重,腦袋比較漿糊,譯文中可能出現一些*符号沒有顯示出來,可提示一下,謝謝。