《C++ Primer 5th》笔记(2 / 19):变量和基本类型
文章目錄
- 基本內(nèi)置類型
- 算術(shù)類型
- 內(nèi)置類型的機(jī)器實(shí)現(xiàn)(類型在物理層面上的說明)
- 建議:如何選擇類型
 
- 類型轉(zhuǎn)換
- 建議:避免無法預(yù)知和依賴于實(shí)現(xiàn)環(huán)境的行為
- 算術(shù)表達(dá)式里使用布爾值
- 含有無符號(hào)類型的表達(dá)式(帶符號(hào)數(shù)會(huì)自動(dòng)地轉(zhuǎn)換成無符號(hào)數(shù))
- 提示:切勿混用帶符號(hào)類型和無符號(hào)類型
 
- 字面值常量
- 整型和浮點(diǎn)型字面值
- 字符和字符串字面值
- 轉(zhuǎn)義序列
- 通過前后綴指定字面值的類型
- 布爾字面值和指針字面值
 
 
- 變量
- 變量定義
- 術(shù)語:何為對(duì)象?
- 初始值
- 列表初始化
- 默認(rèn)初始化
- 提示:未初始化變量引發(fā)運(yùn)行時(shí)故障
 
- 變量聲明和定義的關(guān)系
- 關(guān)鍵概念:靜態(tài)類型
 
- 標(biāo)識(shí)符
- 名字的作用域
- 建議:當(dāng)你第一次使用變量時(shí)再定義它
- 嵌套的作用域
 
 
- 復(fù)合類型
- 引用
- 引用即別名
- 引用的定義
 
- 指針
- 獲取對(duì)象的地址
- 指針值
- 利用指針訪問對(duì)象
- 關(guān)鍵概念:一符多義(& 與 *)
- 空指針
- 建議:初始化所有指針
- 賦值和指針
- 其他指針操作
- void* 指針
 
- 理解復(fù)合類型的聲明
- 定義多個(gè)變量
- 指向指針的指針
- 指向指針的引用(指針的別名)(從右向左閱讀理解)
 
 
- const限定符
- 概述
- 初始化和const
- 默認(rèn)狀態(tài)下,const對(duì)象僅在文件內(nèi)有效(const 常量在多文件中使用方法)
 
- const的引用
- 術(shù)語:常量引用是對(duì)const的引用
- 初始化和對(duì)const的引用
- 對(duì)const的引用可能引用一個(gè)并非const的對(duì)象
 
- 指針和const
- const指針
 
- 頂層const
- constexpr和常量表達(dá)式
- constexpr變量
- 字面值類型
- 指針和constexpr
 
 
- 處理類型
- 類型別名
- 指針、常量和類型別名
 
- auto類型說明符
- 復(fù)合類型、常量和 auto
 
- decltype類型指示符
- decltype和引用
 
 
- 自定義數(shù)據(jù)結(jié)構(gòu)
- 定義Sales_data類型
- 類數(shù)據(jù)成員
 
- 使用Sales_data類
- Sales_data對(duì)象讀入數(shù)據(jù)
- 輸出兩個(gè)Sales_data對(duì)象的和
 
- 編寫自己的頭文件
- 預(yù)處理器
 
 
 
數(shù)據(jù)類型決定了程序中數(shù)據(jù)和操作的意義。如下所示的語句:
i = i + j;其含義依賴于i和j的數(shù)據(jù)類型。如果i和j都是整型數(shù),那么這條語句執(zhí)行的就是最普通的加法運(yùn)算。然而,如果i和j是Sales_item類型(上一章內(nèi)容)的數(shù)據(jù)則上述語句把這兩個(gè)對(duì)象的成分相加。
基本內(nèi)置類型
C++定義基本數(shù)據(jù)類型:
- 算術(shù)類型(arithmetic type) - 字符
- 整型數(shù)
- 布爾值
- 浮點(diǎn)數(shù)
 
- 空類型(void) - 空類型不對(duì)應(yīng)具體的值,僅用于一些特殊的場合,例如最常見的是,當(dāng)函數(shù)不返回任何值時(shí)使用空類型作為返回類型。
 
算術(shù)類型
算術(shù)類型分為:
- 整型(integral type,包括字符和布爾類型在內(nèi))
- 浮點(diǎn)型
算術(shù)類型的尺寸(也就是該類型數(shù)據(jù)所占的比特?cái)?shù))在不同機(jī)器上有所差別。
下表列出了C++標(biāo)準(zhǔn)規(guī)定的尺寸的最小值,同時(shí)允許編譯器賦予這些類型更大的尺寸。某一類型所占的比特?cái)?shù)不同,它所能表示的數(shù)據(jù)范圍也不一樣。
| bool | 布爾類型 | 未定義 | 
| char | 字符 | 8位 | 
| wchar_t | 寬字符 | 16位 | 
| char16_t | Unicode字符 | 16位 | 
| char32_t | Unicode字符 | 32位 | 
| short | 短整型 | 16位 | 
| int | 整型 | 16位 | 
| long | 長整型 | 32位 | 
| long long | 長整型 | 64位 | 
| float | 單精度浮點(diǎn)數(shù) | 6位有效數(shù)字 | 
| double | 雙精度浮點(diǎn)數(shù) | 10位有效數(shù)字 | 
| long double | 擴(kuò)展精度浮點(diǎn)數(shù) | 10位有效數(shù)字 | 
bool
布爾類型(bool)的取值是真(true)或者假(false)。
char
C++提供了幾種字符類型,其中多數(shù)支持國際化。基本的字符類型是char,一個(gè) char的空間應(yīng)確保可以存放機(jī)器基本字符集中任意字符對(duì)應(yīng)的數(shù)字值。也就是說,一個(gè)char的大小和一個(gè)機(jī)器字節(jié)一樣。
其他字符類型用于擴(kuò)展字符集,如 wchar_t、char16_t、char32_t。wchar_t類型用于確保可以存放機(jī)器最大擴(kuò)展字符集中的任意一個(gè)字符,類型 charl6_t和char32_t則為Unicode字符集服務(wù)(Unicode是用于表示所有自然語言中字符的標(biāo)準(zhǔn))。
int
除字符和布爾類型之外,其他整型用于表示(可能)不同尺寸的整數(shù)。C++語言規(guī)定一個(gè)int至少和一個(gè)short一樣大,一個(gè)long至少和一個(gè)int一樣大,一個(gè)long long至少和一個(gè)long一樣大。其中,數(shù)據(jù)類型long long是在C++11中新定義的。
float
浮點(diǎn)型可表示單精度、雙精度和擴(kuò)展精度值。C++標(biāo)準(zhǔn)指定了一個(gè)浮點(diǎn)數(shù)有效位數(shù)的最小值,然而大多數(shù)編譯器都實(shí)現(xiàn)了更高的精度。通常,float 以1個(gè)字(32比特)來表示,double 以2個(gè)字(64比特)來表示,long double 以3或4個(gè)字(96或128比特)來表示。一般來說,類型float和 double分別有7和16個(gè)有效位;類型long double則常常被用于有特殊浮點(diǎn)需求的硬件,它的具體實(shí)現(xiàn)不同,精度也各不相同。
帶符號(hào)類型和無符號(hào)類型
除去布爾型和擴(kuò)展的字符型之外,其他整型可以劃分為:
類型int、short、long和 long long 都是帶符號(hào)的,通過在這些類型名前添加unsigned就可以得到無符號(hào)類型,例如unsigned long。類型unsigned int可以縮寫為unsigned。
與其他整型不同,字符型被分為了三種:char、signed char和unsigned char。
特別需要注意的是:類型char和類型signed char并不一樣。盡管字符型有三種,但是字符的表現(xiàn)形式卻只有兩種:帶符號(hào)的和無符號(hào)的。類型char實(shí)際上會(huì)表現(xiàn)為上述兩種形式中的一種,具體是哪種由編譯器決定。
無符號(hào)類型中所有比特都用來存儲(chǔ)值,例如,8比特的unsigned char可以表示0至255區(qū)間內(nèi)的值。
C++標(biāo)準(zhǔn)并沒有規(guī)定帶符號(hào)類型應(yīng)如何表示,但是約定了在表示范圍內(nèi)正值和負(fù)值的量應(yīng)該平衡。因此,8比特的signed char理論上應(yīng)該可以表示-127至127區(qū)間內(nèi)的值,大多數(shù)現(xiàn)代計(jì)算機(jī)將實(shí)際的表示范圍定為-128至127。
內(nèi)置類型的機(jī)器實(shí)現(xiàn)(類型在物理層面上的說明)
計(jì)算機(jī)以比特序列存儲(chǔ)數(shù)據(jù),每個(gè)比特非0即1,例如:
00011011011100010110010000111011…
大多數(shù)計(jì)算機(jī)以2的整數(shù)次冪個(gè)比特作為塊來處理內(nèi)存,可尋址的最小內(nèi)存塊稱為“字節(jié)(byte)”,存儲(chǔ)的基本單元稱為“字(word)”,它通常由幾個(gè)字節(jié)組成。在C++語言中,一個(gè)字節(jié)要至少能容納機(jī)器基本字符集中的字符。
大多數(shù)機(jī)器的字節(jié)由8比特(bit)構(gòu)成(1byte = 8bits),字則由32或64比特構(gòu)成,也就是4或8字節(jié)(1 word = 4 bytes or 8 bytes)。大多數(shù)計(jì)算機(jī)將內(nèi)存中的每個(gè)字節(jié)與一個(gè)數(shù)字(被稱為“地址(address)”)關(guān)聯(lián)起來。
在一個(gè)字節(jié)為8比特、字為32比特的機(jī)器上,我們可能看到一個(gè)字的內(nèi)存區(qū)域如下所示:
其中,左側(cè)是字節(jié)的地址,右側(cè)是字節(jié)中8比特的具體內(nèi)容。
我們能夠使用某個(gè)地址來表示從這個(gè)地址開始的大小不同的比特串,例如,我們可能會(huì)說地址736424的那個(gè)字或者地址736427的那個(gè)字節(jié)。
為了賦予內(nèi)存中某個(gè)地址明確的含義,必須首先知道存儲(chǔ)在該地址的數(shù)據(jù)的類型。類型決定了數(shù)據(jù)所占的比特?cái)?shù)以及該如何解釋這些比特的內(nèi)容:
- 如果位置736424處的對(duì)象類型是float,并且該機(jī)器中float以32比特存儲(chǔ),那么我們就能知道這個(gè)對(duì)象的內(nèi)容占滿了整個(gè)字。這個(gè) float 數(shù)的實(shí)際值依賴于該機(jī)器是如何存儲(chǔ)浮點(diǎn)數(shù)的。
- 如果位置736424處的對(duì)象類型是unsigned char,并且該機(jī)器使用ISO-Latin-1字符集,則該位置處的字節(jié)表示一個(gè)分號(hào)。
Note:這里在物理層面說明一個(gè)變量的類型的作用。
建議:如何選擇類型
和C語言一樣,C的設(shè)計(jì)準(zhǔn)則之一也是盡可能地接近硬件。C++的算術(shù)類型必須滿足各種硬件特質(zhì),所以它們常常顯得繁雜而令人不知所措。事實(shí)上,大多數(shù)程序員能夠(也應(yīng)該)對(duì)數(shù)據(jù)類型的使用做出限定從而簡化選擇的過程。以下是選擇類型的一些經(jīng)驗(yàn)準(zhǔn)則:
-  當(dāng)明確知曉數(shù)值不可能為負(fù)時(shí),選用無符號(hào)類型。(Note:無負(fù)無符,嗚呼嗚呼) 
-  使用int執(zhí)行整數(shù)運(yùn)算。在實(shí)際應(yīng)用中,short常常顯得太小而long一般和int有一樣的尺寸。如果你的數(shù)值超過了int 的表示范圍,選用long long。(Note:取中庸int,按需用short或long) 
-  在算術(shù)表達(dá)式中盡量不要使用 char或bool,而是只有在存放字符或布爾值時(shí)才使用它們。因?yàn)轭愋蚦har在一些機(jī)器上是有符號(hào)的,而在另一些機(jī)器上又是無符號(hào)的,所以如果使用char進(jìn)行運(yùn)算特別容易出問題。如果你需要使用一個(gè)不大的整數(shù),那么明確指定它的類型是signed char或者unsigned char(If you need a tiny integer, explicitly specify either signed char or unsigned char)。(Note:算數(shù)表達(dá)式盡量不用char或bool) 
-  執(zhí)行浮點(diǎn)數(shù)運(yùn)算選用 double,這是因?yàn)閒loat通常精度不夠而且雙精度浮點(diǎn)數(shù)和單精度浮點(diǎn)數(shù)的計(jì)算代價(jià)相差無幾。事實(shí)上,對(duì)于某些機(jī)器來說,雙精度運(yùn)算甚至比單精度還快。long double提供的精度在一般情況下是沒有必要的,況且它帶來的運(yùn)行時(shí)消耗也不容忽視。(Note:浮點(diǎn)數(shù)直接上double) 
類型轉(zhuǎn)換
對(duì)象的類型定義了對(duì)象能包含的數(shù)據(jù)和能參與的運(yùn)算,其中一種運(yùn)算被大多數(shù)類型支持,就是將對(duì)象從一種給定的類型轉(zhuǎn)換(convert)為另一種相關(guān)類型。
當(dāng)在程序的某處我們使用了一種類型而其實(shí)對(duì)象應(yīng)該取另一種類型時(shí),程序會(huì)自動(dòng)進(jìn)行類型轉(zhuǎn)換。
將來第4章會(huì)有更詳細(xì)的介紹類型轉(zhuǎn)換:
此處,有必要說明當(dāng)給某種類型的對(duì)象強(qiáng)行賦了另一種類型的值時(shí),到底會(huì)發(fā)生什么。
bool b = 42; // b is true /* 當(dāng)我們把一個(gè)非布爾類型的算術(shù)值賦給布爾類型時(shí),初始值為О則結(jié)果為false,否則結(jié)果為true。 */int i = b; // i has value 1 /* 當(dāng)我們把一個(gè)布爾值賦給非布爾類型時(shí),初始值為false 則結(jié)果為0,初始值為true則結(jié)果為1。 */i = 3.14; // i has value 3 /* 當(dāng)我們把一個(gè)浮點(diǎn)數(shù)賦給整數(shù)類型時(shí),進(jìn)行了近似處理。結(jié)果值將僅保留浮點(diǎn)數(shù)中小數(shù)點(diǎn)之前的部分(截?cái)嗳≌麛?shù)部分)。 */double pi = i; // pi has value 3.0 /* 當(dāng)我們把一個(gè)整數(shù)值賦給浮點(diǎn)類型時(shí),小數(shù)部分記為0。如果該整數(shù)所占的空間超過了浮點(diǎn)類型的容量,精度可能有損失。 */unsigned char c = -1; // assuming 8-bit chars, c has value 255 /* 當(dāng)我們賦給無符號(hào)類型一個(gè)超出它表示范圍的值時(shí),結(jié)果是初始值對(duì)無符號(hào)類型表示數(shù)值總數(shù)取模后的余數(shù)。例如,8比特大小的unsigned char可以表示0至255區(qū)間內(nèi)的值,如果我們賦了一個(gè)區(qū)間以外的值,則實(shí)際的結(jié)果是該值對(duì)256取模后所得的余數(shù)。因此,把-1賦給8比特大小的unsigned char所得的結(jié)果是255。Note:x mod y = x - y * ?x / y? (from:https://blog.csdn.net/weixin_43435790/article/details/83181319)-1 mod 256 = -1 - 256 * ?-1 / 256? = -1 - 256 * (-1) = 255個(gè)人認(rèn)為以底層知識(shí)更容易理解這次轉(zhuǎn)換 計(jì)算機(jī)保存數(shù)值都以補(bǔ)碼的形式保存。-1的補(bǔ)碼是11111111,但char的類型是無符號(hào)整形數(shù),編譯器就把11111111當(dāng)作無符號(hào)整形來輸出。11111111當(dāng)作正數(shù)時(shí)值就是255。 */signed char c2 = 256; // assuming 8-bit chars, the value of c2 is undefined /* 當(dāng)我們賦給帶符號(hào)類型一個(gè)超出它表示范圍的值時(shí),結(jié)果是未定義的(undefined)。此時(shí),程序可能繼續(xù)工作、可能崩潰,也可能生成垃圾數(shù)據(jù)。 */類型所能表示的值的范圍決定了轉(zhuǎn)換的過程。
建議:避免無法預(yù)知和依賴于實(shí)現(xiàn)環(huán)境的行為
無法預(yù)知的行為源于編譯器無須(有時(shí)是不能)檢測的錯(cuò)誤。即使代碼編譯通過了,如果程序執(zhí)行了一條未定義的表達(dá)式,仍有可能產(chǎn)生錯(cuò)誤。
不幸的是,在某些情況和/或某些編譯器下,含有無法預(yù)知行為的程序也能正確執(zhí)行。但是我們卻無法保證同樣一個(gè)程序在別的編譯器下能正常工作,甚至已經(jīng)編譯通過的代碼再次執(zhí)行也可能會(huì)出錯(cuò)。此外,也不能認(rèn)為這樣的程序?qū)σ唤M輸入有效,對(duì)另一組輸入就一定有效。
程序也應(yīng)該盡量避免依賴于實(shí)現(xiàn)環(huán)境的行為。如果我們把 int的尺寸看成是一個(gè)確定不變的已知值,那么這樣的程序就稱作不可移植的(nonportable)。當(dāng)程序移植到別的機(jī)器上后,依賴于實(shí)現(xiàn)環(huán)境的程序就可能發(fā)生錯(cuò)誤。要從過去的代碼中定位這類錯(cuò)誤可不是一件輕松愉快的工作。
算術(shù)表達(dá)式里使用布爾值
當(dāng)在程序的某處使用了一種算術(shù)類型的值而其實(shí)所需的是另一種類型的值時(shí),編譯器同樣會(huì)執(zhí)行上述的類型轉(zhuǎn)換。
例如,如果我們使用了一個(gè)非布爾值作為條件,那么它會(huì)被自動(dòng)地轉(zhuǎn)換成布爾值,這一做法和把非布爾值賦給布爾變量時(shí)的操作完全一樣:
int i = 42; if (i) // if條件的值將為truei = 0;如果i的值為0,則條件的值為false;i的所有其他取值(非0)都將使條件為true。
以此類推,如果我們把一個(gè)布爾值用在算術(shù)表達(dá)式里,則它的取值非0即1,所以一般不宜在算術(shù)表達(dá)式里使用布爾值。
含有無符號(hào)類型的表達(dá)式(帶符號(hào)數(shù)會(huì)自動(dòng)地轉(zhuǎn)換成無符號(hào)數(shù))
記住:無符號(hào)類型與有符號(hào)類型混合表達(dá)式中,帶符號(hào)數(shù)會(huì)自動(dòng)地轉(zhuǎn)換成無符號(hào)數(shù)。
盡管我們不會(huì)故意給無符號(hào)對(duì)象賦一個(gè)負(fù)值,卻可能(特別容易)寫出這么做的代碼。
例一:
例如,當(dāng)一個(gè)算術(shù)表達(dá)式中既有無符號(hào)數(shù)又有int 值時(shí),那個(gè)int值就會(huì)轉(zhuǎn)換成無符號(hào)數(shù)。把int轉(zhuǎn)換成無符號(hào)數(shù)的過程和把int直接賦給無符號(hào)變量一樣:
unsigned u = 10; int i = -42; std::cout << i + i << std::endl; // prints -84 std::cout << u + i << std::endl; // if 32-bit ints, prints 4294967264 std::cout << i + u << std::endl; // also prints 4294967264例二:
當(dāng)從無符號(hào)數(shù)中減去一個(gè)值時(shí),不管這個(gè)值是不是無符號(hào)數(shù),我們都必須確保結(jié)果不能是一個(gè)負(fù)值:
unsigned u1 = 42, u2 = 10; std::cout << u1 - u2 << std::endl; // ok: result is 32 std::cout << u2 - u1 << std::endl; // ok: but the result will wrap around, 4294967264例三:
無符號(hào)數(shù)不會(huì)小于0這一事實(shí)同樣關(guān)系到循環(huán)的寫法。例如,寫一個(gè)循環(huán),通過控制變量遞減的方式把從10到0的數(shù)字降序輸出。這個(gè)循環(huán)可能類似于下面的形式:
for (int i = 10; i >= 0; --i)std::cout << i << std::endl;可能你會(huì)覺得反正也不打算輸出負(fù)數(shù),可以用無符號(hào)數(shù)來重寫這個(gè)循環(huán)。
然而,這個(gè)不經(jīng)意的改變卻意味著死循環(huán):
// WRONG: u can never be less than 0; the condition will always succeed for (unsigned u = 10; u >= 0; --u)std::cout << u << std::endl;來看看當(dāng)u等于0時(shí)發(fā)生了什么,這次迭代輸出0,然后繼續(xù)執(zhí)行for語句里的表達(dá)式。表達(dá)式–u從u當(dāng)中減去1,得到的結(jié)果-1并不滿足無符號(hào)數(shù)的要求,此時(shí)像所有表示范圍之外的其他數(shù)字一樣,-1被自動(dòng)地轉(zhuǎn)換成一個(gè)合法的無符號(hào)數(shù)。假設(shè) int類型占32位,則當(dāng)u等于0時(shí),–u的結(jié)果將會(huì)是4294967295。
一種解決的辦法是(不如改回int ╮(╯▽╰)╭),用while語句來代替for語句,因?yàn)榍罢咦屛覀兡軌蛟谳敵鲎兞恐?#xff08;而非之后)先減去1:
unsigned u = 11; // start the loop one past the first element we want to print while (u > 0) {--u; // decrement first, so that the last iteration will print 0std::cout << u << std::endl; }改寫后的循環(huán)先執(zhí)行對(duì)循環(huán)控制變量減1的操作,這樣最后一次迭代時(shí),進(jìn)入循環(huán)的u值為1。此時(shí)將其減1,則這次迭代輸出的數(shù)就是0:下一次再檢驗(yàn)循環(huán)條件時(shí),u的值等于0而無法再進(jìn)入循環(huán)。
因?yàn)槲覀円茸鰷p1的操作,所以初始化u的值應(yīng)該比要輸出的最大值大1。這里,u初始化為11,輸出的最大數(shù)是10。(也就預(yù)大一位)。
提示:切勿混用帶符號(hào)類型和無符號(hào)類型
如果表達(dá)式里既有帶符號(hào)類型又有無符號(hào)類型,當(dāng)帶符號(hào)類型取值為負(fù)時(shí)會(huì)出現(xiàn)異常結(jié)果,這是因?yàn)?strong>帶符號(hào)數(shù)會(huì)自動(dòng)地轉(zhuǎn)換成無符號(hào)數(shù)。
例如,在一個(gè)形如a * b的式子中,如果a=-1,b=1,而且a和b都是int,則表達(dá)式的值顯然為-1。
然而,如果a是int,而b是unsigned,則結(jié)果須視在當(dāng)前機(jī)器上int所占位數(shù)而定。在我們的環(huán)境里,結(jié)果是4294967295。
字面值常量
一個(gè)形如42的值被稱作字面值常量(literal)。每個(gè)字面值常量都對(duì)應(yīng)一種數(shù)據(jù)類型,字面值常量的形式和值決定了它的數(shù)據(jù)類型。
整型和浮點(diǎn)型字面值
我們可以將整型字面值寫作十進(jìn)制數(shù)、八進(jìn)制數(shù)或十六進(jìn)制數(shù)的形式。
以0開頭的整數(shù)代表八進(jìn)制數(shù),以0x或0x開頭的代表十六進(jìn)制數(shù)。
例如,我們能用下面的任意一種形式來表示數(shù)值20:
- 20 十進(jìn)制
- 024 八進(jìn)制
- 0x14 十六進(jìn)制
整型字面值具體的數(shù)據(jù)類型由它的值和符號(hào)決定。默認(rèn)情況下,十進(jìn)制字面值是帶符號(hào)數(shù),八進(jìn)制和十六進(jìn)制字面值既可能是帶符號(hào)的也可能是無符號(hào)的。十進(jìn)制字面值的類型是int、long和 long long 中尺寸最小的那個(gè)(例如,三者當(dāng)中最小是int),當(dāng)然前提是這種類型要能容納下當(dāng)前的值。(帶負(fù)號(hào)的八進(jìn)制、十六進(jìn)數(shù)少見)。
八進(jìn)制和十六進(jìn)制字面值的類型是能容納其數(shù)值的int、unsigned int、long、unsigned long、long long和 unsigned long long中的尺寸最小者。如果一個(gè)字面值連與之關(guān)聯(lián)的最大的數(shù)據(jù)類型都放不下,將產(chǎn)生錯(cuò)誤。類型short沒有對(duì)應(yīng)的字面值。
以U、L等后綴可以代表相應(yīng)的字面值類型。
盡管整型字面值可以存儲(chǔ)在帶符號(hào)數(shù)據(jù)類型中,但嚴(yán)格來說,十進(jìn)制字面值不會(huì)是負(fù)數(shù)。如果我們使用了一個(gè)形如-42的負(fù)十進(jìn)制字面值,那個(gè)負(fù)號(hào)并不在字面值之內(nèi),它的作用僅僅是對(duì)字面值取負(fù)值而已。
浮點(diǎn)型字面值表現(xiàn)為一個(gè)小數(shù)或以科學(xué)計(jì)數(shù)法表示的指數(shù),其中指數(shù)部分用E或e標(biāo)識(shí):
- 3.14159
- 3.14159E0
- 0.
- 0e0
- .001
默認(rèn)的,浮點(diǎn)型字面值是一個(gè)double,我們可以用后綴F等來表示其他浮點(diǎn)型。
字符和字符串字面值
由單引號(hào)括起來的一個(gè)字符稱為char型字面值,雙引號(hào)括起來的零個(gè)或多個(gè)字符則構(gòu)成字符串型字面值。
- ‘a(chǎn)’:字符字面值
- "Hello world! ":字符串字面值
字符串字面值的類型實(shí)際上是由常量字符構(gòu)成的數(shù)組(array)。編譯器在每個(gè)字符串的結(jié)尾處添加一個(gè)空字符(’\0’),因此,字符串字面值的實(shí)際長度要比它的內(nèi)容多1。
例如,
- 字面值’A‘表示的就是單獨(dú)的字符A
- 字符串"A"則代表了一個(gè)字符的數(shù)組,該數(shù)組包含兩個(gè)字符:一個(gè)是字母A、另一個(gè)是空字符(’\0’)。
如果兩個(gè)字符串字面值位置緊鄰且僅由空格、縮進(jìn)和換行符分隔,則它們實(shí)際上是一個(gè)整體。
當(dāng)書寫的字符串字面值比較長,寫在一行里不太合適時(shí),就可以采取分開書寫的方式:
//分多行書寫的字符串字面值 std::cout<< "a really, really long string literal ""that spans two lines" << std::endl; //與Java相比,不用+號(hào)轉(zhuǎn)義序列
有兩類字符程序員不能直接使用:
對(duì)于特殊含義的字符需要用到轉(zhuǎn)義序列(escape sequence),轉(zhuǎn)義序列均以反斜線作為開始,C++語言規(guī)定的轉(zhuǎn)義序列包括:
-  換行符 \n 
-  縱向制表符 \v 
-  反斜線 \ \ 
-  回車符 \r 
-  橫向制表符 \t 
-  退格符 \b 
-  問號(hào) ? 
-  進(jìn)紙符 \f 
-  報(bào)警(響鈴)符 \a 
-  雙引號(hào) \ " 
-  單引號(hào) \ ’ 
在程序中,上述轉(zhuǎn)義序列被當(dāng)作一個(gè)字符使用:
std::cout << '\n'; // prints a newline std::cout << "\tHi!\n"; // prints a tab followd by "Hi!" and a newline(偏僻語法,不能一見知意,少用)我們也可以使用泛化的轉(zhuǎn)義序列,其形式是\x后緊跟1個(gè)或多個(gè)十六進(jìn)制數(shù)字,或者\(yùn)后緊跟1個(gè)、2個(gè)或3個(gè)八進(jìn)制數(shù)字,其中數(shù)字部分表示的是字符對(duì)應(yīng)的數(shù)值。假設(shè)使用的是Latin-1字符集,以下是一些示例:
- \7 (bell)
- \12 (newline)
- \40 (blank)
- \0 (null)
- \115 (‘M’)
- \x4d (‘M’)
我們可以像使用普通字符那樣使用C++語言定義的轉(zhuǎn)義序列:
std::cout << "Hi \x4dO\115!\n"; // prints Hi MOM! followed by a newline std::cout << '\115' << '\n'; // prints M followed by a newline注意,如果反斜線\后面跟著的八進(jìn)制數(shù)字超過3個(gè),只有前3個(gè)數(shù)字與\構(gòu)成轉(zhuǎn)義序列。
例如,"\1234"表示2個(gè)字符,即八進(jìn)制數(shù)123對(duì)應(yīng)的字符以及字符4。
相反,\x要用到后面跟著的所有數(shù)字,例如,"\x1234"表示一個(gè)16位的字符,該字符由這4個(gè)十六進(jìn)制數(shù)所對(duì)應(yīng)的比特唯一確定。
因?yàn)榇蠖鄶?shù)機(jī)器的char型數(shù)據(jù)占8位,所以上面這個(gè)例子可能會(huì)報(bào)錯(cuò)。一般來說,超過8位的十六進(jìn)制字符都是與U等前綴作為開頭的擴(kuò)展字符集一起使用的。
通過前后綴指定字面值的類型
通過添加前綴和后綴,可以改變整型、浮點(diǎn)型和字符型字面值的默認(rèn)類型。
L'a' // wide character literal, type is wchar_t u8"hi!" // utf-8 string literal (utf-8 encodes a Unicode character in 8 bits) 42ULL // unsigned integer literal, type is unsigned long long 1E-3F // single-precision floating-point literal, type is float 3.14159L // extended-precision floating-point literal, type is long double(不要用字母l作后綴)當(dāng)使用一個(gè)長整型字面值時(shí),請(qǐng)使用大寫字母L來標(biāo)記,因?yàn)樾懽帜竘和數(shù)字1太容易混淆了。
通過添加前綴或后綴指定字面值的類型
字符和字符串字面值
| u | Unicode 16字符 | char16_t | 
| U | Unicode 32字符 | char32_t | 
| L | 寬字符 | wchar_t | 
| u8 | UTF8(僅用于字符串字面常量) | char | 
整型字面值
| u or U | unsigned | 
| l or L | long | 
| ll or LL | long long | 
浮點(diǎn)數(shù)字面值
| f or F | float | 
| l or L | long double | 
對(duì)于一個(gè)整型字面值來說,我們能分別指定它是否帶符號(hào)以及占用多少空間。如果后綴中有u,則該字面值屬于無符號(hào)類型,也就是說,以u(píng)為后綴的十進(jìn)制數(shù)、八進(jìn)制數(shù)或十六進(jìn)制數(shù)都將從unsigned int、unsigned long和 unsigned long long中選擇能匹配的空間最小的一個(gè)作為其數(shù)據(jù)類型。
如果后綴中有L,則字面值的類型至少是long; 如果后綴中有LL,則字面值的類型將是long long和unsigned long long 中的一種。顯然我們可以將u與工或LL合在一起使用。例如,以UL為后綴的字面值的數(shù)據(jù)類型將根據(jù)具體數(shù)值情況或者取unsigned long,或者取unsigned long long。
布爾字面值和指針字面值
true和false是布爾類型的字面值:
bool test = false;nullptr是指針字面值。
變量
變量提供一個(gè)具名的、可供程序操作的存儲(chǔ)空間。
C++中的每個(gè)變量都有其數(shù)據(jù)類型,數(shù)據(jù)類型決定著變量:
- 所占內(nèi)存空間的大小和布局方式、(內(nèi)存大小)
- 該空間能存儲(chǔ)的值的范圍,(范圍)
- 以及變量能參與的運(yùn)算。(運(yùn)算)
對(duì)C++程序員來說,“變量(variable)”和“對(duì)象(object)”一般可以互換使用。
變量定義
變量定義的基本形式是:首先是類型說明符(type specifier),隨后緊跟由一個(gè)或多個(gè)變量名組成的列表,其中變量名以逗號(hào)分隔,最后以分號(hào)結(jié)束。
列表中每個(gè)變量名的類型都由類型說明符指定,定義時(shí)還可以為一個(gè)或多個(gè)變量賦初值:
int sum = 0, value, // sum, value, and units_sold have type intunits_sold = 0; // sum and units_sold have initial value 0Sales_item item; // item has type Sales_item// string is a library type, representing a variable-length sequence of characters std::string book("0-201-78345-X"); // book initialized from string literalbook的定義用到了庫類型std: :string,像iostream一樣,string 也是在命名空間std中定義的,我們將在第3章中對(duì)string類型做更詳細(xì)的介紹。眼下,只需了解string是一種表示可變長字符序列的數(shù)據(jù)類型即可。
C++庫提供了幾種初始化string對(duì)象的方法,其中一種是把字面值拷貝給string對(duì)象,因此在上例中,book被初始化為0-201-78345-X。
術(shù)語:何為對(duì)象?
C++程序員們?cè)诤芏鄨龊隙紩?huì)使用對(duì)象(object)這個(gè)名詞。通常情況下,對(duì)象是指一塊能存儲(chǔ)數(shù)據(jù)并具有某種類型的內(nèi)存空間。
- 一些人僅在與類有關(guān)的場景下才使用“對(duì)象”這個(gè)詞。
- 另一些人則把已命名的對(duì)象和未命名的對(duì)象區(qū)分開來,他們把命名了的對(duì)象叫做變量。
- 還有一些人把對(duì)象和值區(qū)分開來,其中對(duì)象指能被程序修改的數(shù)據(jù),而值(value)指只讀的數(shù)據(jù)。
本書遵循大多數(shù)人的習(xí)慣用法,即認(rèn)為對(duì)象是具有某種數(shù)據(jù)類型的內(nèi)存空間。我們?cè)谑褂脤?duì)象這個(gè)詞時(shí),并不嚴(yán)格區(qū)分是類還是內(nèi)置類型,也不區(qū)分是否命名或是否只讀。
(記住:對(duì)象是具有某種數(shù)據(jù)類型的內(nèi)存空間)
初始值
當(dāng)對(duì)象在創(chuàng)建時(shí)獲得了一個(gè)特定的值,我們說這個(gè)對(duì)象被初始化(initialized)了。
用于初始化變量的值可以是任意復(fù)雜的表達(dá)式。
當(dāng)一次定義了兩個(gè)或多個(gè)變量時(shí),對(duì)象的名字隨著定義也就馬上可以使用了。
因此在同一條定義語句中,可以用先定義的變量值去初始化后定義的其他變量。
// ok: price is defined and initialized before it is used to initialize discount double price = 109.99, discount = price * 0.16; // ok: call applyDiscount and use the return value to initialize salePrice double salePrice = applyDiscount(price, discount);在C++語言中,初始化是一個(gè)異常復(fù)雜的問題,我們也將反復(fù)討論這個(gè)問題。
很多程序員對(duì)于用等號(hào)=來初始化變量的方式倍感困惑,這種方式容易讓人認(rèn)為初始化是賦值的一種。事實(shí)上在C++語言中,初始化和賦值是兩個(gè)完全不同的操作。然而在很多編程語言中二者的區(qū)別幾乎可以忽略不計(jì),即使在C++語言中有時(shí)這種區(qū)別也無關(guān)緊要,所以人們特別容易把二者混為一談。
需要強(qiáng)調(diào)的是,這個(gè)概念至關(guān)重要,我們也將在后面不止一次提及這一點(diǎn)。
初始化不是賦值,初始化的含義是創(chuàng)建變量時(shí)賦予其一個(gè)初始值,而賦值的含義是把對(duì)象的當(dāng)前值擦除,而以一個(gè)新值來替代。
(Note: 初始化和賦值是兩碼事,即使它們很相似。當(dāng)某一變量首次用=號(hào)的就是初始化,其他地方用=號(hào)的就是賦值)
列表初始化
C++語言定義了初始化的好幾種不同形式,這也是初始化問題復(fù)雜性的一個(gè)體現(xiàn)。例如,要想定義一個(gè)名為units_sold的int變量并初始化為0,以下的4條語句都可以做到這一點(diǎn):
int units_sold = 0; int units_sold = {0}; int units_sold{0}; int units_sold(0);作為C++11新標(biāo)準(zhǔn)的一部分,用花括號(hào)來初始化變量得到了全面應(yīng)用,而在此之前,這種初始化的形式僅在某些受限的場合下才能使用。出于3.3.1節(jié)將要介紹的原因,這種初始化的形式被稱為列表初始化(list initialization)。現(xiàn)在,無論是初始化對(duì)象還是某些時(shí)候?yàn)閷?duì)象賦新值,都可以使用這樣一組由花括號(hào)括起來的初始值了。
當(dāng)用于內(nèi)置類型的變量時(shí),這種初始化形式有一個(gè)重要特點(diǎn):如果我們使用列表初始化且初始值存在丟失信息的風(fēng)險(xiǎn),則編譯器將報(bào)錯(cuò):
long double ld = 3.1415926536; int a{ld}, b = {ld}; // error: narrowing conversion required int c(ld), d = ld; // ok: but value will be truncated使用long double的值初始化int變量時(shí)可能丟失數(shù)據(jù),所以編譯器拒絕了a和b的初始化請(qǐng)求。
其中,至少ld的小數(shù)部分會(huì)丟失掉,而且int也可能存不下ld的整數(shù)部分。
(Note: 列表初始化變量轉(zhuǎn)型有數(shù)據(jù)丟失報(bào)錯(cuò)功能???)
默認(rèn)初始化
如果定義變量時(shí)沒有指定初值,則變量被默認(rèn)初始化(default initialized),此時(shí)變量被賦予了“默認(rèn)值”。默認(rèn)值到底是什么由變量類型決定,同時(shí)定義變量的位置也會(huì)對(duì)此有影響。
如果是內(nèi)置類型的變量未被顯式初始化,它的值由定義的位置決定。定義于任何函數(shù)體之外的變量被初始化為0。
-  一種例外情況是,定義在函數(shù)體內(nèi)部的內(nèi)置類型變量將不被初始化(uninitialized)。 
-  一個(gè)未被初始化的內(nèi)置類型變量的值是未定義的,如果試圖拷貝或以其他形式訪問此類值將引發(fā)錯(cuò)誤。 
每個(gè)類各自決定其初始化對(duì)象的方式。而且,是否允許不經(jīng)初始化就定義對(duì)象也由類自己決定。如果類允許這種行為,它將決定對(duì)象的初始值到底是什么。
絕大多數(shù)類都支持無須顯式初始化而定義對(duì)象,這樣的類提供了一個(gè)合適的默認(rèn)值。例如,String類規(guī)定如果沒有指定初值則生成一個(gè)空串:
std::string empty;//empty非顯式地初始化為一個(gè)空串 Sales_item item;//被默認(rèn)初始化的sales_item對(duì)象一些類要求每個(gè)對(duì)象都顯式初始化,此時(shí)如果創(chuàng)建了一個(gè)該類的對(duì)象而未對(duì)其做明確的初始化操作,將引發(fā)錯(cuò)誤。
定義于函數(shù)體內(nèi)的內(nèi)置類型的對(duì)象如果沒有初始化,則其值未定義。類的對(duì)象如果沒有顯式地初始化,則其值由類確定。
提示:未初始化變量引發(fā)運(yùn)行時(shí)故障
未初始化的變量含有一個(gè)不確定的值,使用未初始化變量的值是一種錯(cuò)誤的編程行為并且很難調(diào)試。盡管大多數(shù)編譯器都能對(duì)一部分使用未初始化變量的行為提出警告,但嚴(yán)格來說,編譯器并未被要求檢查此類錯(cuò)誤。
使用未初始化的變量將帶來無法預(yù)計(jì)的后果。有時(shí)我們足夠幸運(yùn),一訪問此類對(duì)象程序就崩潰并報(bào)錯(cuò),此時(shí)只要找到崩潰的位置就很容易發(fā)現(xiàn)變量沒被初始化的問題。另外一些時(shí)候,程序會(huì)一直執(zhí)行完并產(chǎn)生錯(cuò)誤的結(jié)果。更糟糕的情況是,程序結(jié)果時(shí)對(duì)時(shí)錯(cuò)、無法把握。而且,往無關(guān)的位置添加代碼還會(huì)導(dǎo)致我們誤以為程序?qū)α?#xff0c;其實(shí)結(jié)果仍舊有錯(cuò)。
建議初始化每一個(gè)內(nèi)置類型的變量。雖然并非必須這么做,但如果我們不能確保初始化后程序安全,那么這么做不失為一種簡單可靠的方法。
(Note: 創(chuàng)建一個(gè)變量都初始化吧。保安全)
變量聲明和定義的關(guān)系
為了允許把程序拆分成多個(gè)邏輯部分來編寫,C++語言支持分離式編譯(separate compilation)機(jī)制,該機(jī)制允許將程序分割為若干個(gè)文件,每個(gè)文件可被獨(dú)立編譯。
如果將程序分為多個(gè)文件,則需要有在文件間共享代碼的方法。例如,一個(gè)文件的代碼可能需要使用另一個(gè)文件中定義的變量。一個(gè)實(shí)際的例子是std: :cout和std::cin,它們定義于標(biāo)準(zhǔn)庫,卻能被我們寫的程序使用。
為了支持分離式編譯,C++語言將聲明和定義區(qū)分開來。
- 聲明(declaration)使得名字為程序所知,一個(gè)文件如果想使用別處定義的名字則必須包含對(duì)那個(gè)名字的聲明。
- 定義(definition)負(fù)責(zé)創(chuàng)建與名字關(guān)聯(lián)的實(shí)體。
變量聲明規(guī)定了變量的類型和名字,在這一點(diǎn)上定義與之相同。但是除此之外,定義還申請(qǐng)存儲(chǔ)空間,也可能會(huì)為變量賦一個(gè)初始值。
如果想聲明一個(gè)變量而非定義它,就在變量名前添加關(guān)鍵字extern,而且不要顯式地初始化變量:
extern int i; // declares but does not define i int j; // declares and defines j任何包含了顯式初始化的聲明即成為定義。我們能給由extern關(guān)鍵字標(biāo)記的變量賦一個(gè)初始值,但是這么做也就抵消了extern的作用。extern語句如果包含初始值就不再是聲明,而變成定義了:
extern double pi = 3.1416; //定義在函數(shù)體內(nèi)部,如果試圖初始化一個(gè)由extern關(guān)鍵字標(biāo)記的變量,將引發(fā)錯(cuò)誤。
聲明和定義的區(qū)別看起來也許微不足道,但實(shí)際上卻非常重要。(重中之重)如果要在多個(gè)文件中使用同一個(gè)變量,就必須將聲明和定義分離。此時(shí),變量的定義必須出現(xiàn)在且只能出現(xiàn)在一個(gè)文件中,而其他用到該變量的文件必須對(duì)其進(jìn)行聲明,卻絕對(duì)不能重復(fù)定義。
(Note: 變量能且只能被定義一次,但是可以被多次聲明(只為使用它)。)
關(guān)于C++語言對(duì)分離式編譯的支持在將來做更詳細(xì)介紹。
關(guān)鍵概念:靜態(tài)類型
C++是一種靜態(tài)類型(statically typed)語言,其含義是在編譯階段檢查類型。其中,檢查類型的過程稱為類型檢查(type checking)。
對(duì)象的類型決定了對(duì)象所能參與的運(yùn)算。在C++語言中,編譯器負(fù)責(zé)檢查數(shù)據(jù)類型是否支持要執(zhí)行的運(yùn)算,如果試圖執(zhí)行類型不支持的運(yùn)算,編譯器將報(bào)錯(cuò)并且不會(huì)生成可執(zhí)行文件。
程序越復(fù)雜,靜態(tài)類型檢查越有助于發(fā)現(xiàn)問題。然而,前提是編譯器必須知道每一個(gè)實(shí)體對(duì)象的類型,這就要求我們?cè)谑褂媚硞€(gè)變量之前必須聲明其類型。
標(biāo)識(shí)符
C++的標(biāo)識(shí)符(identifier)由字母、數(shù)字和下畫線組成,其中必須以字母或下畫線開頭。標(biāo)識(shí)符的長度沒有限制,但是對(duì)大小寫字母敏感:
// defines four different int variables int somename, someName, SomeName, SOMENAME;下面兩表所示,C++語言保留了一些名字供語言本身使用,這些名字不能被用作標(biāo)識(shí)符。
同時(shí),C++也為標(biāo)準(zhǔn)庫保留了一些名字。用戶自定義的標(biāo)識(shí)符中不能連續(xù)出現(xiàn)兩個(gè)下畫線,也不能以下畫線緊連大寫字母開頭。此外,定義在函數(shù)體外的標(biāo)識(shí)符不能以下畫線開頭。
C++關(guān)鍵字
| alignas | continue | friend | register | true | 
| alignof | decltype | goto | reinterpret_cast | try | 
| asm | default | if | return | typedef | 
| auto | delete | inline | short | typeid | 
| bool | do | int | signed | typename | 
| break | double | long | sizeof | union | 
| case | dynamic_cast | mutable | static | unsigned | 
| catch | else | namespace | static_assert | using | 
| char | enum | new | static_cast | virtual | 
| char16_t | explicit | noexcept | struct | void | 
| char32_t | export | nullptr | switch | volatile | 
| class | extern | operator | template | wchar_t | 
| const | false | private | this | while | 
| constexpr | float | protected | thread_local | |
| const_cast | for | public | throw | 
C++操作符替代名
| and | compl | or_eq | 
| and_eq | not | xor | 
| bitand | not_eq | xor_eq | 
| bitor | or | 
變量命名規(guī)范
變量命名有許多約定俗成的規(guī)范,下面的這些規(guī)范能有效提高程序的可讀性:
- 標(biāo)識(shí)符要能體現(xiàn)實(shí)際含義。
- 變量名一般用小寫字母,如 index,不要使用Index或INDEX。
- 用戶自定義的類名一般以大寫字母開頭,如 Sales_item。
- 如果標(biāo)識(shí)符由多個(gè)單詞組成,則單詞間應(yīng)有明顯區(qū)分,如 student_loan或studentLoan,不要使用studentloan。
對(duì)于命名規(guī)范來說,若能堅(jiān)持,必將有效。
(Note:使用Java駝峰命名法吧)
名字的作用域
不論是在程序的什么位置,使用到的每個(gè)名字都會(huì)指向一個(gè)特定的實(shí)體:變量、函數(shù)、類型等。然而,同一個(gè)名字如果出現(xiàn)在程序的不同位置,也可能指向的是不同實(shí)體。
作用域(scope)是程序的一部分,在其中名字有其特定的含義。C++語言中大多數(shù)作用域都以花括號(hào)分隔。
同一個(gè)名字在不同的作用域中可能指向不同的實(shí)體。名字的有效區(qū)域始于名字的聲明語句,以聲明語句所在的作用域末端為結(jié)束。
#include <iostream> int main() {int sum = 0;// sum values from 1 through 10 inclusivefor (int val = 1; val <= 10; ++val)sum += val;// equivalent to sum = sum + valstd::cout << "Sum of 1 to 10 inclusive is "<< sum << std::endl;return 0; }這段程序定義了3個(gè)名字: main、sum和val,同時(shí)使用了命名空間名字std,該空間提供了2個(gè)名字cout和 cin供程序使用。
名字main定義于所有花括號(hào)之外,它和其他大多數(shù)定義在函數(shù)體之外的名字一樣擁有全局作用域(global scope)。一旦聲明之后,全局作用域內(nèi)的名字在整個(gè)程序的范圍內(nèi)都可使用。
名字sum定義于main函數(shù)所限定的作用域之內(nèi),從聲明sum開始直到main函數(shù)結(jié)束為止都可以訪問它,但是出了main函數(shù)所在的塊就無法訪問了,因此說變量sum擁有塊作用域(block scope)。名字val定義于 for語句內(nèi),在for語句之內(nèi)可以訪問val,但是在main函數(shù)的其他部分就不能訪問它了。
建議:當(dāng)你第一次使用變量時(shí)再定義它
一般來說,在對(duì)象第一次被使用的地方附近定義它是一種好的選擇,因?yàn)檫@樣做有助于更容易地找到變量的定義。更重要的是,當(dāng)變量的定義與它第一次被使用的地方很近時(shí),我們也會(huì)賦給它一個(gè)比較合理的初始值。
嵌套的作用域
作用域能彼此包含,被包含(或者說被嵌套)的作用域稱為內(nèi)層作用域(inner scope),包含著別的作用域的作用域稱為外層作用域(outer scope)。
作用域中一旦聲明了某個(gè)名字,它所嵌套著的所有作用域中都能訪問該名字。同時(shí),允許在內(nèi)層作用域中重新定義外層作用域已有的名字:
#include <iostream> // Program for illustration purposes only: It is bad style for a function // to use a global variable and also define a local variable with the same name int reused = 42; // reused has global scope int main() {int unique = 0; // unique has block scope// output #1: uses global reused; prints 42 0std::cout << reused << " " << unique << std::endl;int reused = 0;// new, local object named reused hides global reused// output #2: uses local reused; prints 0 0std::cout << reused << " " << unique << std::endl;// output #3: explicitly requests the global reused; prints 42 0std::cout << ::reused << " " << unique << std::endl;return 0; }-  輸出#1出現(xiàn)在局部變量 reused定義之前,因此這條語句使用全局作用域中定義的名字reused,輸出42 0。 
-  輸出#2發(fā)生在局部變量reused定義之后,此時(shí)局部變量reused正在作用域內(nèi)(in scope),因此第二條輸出語句使用的是局部變量reused而非全局變量,輸出0 0。 
-  輸出#3使用作用域操作符來覆蓋默認(rèn)的作用域規(guī)則,因?yàn)槿肿饔糜虮旧聿]有名字,所以當(dāng)作用域操作符的左側(cè)為空時(shí),向全局作用域發(fā)出請(qǐng)求獲取作用域操作符右側(cè)名字對(duì)應(yīng)的變量。結(jié)果是,第三條輸出語句使用全局變量reused,輸出42 0。 
如果函數(shù)有可能用到某全局變量,則不宜再定義一個(gè)同名的局部變量。(各個(gè)變量盡量在可控范圍有獨(dú)一名字)
復(fù)合類型
復(fù)合類型( compound type)是指基于其他類型定義的類型。C++語言有幾種復(fù)合類型,本章將介紹其中的兩種:引用和指針。
與我們已經(jīng)掌握的變量聲明相比,定義復(fù)合類型的變量要復(fù)雜很多。
上一節(jié)提到,一條簡單的聲明語句由一個(gè)數(shù)據(jù)類型和緊隨其后的一個(gè)變量名列表組成。
其實(shí)更通用的描述是,一條聲明語句由一個(gè)基本數(shù)據(jù)類型(base type)和緊隨其后的一個(gè)聲明符(declarator)列表組成。每個(gè)聲明符命名了一個(gè)變量并指定該變量為與基本數(shù)據(jù)類型有關(guān)的某種類型。
(Note: ,1條聲明語句=1個(gè)基本數(shù)據(jù)類型+1個(gè)聲明符)
目前為止,我們所接觸的聲明語句中,聲明符其實(shí)就是變量名,此時(shí)變量的類型也就是聲明的基本數(shù)據(jù)類型。其實(shí)還可能有更復(fù)雜的聲明符,它基于基本數(shù)據(jù)類型得到更復(fù)雜的類型,并把它指定給變量。
(系好安全帶吧,少年!)
引用
C++11中新增了一種引用:所謂的“右值引用(rvalue reference)”,之后會(huì)做更詳細(xì)的介紹。這種引用主要用于內(nèi)置類。
嚴(yán)格來說,當(dāng)我們使用術(shù)語“引用(reference)”時(shí),指的其實(shí)是“左值引用(Ivalue reference)”。
引用(reference)為對(duì)象起了另外一個(gè)名字,引用類型引用(refers to)另外一種類型。
通過將聲明符寫成&d的形式來定義引用類型,其中d是聲明的變量名:
int ival = 1024; int &refVal = ival; // refVal refers to (is another name for) ival int &refVal2; // error: a reference must be initialized一般在初始化變量時(shí),初始值會(huì)被拷貝到新建的對(duì)象中。然而定義引用時(shí),程序把引用和它的初始值綁定(bind)在一起,而不是將初始值拷貝給引用。一旦初始化完成,引用將和它的初始值對(duì)象一直綁定在一起。因?yàn)闊o法令引用重新綁定到另外一個(gè)對(duì)象,因此引用必須初始化。(引用一次性的)
引用即別名
引用并非對(duì)象,相反的,它只是為一個(gè)已經(jīng)存在的對(duì)象所起的另外一個(gè)名字。
(對(duì)象是具有某種數(shù)據(jù)類型的內(nèi)存空間)(引用只是對(duì)象的別名)
定義了一個(gè)引用之后,對(duì)其進(jìn)行的所有操作都是在與之綁定的對(duì)象上進(jìn)行的:
refVal = 2; // assigns 2 to the object to which refVal refers, i.e., to ival int ii = refVal; // same as ii = ival為引用賦值,實(shí)際上是把值賦給了與引用綁定的對(duì)象。獲取引用的值,實(shí)際上是獲取了與引用綁定的對(duì)象的值。同理,以引用作為初始值,實(shí)際上是以與引用綁定的對(duì)象作為初始值:
// ok: refVal3 is bound to the object to which refVal is bound, i.e., to ival int &refVal3 = refVal; // initializes i from the value in the object to which refVal is bound int i = refVal; // ok: initializes i to the same value as ival因?yàn)橐帽旧聿皇且粋€(gè)對(duì)象,所以不能定義引用的引用。
引用的定義
允許在一條語句中定義多個(gè)引用,其中每個(gè)引用標(biāo)識(shí)符都必須以符號(hào)&開頭:
int i = 1024, i2 = 2048; // i and i2 are both ints int &r = i, r2 = i2; // r is a reference bound to i; r2 is an int int i3 = 1024, &ri = i3; // i3 is an int; ri is a reference bound to i3 int &r3 = i3, &r4 = i2; // both r3 and r4 are references所有引用的類型都要和與之綁定的對(duì)象嚴(yán)格匹配。而且,引用只能綁定在對(duì)象上,而不能與字面值或某個(gè)表達(dá)式的計(jì)算結(jié)果綁定在一起:
int &refVal4 = 10; // error: initializer must be an object double dval = 3.14; int &refVal5 = dval; // error: initializer must be an int object(Note: 引用(reference)為對(duì)象起了另外一個(gè)名字(起別名),引用類型引用(refers to)另外一種類型。)
指針
指針(pointer)是“指向(point to)”另外一種類型的復(fù)合類型。與引用類似,指針也實(shí)現(xiàn)了對(duì)其他對(duì)象的間接訪問。
然而指針與引用相比又有很多不同點(diǎn):
指針通常難以理解,即使是有經(jīng)驗(yàn)的程序員也常常因?yàn)檎{(diào)試指針引發(fā)的錯(cuò)誤而被備受折磨。
定義指針類型的方法將聲明符寫成*d的形式,其中d是變量名。如果在一條語句中定義了幾個(gè)指針變量,每個(gè)變量前面都必須有符號(hào)*:
int *ip1, *ip2; // both ip1 and ip2 are pointers to int double dp, *dp2; // dp2 is a pointer to double; dp is a double獲取對(duì)象的地址
指針存放某個(gè)對(duì)象的地址,要想獲取該地址,需要使用取地址符 address-of operator(操作符&):
int ival = 42; int *p = &ival; // p holds the address of ival; p is a pointer to ival第二條語句把p定義為一個(gè)指向 int 的指針,隨后初始化p令其指向名為 ival的int對(duì)象。
因?yàn)橐貌皇菍?duì)象,沒有實(shí)際地址,所以不能定義指向引用的指針。
(Note: &取地址符 跟 引用&號(hào)是兩碼事,注意區(qū)分)
其他所有指針的類型都要和它所指向的對(duì)象嚴(yán)格匹配:(有兩種例外情況,日后再介紹)
(對(duì)象是具有某種數(shù)據(jù)類型的內(nèi)存空間)
double dval; double *pd = &dval; // ok: initializer is the address of a double double *pd2 = pd; // ok: initializer is a pointer to double//類型不同,不能亂指 int *pi = pd; // error: types of pi and pd differ pi = &dval; // error: assigning the address of a double to a pointer to int因?yàn)樵诼暶髡Z句中指針的類型實(shí)際上被用于指定它所指向?qū)ο蟮念愋?#xff0c;所以二者必須匹配。如果指針指向了一個(gè)其他類型的對(duì)象,對(duì)該對(duì)象的操作將發(fā)生錯(cuò)誤。
指針值
指針的值(即地址)應(yīng)屬下列4種狀態(tài)之一:
試圖拷貝或以其他方式訪問無效指針的值都將引發(fā)錯(cuò)誤。編譯器并不負(fù)責(zé)檢查此類錯(cuò)誤,這一點(diǎn)和試圖使用未經(jīng)初始化的變量是一樣的。訪問無效指針的后果無法預(yù)計(jì),因此程序員必須清楚任意給定的指針是否有效。
盡管第2種和第3種形式的指針是有效的,但其使用同樣受到限制。顯然這些指針沒有指向任何具體對(duì)象,所以試圖訪問此類指針(假定的)對(duì)象的行為不被允許。如果這樣做了,后果也無法預(yù)計(jì)。
利用指針訪問對(duì)象
如果指針指向了一個(gè)對(duì)象,則允許使用解引用符 dereference operator(操作符*)來訪問該對(duì)象:
int ival = 42; int *p = &ival; // p holds the address of ival; p is a pointer to ival cout << *p; // * yields the object to which p points; prints 42(Note: * & 位置不同傻傻分不清楚。)
(一條聲明語句由一個(gè)基本數(shù)據(jù)類型(base type)和緊隨其后的一個(gè)聲明符(declarator)列表組成)
int a = 1; int &aa = a;//這里&為引用聲明符(這名我自起的)int ival = 42;jie int *p = &ival;//這里*為指針聲明符(這名我自起的),&為取地址符 cout << *p;//這里*為解引用符對(duì)指針解引用會(huì)得出所指的對(duì)象,因此如果給解引用的結(jié)果賦值,實(shí)際上也就是給指針?biāo)傅膶?duì)象賦值:
*p = 0; // * yields the object; we assign a new value to ival through p,這里*是解引用符 cout << *p; // prints 0如上述程序所示,為*p賦值實(shí)際上是為p所指的對(duì)象賦值。
解引用操作僅適用于那些確實(shí)指向了某個(gè)對(duì)象的有效指針。
關(guān)鍵概念:一符多義(& 與 *)
像&和*這樣的符號(hào),既能用作表達(dá)式里的運(yùn)算符(&按位與符或取址符,乘法符或解引用符*),也能作為聲明的一部分出現(xiàn)(&引用聲明符,*指針聲明符),符號(hào)的上下文決定了符號(hào)的意義:
int i = 42; int &r = i; //這里&為引用聲明符, & follows a type and is part of a declaration; r is a reference int *p; //這里*為指針聲明符, * follows a type and is part of a declaration; p is a pointer p = &i; // 這里&為取值符, & is used in an expression as the address-of operator *p = i; // 這里*為解引用符 * is used in an expression as the dereference operator int &r2 = *p; // & is part of the declaration; * is the dereference operator-  在聲明語句中,&和*用于組成復(fù)合類型(我個(gè)人將它們分別稱為引用聲明符、指針聲明符)。 
-  在表達(dá)式中,&和*又轉(zhuǎn)變成運(yùn)算符(操作數(shù)為1個(gè)時(shí),分別稱為取值符、解引用符。操作數(shù)為2個(gè)時(shí),分別稱為按位與符、乘法符)。 
在不同場景下出現(xiàn)的雖然是同一個(gè)符號(hào),但是由于含義截然不同,所以我們完全可以把它當(dāng)作不同的符號(hào)來看待。
空指針
空指針(null pointer)不指向任何對(duì)象,在試圖使用一個(gè)指針之前代碼可以首先檢查它是否為空。以下列出幾個(gè)生成空指針的方法:
int *p1 = nullptr; // equivalent to int *p1 = 0; int *p2 = 0; // directly initializes p2 from the literal constant 0 // must #include cstdlib int *p3 = NULL; // equivalent to int *p3 = 0;得到空指針最直接的辦法就是用字面值nullptr來初始化指針,這也是C++11新標(biāo)準(zhǔn)剛剛引入的一種方法。nullptr是一種特殊類型的字面值,它可以被轉(zhuǎn)換成任意其他的指針類型。另一種辦法就如對(duì)p2的定義一樣,也可以通過將指針初始化為字面值0來生成空指針。
過去的程序還會(huì)用到一個(gè)名為NULL的預(yù)處理變量(preprocessor variable)來給指針賦值,這個(gè)變量在頭文件cstdlib中定義,它的值就是0。
稍微介紹一點(diǎn)關(guān)于預(yù)處理器的知識(shí),現(xiàn)在只要知道預(yù)處理器是運(yùn)行于編譯過程之前的一段程序就可以了。預(yù)處理變量不屬于命名空間std,它由預(yù)處理器負(fù)責(zé)管理,因此我們可以直接使用預(yù)處理變量而無須在前面加上std: :。
當(dāng)用到一個(gè)預(yù)處理變量時(shí),預(yù)處理器會(huì)自動(dòng)地將它替換為實(shí)際值,因此用NULL初始化指針和用0初始化指針是一樣的。在新標(biāo)準(zhǔn)下,現(xiàn)在的C++程序最好使用nullptr,同時(shí)盡量避免使用NULL。
把int變量直接賦給指針是錯(cuò)誤的操作,即使int變量的值恰好等于0也不行。
int zero = 0; pi = zero; // error: cannot assign an int to a pointer建議:初始化所有指針
使用未經(jīng)初始化的指針是引發(fā)運(yùn)行時(shí)錯(cuò)誤的一大原因。(像Java的NullPointerException)
和其他變量一樣,訪問未經(jīng)初始化的指針?biāo)l(fā)的后果也是無法預(yù)計(jì)的。通常這一行為將造成程序崩潰,而且一旦崩潰,要想定位到出錯(cuò)位置將是特別棘手的問題。
在大多數(shù)編譯器環(huán)境下,如果使用了未經(jīng)初始化的指針,則該指針?biāo)純?nèi)存空間的當(dāng)前內(nèi)容將被看作一個(gè)地址值。訪問該指針,相當(dāng)于去訪問一個(gè)本不存在的位置上的本不存在的對(duì)象。糟糕的是,如果指針?biāo)純?nèi)存空間中恰好有內(nèi)容,而這些內(nèi)容又被當(dāng)作了某個(gè)地址,我們就很難分清它到底是合法的還是非法的了。(指針未初始化會(huì)出現(xiàn)的糟糕狀況)
因此建議初始化所有的指針,并且在可能的情況下,盡量等定義了對(duì)象之后再定義指向它的指針。如果實(shí)在不清楚指針應(yīng)該指向何處,就把它初始化為nullptr或者0,這樣程序就能檢測并知道它沒有指向任何具體的對(duì)象了。
賦值和指針
指針和引用都能提供對(duì)其他對(duì)象的間接訪問,然而在具體實(shí)現(xiàn)細(xì)節(jié)上二者有很大不同:
有時(shí)候要想搞清楚一條賦值語句到底是改變了指針的值還是改變了指針?biāo)笇?duì)象的值不太容易,最好的辦法就是記住賦值永遠(yuǎn)改變的是等號(hào)左側(cè)的對(duì)象。當(dāng)寫出如下語句時(shí),
pi = &ival; // value in pi is changed; pi now points to ival意思是為 pi賦一個(gè)新的值,也就是改變了那個(gè)存放在pi內(nèi)的地址值。相反的,如果寫出如下語句,
*pi = 0; // value in ival is changed; pi is unchanged則*pi(也就是指針pi指向的那個(gè)對(duì)象)發(fā)生改變。
其他指針操作
只要指針擁有一個(gè)合法值,就能將它用在條件表達(dá)式中。和采用算術(shù)值作為條件遵循的規(guī)則類似,如果指針的值是0,條件取false:
int ival = 1024; int *pi = 0; // pi is a valid, null pointer int *pi2 = &ival; // pi2 is a valid pointer that holds the address of ival if (pi) // pi has value 0, so condition evaluates as false// ... if (pi2) // pi2 points to ival, so it is not 0; the condition evaluates as true// ...任何非0指針對(duì)應(yīng)的條件值都是true。
對(duì)于兩個(gè)類型相同的合法指針,可以用相等操作符(=)或不相等操作符(!=)來比較它們,比較的結(jié)果是布爾類型。如果兩個(gè)指針存放的地址值相同,則它們相等;反之它們不相等。
這里兩個(gè)指針存放的地址值相同(兩個(gè)指針相等)有三種可能:
需要注意的是,一個(gè)指針指向某對(duì)象,同時(shí)另一個(gè)指針指向另外對(duì)象的下一地址,此時(shí)也有可能出現(xiàn)這兩個(gè)指針值相同的情況,即指針相等。
因?yàn)樯鲜霾僮饕玫街羔樀闹?#xff0c;所以不論是作為條件出現(xiàn)還是參與比較運(yùn)算(再數(shù)組中用),都必須使用合法指針,使用非法指針作為條件或進(jìn)行比較都會(huì)引發(fā)不可預(yù)計(jì)的后果。
void* 指針
void*是一種特殊的指針類型,可用于存放任意對(duì)象的地址。一個(gè)void*指針存放著一個(gè)地址,這一點(diǎn)和其他指針類似。
不同的是,我們對(duì)該地址中到底是個(gè)什么類型的對(duì)象并不了解:
double obj = 3.14, *pd = &obj; // ok: void* can hold the address value of any data pointer type void *pv = &obj; // obj can be an object of any type pv = pd; // pv can hold a pointer to any type利用void*指針能做的事兒比較有限:
- 拿它和別的指針比較、
- 作為函數(shù)的輸入或輸出,
- 賦給另外一個(gè)void*指針。
不能直接操作void*指針?biāo)傅膶?duì)象,因?yàn)槲覀儾⒉恢肋@個(gè)對(duì)象到底是什么類型,也就無法確定能在這個(gè)對(duì)象上做哪些操作。
概括說來,以void*的視角來看內(nèi)存空間也就僅僅是內(nèi)存空間,沒辦法訪問內(nèi)存空間中所存的對(duì)象,不過,是有獲取void*指針?biāo)娴刂返姆椒?#xff0c;日后介紹。
理解復(fù)合類型的聲明
如前所述,變量的定義包括一個(gè)基本數(shù)據(jù)類型(base type)和一組聲明符。在同一條定義語句中,雖然基本數(shù)據(jù)類型只有一個(gè),但是聲明符的形式卻可以不同。也就是說,一條定義語句可能定義出不同類型的變量:
// i is an int; p is a pointer to int; r is a reference to int int i = 1024, *p = &i, &r = i;很多程序員容易迷惑于基本數(shù)據(jù)類型和類型修飾符的關(guān)系,其實(shí)后者不過是聲明符的一部分罷了。
定義多個(gè)變量
經(jīng)常有一種觀點(diǎn)會(huì)誤以為,在定義語句中,類型修飾符(*或s)作用于本次定義的全部變量。造成這種錯(cuò)誤看法的原因有很多,其中之一是我們可以把空格寫在類型修飾符和變量名中間:
int* p; // legal but might be misleading我們說這種寫法可能產(chǎn)生誤導(dǎo)是因?yàn)閕nt*放在一起好像是這條語句中所有變量共同的類型一樣。其實(shí)恰恰相反,基本數(shù)據(jù)類型是int而非int*。*僅僅是修飾了p而已,對(duì)該聲明語句中的其他變量,它并不產(chǎn)生任何作用:
int* p1, p2;// p1 is a pointer to int; p2 is an int涉及指針或引用的聲明,一般有兩種寫法。
第一種把修飾符和變量標(biāo)識(shí)符寫在一起:(推薦)
int *p1, *p2; // both p1 and p2 are pointers to int這種形式著重強(qiáng)調(diào)變量具有的復(fù)合類型。
第二種把修飾符和類型名寫在一起,并且每條語句只定義一個(gè)變量:
int* p1; // p1 is a pointer to int int* p2; // p2 is a pointer to int這種形式著重強(qiáng)調(diào)本次聲明定義了一種復(fù)合類型。
上述兩種定義指針或引用的不同方法沒有孰對(duì)孰錯(cuò)之分,關(guān)鍵是選擇并堅(jiān)持其中的一種寫法,不要總是變來變?nèi)ァ?/strong>
我們接下都采用第一種寫法,將*(或是&)與變量名連在一起。(聲明符與變量)
指向指針的指針
一般來說,聲明符中修飾符的個(gè)數(shù)并沒有限制。當(dāng)有多個(gè)修飾符連寫在一起時(shí),按照其邏輯關(guān)系詳加解釋即可。以指針為例,指針是內(nèi)存中的對(duì)象,像其他對(duì)象一樣也有自己的地址,因此允許把指針的地址再存放到另一個(gè)指針當(dāng)中。
通過*的個(gè)數(shù)可以區(qū)分指針的級(jí)別。也就是說,**表示指向指針的指針,***表示指向指針的指針的指針,以此類推:
int ival = 1024; int *pi = &ival; // pi points to an int int **ppi = π // ppi points to a pointer to an int此處pi是指向int型數(shù)的指針,而ppi是指向int型指針的指針,下圖描述了它們之間的關(guān)系。
解引用int型指針會(huì)得到一個(gè)int型的數(shù),同樣,解引用指向指針的指針會(huì)得到一個(gè)指針。此時(shí)為了訪問最原始的那個(gè)對(duì)象,需要對(duì)指針的指針做兩次解引用:
cout << "The value of ival\n"<< "direct value: " << ival << "\n"li<< "indirect value: " << *pi << "\n"<< "doubly indirect value: " << **ppi//兩次解引用<< endl;該程序使用三種不同的方式輸出了變量ival的值:
第一種直接輸出;
第二種通過int型指針pi輸出;
第三種兩次解引用ppi,取得ival的值。
指向指針的引用(指針的別名)(從右向左閱讀理解)
引用本身不是一個(gè)對(duì)象,因此不能定義指向引用的指針。但指針是對(duì)象,所以存在對(duì)指針的引用:(指針不能指向引用)
int i = 42; int *p; // p is a pointer to int int *&r = p; // r is a reference to the pointer p //對(duì)指針的引用//int &r = *p;//可以有這種寫法,有點(diǎn)懵,日后注意r = &i; // r refers to a pointer; assigning &i to r makes p point to i *r = 0; // dereferencing r yields i, the object to which p points; changes i to 0要理解r的類型到底是什么,最簡單的辦法是從右向左閱讀r的定義。
離變量名最近的符號(hào)(此例中是&r的符號(hào)&)對(duì)變量的類型有最直接的影響,因此r是一個(gè)引用。聲明符的其余部分用以確定r引用的類型是什么,此例中的符號(hào)*說明r引用的是一個(gè)指針。最后,聲明的基本數(shù)據(jù)類型部分指出r引用的是一個(gè)int指針。
面對(duì)一條比較復(fù)雜的指針或引用的聲明語句時(shí),從右向左閱讀有助于弄清楚它的真實(shí)含義。
我寫的小程序來解惑:
#include <iostream> #include "Sales_item.h" int main() {int i = 1;int *p = &i;int &r = *p;int *&r2 = p;//對(duì)指針的引用//int &r1 = 1; //errorint *p2 = &r;//int *p2 = r; //errorstd::cout<<r<<std::endl;std::cout<<*p2<<std::endl;std::cout<<*r2<<std::endl;std::cout<<p<<std::endl;std::cout<<&r<<std::endl;std::cout<<r2<<std::endl;std::cout<<p2<<std::endl;return 0; }輸出結(jié)果:
1 1 1 0x61fe34 0x61fe34 0x61fe34 0x61fe34Process returned 0 (0x0) execution time : 0.048 s Press any key to continue.(Note:)
(引用與指針一起來聲明,有點(diǎn)懵,記住:引用本身不是一個(gè)對(duì)象,因此不能定義指向引用的指針。但指針是對(duì)象,所以存在對(duì)指針的引用)
(引用->指針 OK,指針->引用 NO)
int i = 1; int *p = &i;int &r = *p; //int *p2 = r; //指針->引用 NO int *p2 = &r; //這個(gè)r還是可以取址的,這是懵點(diǎn),(引用只是對(duì)象的別名)
const限定符
概述
有時(shí)我們希望定義這樣一種變量,它的值不能被改變。(Note: 只讀變量)
例如,用一個(gè)變量來表示緩沖區(qū)的大小。使用變量的好處是當(dāng)我們覺得緩沖區(qū)大小不再合適時(shí),很容易對(duì)其進(jìn)行調(diào)整。另一方面,也應(yīng)隨時(shí)警惕防止程序一不小心改變了這個(gè)值。為了滿足這一要求,可以用關(guān)鍵字const對(duì)變量的類型加以限定:
const int bufSize = 512; //輸入緩沖區(qū)大小這樣就把bufSize定義成了一個(gè)常量。任何試圖為bufSize賦值的行為都將引發(fā)錯(cuò)誤:
bufSize = 1024; //錯(cuò)誤:試圖向const對(duì)象寫值因?yàn)閏onst對(duì)象一旦創(chuàng)建后其值就不能再改變,所以const對(duì)象必須初始化。一如既往,初始值可以是任意復(fù)雜的表達(dá)式:
const int i = get_size(); // ok: initialized at run time const int j = 42; // ok: initialized at compile time const int k; // error: k is uninitialized const初始化和const
正如之前反復(fù)提到的,對(duì)象的類型決定了其上的操作。與非 const類型所能參與的操作相比,const類型的對(duì)象能完成其中大部分,但也不是所有的操作都適合。主要的限制就是只能在const類型的對(duì)象上執(zhí)行不改變其內(nèi)容的操作。
例如,const int和普通的int一樣都能參與算術(shù)運(yùn)算,也都能轉(zhuǎn)換成一個(gè)布爾值,等等。
在不改變 const對(duì)象的操作中還有一種是初始化,如果利用一個(gè)對(duì)象去初始化另外一個(gè)對(duì)象,則它們是不是const都無關(guān)緊要:
int i = 42; const int ci = i; // ok: the value in i is copied into ci int j = ci; // ok: the value in ci is copied into j盡管ci是整型常量,但無論如何ci中的值還是一個(gè)整型數(shù)。ci的常量特征僅僅在執(zhí)行改變ci的操作時(shí)才會(huì)發(fā)揮作用。當(dāng)用ci去初始化j時(shí),根本無須在意ci是不是一個(gè)常量。拷貝一個(gè)對(duì)象的值并不會(huì)改變它,一旦拷貝完成,新的對(duì)象就和原來的對(duì)象沒什么關(guān)系了。
默認(rèn)狀態(tài)下,const對(duì)象僅在文件內(nèi)有效(const 常量在多文件中使用方法)
當(dāng)以編譯時(shí)初始化的方式定義一個(gè)const對(duì)象時(shí),就如對(duì)bufsize的定義一樣:
const int bufSize = 512;//輸入緩沖區(qū)大小編譯器將在編譯過程中把用到該變量的地方都替換成對(duì)應(yīng)的值。也就是說,編譯器會(huì)找到代碼中所有用到bufSize的地方,然后用512替換。
為了執(zhí)行上述替換,編譯器必須知道變量的初始值。如果程序包含多個(gè)文件,則每個(gè)用了const對(duì)象的文件都必須得能訪問到它的初始值才行。要做到這一點(diǎn),就必須在每一個(gè)用到變量的文件中都有對(duì)它的定義。為了支持這一用法,同時(shí)避免對(duì)同一變量的重復(fù)定義,默認(rèn)情況下,const對(duì)象被設(shè)定為僅在文件內(nèi)有效。當(dāng)多個(gè)文件中出現(xiàn)了同名的const變量時(shí),其實(shí)等同于在不同文件中分別定義了獨(dú)立的變量。
某些時(shí)候有這樣一種 const變量,它的初始值不是一個(gè)常量表達(dá)式,但又確實(shí)有必要在文件間共享。這種情況下,我們不希望編譯器為每個(gè)文件分別生成獨(dú)立的變量。相反,我們想讓這類const對(duì)象像其他(非常量)對(duì)象一樣工作,也就是說,只在一個(gè)文件中定義const,而在其他多個(gè)文件中聲明并使用它。
解決的辦法是,對(duì)于const變量不管是聲明還是定義都添加extern關(guān)鍵字,這樣只需定義一次就可以了:
//file_1.cc定義并初始化了一個(gè)常量,該常量能被其他文件訪問 extern const int bufSize = fcn();//file_l.h頭文件 extern const int bufSize; //與file_1.cc中定義的bufSize是同一個(gè)如上述程序所示,file_1.cc定義并初始化了bufsize。因?yàn)檫@條語句包含了初始值,所以它(顯然〉是一次定義。然而,因?yàn)閎ufsize是一個(gè)常量,必須用extern加以限定使其被其他文件使用。
file_1.h頭文件中的聲明也由extern做了限定,其作用是指明bufsize并非本文件所獨(dú)有,它的定義將在別處出現(xiàn)。
如果想在多個(gè)文件之間共享const對(duì)象,必須在變量的定義之前添加extern關(guān)鍵字。
const的引用
可以把引用綁定到const對(duì)象上,就像綁定到其他對(duì)象上一樣,我們稱之為對(duì)常量的引用(reference to const)。與普通引用不同的是,對(duì)常量的引用不能被用作修改它所綁定的對(duì)象:
const int ci = 1024; const int &r1 = ci; // ok: both reference and underlying object are const r1 = 42; // error: r1 is a reference to const int &r2 = ci; // error: non const reference to a const object因?yàn)椴辉试S直接為ci賦值,當(dāng)然也就不能通過引用去改變ci。因此,對(duì)r2的初始化是錯(cuò)誤的。假設(shè)該初始化合法,則可以通過r2來改變它引用對(duì)象的值,這顯然是不正確的。
術(shù)語:常量引用是對(duì)const的引用
C++程序員們經(jīng)常把詞組“對(duì)const的引用”簡稱為“常量引用”,這一簡稱還是挺靠譜的,不過前提是你得時(shí)刻記得這就是個(gè)簡稱而已。
嚴(yán)格來說,并不存在常量引用。因?yàn)橐貌皇且粋€(gè)對(duì)象,所以我們沒法讓引用本身恒定不變。事實(shí)上,由于C+語言并不允許隨意改變引用所綁定的對(duì)象,所以從這層意義上理解所有的引用又都算是常量。引用的對(duì)象是常量還是非常量可以決定其所能參與的操作,卻無論如何都不會(huì)影響到引用和對(duì)象的綁定關(guān)系本身。
初始化和對(duì)const的引用
前文提到,引用的類型必須與其所引用對(duì)象的類型一致,但是有兩個(gè)例外。
第一種例外情況就是在初始化常量引用時(shí)允許用任意表達(dá)式作為初始值,只要該表達(dá)式的結(jié)果能轉(zhuǎn)換成引用的類型即可。尤其,允許為一個(gè)常量引用綁定非常量的對(duì)象、字面值,甚至是個(gè)一般表達(dá)式:
int i = 42; const int &r1 = i; // we can bind a const int& to a plain int object const int &r2 = 42; // ok: r1 is a reference to const const int &r3 = r1 * 2; // ok: r3 is a reference to const int &r4 = r * 2; // error: r4 is a plain, non const reference要想理解這種例外情況的原因,最簡單的辦法是弄清楚當(dāng)一個(gè)常量引用被綁定到另外一種類型上時(shí)到底發(fā)生了什么:
double dval = 3.14; const int &ri = dval;此處ri引用了一個(gè)int型的數(shù)。對(duì)ri的操作應(yīng)該是整數(shù)運(yùn)算,但dval卻是一個(gè)雙精度浮點(diǎn)數(shù)而非整數(shù)。因此為了確保讓ri綁定一個(gè)整數(shù),編譯器把上述代碼變成了如下形式:
const int temp = dval;//由雙精度浮點(diǎn)數(shù)生成一個(gè)臨時(shí)的整型常量 const int &ri = temp;//讓ri綁定這個(gè)臨時(shí)量在這種情況下,ri綁定了一個(gè)臨時(shí)量(temporary)對(duì)象。所謂臨時(shí)量對(duì)象就是當(dāng)編譯器需要一個(gè)空間來暫存表達(dá)式的求值結(jié)果時(shí)臨時(shí)創(chuàng)建的一個(gè)未命名的對(duì)象。C++程序員們常常把臨時(shí)量對(duì)象簡稱為臨時(shí)量。
接下來探討當(dāng)ri不是常量時(shí),如果執(zhí)行了類似于上面的初始化過程將帶來什么樣的后果。
如果ri不是常量,就允許對(duì)ri賦值,這樣就會(huì)改變r(jià)i所引用對(duì)象的值。注意,此時(shí)綁定的對(duì)象是一個(gè)臨時(shí)量而非dval。程序員既然讓 ri引用dval,就肯定想通過ri改變dval的值,否則干什么要給ri賦值呢?如此看來,既然大家基本上不會(huì)想著把引用綁定到臨時(shí)量上,C++語言也就把這種行為歸為非法。
(Note: 第二種例外情況在哪???)
對(duì)const的引用可能引用一個(gè)并非const的對(duì)象
必須認(rèn)識(shí)到,常量引用僅對(duì)引用可參與的操作做出了限定,對(duì)于引用的對(duì)象本身是不是一個(gè)常量未作限定。因?yàn)閷?duì)象也可能是個(gè)非常量,所以允許通過其他途徑改變它的值:
int i = 42; int &r1 = i; // r1 bound to i const int &r2 = i; // r2 also bound to i; but cannot be used to change i r1 = 0; // r1 is not const; i is now 0 r2 = 0; // error: r2 is a reference to constr2綁定(非常量)整數(shù)i是合法的行為。然而,不允許通過r2修改i的值。盡管如此,i的值仍然允許通過其他途徑修改,既可以直接給i賦值,也可以通過像r1一樣綁定到i的其他引用來修改。(防不勝防)(Note: 真麻煩)
指針和const
與引用一樣,也可以令指針指向常量或非常量。類似于常量引用,指向常量的指針(pointer to const)不能用于改變其所指對(duì)象的值。要想存放常量對(duì)象的地址,只能使用指向常量的指針:
const double pi = 3.14; // pi is const; its value may not be changed double *ptr = π // error: ptr is a plain pointer const double *cptr = π // ok: cptr may point to a double that is const *cptr = 42; // error: cannot assign to *cptr前文提到,指針的類型必須與其所指對(duì)象的類型一致,但是有兩個(gè)例外。第一種例外情況是允許令一個(gè)指向常量的指針指向一個(gè)非常量對(duì)象:
double dval = 3.14; // dval is a double; its value can be changed cptr = &dval; // ok: but can't change dval through cptr和常量引用一樣,指向常量的指針也沒有規(guī)定其所指的對(duì)象必須是一個(gè)常量。所謂指向常量的指針僅僅要求不能通過該指針改變對(duì)象的值,而沒有規(guī)定那個(gè)對(duì)象的值不能通過其他途徑改變。(也就是說還有其他路子改變對(duì)象的值,可查看第一節(jié))。
試試這樣想吧:所謂指向常量的指針或引用,不過是指針或引用“自以為是”罷了,它們覺得自己指向了常量,所以自覺地不去改變所指對(duì)象的值。It may be helpful to think of pointers and references to const as pointers or references “that think they point or refer to const.
(第二中例外沒寫)
const指針
(上一節(jié)講的是指向常量的指針,這節(jié)將常量指針)
指針是對(duì)象而引用不是,因此就像其他對(duì)象類型一樣,允許把指針本身定為常量。常量指針(const pointer)必須初始化,而且一旦初始化完成,則它的值(也就是存放在指針中的那個(gè)地址)就不能再改變了。把*放在const關(guān)鍵字之前用以說明指針是一個(gè)常量,這樣的書寫形式隱含著一層意味,即不變的是指針本身的值而非指向的那個(gè)值:
int errNumb = 0; int *const curErr = &errNumb; // curErr will always point to errNumb const double pi = 3.14159; const double *const pip = π // pip is a const pointer to a const object前文提到,要想弄清楚這些聲明的含義最行之有效的辦法是從右向左閱讀。(Note:這是理解這些復(fù)雜申明語句的關(guān)鍵)
此例中,
對(duì)象是指一塊能存儲(chǔ)數(shù)據(jù)并具有某種類型的內(nèi)存空間。
與之相似,我們也能推斷出,pip是一個(gè)常量指針,它指向的對(duì)象是一個(gè)雙精度浮點(diǎn)型常量。
指針本身是一個(gè)常量并不意味著不能通過指針修改其所指對(duì)象的值,能否這樣做完全依賴于所指對(duì)象的類型。例如,pip是一個(gè)指向常量的常量指針,則不論是 pip 所指的對(duì)象值還是pip自己存儲(chǔ)的那個(gè)地址都不能改變。
相反的,curErr指向的是一個(gè)一般的非常量整數(shù),那么就完全可以用curErr去修改errNumb 的值:
*pip = 2.72; // error: pip is a pointer to const//指向 // if the object to which curErr points (i.e., errNumb) is nonzero if (*curErr) {errorHandler();*curErr = 0; // ok: reset the value of the object to which curErr is bound curr常量指針指向的是一個(gè)int對(duì)象,這對(duì)象可以改變 }頂層const
如前所述,指針本身是一個(gè)對(duì)象,它又可以指向另外一個(gè)對(duì)象。因此,指針本身是不是常量以及指針?biāo)傅氖遣皇且粋€(gè)常量就是兩個(gè)相互獨(dú)立的問題。
用名詞頂層const(top-level const)表示指針本身是個(gè)常量,而用名詞底層const (low-level const)表示指針?biāo)傅膶?duì)象是一個(gè)常量。
更一般的:
頂層const可以表示任意的對(duì)象是常量,這一點(diǎn)對(duì)任何數(shù)據(jù)類型都適用,如算術(shù)類型、類、指針等。
底層const則與指針和引用等復(fù)合類型的基本類型部分有關(guān)。比較特殊的是,指針類型既可以是頂層const也可以是底層const,這一點(diǎn)和其他類型相比區(qū)別明顯:
(助記:頂常底復(fù))
int i = 0; //頂層const:表示任意的對(duì)象是常量 int *const p1 = &i; // we can't change the value of p1; const is top-level const int ci = 42; // we cannot change ci; const is top-level//底層const:與指針和引用等復(fù)合類型的基本類型部分有關(guān) const int *p2 = &ci; // we can change p2; const is low-level const int &r = ci; // const in reference types is always low-level//頂層const又是底層const const int *const p3 = p2; // right-most const is top-level, left-most is not(Note:根據(jù)const所在位置來判斷頂層const或底層const不管用)
當(dāng)執(zhí)行對(duì)象的拷貝操作時(shí),常量是頂層const還是底層const區(qū)別明顯。
其中,頂層const不受什么影響:
i = ci; // ok: copying the value of ci; top-level const in ci is ignored p2 = p3; // ok: pointed-to type matches; top-level const in p3 is ignored執(zhí)行拷貝操作并不會(huì)改變被拷貝對(duì)象的值,因此,拷入和拷出的對(duì)象是否是常量都沒什么影響。
另一方面,底層 const 的限制卻不能忽視。當(dāng)執(zhí)行對(duì)象的拷貝操作時(shí),拷入和拷出的對(duì)象必須具有相同的底層 const 資格,或者兩個(gè)對(duì)象的數(shù)據(jù)類型必須能夠轉(zhuǎn)換。一般來說,非常量可以轉(zhuǎn)換成常量,反之則不行:
int *p = p3; // error: p3 has a low-level const but p doesn't p2 = p3; // ok: p2 has the same low-level const qualification as p3 p2 = &i; // ok: we can convert int* to const int* int &r = ci; // error: can't bind an ordinary int& to a const int object const int &r2 = i; // ok: can bind const int& to plain intp3既是頂層const也是底層const,拷貝p3時(shí)可以不在乎它是一個(gè)頂層const,但是必須清楚它指向的對(duì)象得是一個(gè)常量。因此,不能用p3去初始化p,因?yàn)閜指向的是一個(gè)普通的(非常量)整數(shù)。另一方面,p3的值可以賦給p2,是因?yàn)檫@兩個(gè)指針都是底層const,盡管p3同時(shí)也是一個(gè)常量指針(頂層const),僅就這次賦值而言不會(huì)有什么影響。
(Note:這節(jié)不好懂)
constexpr和常量表達(dá)式
常量表達(dá)式(const expression)是指值不會(huì)改變并且在編譯過程就能得到計(jì)算結(jié)果的表達(dá)式。顯然,字面值屬于常量表達(dá)式,用常量表達(dá)式初始化的const對(duì)象也是常量表達(dá)式。后面將會(huì)提到,C++語言中有幾種情況下是要用到常量表達(dá)式的。
一個(gè)對(duì)象(或表達(dá)式)是不是常量表達(dá)式由它的數(shù)據(jù)類型和初始值共同決定,例如:
const int max_files = 20; // max_files is a constant expression const int limit = max_files + 1; // limit is a constant expression int staff_size = 27; // staff_size is not a constant expression const int sz = get_size(); // sz is not a constant expression盡管staff_size的初始值是個(gè)字面值常量,但由于它的數(shù)據(jù)類型只是一個(gè)普通int而非const int,所以它不屬于常量表達(dá)式。另一方面,盡管 sz本身是一個(gè)常量,但它的具體值直到運(yùn)行時(shí)才能獲取到,所以也不是常量表達(dá)式。
constexpr變量
在一個(gè)復(fù)雜系統(tǒng)中,很難(幾乎肯定不能)分辨一個(gè)初始值到底是不是常量表達(dá)式。當(dāng)然可以定義一個(gè) const變量并把它的初始值設(shè)為我們認(rèn)為的某個(gè)常量表達(dá)式,但在實(shí)際使用時(shí),盡管要求如此卻常常發(fā)現(xiàn)初始值并非常量表達(dá)式的情況。可以這么說,在此種情況下,對(duì)象的定義和使用根本就是兩回事兒。
C++11新標(biāo)準(zhǔn)規(guī)定,允許將變量聲明為constexpr類型以便由編譯器來驗(yàn)證變量的值是否是一個(gè)常量表達(dá)式。聲明為constexpr的變量一定是一個(gè)常量,而且必須用常量表達(dá)式初始化:
constexpr int mf = 20; // 20 is a constant expression constexpr int limit = mf + 1; // mf + 1 is a constant expression constexpr int sz = size(); // ok only if size is a constexpr function盡管不能使用普通函數(shù)作為constexpr變量的初始值,但是,將要介紹的,新標(biāo)準(zhǔn)允許定義一種特殊的constexpr函數(shù)。這種函數(shù)應(yīng)該足夠簡單以使得編譯時(shí)就可以計(jì)算其結(jié)果,這樣就能用constexpr函數(shù)去初始化 constexpr變量了。
一般來說,如果你認(rèn)定變量是一個(gè)常量表達(dá)式,那就把它聲明成 constexpr類型。
字面值類型
常量表達(dá)式的值需要在編譯時(shí)就得到計(jì)算,因此對(duì)聲明constexpr時(shí)用到的類型必須有所限制。因?yàn)檫@些類型一般比較簡單,值也顯而易見、容易得到,就把它們稱為“字面值類型”( literal type)。
到目前為止接觸過的數(shù)據(jù)類型中,算術(shù)類型、引用和指針都屬于字面值類型。
自定義類sales_item、IO 庫、string 類型則不屬于字面值類型,也就不能被定義成constexpr。
盡管指針和引用都能定義成constexpr,但它們的初始值卻受到嚴(yán)格限制。一個(gè)constexpr指針的初始值必須是nullptr或者0,或者是存儲(chǔ)于某個(gè)固定地址中的對(duì)象。
將要提到,函數(shù)體內(nèi)定義的變量一般來說并非存放在固定地址中,因此constexpr指針不能指向這樣的變量。相反的,定義于所有函數(shù)體之外的對(duì)象其地址固定不變,能用來初始化constexpr指針。
將提到,允許函數(shù)定義一類有效范圍超出函數(shù)本身的變量,這類變量和定義在函數(shù)體之外的變量一樣也有固定地址。因此,constexpr引用能綁定到這樣的變量上,constexpr 指針也能指向這樣的變量。
(Note:哪些能定義constexpr,哪些不能定義constexpr)
指針和constexpr
必須明確一點(diǎn),在constexpr聲明中如果定義了一個(gè)指針,限定符constexpr僅對(duì)指針有效,與指針?biāo)傅膶?duì)象無關(guān):
const int *p = nullptr; // p is a pointer to a const int constexpr int *q = nullptr; // q is a const pointer to intp和q的類型相差甚遠(yuǎn),p是一個(gè)指向常量的指針,而q是一個(gè)常量指針,其中的關(guān)鍵在于constexpr把它所定義的對(duì)象置為了頂層const。
與其他常量指針類似,constexpr指針既可以指向常量也可以指向一個(gè)非常量:
constexpr int *np = nullptr; // np is a constant pointer to int that is null int j = 0; constexpr int i = 42; // type of i is const int// i and j must be defined outside any function constexpr const int *p = &i; // p is a constant pointer to the const int i constexpr int *p1 = &j; // p1 is a constant pointer to the int j處理類型
隨著程序越來越復(fù)雜,程序中用到的類型也越來越復(fù)雜,這種復(fù)雜性體現(xiàn)在兩個(gè)方面。
類型別名
類型別名(type alias)是一個(gè)名字,它是某種類型的同義詞。使用類型別名有很多好處,它讓復(fù)雜的類型名字變得簡單明了、易于理解和使用,還有助于程序員清楚地知道使用該類型的真實(shí)目的。
有兩種方法可用于定義類型別名。傳統(tǒng)的方法是使用關(guān)鍵字typedef:
typedef double wages; // wages is a synonym for double typedef wages base, *p; // base is a synonym for double, p for double*其中,關(guān)鍵字typedef作為聲明語句中的基本數(shù)據(jù)類型的一部分出現(xiàn)。含有 typedef 的聲明語句定義的不再是變量而是類型別名。和以前的聲明語句一樣,這里的聲明符也可以包含類型修飾,從而也能由基本數(shù)據(jù)類型構(gòu)造出復(fù)合類型來。
C++11新標(biāo)準(zhǔn)規(guī)定了一種新的方法,使用別名聲明(alias declaration)來定義類型的別名:
using SI = Sales_item; // SI is a synonym for Sales_item這種方法用關(guān)鍵字using 作為別名聲明的開始,其后緊跟別名和等號(hào),其作用是把等號(hào)左側(cè)的名字規(guī)定成等號(hào)右側(cè)類型的別名。
類型別名和類型的名字等價(jià),只要是類型的名字能出現(xiàn)的地方,就能使用類型別名:
wages hourly, weekly; // same as double hourly, weekly; SI item; // same as Sales_item item指針、常量和類型別名
如果某個(gè)類型別名指代的是復(fù)合類型或常量,那么把它用到聲明語句里就會(huì)產(chǎn)生意想不到的后果。例如下面的聲明語句用到了類型pstring,它實(shí)際上是類型char*的別名:
typedef char *pstring; const pstring cstr = 0; // cstr is a constant pointer to char const pstring *ps; // ps is a pointer to a constant pointer to char//從指向指針的指針上述兩條聲明語句的基本數(shù)據(jù)類型都是const pstring,和過去一樣,const是對(duì)給定類型的修飾。pstring 實(shí)際上是指向char 的指針,因此,const pstring 就是指向char的常量指針,而非指向常量字符的指針。
遇到一條使用了類型別名的聲明語句時(shí),人們往往會(huì)錯(cuò)誤地嘗試把類型別名替換成它本來的樣子,以理解該語句的含義:(Note:不能像代數(shù)那樣代入)
const char *cstr = 0; // wrong interpretation of const pstring cstr再強(qiáng)調(diào)一遍:這種理解是錯(cuò)誤的。
聲明語句中用到pstring 時(shí),其基本數(shù)據(jù)類型是指針。可是用char*重寫了聲明語句后,數(shù)據(jù)類型就變成了char,*成為了聲明符的一部分。However, this interpretation is wrong. When we use pstring in a declaration, the base type of the declaration is a pointer type. When we rewrite the declaration using char*, the base type is char and the * is part of the declarator.
這樣改寫的結(jié)果是,const char成了基本數(shù)據(jù)類型。前后兩種聲明含義截然不同,前者聲明了一個(gè)指向char的常量指針,改寫后的形式則聲明了一個(gè)指向const char的指針。 In this case, const char is the base type. This rewrite declares cstr as a pointer to const char rather than as a const pointer to char.
(Note:)
const (char *)cstr = 0;//我是這樣理解的,一個(gè)指向char的常量指針auto類型說明符
C++11新特性
編程時(shí)常常需要把表達(dá)式的值賦給變量,這就要求在聲明變量的時(shí)候清楚地知道表達(dá)式的類型。然而要做到這一點(diǎn)并非那么容易,有時(shí)甚至根本做不到。為了解決這個(gè)問題,C++11新標(biāo)準(zhǔn)引入了auto類型說明符,用它就能讓編譯器替我們?nèi)シ治霰磉_(dá)式所屬的類型。和原來那些只對(duì)應(yīng)一種特定類型的說明符(比如 double)不同,auto 讓編譯器通過初始值來推算變量的類型。顯然,auto定義的變量必須有初始值:
// the type of item is deduced from the type of the result of adding val1 and val2 auto item = val1 + val2; // item initialized to the result of val1 + val2此處編譯器將根據(jù)val1和val2相加的結(jié)果來推斷item的類型。如果val1和val2是類Sales _item(具體查看上一章)的對(duì)象,則item的類型就是Sales_item;如果這兩個(gè)變量的類型是double,則item的類型就是double,以此類推。
使用auto也能在一條語句中聲明多個(gè)變量。因?yàn)橐粭l聲明語句只能有一個(gè)基本數(shù)據(jù)類型,所以該語句中所有變量的初始基本數(shù)據(jù)類型都必須一樣:
auto i = 0, *p = &i; // ok: i is int and p is a pointer to int auto sz = 0, pi = 3.14; // error: inconsistent types for sz and pi復(fù)合類型、常量和 auto
編譯器推斷出來的auto類型有時(shí)候和初始值的類型并不完全一樣,編譯器會(huì)適當(dāng)?shù)馗淖兘Y(jié)果類型使其更符合初始化規(guī)則。
首先,正如我們所熟知的,使用引用其實(shí)是使用引用的對(duì)象,特別是當(dāng)引用被用作初始值時(shí),真正參與初始化的其實(shí)是引用對(duì)象的值。此時(shí)編譯器以引用對(duì)象的類型作為auto的類型:
int i = 0, &r = i; auto a = r; // a is an int (r is an alias for i, which has type int)其次,auto一般會(huì)忽略掉頂層const,同時(shí)底層const則會(huì)保留下來,比如當(dāng)初始值是一個(gè)指向常量的指針時(shí):
const int ci = i, &cr = ci; auto b = ci; // b is an int (top-level const in ci is dropped) auto c = cr; // c is an int (cr is an alias for ci whose const is top-level) auto d = &i; // d is an int*(& of an int object is int*) auto e = &ci; // e is const int*(& of a const object is low-level const)如果希望推斷出的auto類型是一個(gè)頂層const,需要明確指出:
const auto f = ci; // deduced type of ci is int; f has type const int還可以將引用的類型設(shè)為auto,此時(shí)原來的初始化規(guī)則仍然適用:
auto &g = ci; // g is a const int& that is bound to ci auto &h = 42; // error: we can't bind a plain reference to a literal const auto &j = 42; // ok: we can bind a const reference to a literal設(shè)置一個(gè)類型為auto的引用時(shí),初始值中的頂層常量屬性仍然保留。和往常一樣,如果我們給初始值綁定一個(gè)引用,則此時(shí)的常量就不是頂層常量了。
要在一條語句中定義多個(gè)變量,切記,符號(hào)&和*只從屬于某個(gè)聲明符,而非基本數(shù)據(jù)類型的一部分,因此初始值必須是同一種類型:
auto k = ci, &l = i; // k is int; l is int& auto &m = ci, *p = &ci; // m is a const int&;p is a pointer to const int // error: type deduced from i is int; type deduced from &ci is const int auto &n = i, *p2 = &ci;(Note:符號(hào)&和*只從屬于某個(gè)聲明符,而非基本數(shù)據(jù)類型的一部分,這一句很重要)。
decltype類型指示符
C++11新標(biāo)準(zhǔn)
有時(shí)會(huì)遇到這種情況:希望從表達(dá)式的類型推斷出要定義的變量的類型,但是不想用該表達(dá)式的值初始化變量。為了滿足這一要求,C++11新標(biāo)準(zhǔn)引入了第二種類型說明符decltype,它的作用是選擇并返回操作數(shù)的數(shù)據(jù)類型。在此過程中,編譯器分析表達(dá)式并得到它的類型,卻不實(shí)際計(jì)算表達(dá)式的值:
decltype(f()) sum = x; // sum has whatever type f returns編譯器并不實(shí)際調(diào)用函數(shù)f,而是使用當(dāng)調(diào)用發(fā)生時(shí)f的返回值類型作為sum的類型。換句話說,編譯器為sum 指定的類型是什么呢?就是假如f被調(diào)用的話將會(huì)返回的那個(gè)類型。
decltype處理頂層const和引用的方式與auto有些許不同。如果decltype使用的表達(dá)式是一個(gè)變量,則 decltype返回該變量的類型(包括頂層const和引用在內(nèi)):
const int ci = 0, &cj = ci; decltype(ci) x = 0; // x has type const int decltype(cj) y = x; // y has type const int& and is bound to x decltype(cj) z; // error: z is a reference and must be initialized因?yàn)閏j是一個(gè)引用,decltype(cj)的結(jié)果就是引用類型,因此作為引用的z必須被初始化。
需要指出的是,引用從來都作為其所指對(duì)象的同義詞出現(xiàn),只有用在 decltype 處是一個(gè)例外。
decltype和引用
如果decltype使用的表達(dá)式不是一個(gè)變量,則decltype返回表達(dá)式結(jié)果對(duì)應(yīng)的類型。有些表達(dá)式將向decltype返回一個(gè)引用類型。一般來說當(dāng)這種情況發(fā)生時(shí),意味著該表達(dá)式的結(jié)果對(duì)象能作為一條賦值語句的左值:
// decltype of an expression can be a reference type int i = 42, *p = &i, &r = i; decltype(r + 0) b; // ok: addition yields an int; b is an (uninitialized) int decltype(*p) c; // error: c is int& and must be initialized因?yàn)閞是一個(gè)引用,因此 decltype?的結(jié)果是引用類型。如果想讓結(jié)果類型是r所指的類型,可以把r作為表達(dá)式的一部分,如r+0,顯然這個(gè)表達(dá)式的結(jié)果將是一個(gè)具體值而非一個(gè)引用。
另一方面,如果表達(dá)式的內(nèi)容是解引用操作,則decltype 將得到引用類型。正如我們所熟悉的那樣,解引用指針可以得到指針?biāo)傅膶?duì)象,而且還能給這個(gè)對(duì)象賦值。因此,decltype (*p)的結(jié)果類型就是int&,而非int。
decltype和 auto的另一處重要區(qū)別是,decltype的結(jié)果類型與表達(dá)式形式密切相關(guān)。有一種情況需要特別注意:對(duì)于 decltype所用的表達(dá)式來說,如果變量名加上了一對(duì)括號(hào),則得到的類型與不加括號(hào)時(shí)會(huì)有不同。
- 如果 decltype使用的是一個(gè)不加括號(hào)的變量,則得到的結(jié)果就是該變量的類型;
- 如果給變量加上了一層或多層括號(hào),編譯器就會(huì)把它當(dāng)成是一個(gè)表達(dá)式。
變量是一種可以作為賦值語句左值的特殊表達(dá)式,所以這樣的decltype就會(huì)得到引用類型:
// decltype of a parenthesized variable is always a reference decltype((i)) d; // error: d is int& and must be initialized decltype(i) e; // ok: e is an (uninitialized) int切記:decltype ((variable))(注意是雙層括號(hào))的結(jié)果永遠(yuǎn)是引用,而decltype(variable)結(jié)果只有當(dāng) variable本身就是一個(gè)引用時(shí)才是引用。
自定義數(shù)據(jù)結(jié)構(gòu)
從最基本的層面理解,數(shù)據(jù)結(jié)構(gòu)是把一組相關(guān)的數(shù)據(jù)元素組織起來然后使用它們的策略和方法。
舉一個(gè)例子,我們的Sales_item類把書本的ISBN編號(hào)、售出量及銷售收入等數(shù)據(jù)組織在了一起,并且提供諸如isbn函數(shù)、>>、<<、+、+=等運(yùn)算在內(nèi)的一系列操作,sales_item類就是一個(gè)數(shù)據(jù)結(jié)構(gòu)。
C++語言允許用戶以類的形式自定義數(shù)據(jù)類型,而庫類型string、 istream、ostream等也都是以類的形式定義的,就像上一章Sales_item類型一樣。
定義Sales_data類型
盡管我們還寫不出完整的Sales_item類,但是可以嘗試著把那些數(shù)據(jù)元素組織到一起形成一個(gè)簡單點(diǎn)兒的類。初步的想法是用戶能直接訪問其中的數(shù)據(jù)元素,也能實(shí)現(xiàn)一些基本的操作。
既然我們籌劃的這個(gè)數(shù)據(jù)結(jié)構(gòu)不帶有任何運(yùn)算功能,不妨把它命名為 Sales_data以示與Sales_item的區(qū)別。Sales_data初步定義如下:
struct Sales_data {std::string bookNo;unsigned units_sold = 0;double revenue = 0.0; };我們的類以關(guān)鍵字struct開始,緊跟著類名和類體(其中類體部分可以為空)。類體由花括號(hào)包圍形成了一個(gè)新的作用域。類內(nèi)部定義的名字必須唯一,但是可以與類外部定義的名字重復(fù)。
類體右側(cè)的表示結(jié)束的花括號(hào)后必須寫一個(gè)分號(hào),這是因?yàn)轭愺w后面可以緊跟變量名以示對(duì)該類型對(duì)象的定義,所以分號(hào)必不可少:
struct Sales_data { /* ... */ } accum, trans, *salesptr; // equivalent, but better way to define these objects struct Sales_data { /* ... */ }; Sales_data accum, trans, *salesptr;分號(hào)表示聲明符(通常為空)的結(jié)束。一般來說,最好不要把對(duì)象的定義和類的定義放在一起。這么做無異于把兩種不同實(shí)體的定義混在了一條語句里,一會(huì)兒定義類,一會(huì)兒又定義變量,顯然這是一種不被建議的行為。
很多新手程序員經(jīng)常忘了在類定義的最后加上分號(hào)。
類數(shù)據(jù)成員
類體定義類的成員,我們的類只有數(shù)據(jù)成員(data member)。類的數(shù)據(jù)成員定義了類的對(duì)象的具體內(nèi)容,每個(gè)對(duì)象有自己的一份數(shù)據(jù)成員拷貝。修改一個(gè)對(duì)象的數(shù)據(jù)成員,不會(huì)影響其他Sales_data的對(duì)象。
定義數(shù)據(jù)成員的方法和定義普通變量一樣:首先說明一個(gè)基本類型,隨后緊跟一個(gè)或多個(gè)聲明符。我們的類有3個(gè)數(shù)據(jù)成員:
每個(gè)Sales_data的對(duì)象都將包括這3個(gè)數(shù)據(jù)成員。
C++11新標(biāo)準(zhǔn)規(guī)定,可以為數(shù)據(jù)成員提供一個(gè)類內(nèi)初始值(in-class initializer)。創(chuàng)建對(duì)象時(shí),類內(nèi)初始值將用于初始化數(shù)據(jù)成員。沒有初始值的成員將被默認(rèn)初始化(函數(shù)體內(nèi)的默認(rèn)不初始化,函數(shù)體外的都默認(rèn)初始化)。因此當(dāng)定義Sales_data的對(duì)象時(shí),units_sold和revenue都將初始化為0,bookNo將初始化為空字符串。
用戶可以使用C++語言提供的另外一個(gè)關(guān)鍵字class來定義自己的數(shù)據(jù)結(jié)構(gòu),到時(shí)也將說明現(xiàn)在我們使用struct 的原因。現(xiàn)在使用struct定義自己的數(shù)據(jù)類型。
和Sales_item類不同的是,我們自定義的sales_data類沒有提供任何操作,sales_data類的使用者如果想執(zhí)行什么操作就必須自己動(dòng)手實(shí)現(xiàn)。例如,寫一段程序?qū)崿F(xiàn)求兩次交易相加結(jié)果的功能。程序的輸入是下面這兩條交易記錄:
0-201-78345-x 3 20.00 0-201-78345-x 2 25.00每筆交易記錄著圖書的ISBN編號(hào)、售出數(shù)量和售出單價(jià)。
使用Sales_data類
因?yàn)閟ales_data類沒有提供任何操作,所以我們必須自己編碼實(shí)現(xiàn)輸入、輸出和相加的功能。假設(shè)已知Sales_data類定義于Sales_data.h文件內(nèi)。
#include <iostream> #include <string> #include "Sales_data.h" int main() {Sales_data data1, data2;// code to read into data1 and data2// code to check whether data1 and data2 have the same ISBN// and if so print the sum of data1 and data2 }Sales_data對(duì)象讀入數(shù)據(jù)
在此之前,我們先了解一點(diǎn)兒關(guān)于string 的知識(shí)以便定義和使用我們的ISBN成員。string類型其實(shí)就是字符的序列,它的操作有>>、<<和==等,功能分別是讀入字符串、寫出字符串和比較字符串。這樣我們就能書寫代碼讀入兩筆交易了:
double price = 0; // price per book, used to calculate total revenue // read the first transactions: ISBN, number of books sold, price per book std::cin >> data1.bookNo >> data1.units_sold >> price; // calculate total revenue from price and units_sold data1.revenue = data1.units_sold * price;// read the second transaction std::cin >> data2.bookNo >> data2.units_sold >> price; data2.revenue = data2.units_sold * price;輸出兩個(gè)Sales_data對(duì)象的和
剩下的工作就是檢查兩筆交易涉及的工SBN編號(hào)是否相同了。如果相同輸出它們的和,否則輸出一條報(bào)錯(cuò)信息:
if (data1.bookNo == data2.bookNo) {unsigned totalCnt = data1.units_sold + data2.units_sold;double totalRevenue = data1.revenue + data2.revenue;// print: ISBN, total sold, total revenue, average price per bookstd::cout << data1.bookNo << " " << totalCnt<< " " << totalRevenue << " ";if (totalCnt != 0)std::cout << totalRevenue/totalCnt << std::endl;elsestd::cout << "(no sales)" << std::endl;return 0; // indicate success } else { // transactions weren't for the same ISBNstd::cerr << "Data must refer to the same ISBN" << std::endl;return -1; // indicate failure }編寫自己的頭文件
函數(shù)體內(nèi)定義類(先了解一下),但是這樣的類畢競受到了一些限制。所以,類一般都不定義在函數(shù)體內(nèi)。當(dāng)在函數(shù)體外部定義類時(shí),在各個(gè)指定的源文件中可能只有一處該類的定義。而且,如果要在不同文件中使用同一個(gè)類,類的定義就必須保持一致。
為了確保各個(gè)文件中類的定義一致,類通常被定義在頭文件中,而且類所在頭文件的名字應(yīng)與類的名字一樣。例如,庫類型string在名為string 的頭文件中定義。又如,我們應(yīng)該把Sales_data類定義在名為sales_data.h 的頭文件中。
頭文件通常包含那些只能被定義一次的實(shí)體,如類、const和 constexpr變量等。
頭文件也經(jīng)常用到其他頭文件的功能。
例如,我們的Sales_data類包含有一個(gè)string 成員,所以Sales_data.h必須包含string.h頭文件。同時(shí),使用sales_data類的程序?yàn)榱四懿僮鱞ookNo成員需要再一次包含string.h頭文件。
這樣,事實(shí)上使用sales_data類的程序就先后兩次包含了string.h頭文件:一次是直接包含的,另有一次是隨著包含sales_data.h 被隱式地包含進(jìn)來的。有必要在書寫頭文件時(shí)做適當(dāng)處理,使其遇到多次包含的情況也能安全和正常地工作。
頭文件一旦改變,相關(guān)的源文件必須重新編譯以獲取更新過的聲明。
預(yù)處理器
確保頭文件多次包含仍能安全工作的常用技術(shù)是預(yù)處理器(preprocessor),它由C++語言從C語言繼承而來。預(yù)處理器是在編譯之前執(zhí)行的一段程序,可以部分地改變我們所寫的程序。之前已經(jīng)用到了一項(xiàng)預(yù)處理功能#include,當(dāng)預(yù)處理器看到#include標(biāo)記時(shí)就會(huì)用指定的頭文件的內(nèi)容代替#include。
C++程序還會(huì)用到的一項(xiàng)預(yù)處理功能是頭文件保護(hù)符(header guard),頭文件保護(hù)符依賴于預(yù)處理變量。預(yù)處理變量有兩種狀態(tài):已定義和未定義。
- #define指令把一個(gè)名字設(shè)定為預(yù)處理變量,
另外兩個(gè)指令則分別檢查某個(gè)指定的預(yù)處理變量是否已經(jīng)定義:
- #ifdef當(dāng)且僅當(dāng)變量已定義時(shí)為真,
- #ifndef當(dāng)且僅當(dāng)變量未定義時(shí)為真。
一旦檢查結(jié)果為真,則執(zhí)行后續(xù)操作直至遇到 #endif 指令為止。
使用這些功能就能有效地防止重復(fù)包含的發(fā)生:
#ifndef SALES_DATA_H #define SALES_DATA_H #include <string> struct Sales_data {std::string bookNo;unsigned units_sold = 0;double revenue = 0.0; }; #endif第一次包含sales_data.h時(shí),#ifndef的檢查結(jié)果為真,預(yù)處理器將順序執(zhí)行后面的操作直至遇到#endif 為止。此時(shí),預(yù)處理變量SALES_DATA_H的值將變?yōu)橐讯x,而且sales_data. h也會(huì)被拷貝到我們的程序中來。
后面如果再一次包含sales_data.h,則#ifndef 的檢查結(jié)果將為假,編譯器將忽略#ifndef到#endif之間的部分。
預(yù)處理變量無視C++語言中關(guān)于作用域的規(guī)則。
整個(gè)程序中的預(yù)處理變量包括頭文件保護(hù)符必須唯一,通常的做法是基于頭文件中類的名字來構(gòu)建保護(hù)符的名字,以確保其唯一性。為了避免與程序中的其他實(shí)體發(fā)生名字沖突,一般把預(yù)處理變量的名字全部大寫。
頭文件即使(目前還)沒有被包含在任何其他頭文件中,也應(yīng)該設(shè)置保護(hù)符。頭文件保護(hù)符很簡單,程序員只要習(xí)慣性地加上就可以了,沒必要太在乎你的程序到底需不需要。
(Note:日后寫頭文件都要設(shè)置保護(hù)符)
總結(jié)
以上是生活随笔為你收集整理的《C++ Primer 5th》笔记(2 / 19):变量和基本类型的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: 《Java8实战》笔记(10):用Opt
- 下一篇: 《Unity2018入门与实战》笔记(9
