《C++ Primer 5th》笔记(2 / 19):变量和基本类型
文章目錄
- 基本內(nèi)置類型
- 算術(shù)類型
- 內(nèi)置類型的機器實現(xiàn)(類型在物理層面上的說明)
- 建議:如何選擇類型
- 類型轉(zhuǎn)換
- 建議:避免無法預(yù)知和依賴于實現(xiàn)環(huán)境的行為
- 算術(shù)表達(dá)式里使用布爾值
- 含有無符號類型的表達(dá)式(帶符號數(shù)會自動地轉(zhuǎn)換成無符號數(shù))
- 提示:切勿混用帶符號類型和無符號類型
- 字面值常量
- 整型和浮點型字面值
- 字符和字符串字面值
- 轉(zhuǎn)義序列
- 通過前后綴指定字面值的類型
- 布爾字面值和指針字面值
- 變量
- 變量定義
- 術(shù)語:何為對象?
- 初始值
- 列表初始化
- 默認(rèn)初始化
- 提示:未初始化變量引發(fā)運行時故障
- 變量聲明和定義的關(guān)系
- 關(guān)鍵概念:靜態(tài)類型
- 標(biāo)識符
- 名字的作用域
- 建議:當(dāng)你第一次使用變量時再定義它
- 嵌套的作用域
- 復(fù)合類型
- 引用
- 引用即別名
- 引用的定義
- 指針
- 獲取對象的地址
- 指針值
- 利用指針訪問對象
- 關(guān)鍵概念:一符多義(& 與 *)
- 空指針
- 建議:初始化所有指針
- 賦值和指針
- 其他指針操作
- void* 指針
- 理解復(fù)合類型的聲明
- 定義多個變量
- 指向指針的指針
- 指向指針的引用(指針的別名)(從右向左閱讀理解)
- const限定符
- 概述
- 初始化和const
- 默認(rèn)狀態(tài)下,const對象僅在文件內(nèi)有效(const 常量在多文件中使用方法)
- const的引用
- 術(shù)語:常量引用是對const的引用
- 初始化和對const的引用
- 對const的引用可能引用一個并非const的對象
- 指針和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對象讀入數(shù)據(jù)
- 輸出兩個Sales_data對象的和
- 編寫自己的頭文件
- 預(yù)處理器
數(shù)據(jù)類型決定了程序中數(shù)據(jù)和操作的意義。如下所示的語句:
i = i + j;其含義依賴于i和j的數(shù)據(jù)類型。如果i和j都是整型數(shù),那么這條語句執(zhí)行的就是最普通的加法運算。然而,如果i和j是Sales_item類型(上一章內(nèi)容)的數(shù)據(jù)則上述語句把這兩個對象的成分相加。
基本內(nèi)置類型
C++定義基本數(shù)據(jù)類型:
- 算術(shù)類型(arithmetic type)
- 字符
- 整型數(shù)
- 布爾值
- 浮點數(shù)
- 空類型(void)
- 空類型不對應(yīng)具體的值,僅用于一些特殊的場合,例如最常見的是,當(dāng)函數(shù)不返回任何值時使用空類型作為返回類型。
算術(shù)類型
算術(shù)類型分為:
- 整型(integral type,包括字符和布爾類型在內(nèi))
- 浮點型
算術(shù)類型的尺寸(也就是該類型數(shù)據(jù)所占的比特數(shù))在不同機器上有所差別。
下表列出了C++標(biāo)準(zhǔn)規(guī)定的尺寸的最小值,同時允許編譯器賦予這些類型更大的尺寸。某一類型所占的比特數(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 | 單精度浮點數(shù) | 6位有效數(shù)字 |
| double | 雙精度浮點數(shù) | 10位有效數(shù)字 |
| long double | 擴(kuò)展精度浮點數(shù) | 10位有效數(shù)字 |
bool
布爾類型(bool)的取值是真(true)或者假(false)。
char
C++提供了幾種字符類型,其中多數(shù)支持國際化。基本的字符類型是char,一個 char的空間應(yīng)確保可以存放機器基本字符集中任意字符對應(yīng)的數(shù)字值。也就是說,一個char的大小和一個機器字節(jié)一樣。
其他字符類型用于擴(kuò)展字符集,如 wchar_t、char16_t、char32_t。wchar_t類型用于確保可以存放機器最大擴(kuò)展字符集中的任意一個字符,類型 charl6_t和char32_t則為Unicode字符集服務(wù)(Unicode是用于表示所有自然語言中字符的標(biāo)準(zhǔn))。
int
除字符和布爾類型之外,其他整型用于表示(可能)不同尺寸的整數(shù)。C++語言規(guī)定一個int至少和一個short一樣大,一個long至少和一個int一樣大,一個long long至少和一個long一樣大。其中,數(shù)據(jù)類型long long是在C++11中新定義的。
float
浮點型可表示單精度、雙精度和擴(kuò)展精度值。C++標(biāo)準(zhǔn)指定了一個浮點數(shù)有效位數(shù)的最小值,然而大多數(shù)編譯器都實現(xiàn)了更高的精度。通常,float 以1個字(32比特)來表示,double 以2個字(64比特)來表示,long double 以3或4個字(96或128比特)來表示。一般來說,類型float和 double分別有7和16個有效位;類型long double則常常被用于有特殊浮點需求的硬件,它的具體實現(xiàn)不同,精度也各不相同。
帶符號類型和無符號類型
除去布爾型和擴(kuò)展的字符型之外,其他整型可以劃分為:
類型int、short、long和 long long 都是帶符號的,通過在這些類型名前添加unsigned就可以得到無符號類型,例如unsigned long。類型unsigned int可以縮寫為unsigned。
與其他整型不同,字符型被分為了三種:char、signed char和unsigned char。
特別需要注意的是:類型char和類型signed char并不一樣。盡管字符型有三種,但是字符的表現(xiàn)形式卻只有兩種:帶符號的和無符號的。類型char實際上會表現(xiàn)為上述兩種形式中的一種,具體是哪種由編譯器決定。
無符號類型中所有比特都用來存儲值,例如,8比特的unsigned char可以表示0至255區(qū)間內(nèi)的值。
C++標(biāo)準(zhǔn)并沒有規(guī)定帶符號類型應(yīng)如何表示,但是約定了在表示范圍內(nèi)正值和負(fù)值的量應(yīng)該平衡。因此,8比特的signed char理論上應(yīng)該可以表示-127至127區(qū)間內(nèi)的值,大多數(shù)現(xiàn)代計算機將實際的表示范圍定為-128至127。
內(nèi)置類型的機器實現(xiàn)(類型在物理層面上的說明)
計算機以比特序列存儲數(shù)據(jù),每個比特非0即1,例如:
00011011011100010110010000111011…
大多數(shù)計算機以2的整數(shù)次冪個比特作為塊來處理內(nèi)存,可尋址的最小內(nèi)存塊稱為“字節(jié)(byte)”,存儲的基本單元稱為“字(word)”,它通常由幾個字節(jié)組成。在C++語言中,一個字節(jié)要至少能容納機器基本字符集中的字符。
大多數(shù)機器的字節(jié)由8比特(bit)構(gòu)成(1byte = 8bits),字則由32或64比特構(gòu)成,也就是4或8字節(jié)(1 word = 4 bytes or 8 bytes)。大多數(shù)計算機將內(nèi)存中的每個字節(jié)與一個數(shù)字(被稱為“地址(address)”)關(guān)聯(lián)起來。
在一個字節(jié)為8比特、字為32比特的機器上,我們可能看到一個字的內(nèi)存區(qū)域如下所示:
其中,左側(cè)是字節(jié)的地址,右側(cè)是字節(jié)中8比特的具體內(nèi)容。
我們能夠使用某個地址來表示從這個地址開始的大小不同的比特串,例如,我們可能會說地址736424的那個字或者地址736427的那個字節(jié)。
為了賦予內(nèi)存中某個地址明確的含義,必須首先知道存儲在該地址的數(shù)據(jù)的類型。類型決定了數(shù)據(jù)所占的比特數(shù)以及該如何解釋這些比特的內(nèi)容:
- 如果位置736424處的對象類型是float,并且該機器中float以32比特存儲,那么我們就能知道這個對象的內(nèi)容占滿了整個字。這個 float 數(shù)的實際值依賴于該機器是如何存儲浮點數(shù)的。
- 如果位置736424處的對象類型是unsigned char,并且該機器使用ISO-Latin-1字符集,則該位置處的字節(jié)表示一個分號。
Note:這里在物理層面說明一個變量的類型的作用。
建議:如何選擇類型
和C語言一樣,C的設(shè)計準(zhǔn)則之一也是盡可能地接近硬件。C++的算術(shù)類型必須滿足各種硬件特質(zhì),所以它們常常顯得繁雜而令人不知所措。事實上,大多數(shù)程序員能夠(也應(yīng)該)對數(shù)據(jù)類型的使用做出限定從而簡化選擇的過程。以下是選擇類型的一些經(jīng)驗準(zhǔn)則:
-
當(dāng)明確知曉數(shù)值不可能為負(fù)時,選用無符號類型。(Note:無負(fù)無符,嗚呼嗚呼)
-
使用int執(zhí)行整數(shù)運算。在實際應(yīng)用中,short常常顯得太小而long一般和int有一樣的尺寸。如果你的數(shù)值超過了int 的表示范圍,選用long long。(Note:取中庸int,按需用short或long)
-
在算術(shù)表達(dá)式中盡量不要使用 char或bool,而是只有在存放字符或布爾值時才使用它們。因為類型char在一些機器上是有符號的,而在另一些機器上又是無符號的,所以如果使用char進(jìn)行運算特別容易出問題。如果你需要使用一個不大的整數(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í)行浮點數(shù)運算選用 double,這是因為float通常精度不夠而且雙精度浮點數(shù)和單精度浮點數(shù)的計算代價相差無幾。事實上,對于某些機器來說,雙精度運算甚至比單精度還快。long double提供的精度在一般情況下是沒有必要的,況且它帶來的運行時消耗也不容忽視。(Note:浮點數(shù)直接上double)
類型轉(zhuǎn)換
對象的類型定義了對象能包含的數(shù)據(jù)和能參與的運算,其中一種運算被大多數(shù)類型支持,就是將對象從一種給定的類型轉(zhuǎn)換(convert)為另一種相關(guān)類型。
當(dāng)在程序的某處我們使用了一種類型而其實對象應(yīng)該取另一種類型時,程序會自動進(jìn)行類型轉(zhuǎn)換。
將來第4章會有更詳細(xì)的介紹類型轉(zhuǎn)換:
此處,有必要說明當(dāng)給某種類型的對象強行賦了另一種類型的值時,到底會發(fā)生什么。
bool b = 42; // b is true /* 當(dāng)我們把一個非布爾類型的算術(shù)值賦給布爾類型時,初始值為О則結(jié)果為false,否則結(jié)果為true。 */int i = b; // i has value 1 /* 當(dāng)我們把一個布爾值賦給非布爾類型時,初始值為false 則結(jié)果為0,初始值為true則結(jié)果為1。 */i = 3.14; // i has value 3 /* 當(dāng)我們把一個浮點數(shù)賦給整數(shù)類型時,進(jìn)行了近似處理。結(jié)果值將僅保留浮點數(shù)中小數(shù)點之前的部分(截斷取整數(shù)部分)。 */double pi = i; // pi has value 3.0 /* 當(dāng)我們把一個整數(shù)值賦給浮點類型時,小數(shù)部分記為0。如果該整數(shù)所占的空間超過了浮點類型的容量,精度可能有損失。 */unsigned char c = -1; // assuming 8-bit chars, c has value 255 /* 當(dāng)我們賦給無符號類型一個超出它表示范圍的值時,結(jié)果是初始值對無符號類型表示數(shù)值總數(shù)取模后的余數(shù)。例如,8比特大小的unsigned char可以表示0至255區(qū)間內(nèi)的值,如果我們賦了一個區(qū)間以外的值,則實際的結(jié)果是該值對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個人認(rèn)為以底層知識更容易理解這次轉(zhuǎn)換 計算機保存數(shù)值都以補碼的形式保存。-1的補碼是11111111,但char的類型是無符號整形數(shù),編譯器就把11111111當(dāng)作無符號整形來輸出。11111111當(dāng)作正數(shù)時值就是255。 */signed char c2 = 256; // assuming 8-bit chars, the value of c2 is undefined /* 當(dāng)我們賦給帶符號類型一個超出它表示范圍的值時,結(jié)果是未定義的(undefined)。此時,程序可能繼續(xù)工作、可能崩潰,也可能生成垃圾數(shù)據(jù)。 */類型所能表示的值的范圍決定了轉(zhuǎn)換的過程。
建議:避免無法預(yù)知和依賴于實現(xiàn)環(huán)境的行為
無法預(yù)知的行為源于編譯器無須(有時是不能)檢測的錯誤。即使代碼編譯通過了,如果程序執(zhí)行了一條未定義的表達(dá)式,仍有可能產(chǎn)生錯誤。
不幸的是,在某些情況和/或某些編譯器下,含有無法預(yù)知行為的程序也能正確執(zhí)行。但是我們卻無法保證同樣一個程序在別的編譯器下能正常工作,甚至已經(jīng)編譯通過的代碼再次執(zhí)行也可能會出錯。此外,也不能認(rèn)為這樣的程序?qū)σ唤M輸入有效,對另一組輸入就一定有效。
程序也應(yīng)該盡量避免依賴于實現(xiàn)環(huán)境的行為。如果我們把 int的尺寸看成是一個確定不變的已知值,那么這樣的程序就稱作不可移植的(nonportable)。當(dāng)程序移植到別的機器上后,依賴于實現(xiàn)環(huán)境的程序就可能發(fā)生錯誤。要從過去的代碼中定位這類錯誤可不是一件輕松愉快的工作。
算術(shù)表達(dá)式里使用布爾值
當(dāng)在程序的某處使用了一種算術(shù)類型的值而其實所需的是另一種類型的值時,編譯器同樣會執(zhí)行上述的類型轉(zhuǎn)換。
例如,如果我們使用了一個非布爾值作為條件,那么它會被自動地轉(zhuǎn)換成布爾值,這一做法和把非布爾值賦給布爾變量時的操作完全一樣:
int i = 42; if (i) // if條件的值將為truei = 0;如果i的值為0,則條件的值為false;i的所有其他取值(非0)都將使條件為true。
以此類推,如果我們把一個布爾值用在算術(shù)表達(dá)式里,則它的取值非0即1,所以一般不宜在算術(shù)表達(dá)式里使用布爾值。
含有無符號類型的表達(dá)式(帶符號數(shù)會自動地轉(zhuǎn)換成無符號數(shù))
記住:無符號類型與有符號類型混合表達(dá)式中,帶符號數(shù)會自動地轉(zhuǎn)換成無符號數(shù)。
盡管我們不會故意給無符號對象賦一個負(fù)值,卻可能(特別容易)寫出這么做的代碼。
例一:
例如,當(dāng)一個算術(shù)表達(dá)式中既有無符號數(shù)又有int 值時,那個int值就會轉(zhuǎn)換成無符號數(shù)。把int轉(zhuǎn)換成無符號數(shù)的過程和把int直接賦給無符號變量一樣:
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)從無符號數(shù)中減去一個值時,不管這個值是不是無符號數(shù),我們都必須確保結(jié)果不能是一個負(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例三:
無符號數(shù)不會小于0這一事實同樣關(guān)系到循環(huán)的寫法。例如,寫一個循環(huán),通過控制變量遞減的方式把從10到0的數(shù)字降序輸出。這個循環(huán)可能類似于下面的形式:
for (int i = 10; i >= 0; --i)std::cout << i << std::endl;可能你會覺得反正也不打算輸出負(fù)數(shù),可以用無符號數(shù)來重寫這個循環(huán)。
然而,這個不經(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時發(fā)生了什么,這次迭代輸出0,然后繼續(xù)執(zhí)行for語句里的表達(dá)式。表達(dá)式–u從u當(dāng)中減去1,得到的結(jié)果-1并不滿足無符號數(shù)的要求,此時像所有表示范圍之外的其他數(shù)字一樣,-1被自動地轉(zhuǎn)換成一個合法的無符號數(shù)。假設(shè) int類型占32位,則當(dāng)u等于0時,–u的結(jié)果將會是4294967295。
一種解決的辦法是(不如改回int ╮(╯▽╰)╭),用while語句來代替for語句,因為前者讓我們能夠在輸出變量之前(而非之后)先減去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í)行對循環(huán)控制變量減1的操作,這樣最后一次迭代時,進(jìn)入循環(huán)的u值為1。此時將其減1,則這次迭代輸出的數(shù)就是0:下一次再檢驗循環(huán)條件時,u的值等于0而無法再進(jìn)入循環(huán)。
因為我們要先做減1的操作,所以初始化u的值應(yīng)該比要輸出的最大值大1。這里,u初始化為11,輸出的最大數(shù)是10。(也就預(yù)大一位)。
提示:切勿混用帶符號類型和無符號類型
如果表達(dá)式里既有帶符號類型又有無符號類型,當(dāng)帶符號類型取值為負(fù)時會出現(xiàn)異常結(jié)果,這是因為帶符號數(shù)會自動地轉(zhuǎn)換成無符號數(shù)。
例如,在一個形如a * b的式子中,如果a=-1,b=1,而且a和b都是int,則表達(dá)式的值顯然為-1。
然而,如果a是int,而b是unsigned,則結(jié)果須視在當(dāng)前機器上int所占位數(shù)而定。在我們的環(huán)境里,結(jié)果是4294967295。
字面值常量
一個形如42的值被稱作字面值常量(literal)。每個字面值常量都對應(yīng)一種數(shù)據(jù)類型,字面值常量的形式和值決定了它的數(shù)據(jù)類型。
整型和浮點型字面值
我們可以將整型字面值寫作十進(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ù)類型由它的值和符號決定。默認(rèn)情況下,十進(jìn)制字面值是帶符號數(shù),八進(jìn)制和十六進(jìn)制字面值既可能是帶符號的也可能是無符號的。十進(jìn)制字面值的類型是int、long和 long long 中尺寸最小的那個(例如,三者當(dāng)中最小是int),當(dāng)然前提是這種類型要能容納下當(dāng)前的值。(帶負(fù)號的八進(jìn)制、十六進(jìn)數(shù)少見)。
八進(jìn)制和十六進(jìn)制字面值的類型是能容納其數(shù)值的int、unsigned int、long、unsigned long、long long和 unsigned long long中的尺寸最小者。如果一個字面值連與之關(guān)聯(lián)的最大的數(shù)據(jù)類型都放不下,將產(chǎn)生錯誤。類型short沒有對應(yīng)的字面值。
以U、L等后綴可以代表相應(yīng)的字面值類型。
盡管整型字面值可以存儲在帶符號數(shù)據(jù)類型中,但嚴(yán)格來說,十進(jìn)制字面值不會是負(fù)數(shù)。如果我們使用了一個形如-42的負(fù)十進(jìn)制字面值,那個負(fù)號并不在字面值之內(nèi),它的作用僅僅是對字面值取負(fù)值而已。
浮點型字面值表現(xiàn)為一個小數(shù)或以科學(xué)計數(shù)法表示的指數(shù),其中指數(shù)部分用E或e標(biāo)識:
- 3.14159
- 3.14159E0
- 0.
- 0e0
- .001
默認(rèn)的,浮點型字面值是一個double,我們可以用后綴F等來表示其他浮點型。
字符和字符串字面值
由單引號括起來的一個字符稱為char型字面值,雙引號括起來的零個或多個字符則構(gòu)成字符串型字面值。
- ‘a(chǎn)’:字符字面值
- "Hello world! ":字符串字面值
字符串字面值的類型實際上是由常量字符構(gòu)成的數(shù)組(array)。編譯器在每個字符串的結(jié)尾處添加一個空字符(’\0’),因此,字符串字面值的實際長度要比它的內(nèi)容多1。
例如,
- 字面值’A‘表示的就是單獨的字符A
- 字符串"A"則代表了一個字符的數(shù)組,該數(shù)組包含兩個字符:一個是字母A、另一個是空字符(’\0’)。
如果兩個字符串字面值位置緊鄰且僅由空格、縮進(jìn)和換行符分隔,則它們實際上是一個整體。
當(dāng)書寫的字符串字面值比較長,寫在一行里不太合適時,就可以采取分開書寫的方式:
//分多行書寫的字符串字面值 std::cout<< "a really, really long string literal ""that spans two lines" << std::endl; //與Java相比,不用+號轉(zhuǎn)義序列
有兩類字符程序員不能直接使用:
對于特殊含義的字符需要用到轉(zhuǎn)義序列(escape sequence),轉(zhuǎn)義序列均以反斜線作為開始,C++語言規(guī)定的轉(zhuǎn)義序列包括:
-
換行符 \n
-
縱向制表符 \v
-
反斜線 \ \
-
回車符 \r
-
橫向制表符 \t
-
退格符 \b
-
問號 ?
-
進(jìn)紙符 \f
-
報警(響鈴)符 \a
-
雙引號 \ "
-
單引號 \ ’
在程序中,上述轉(zhuǎn)義序列被當(dāng)作一個字符使用:
std::cout << '\n'; // prints a newline std::cout << "\tHi!\n"; // prints a tab followd by "Hi!" and a newline(偏僻語法,不能一見知意,少用)我們也可以使用泛化的轉(zhuǎn)義序列,其形式是\x后緊跟1個或多個十六進(jìn)制數(shù)字,或者\后緊跟1個、2個或3個八進(jìn)制數(shù)字,其中數(shù)字部分表示的是字符對應(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個,只有前3個數(shù)字與\構(gòu)成轉(zhuǎn)義序列。
例如,"\1234"表示2個字符,即八進(jìn)制數(shù)123對應(yīng)的字符以及字符4。
相反,\x要用到后面跟著的所有數(shù)字,例如,"\x1234"表示一個16位的字符,該字符由這4個十六進(jìn)制數(shù)所對應(yīng)的比特唯一確定。
因為大多數(shù)機器的char型數(shù)據(jù)占8位,所以上面這個例子可能會報錯。一般來說,超過8位的十六進(jìn)制字符都是與U等前綴作為開頭的擴(kuò)展字符集一起使用的。
通過前后綴指定字面值的類型
通過添加前綴和后綴,可以改變整型、浮點型和字符型字面值的默認(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)使用一個長整型字面值時,請使用大寫字母L來標(biāo)記,因為小寫字母l和數(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 |
浮點數(shù)字面值
| f or F | float |
| l or L | long double |
對于一個整型字面值來說,我們能分別指定它是否帶符號以及占用多少空間。如果后綴中有u,則該字面值屬于無符號類型,也就是說,以u為后綴的十進(jìn)制數(shù)、八進(jìn)制數(shù)或十六進(jìn)制數(shù)都將從unsigned int、unsigned long和 unsigned long long中選擇能匹配的空間最小的一個作為其數(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是指針字面值。
變量
變量提供一個具名的、可供程序操作的存儲空間。
C++中的每個變量都有其數(shù)據(jù)類型,數(shù)據(jù)類型決定著變量:
- 所占內(nèi)存空間的大小和布局方式、(內(nèi)存大小)
- 該空間能存儲的值的范圍,(范圍)
- 以及變量能參與的運算。(運算)
對C++程序員來說,“變量(variable)”和“對象(object)”一般可以互換使用。
變量定義
變量定義的基本形式是:首先是類型說明符(type specifier),隨后緊跟由一個或多個變量名組成的列表,其中變量名以逗號分隔,最后以分號結(jié)束。
列表中每個變量名的類型都由類型說明符指定,定義時還可以為一個或多個變量賦初值:
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章中對string類型做更詳細(xì)的介紹。眼下,只需了解string是一種表示可變長字符序列的數(shù)據(jù)類型即可。
C++庫提供了幾種初始化string對象的方法,其中一種是把字面值拷貝給string對象,因此在上例中,book被初始化為0-201-78345-X。
術(shù)語:何為對象?
C++程序員們在很多場合都會使用對象(object)這個名詞。通常情況下,對象是指一塊能存儲數(shù)據(jù)并具有某種類型的內(nèi)存空間。
- 一些人僅在與類有關(guān)的場景下才使用“對象”這個詞。
- 另一些人則把已命名的對象和未命名的對象區(qū)分開來,他們把命名了的對象叫做變量。
- 還有一些人把對象和值區(qū)分開來,其中對象指能被程序修改的數(shù)據(jù),而值(value)指只讀的數(shù)據(jù)。
本書遵循大多數(shù)人的習(xí)慣用法,即認(rèn)為對象是具有某種數(shù)據(jù)類型的內(nèi)存空間。我們在使用對象這個詞時,并不嚴(yán)格區(qū)分是類還是內(nèi)置類型,也不區(qū)分是否命名或是否只讀。
(記住:對象是具有某種數(shù)據(jù)類型的內(nèi)存空間)
初始值
當(dāng)對象在創(chuàng)建時獲得了一個特定的值,我們說這個對象被初始化(initialized)了。
用于初始化變量的值可以是任意復(fù)雜的表達(dá)式。
當(dāng)一次定義了兩個或多個變量時,對象的名字隨著定義也就馬上可以使用了。
因此在同一條定義語句中,可以用先定義的變量值去初始化后定義的其他變量。
// 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++語言中,初始化是一個異常復(fù)雜的問題,我們也將反復(fù)討論這個問題。
很多程序員對于用等號=來初始化變量的方式倍感困惑,這種方式容易讓人認(rèn)為初始化是賦值的一種。事實上在C++語言中,初始化和賦值是兩個完全不同的操作。然而在很多編程語言中二者的區(qū)別幾乎可以忽略不計,即使在C++語言中有時這種區(qū)別也無關(guān)緊要,所以人們特別容易把二者混為一談。
需要強調(diào)的是,這個概念至關(guān)重要,我們也將在后面不止一次提及這一點。
初始化不是賦值,初始化的含義是創(chuàng)建變量時賦予其一個初始值,而賦值的含義是把對象的當(dāng)前值擦除,而以一個新值來替代。
(Note: 初始化和賦值是兩碼事,即使它們很相似。當(dāng)某一變量首次用=號的就是初始化,其他地方用=號的就是賦值)
列表初始化
C++語言定義了初始化的好幾種不同形式,這也是初始化問題復(fù)雜性的一個體現(xiàn)。例如,要想定義一個名為units_sold的int變量并初始化為0,以下的4條語句都可以做到這一點:
int units_sold = 0; int units_sold = {0}; int units_sold{0}; int units_sold(0);作為C++11新標(biāo)準(zhǔn)的一部分,用花括號來初始化變量得到了全面應(yīng)用,而在此之前,這種初始化的形式僅在某些受限的場合下才能使用。出于3.3.1節(jié)將要介紹的原因,這種初始化的形式被稱為列表初始化(list initialization)。現(xiàn)在,無論是初始化對象還是某些時候為對象賦新值,都可以使用這樣一組由花括號括起來的初始值了。
當(dāng)用于內(nèi)置類型的變量時,這種初始化形式有一個重要特點:如果我們使用列表初始化且初始值存在丟失信息的風(fēng)險,則編譯器將報錯:
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ù)據(jù),所以編譯器拒絕了a和b的初始化請求。
其中,至少ld的小數(shù)部分會丟失掉,而且int也可能存不下ld的整數(shù)部分。
(Note: 列表初始化變量轉(zhuǎn)型有數(shù)據(jù)丟失報錯功能???)
默認(rèn)初始化
如果定義變量時沒有指定初值,則變量被默認(rèn)初始化(default initialized),此時變量被賦予了“默認(rèn)值”。默認(rèn)值到底是什么由變量類型決定,同時定義變量的位置也會對此有影響。
如果是內(nèi)置類型的變量未被顯式初始化,它的值由定義的位置決定。定義于任何函數(shù)體之外的變量被初始化為0。
-
一種例外情況是,定義在函數(shù)體內(nèi)部的內(nèi)置類型變量將不被初始化(uninitialized)。
-
一個未被初始化的內(nèi)置類型變量的值是未定義的,如果試圖拷貝或以其他形式訪問此類值將引發(fā)錯誤。
每個類各自決定其初始化對象的方式。而且,是否允許不經(jīng)初始化就定義對象也由類自己決定。如果類允許這種行為,它將決定對象的初始值到底是什么。
絕大多數(shù)類都支持無須顯式初始化而定義對象,這樣的類提供了一個合適的默認(rèn)值。例如,String類規(guī)定如果沒有指定初值則生成一個空串:
std::string empty;//empty非顯式地初始化為一個空串 Sales_item item;//被默認(rèn)初始化的sales_item對象一些類要求每個對象都顯式初始化,此時如果創(chuàng)建了一個該類的對象而未對其做明確的初始化操作,將引發(fā)錯誤。
定義于函數(shù)體內(nèi)的內(nèi)置類型的對象如果沒有初始化,則其值未定義。類的對象如果沒有顯式地初始化,則其值由類確定。
提示:未初始化變量引發(fā)運行時故障
未初始化的變量含有一個不確定的值,使用未初始化變量的值是一種錯誤的編程行為并且很難調(diào)試。盡管大多數(shù)編譯器都能對一部分使用未初始化變量的行為提出警告,但嚴(yán)格來說,編譯器并未被要求檢查此類錯誤。
使用未初始化的變量將帶來無法預(yù)計的后果。有時我們足夠幸運,一訪問此類對象程序就崩潰并報錯,此時只要找到崩潰的位置就很容易發(fā)現(xiàn)變量沒被初始化的問題。另外一些時候,程序會一直執(zhí)行完并產(chǎn)生錯誤的結(jié)果。更糟糕的情況是,程序結(jié)果時對時錯、無法把握。而且,往無關(guān)的位置添加代碼還會導(dǎo)致我們誤以為程序?qū)α?#xff0c;其實結(jié)果仍舊有錯。
建議初始化每一個內(nèi)置類型的變量。雖然并非必須這么做,但如果我們不能確保初始化后程序安全,那么這么做不失為一種簡單可靠的方法。
(Note: 創(chuàng)建一個變量都初始化吧。保安全)
變量聲明和定義的關(guān)系
為了允許把程序拆分成多個邏輯部分來編寫,C++語言支持分離式編譯(separate compilation)機制,該機制允許將程序分割為若干個文件,每個文件可被獨立編譯。
如果將程序分為多個文件,則需要有在文件間共享代碼的方法。例如,一個文件的代碼可能需要使用另一個文件中定義的變量。一個實際的例子是std: :cout和std::cin,它們定義于標(biāo)準(zhǔn)庫,卻能被我們寫的程序使用。
為了支持分離式編譯,C++語言將聲明和定義區(qū)分開來。
- 聲明(declaration)使得名字為程序所知,一個文件如果想使用別處定義的名字則必須包含對那個名字的聲明。
- 定義(definition)負(fù)責(zé)創(chuàng)建與名字關(guān)聯(lián)的實體。
變量聲明規(guī)定了變量的類型和名字,在這一點上定義與之相同。但是除此之外,定義還申請存儲空間,也可能會為變量賦一個初始值。
如果想聲明一個變量而非定義它,就在變量名前添加關(guān)鍵字extern,而且不要顯式地初始化變量:
extern int i; // declares but does not define i int j; // declares and defines j任何包含了顯式初始化的聲明即成為定義。我們能給由extern關(guān)鍵字標(biāo)記的變量賦一個初始值,但是這么做也就抵消了extern的作用。extern語句如果包含初始值就不再是聲明,而變成定義了:
extern double pi = 3.1416; //定義在函數(shù)體內(nèi)部,如果試圖初始化一個由extern關(guān)鍵字標(biāo)記的變量,將引發(fā)錯誤。
聲明和定義的區(qū)別看起來也許微不足道,但實際上卻非常重要。(重中之重)如果要在多個文件中使用同一個變量,就必須將聲明和定義分離。此時,變量的定義必須出現(xiàn)在且只能出現(xiàn)在一個文件中,而其他用到該變量的文件必須對其進(jìn)行聲明,卻絕對不能重復(fù)定義。
(Note: 變量能且只能被定義一次,但是可以被多次聲明(只為使用它)。)
關(guān)于C++語言對分離式編譯的支持在將來做更詳細(xì)介紹。
關(guān)鍵概念:靜態(tài)類型
C++是一種靜態(tài)類型(statically typed)語言,其含義是在編譯階段檢查類型。其中,檢查類型的過程稱為類型檢查(type checking)。
對象的類型決定了對象所能參與的運算。在C++語言中,編譯器負(fù)責(zé)檢查數(shù)據(jù)類型是否支持要執(zhí)行的運算,如果試圖執(zhí)行類型不支持的運算,編譯器將報錯并且不會生成可執(zhí)行文件。
程序越復(fù)雜,靜態(tài)類型檢查越有助于發(fā)現(xiàn)問題。然而,前提是編譯器必須知道每一個實體對象的類型,這就要求我們在使用某個變量之前必須聲明其類型。
標(biāo)識符
C++的標(biāo)識符(identifier)由字母、數(shù)字和下畫線組成,其中必須以字母或下畫線開頭。標(biāo)識符的長度沒有限制,但是對大小寫字母敏感:
// defines four different int variables int somename, someName, SomeName, SOMENAME;下面兩表所示,C++語言保留了一些名字供語言本身使用,這些名字不能被用作標(biāo)識符。
同時,C++也為標(biāo)準(zhǔn)庫保留了一些名字。用戶自定義的標(biāo)識符中不能連續(xù)出現(xiàn)兩個下畫線,也不能以下畫線緊連大寫字母開頭。此外,定義在函數(shù)體外的標(biāo)識符不能以下畫線開頭。
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)識符要能體現(xiàn)實際含義。
- 變量名一般用小寫字母,如 index,不要使用Index或INDEX。
- 用戶自定義的類名一般以大寫字母開頭,如 Sales_item。
- 如果標(biāo)識符由多個單詞組成,則單詞間應(yīng)有明顯區(qū)分,如 student_loan或studentLoan,不要使用studentloan。
對于命名規(guī)范來說,若能堅持,必將有效。
(Note:使用Java駝峰命名法吧)
名字的作用域
不論是在程序的什么位置,使用到的每個名字都會指向一個特定的實體:變量、函數(shù)、類型等。然而,同一個名字如果出現(xiàn)在程序的不同位置,也可能指向的是不同實體。
作用域(scope)是程序的一部分,在其中名字有其特定的含義。C++語言中大多數(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個名字: main、sum和val,同時使用了命名空間名字std,該空間提供了2個名字cout和 cin供程序使用。
名字main定義于所有花括號之外,它和其他大多數(shù)定義在函數(shù)體之外的名字一樣擁有全局作用域(global scope)。一旦聲明之后,全局作用域內(nèi)的名字在整個程序的范圍內(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)你第一次使用變量時再定義它
一般來說,在對象第一次被使用的地方附近定義它是一種好的選擇,因為這樣做有助于更容易地找到變量的定義。更重要的是,當(dāng)變量的定義與它第一次被使用的地方很近時,我們也會賦給它一個比較合理的初始值。
嵌套的作用域
作用域能彼此包含,被包含(或者說被嵌套)的作用域稱為內(nèi)層作用域(inner scope),包含著別的作用域的作用域稱為外層作用域(outer scope)。
作用域中一旦聲明了某個名字,它所嵌套著的所有作用域中都能訪問該名字。同時,允許在內(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定義之后,此時局部變量reused正在作用域內(nèi)(in scope),因此第二條輸出語句使用的是局部變量reused而非全局變量,輸出0 0。
-
輸出#3使用作用域操作符來覆蓋默認(rèn)的作用域規(guī)則,因為全局作用域本身并沒有名字,所以當(dāng)作用域操作符的左側(cè)為空時,向全局作用域發(fā)出請求獲取作用域操作符右側(cè)名字對應(yīng)的變量。結(jié)果是,第三條輸出語句使用全局變量reused,輸出42 0。
如果函數(shù)有可能用到某全局變量,則不宜再定義一個同名的局部變量。(各個變量盡量在可控范圍有獨一名字)
復(fù)合類型
復(fù)合類型( compound type)是指基于其他類型定義的類型。C++語言有幾種復(fù)合類型,本章將介紹其中的兩種:引用和指針。
與我們已經(jīng)掌握的變量聲明相比,定義復(fù)合類型的變量要復(fù)雜很多。
上一節(jié)提到,一條簡單的聲明語句由一個數(shù)據(jù)類型和緊隨其后的一個變量名列表組成。
其實更通用的描述是,一條聲明語句由一個基本數(shù)據(jù)類型(base type)和緊隨其后的一個聲明符(declarator)列表組成。每個聲明符命名了一個變量并指定該變量為與基本數(shù)據(jù)類型有關(guān)的某種類型。
(Note: ,1條聲明語句=1個基本數(shù)據(jù)類型+1個聲明符)
目前為止,我們所接觸的聲明語句中,聲明符其實就是變量名,此時變量的類型也就是聲明的基本數(shù)據(jù)類型。其實還可能有更復(fù)雜的聲明符,它基于基本數(shù)據(jù)類型得到更復(fù)雜的類型,并把它指定給變量。
(系好安全帶吧,少年!)
引用
C++11中新增了一種引用:所謂的“右值引用(rvalue reference)”,之后會做更詳細(xì)的介紹。這種引用主要用于內(nèi)置類。
嚴(yán)格來說,當(dāng)我們使用術(shù)語“引用(reference)”時,指的其實是“左值引用(Ivalue reference)”。
引用(reference)為對象起了另外一個名字,引用類型引用(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一般在初始化變量時,初始值會被拷貝到新建的對象中。然而定義引用時,程序把引用和它的初始值綁定(bind)在一起,而不是將初始值拷貝給引用。一旦初始化完成,引用將和它的初始值對象一直綁定在一起。因為無法令引用重新綁定到另外一個對象,因此引用必須初始化。(引用一次性的)
引用即別名
引用并非對象,相反的,它只是為一個已經(jīng)存在的對象所起的另外一個名字。
(對象是具有某種數(shù)據(jù)類型的內(nèi)存空間)(引用只是對象的別名)
定義了一個引用之后,對其進(jìn)行的所有操作都是在與之綁定的對象上進(jìn)行的:
refVal = 2; // assigns 2 to the object to which refVal refers, i.e., to ival int ii = refVal; // same as ii = ival為引用賦值,實際上是把值賦給了與引用綁定的對象。獲取引用的值,實際上是獲取了與引用綁定的對象的值。同理,以引用作為初始值,實際上是以與引用綁定的對象作為初始值:
// 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因為引用本身不是一個對象,所以不能定義引用的引用。
引用的定義
允許在一條語句中定義多個引用,其中每個引用標(biā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所有引用的類型都要和與之綁定的對象嚴(yán)格匹配。而且,引用只能綁定在對象上,而不能與字面值或某個表達(dá)式的計算結(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)為對象起了另外一個名字(起別名),引用類型引用(refers to)另外一種類型。)
指針
指針(pointer)是“指向(point to)”另外一種類型的復(fù)合類型。與引用類似,指針也實現(xiàn)了對其他對象的間接訪問。
然而指針與引用相比又有很多不同點:
指針通常難以理解,即使是有經(jīng)驗的程序員也常常因為調(diào)試指針引發(fā)的錯誤而被備受折磨。
定義指針類型的方法將聲明符寫成*d的形式,其中d是變量名。如果在一條語句中定義了幾個指針變量,每個變量前面都必須有符號*:
int *ip1, *ip2; // both ip1 and ip2 are pointers to int double dp, *dp2; // dp2 is a pointer to double; dp is a double獲取對象的地址
指針存放某個對象的地址,要想獲取該地址,需要使用取地址符 address-of operator(操作符&):
int ival = 42; int *p = &ival; // p holds the address of ival; p is a pointer to ival第二條語句把p定義為一個指向 int 的指針,隨后初始化p令其指向名為 ival的int對象。
因為引用不是對象,沒有實際地址,所以不能定義指向引用的指針。
(Note: &取地址符 跟 引用&號是兩碼事,注意區(qū)分)
其他所有指針的類型都要和它所指向的對象嚴(yán)格匹配:(有兩種例外情況,日后再介紹)
(對象是具有某種數(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因為在聲明語句中指針的類型實際上被用于指定它所指向?qū)ο蟮念愋?#xff0c;所以二者必須匹配。如果指針指向了一個其他類型的對象,對該對象的操作將發(fā)生錯誤。
指針值
指針的值(即地址)應(yīng)屬下列4種狀態(tài)之一:
試圖拷貝或以其他方式訪問無效指針的值都將引發(fā)錯誤。編譯器并不負(fù)責(zé)檢查此類錯誤,這一點和試圖使用未經(jīng)初始化的變量是一樣的。訪問無效指針的后果無法預(yù)計,因此程序員必須清楚任意給定的指針是否有效。
盡管第2種和第3種形式的指針是有效的,但其使用同樣受到限制。顯然這些指針沒有指向任何具體對象,所以試圖訪問此類指針(假定的)對象的行為不被允許。如果這樣做了,后果也無法預(yù)計。
利用指針訪問對象
如果指針指向了一個對象,則允許使用解引用符 dereference operator(操作符*)來訪問該對象:
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: * & 位置不同傻傻分不清楚。)
(一條聲明語句由一個基本數(shù)據(jù)類型(base type)和緊隨其后的一個聲明符(declarator)列表組成)
int a = 1; int &aa = a;//這里&為引用聲明符(這名我自起的)int ival = 42;jie int *p = &ival;//這里*為指針聲明符(這名我自起的),&為取地址符 cout << *p;//這里*為解引用符對指針解引用會得出所指的對象,因此如果給解引用的結(jié)果賦值,實際上也就是給指針?biāo)傅膶ο筚x值:
*p = 0; // * yields the object; we assign a new value to ival through p,這里*是解引用符 cout << *p; // prints 0如上述程序所示,為*p賦值實際上是為p所指的對象賦值。
解引用操作僅適用于那些確實指向了某個對象的有效指針。
關(guān)鍵概念:一符多義(& 與 *)
像&和*這樣的符號,既能用作表達(dá)式里的運算符(&按位與符或取址符,乘法符或解引用符*),也能作為聲明的一部分出現(xiàn)(&引用聲明符,*指針聲明符),符號的上下文決定了符號的意義:
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ù)合類型(我個人將它們分別稱為引用聲明符、指針聲明符)。
-
在表達(dá)式中,&和*又轉(zhuǎn)變成運算符(操作數(shù)為1個時,分別稱為取值符、解引用符。操作數(shù)為2個時,分別稱為按位與符、乘法符)。
在不同場景下出現(xiàn)的雖然是同一個符號,但是由于含義截然不同,所以我們完全可以把它當(dāng)作不同的符號來看待。
空指針
空指針(null pointer)不指向任何對象,在試圖使用一個指針之前代碼可以首先檢查它是否為空。以下列出幾個生成空指針的方法:
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)換成任意其他的指針類型。另一種辦法就如對p2的定義一樣,也可以通過將指針初始化為字面值0來生成空指針。
過去的程序還會用到一個名為NULL的預(yù)處理變量(preprocessor variable)來給指針賦值,這個變量在頭文件cstdlib中定義,它的值就是0。
稍微介紹一點關(guān)于預(yù)處理器的知識,現(xiàn)在只要知道預(yù)處理器是運行于編譯過程之前的一段程序就可以了。預(yù)處理變量不屬于命名空間std,它由預(yù)處理器負(fù)責(zé)管理,因此我們可以直接使用預(yù)處理變量而無須在前面加上std: :。
當(dāng)用到一個預(yù)處理變量時,預(yù)處理器會自動地將它替換為實際值,因此用NULL初始化指針和用0初始化指針是一樣的。在新標(biāo)準(zhǔn)下,現(xiàn)在的C++程序最好使用nullptr,同時盡量避免使用NULL。
把int變量直接賦給指針是錯誤的操作,即使int變量的值恰好等于0也不行。
int zero = 0; pi = zero; // error: cannot assign an int to a pointer建議:初始化所有指針
使用未經(jīng)初始化的指針是引發(fā)運行時錯誤的一大原因。(像Java的NullPointerException)
和其他變量一樣,訪問未經(jīng)初始化的指針?biāo)l(fā)的后果也是無法預(yù)計的。通常這一行為將造成程序崩潰,而且一旦崩潰,要想定位到出錯位置將是特別棘手的問題。
在大多數(shù)編譯器環(huán)境下,如果使用了未經(jīng)初始化的指針,則該指針?biāo)純?nèi)存空間的當(dāng)前內(nèi)容將被看作一個地址值。訪問該指針,相當(dāng)于去訪問一個本不存在的位置上的本不存在的對象。糟糕的是,如果指針?biāo)純?nèi)存空間中恰好有內(nèi)容,而這些內(nèi)容又被當(dāng)作了某個地址,我們就很難分清它到底是合法的還是非法的了。(指針未初始化會出現(xiàn)的糟糕狀況)
因此建議初始化所有的指針,并且在可能的情況下,盡量等定義了對象之后再定義指向它的指針。如果實在不清楚指針應(yīng)該指向何處,就把它初始化為nullptr或者0,這樣程序就能檢測并知道它沒有指向任何具體的對象了。
賦值和指針
指針和引用都能提供對其他對象的間接訪問,然而在具體實現(xiàn)細(xì)節(jié)上二者有很大不同:
有時候要想搞清楚一條賦值語句到底是改變了指針的值還是改變了指針?biāo)笇ο蟮闹挡惶菀?#xff0c;最好的辦法就是記住賦值永遠(yuǎn)改變的是等號左側(cè)的對象。當(dāng)寫出如下語句時,
pi = &ival; // value in pi is changed; pi now points to ival意思是為 pi賦一個新的值,也就是改變了那個存放在pi內(nèi)的地址值。相反的,如果寫出如下語句,
*pi = 0; // value in ival is changed; pi is unchanged則*pi(也就是指針pi指向的那個對象)發(fā)生改變。
其他指針操作
只要指針擁有一個合法值,就能將它用在條件表達(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指針對應(yīng)的條件值都是true。
對于兩個類型相同的合法指針,可以用相等操作符(=)或不相等操作符(!=)來比較它們,比較的結(jié)果是布爾類型。如果兩個指針存放的地址值相同,則它們相等;反之它們不相等。
這里兩個指針存放的地址值相同(兩個指針相等)有三種可能:
需要注意的是,一個指針指向某對象,同時另一個指針指向另外對象的下一地址,此時也有可能出現(xiàn)這兩個指針值相同的情況,即指針相等。
因為上述操作要用到指針的值,所以不論是作為條件出現(xiàn)還是參與比較運算(再數(shù)組中用),都必須使用合法指針,使用非法指針作為條件或進(jìn)行比較都會引發(fā)不可預(yù)計的后果。
void* 指針
void*是一種特殊的指針類型,可用于存放任意對象的地址。一個void*指針存放著一個地址,這一點和其他指針類似。
不同的是,我們對該地址中到底是個什么類型的對象并不了解:
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ù)的輸入或輸出,
- 賦給另外一個void*指針。
不能直接操作void*指針?biāo)傅膶ο?#xff0c;因為我們并不知道這個對象到底是什么類型,也就無法確定能在這個對象上做哪些操作。
概括說來,以void*的視角來看內(nèi)存空間也就僅僅是內(nèi)存空間,沒辦法訪問內(nèi)存空間中所存的對象,不過,是有獲取void*指針?biāo)娴刂返姆椒?#xff0c;日后介紹。
理解復(fù)合類型的聲明
如前所述,變量的定義包括一個基本數(shù)據(jù)類型(base type)和一組聲明符。在同一條定義語句中,雖然基本數(shù)據(jù)類型只有一個,但是聲明符的形式卻可以不同。也就是說,一條定義語句可能定義出不同類型的變量:
// 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)系,其實后者不過是聲明符的一部分罷了。
定義多個變量
經(jīng)常有一種觀點會誤以為,在定義語句中,類型修飾符(*或s)作用于本次定義的全部變量。造成這種錯誤看法的原因有很多,其中之一是我們可以把空格寫在類型修飾符和變量名中間:
int* p; // legal but might be misleading我們說這種寫法可能產(chǎn)生誤導(dǎo)是因為int*放在一起好像是這條語句中所有變量共同的類型一樣。其實恰恰相反,基本數(shù)據(jù)類型是int而非int*。*僅僅是修飾了p而已,對該聲明語句中的其他變量,它并不產(chǎn)生任何作用:
int* p1, p2;// p1 is a pointer to int; p2 is an int涉及指針或引用的聲明,一般有兩種寫法。
第一種把修飾符和變量標(biāo)識符寫在一起:(推薦)
int *p1, *p2; // both p1 and p2 are pointers to int這種形式著重強調(diào)變量具有的復(fù)合類型。
第二種把修飾符和類型名寫在一起,并且每條語句只定義一個變量:
int* p1; // p1 is a pointer to int int* p2; // p2 is a pointer to int這種形式著重強調(diào)本次聲明定義了一種復(fù)合類型。
上述兩種定義指針或引用的不同方法沒有孰對孰錯之分,關(guān)鍵是選擇并堅持其中的一種寫法,不要總是變來變?nèi)ァ?/strong>
我們接下都采用第一種寫法,將*(或是&)與變量名連在一起。(聲明符與變量)
指向指針的指針
一般來說,聲明符中修飾符的個數(shù)并沒有限制。當(dāng)有多個修飾符連寫在一起時,按照其邏輯關(guān)系詳加解釋即可。以指針為例,指針是內(nèi)存中的對象,像其他對象一樣也有自己的地址,因此允許把指針的地址再存放到另一個指針當(dāng)中。
通過*的個數(shù)可以區(qū)分指針的級別。也就是說,**表示指向指針的指針,***表示指向指針的指針的指針,以此類推:
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型指針會得到一個int型的數(shù),同樣,解引用指向指針的指針會得到一個指針。此時為了訪問最原始的那個對象,需要對指針的指針做兩次解引用:
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的值。
指向指針的引用(指針的別名)(從右向左閱讀理解)
引用本身不是一個對象,因此不能定義指向引用的指針。但指針是對象,所以存在對指針的引用:(指針不能指向引用)
int i = 42; int *p; // p is a pointer to int int *&r = p; // r is a reference to the pointer p //對指針的引用//int &r = *p;//可以有這種寫法,有點懵,日后注意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的定義。
離變量名最近的符號(此例中是&r的符號&)對變量的類型有最直接的影響,因此r是一個引用。聲明符的其余部分用以確定r引用的類型是什么,此例中的符號*說明r引用的是一個指針。最后,聲明的基本數(shù)據(jù)類型部分指出r引用的是一個int指針。
面對一條比較復(fù)雜的指針或引用的聲明語句時,從右向左閱讀有助于弄清楚它的真實含義。
我寫的小程序來解惑:
#include <iostream> #include "Sales_item.h" int main() {int i = 1;int *p = &i;int &r = *p;int *&r2 = p;//對指針的引用//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:)
(引用與指針一起來聲明,有點懵,記住:引用本身不是一個對象,因此不能定義指向引用的指針。但指針是對象,所以存在對指針的引用)
(引用->指針 OK,指針->引用 NO)
int i = 1; int *p = &i;int &r = *p; //int *p2 = r; //指針->引用 NO int *p2 = &r; //這個r還是可以取址的,這是懵點,(引用只是對象的別名)
const限定符
概述
有時我們希望定義這樣一種變量,它的值不能被改變。(Note: 只讀變量)
例如,用一個變量來表示緩沖區(qū)的大小。使用變量的好處是當(dāng)我們覺得緩沖區(qū)大小不再合適時,很容易對其進(jìn)行調(diào)整。另一方面,也應(yīng)隨時警惕防止程序一不小心改變了這個值。為了滿足這一要求,可以用關(guān)鍵字const對變量的類型加以限定:
const int bufSize = 512; //輸入緩沖區(qū)大小這樣就把bufSize定義成了一個常量。任何試圖為bufSize賦值的行為都將引發(fā)錯誤:
bufSize = 1024; //錯誤:試圖向const對象寫值因為const對象一旦創(chuàng)建后其值就不能再改變,所以const對象必須初始化。一如既往,初始值可以是任意復(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ù)提到的,對象的類型決定了其上的操作。與非 const類型所能參與的操作相比,const類型的對象能完成其中大部分,但也不是所有的操作都適合。主要的限制就是只能在const類型的對象上執(zhí)行不改變其內(nèi)容的操作。
例如,const int和普通的int一樣都能參與算術(shù)運算,也都能轉(zhuǎn)換成一個布爾值,等等。
在不改變 const對象的操作中還有一種是初始化,如果利用一個對象去初始化另外一個對象,則它們是不是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中的值還是一個整型數(shù)。ci的常量特征僅僅在執(zhí)行改變ci的操作時才會發(fā)揮作用。當(dāng)用ci去初始化j時,根本無須在意ci是不是一個常量。拷貝一個對象的值并不會改變它,一旦拷貝完成,新的對象就和原來的對象沒什么關(guān)系了。
默認(rèn)狀態(tài)下,const對象僅在文件內(nèi)有效(const 常量在多文件中使用方法)
當(dāng)以編譯時初始化的方式定義一個const對象時,就如對bufsize的定義一樣:
const int bufSize = 512;//輸入緩沖區(qū)大小編譯器將在編譯過程中把用到該變量的地方都替換成對應(yīng)的值。也就是說,編譯器會找到代碼中所有用到bufSize的地方,然后用512替換。
為了執(zhí)行上述替換,編譯器必須知道變量的初始值。如果程序包含多個文件,則每個用了const對象的文件都必須得能訪問到它的初始值才行。要做到這一點,就必須在每一個用到變量的文件中都有對它的定義。為了支持這一用法,同時避免對同一變量的重復(fù)定義,默認(rèn)情況下,const對象被設(shè)定為僅在文件內(nèi)有效。當(dāng)多個文件中出現(xiàn)了同名的const變量時,其實等同于在不同文件中分別定義了獨立的變量。
某些時候有這樣一種 const變量,它的初始值不是一個常量表達(dá)式,但又確實有必要在文件間共享。這種情況下,我們不希望編譯器為每個文件分別生成獨立的變量。相反,我們想讓這類const對象像其他(非常量)對象一樣工作,也就是說,只在一個文件中定義const,而在其他多個文件中聲明并使用它。
解決的辦法是,對于const變量不管是聲明還是定義都添加extern關(guān)鍵字,這樣只需定義一次就可以了:
//file_1.cc定義并初始化了一個常量,該常量能被其他文件訪問 extern const int bufSize = fcn();//file_l.h頭文件 extern const int bufSize; //與file_1.cc中定義的bufSize是同一個如上述程序所示,file_1.cc定義并初始化了bufsize。因為這條語句包含了初始值,所以它(顯然〉是一次定義。然而,因為bufsize是一個常量,必須用extern加以限定使其被其他文件使用。
file_1.h頭文件中的聲明也由extern做了限定,其作用是指明bufsize并非本文件所獨有,它的定義將在別處出現(xiàn)。
如果想在多個文件之間共享const對象,必須在變量的定義之前添加extern關(guān)鍵字。
const的引用
可以把引用綁定到const對象上,就像綁定到其他對象上一樣,我們稱之為對常量的引用(reference to const)。與普通引用不同的是,對常量的引用不能被用作修改它所綁定的對象:
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因為不允許直接為ci賦值,當(dāng)然也就不能通過引用去改變ci。因此,對r2的初始化是錯誤的。假設(shè)該初始化合法,則可以通過r2來改變它引用對象的值,這顯然是不正確的。
術(shù)語:常量引用是對const的引用
C++程序員們經(jīng)常把詞組“對const的引用”簡稱為“常量引用”,這一簡稱還是挺靠譜的,不過前提是你得時刻記得這就是個簡稱而已。
嚴(yán)格來說,并不存在常量引用。因為引用不是一個對象,所以我們沒法讓引用本身恒定不變。事實上,由于C+語言并不允許隨意改變引用所綁定的對象,所以從這層意義上理解所有的引用又都算是常量。引用的對象是常量還是非常量可以決定其所能參與的操作,卻無論如何都不會影響到引用和對象的綁定關(guān)系本身。
初始化和對const的引用
前文提到,引用的類型必須與其所引用對象的類型一致,但是有兩個例外。
第一種例外情況就是在初始化常量引用時允許用任意表達(dá)式作為初始值,只要該表達(dá)式的結(jié)果能轉(zhuǎn)換成引用的類型即可。尤其,允許為一個常量引用綁定非常量的對象、字面值,甚至是個一般表達(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)一個常量引用被綁定到另外一種類型上時到底發(fā)生了什么:
double dval = 3.14; const int &ri = dval;此處ri引用了一個int型的數(shù)。對ri的操作應(yīng)該是整數(shù)運算,但dval卻是一個雙精度浮點數(shù)而非整數(shù)。因此為了確保讓ri綁定一個整數(shù),編譯器把上述代碼變成了如下形式:
const int temp = dval;//由雙精度浮點數(shù)生成一個臨時的整型常量 const int &ri = temp;//讓ri綁定這個臨時量在這種情況下,ri綁定了一個臨時量(temporary)對象。所謂臨時量對象就是當(dāng)編譯器需要一個空間來暫存表達(dá)式的求值結(jié)果時臨時創(chuàng)建的一個未命名的對象。C++程序員們常常把臨時量對象簡稱為臨時量。
接下來探討當(dāng)ri不是常量時,如果執(zhí)行了類似于上面的初始化過程將帶來什么樣的后果。
如果ri不是常量,就允許對ri賦值,這樣就會改變ri所引用對象的值。注意,此時綁定的對象是一個臨時量而非dval。程序員既然讓 ri引用dval,就肯定想通過ri改變dval的值,否則干什么要給ri賦值呢?如此看來,既然大家基本上不會想著把引用綁定到臨時量上,C++語言也就把這種行為歸為非法。
(Note: 第二種例外情況在哪???)
對const的引用可能引用一個并非const的對象
必須認(rèn)識到,常量引用僅對引用可參與的操作做出了限定,對于引用的對象本身是不是一個常量未作限定。因為對象也可能是個非常量,所以允許通過其他途徑改變它的值:
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)不能用于改變其所指對象的值。要想存放常量對象的地址,只能使用指向常量的指針:
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前文提到,指針的類型必須與其所指對象的類型一致,但是有兩個例外。第一種例外情況是允許令一個指向常量的指針指向一個非常量對象:
double dval = 3.14; // dval is a double; its value can be changed cptr = &dval; // ok: but can't change dval through cptr和常量引用一樣,指向常量的指針也沒有規(guī)定其所指的對象必須是一個常量。所謂指向常量的指針僅僅要求不能通過該指針改變對象的值,而沒有規(guī)定那個對象的值不能通過其他途徑改變。(也就是說還有其他路子改變對象的值,可查看第一節(jié))。
試試這樣想吧:所謂指向常量的指針或引用,不過是指針或引用“自以為是”罷了,它們覺得自己指向了常量,所以自覺地不去改變所指對象的值。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é)將常量指針)
指針是對象而引用不是,因此就像其他對象類型一樣,允許把指針本身定為常量。常量指針(const pointer)必須初始化,而且一旦初始化完成,則它的值(也就是存放在指針中的那個地址)就不能再改變了。把*放在const關(guān)鍵字之前用以說明指針是一個常量,這樣的書寫形式隱含著一層意味,即不變的是指針本身的值而非指向的那個值:
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)鍵)
此例中,
對象是指一塊能存儲數(shù)據(jù)并具有某種類型的內(nèi)存空間。
與之相似,我們也能推斷出,pip是一個常量指針,它指向的對象是一個雙精度浮點型常量。
指針本身是一個常量并不意味著不能通過指針修改其所指對象的值,能否這樣做完全依賴于所指對象的類型。例如,pip是一個指向常量的常量指針,則不論是 pip 所指的對象值還是pip自己存儲的那個地址都不能改變。
相反的,curErr指向的是一個一般的非常量整數(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常量指針指向的是一個int對象,這對象可以改變 }頂層const
如前所述,指針本身是一個對象,它又可以指向另外一個對象。因此,指針本身是不是常量以及指針?biāo)傅氖遣皇且粋€常量就是兩個相互獨立的問題。
用名詞頂層const(top-level const)表示指針本身是個常量,而用名詞底層const (low-level const)表示指針?biāo)傅膶ο笫且粋€常量。
更一般的:
頂層const可以表示任意的對象是常量,這一點對任何數(shù)據(jù)類型都適用,如算術(shù)類型、類、指針等。
底層const則與指針和引用等復(fù)合類型的基本類型部分有關(guān)。比較特殊的是,指針類型既可以是頂層const也可以是底層const,這一點和其他類型相比區(qū)別明顯:
(助記:頂常底復(fù))
int i = 0; //頂層const:表示任意的對象是常量 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í)行對象的拷貝操作時,常量是頂層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í)行拷貝操作并不會改變被拷貝對象的值,因此,拷入和拷出的對象是否是常量都沒什么影響。
另一方面,底層 const 的限制卻不能忽視。當(dāng)執(zhí)行對象的拷貝操作時,拷入和拷出的對象必須具有相同的底層 const 資格,或者兩個對象的數(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時可以不在乎它是一個頂層const,但是必須清楚它指向的對象得是一個常量。因此,不能用p3去初始化p,因為p指向的是一個普通的(非常量)整數(shù)。另一方面,p3的值可以賦給p2,是因為這兩個指針都是底層const,盡管p3同時也是一個常量指針(頂層const),僅就這次賦值而言不會有什么影響。
(Note:這節(jié)不好懂)
constexpr和常量表達(dá)式
常量表達(dá)式(const expression)是指值不會改變并且在編譯過程就能得到計算結(jié)果的表達(dá)式。顯然,字面值屬于常量表達(dá)式,用常量表達(dá)式初始化的const對象也是常量表達(dá)式。后面將會提到,C++語言中有幾種情況下是要用到常量表達(dá)式的。
一個對象(或表達(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的初始值是個字面值常量,但由于它的數(shù)據(jù)類型只是一個普通int而非const int,所以它不屬于常量表達(dá)式。另一方面,盡管 sz本身是一個常量,但它的具體值直到運行時才能獲取到,所以也不是常量表達(dá)式。
constexpr變量
在一個復(fù)雜系統(tǒng)中,很難(幾乎肯定不能)分辨一個初始值到底是不是常量表達(dá)式。當(dāng)然可以定義一個 const變量并把它的初始值設(shè)為我們認(rèn)為的某個常量表達(dá)式,但在實際使用時,盡管要求如此卻常常發(fā)現(xiàn)初始值并非常量表達(dá)式的情況。可以這么說,在此種情況下,對象的定義和使用根本就是兩回事兒。
C++11新標(biāo)準(zhǔn)規(guī)定,允許將變量聲明為constexpr類型以便由編譯器來驗證變量的值是否是一個常量表達(dá)式。聲明為constexpr的變量一定是一個常量,而且必須用常量表達(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)該足夠簡單以使得編譯時就可以計算其結(jié)果,這樣就能用constexpr函數(shù)去初始化 constexpr變量了。
一般來說,如果你認(rèn)定變量是一個常量表達(dá)式,那就把它聲明成 constexpr類型。
字面值類型
常量表達(dá)式的值需要在編譯時就得到計算,因此對聲明constexpr時用到的類型必須有所限制。因為這些類型一般比較簡單,值也顯而易見、容易得到,就把它們稱為“字面值類型”( literal type)。
到目前為止接觸過的數(shù)據(jù)類型中,算術(shù)類型、引用和指針都屬于字面值類型。
自定義類sales_item、IO 庫、string 類型則不屬于字面值類型,也就不能被定義成constexpr。
盡管指針和引用都能定義成constexpr,但它們的初始值卻受到嚴(yán)格限制。一個constexpr指針的初始值必須是nullptr或者0,或者是存儲于某個固定地址中的對象。
將要提到,函數(shù)體內(nèi)定義的變量一般來說并非存放在固定地址中,因此constexpr指針不能指向這樣的變量。相反的,定義于所有函數(shù)體之外的對象其地址固定不變,能用來初始化constexpr指針。
將提到,允許函數(shù)定義一類有效范圍超出函數(shù)本身的變量,這類變量和定義在函數(shù)體之外的變量一樣也有固定地址。因此,constexpr引用能綁定到這樣的變量上,constexpr 指針也能指向這樣的變量。
(Note:哪些能定義constexpr,哪些不能定義constexpr)
指針和constexpr
必須明確一點,在constexpr聲明中如果定義了一個指針,限定符constexpr僅對指針有效,與指針?biāo)傅膶ο鬅o關(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是一個指向常量的指針,而q是一個常量指針,其中的關(guān)鍵在于constexpr把它所定義的對象置為了頂層const。
與其他常量指針類似,constexpr指針既可以指向常量也可以指向一個非常量:
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)在兩個方面。
類型別名
類型別名(type alias)是一個名字,它是某種類型的同義詞。使用類型別名有很多好處,它讓復(fù)雜的類型名字變得簡單明了、易于理解和使用,還有助于程序員清楚地知道使用該類型的真實目的。
有兩種方法可用于定義類型別名。傳統(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 作為別名聲明的開始,其后緊跟別名和等號,其作用是把等號左側(cè)的名字規(guī)定成等號右側(cè)類型的別名。
類型別名和類型的名字等價,只要是類型的名字能出現(xiàn)的地方,就能使用類型別名:
wages hourly, weekly; // same as double hourly, weekly; SI item; // same as Sales_item item指針、常量和類型別名
如果某個類型別名指代的是復(fù)合類型或常量,那么把它用到聲明語句里就會產(chǎn)生意想不到的后果。例如下面的聲明語句用到了類型pstring,它實際上是類型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是對給定類型的修飾。pstring 實際上是指向char 的指針,因此,const pstring 就是指向char的常量指針,而非指向常量字符的指針。
遇到一條使用了類型別名的聲明語句時,人們往往會錯誤地嘗試把類型別名替換成它本來的樣子,以理解該語句的含義:(Note:不能像代數(shù)那樣代入)
const char *cstr = 0; // wrong interpretation of const pstring cstr再強調(diào)一遍:這種理解是錯誤的。
聲明語句中用到pstring 時,其基本數(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ù)類型。前后兩種聲明含義截然不同,前者聲明了一個指向char的常量指針,改寫后的形式則聲明了一個指向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;//我是這樣理解的,一個指向char的常量指針auto類型說明符
C++11新特性
編程時常常需要把表達(dá)式的值賦給變量,這就要求在聲明變量的時候清楚地知道表達(dá)式的類型。然而要做到這一點并非那么容易,有時甚至根本做不到。為了解決這個問題,C++11新標(biāo)準(zhǔn)引入了auto類型說明符,用它就能讓編譯器替我們?nèi)シ治霰磉_(dá)式所屬的類型。和原來那些只對應(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(具體查看上一章)的對象,則item的類型就是Sales_item;如果這兩個變量的類型是double,則item的類型就是double,以此類推。
使用auto也能在一條語句中聲明多個變量。因為一條聲明語句只能有一個基本數(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類型有時候和初始值的類型并不完全一樣,編譯器會適當(dāng)?shù)馗淖兘Y(jié)果類型使其更符合初始化規(guī)則。
首先,正如我們所熟知的,使用引用其實是使用引用的對象,特別是當(dāng)引用被用作初始值時,真正參與初始化的其實是引用對象的值。此時編譯器以引用對象的類型作為auto的類型:
int i = 0, &r = i; auto a = r; // a is an int (r is an alias for i, which has type int)其次,auto一般會忽略掉頂層const,同時底層const則會保留下來,比如當(dāng)初始值是一個指向常量的指針時:
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類型是一個頂層const,需要明確指出:
const auto f = ci; // deduced type of ci is int; f has type const int還可以將引用的類型設(shè)為auto,此時原來的初始化規(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è)置一個類型為auto的引用時,初始值中的頂層常量屬性仍然保留。和往常一樣,如果我們給初始值綁定一個引用,則此時的常量就不是頂層常量了。
要在一條語句中定義多個變量,切記,符號&和*只從屬于某個聲明符,而非基本數(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:符號&和*只從屬于某個聲明符,而非基本數(shù)據(jù)類型的一部分,這一句很重要)。
decltype類型指示符
C++11新標(biāo)準(zhǔn)
有時會遇到這種情況:希望從表達(dá)式的類型推斷出要定義的變量的類型,但是不想用該表達(dá)式的值初始化變量。為了滿足這一要求,C++11新標(biāo)準(zhǔn)引入了第二種類型說明符decltype,它的作用是選擇并返回操作數(shù)的數(shù)據(jù)類型。在此過程中,編譯器分析表達(dá)式并得到它的類型,卻不實際計算表達(dá)式的值:
decltype(f()) sum = x; // sum has whatever type f returns編譯器并不實際調(diào)用函數(shù)f,而是使用當(dāng)調(diào)用發(fā)生時f的返回值類型作為sum的類型。換句話說,編譯器為sum 指定的類型是什么呢?就是假如f被調(diào)用的話將會返回的那個類型。
decltype處理頂層const和引用的方式與auto有些許不同。如果decltype使用的表達(dá)式是一個變量,則 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因為cj是一個引用,decltype(cj)的結(jié)果就是引用類型,因此作為引用的z必須被初始化。
需要指出的是,引用從來都作為其所指對象的同義詞出現(xiàn),只有用在 decltype 處是一個例外。
decltype和引用
如果decltype使用的表達(dá)式不是一個變量,則decltype返回表達(dá)式結(jié)果對應(yīng)的類型。有些表達(dá)式將向decltype返回一個引用類型。一般來說當(dāng)這種情況發(fā)生時,意味著該表達(dá)式的結(jié)果對象能作為一條賦值語句的左值:
// 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因為r是一個引用,因此 decltype?的結(jié)果是引用類型。如果想讓結(jié)果類型是r所指的類型,可以把r作為表達(dá)式的一部分,如r+0,顯然這個表達(dá)式的結(jié)果將是一個具體值而非一個引用。
另一方面,如果表達(dá)式的內(nèi)容是解引用操作,則decltype 將得到引用類型。正如我們所熟悉的那樣,解引用指針可以得到指針?biāo)傅膶ο?#xff0c;而且還能給這個對象賦值。因此,decltype (*p)的結(jié)果類型就是int&,而非int。
decltype和 auto的另一處重要區(qū)別是,decltype的結(jié)果類型與表達(dá)式形式密切相關(guān)。有一種情況需要特別注意:對于 decltype所用的表達(dá)式來說,如果變量名加上了一對括號,則得到的類型與不加括號時會有不同。
- 如果 decltype使用的是一個不加括號的變量,則得到的結(jié)果就是該變量的類型;
- 如果給變量加上了一層或多層括號,編譯器就會把它當(dāng)成是一個表達(dá)式。
變量是一種可以作為賦值語句左值的特殊表達(dá)式,所以這樣的decltype就會得到引用類型:
// 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))(注意是雙層括號)的結(jié)果永遠(yuǎn)是引用,而decltype(variable)結(jié)果只有當(dāng) variable本身就是一個引用時才是引用。
自定義數(shù)據(jù)結(jié)構(gòu)
從最基本的層面理解,數(shù)據(jù)結(jié)構(gòu)是把一組相關(guān)的數(shù)據(jù)元素組織起來然后使用它們的策略和方法。
舉一個例子,我們的Sales_item類把書本的ISBN編號、售出量及銷售收入等數(shù)據(jù)組織在了一起,并且提供諸如isbn函數(shù)、>>、<<、+、+=等運算在內(nèi)的一系列操作,sales_item類就是一個數(shù)據(jù)結(jié)構(gòu)。
C++語言允許用戶以類的形式自定義數(shù)據(jù)類型,而庫類型string、 istream、ostream等也都是以類的形式定義的,就像上一章Sales_item類型一樣。
定義Sales_data類型
盡管我們還寫不出完整的Sales_item類,但是可以嘗試著把那些數(shù)據(jù)元素組織到一起形成一個簡單點兒的類。初步的想法是用戶能直接訪問其中的數(shù)據(jù)元素,也能實現(xiàn)一些基本的操作。
既然我們籌劃的這個數(shù)據(jù)結(jié)構(gòu)不帶有任何運算功能,不妨把它命名為 Sales_data以示與Sales_item的區(qū)別。Sales_data初步定義如下:
struct Sales_data {std::string bookNo;unsigned units_sold = 0;double revenue = 0.0; };我們的類以關(guān)鍵字struct開始,緊跟著類名和類體(其中類體部分可以為空)。類體由花括號包圍形成了一個新的作用域。類內(nèi)部定義的名字必須唯一,但是可以與類外部定義的名字重復(fù)。
類體右側(cè)的表示結(jié)束的花括號后必須寫一個分號,這是因為類體后面可以緊跟變量名以示對該類型對象的定義,所以分號必不可少:
struct Sales_data { /* ... */ } accum, trans, *salesptr; // equivalent, but better way to define these objects struct Sales_data { /* ... */ }; Sales_data accum, trans, *salesptr;分號表示聲明符(通常為空)的結(jié)束。一般來說,最好不要把對象的定義和類的定義放在一起。這么做無異于把兩種不同實體的定義混在了一條語句里,一會兒定義類,一會兒又定義變量,顯然這是一種不被建議的行為。
很多新手程序員經(jīng)常忘了在類定義的最后加上分號。
類數(shù)據(jù)成員
類體定義類的成員,我們的類只有數(shù)據(jù)成員(data member)。類的數(shù)據(jù)成員定義了類的對象的具體內(nèi)容,每個對象有自己的一份數(shù)據(jù)成員拷貝。修改一個對象的數(shù)據(jù)成員,不會影響其他Sales_data的對象。
定義數(shù)據(jù)成員的方法和定義普通變量一樣:首先說明一個基本類型,隨后緊跟一個或多個聲明符。我們的類有3個數(shù)據(jù)成員:
每個Sales_data的對象都將包括這3個數(shù)據(jù)成員。
C++11新標(biāo)準(zhǔn)規(guī)定,可以為數(shù)據(jù)成員提供一個類內(nèi)初始值(in-class initializer)。創(chuàng)建對象時,類內(nèi)初始值將用于初始化數(shù)據(jù)成員。沒有初始值的成員將被默認(rèn)初始化(函數(shù)體內(nèi)的默認(rèn)不初始化,函數(shù)體外的都默認(rèn)初始化)。因此當(dāng)定義Sales_data的對象時,units_sold和revenue都將初始化為0,bookNo將初始化為空字符串。
用戶可以使用C++語言提供的另外一個關(guān)鍵字class來定義自己的數(shù)據(jù)結(jié)構(gòu),到時也將說明現(xiàn)在我們使用struct 的原因。現(xiàn)在使用struct定義自己的數(shù)據(jù)類型。
和Sales_item類不同的是,我們自定義的sales_data類沒有提供任何操作,sales_data類的使用者如果想執(zhí)行什么操作就必須自己動手實現(xiàn)。例如,寫一段程序?qū)崿F(xiàn)求兩次交易相加結(jié)果的功能。程序的輸入是下面這兩條交易記錄:
0-201-78345-x 3 20.00 0-201-78345-x 2 25.00每筆交易記錄著圖書的ISBN編號、售出數(shù)量和售出單價。
使用Sales_data類
因為sales_data類沒有提供任何操作,所以我們必須自己編碼實現(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對象讀入數(shù)據(jù)
在此之前,我們先了解一點兒關(guān)于string 的知識以便定義和使用我們的ISBN成員。string類型其實就是字符的序列,它的操作有>>、<<和==等,功能分別是讀入字符串、寫出字符串和比較字符串。這樣我們就能書寫代碼讀入兩筆交易了:
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;輸出兩個Sales_data對象的和
剩下的工作就是檢查兩筆交易涉及的工SBN編號是否相同了。如果相同輸出它們的和,否則輸出一條報錯信息:
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ù)體外部定義類時,在各個指定的源文件中可能只有一處該類的定義。而且,如果要在不同文件中使用同一個類,類的定義就必須保持一致。
為了確保各個文件中類的定義一致,類通常被定義在頭文件中,而且類所在頭文件的名字應(yīng)與類的名字一樣。例如,庫類型string在名為string 的頭文件中定義。又如,我們應(yīng)該把Sales_data類定義在名為sales_data.h 的頭文件中。
頭文件通常包含那些只能被定義一次的實體,如類、const和 constexpr變量等。
頭文件也經(jīng)常用到其他頭文件的功能。
例如,我們的Sales_data類包含有一個string 成員,所以Sales_data.h必須包含string.h頭文件。同時,使用sales_data類的程序為了能操作bookNo成員需要再一次包含string.h頭文件。
這樣,事實上使用sales_data類的程序就先后兩次包含了string.h頭文件:一次是直接包含的,另有一次是隨著包含sales_data.h 被隱式地包含進(jìn)來的。有必要在書寫頭文件時做適當(dāng)處理,使其遇到多次包含的情況也能安全和正常地工作。
頭文件一旦改變,相關(guān)的源文件必須重新編譯以獲取更新過的聲明。
預(yù)處理器
確保頭文件多次包含仍能安全工作的常用技術(shù)是預(yù)處理器(preprocessor),它由C++語言從C語言繼承而來。預(yù)處理器是在編譯之前執(zhí)行的一段程序,可以部分地改變我們所寫的程序。之前已經(jīng)用到了一項預(yù)處理功能#include,當(dāng)預(yù)處理器看到#include標(biāo)記時就會用指定的頭文件的內(nèi)容代替#include。
C++程序還會用到的一項預(yù)處理功能是頭文件保護(hù)符(header guard),頭文件保護(hù)符依賴于預(yù)處理變量。預(yù)處理變量有兩種狀態(tài):已定義和未定義。
- #define指令把一個名字設(shè)定為預(yù)處理變量,
另外兩個指令則分別檢查某個指定的預(yù)處理變量是否已經(jīng)定義:
- #ifdef當(dāng)且僅當(dāng)變量已定義時為真,
- #ifndef當(dāng)且僅當(dāng)變量未定義時為真。
一旦檢查結(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時,#ifndef的檢查結(jié)果為真,預(yù)處理器將順序執(zhí)行后面的操作直至遇到#endif 為止。此時,預(yù)處理變量SALES_DATA_H的值將變?yōu)橐讯x,而且sales_data. h也會被拷貝到我們的程序中來。
后面如果再一次包含sales_data.h,則#ifndef 的檢查結(jié)果將為假,編譯器將忽略#ifndef到#endif之間的部分。
預(yù)處理變量無視C++語言中關(guān)于作用域的規(guī)則。
整個程序中的預(yù)處理變量包括頭文件保護(hù)符必須唯一,通常的做法是基于頭文件中類的名字來構(gòu)建保護(hù)符的名字,以確保其唯一性。為了避免與程序中的其他實體發(fā)生名字沖突,一般把預(yù)處理變量的名字全部大寫。
頭文件即使(目前還)沒有被包含在任何其他頭文件中,也應(yīng)該設(shè)置保護(hù)符。頭文件保護(hù)符很簡單,程序員只要習(xí)慣性地加上就可以了,沒必要太在乎你的程序到底需不需要。
(Note:日后寫頭文件都要設(shè)置保護(hù)符)
總結(jié)
以上是生活随笔為你收集整理的《C++ Primer 5th》笔记(2 / 19):变量和基本类型的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《Java8实战》笔记(10):用Opt
- 下一篇: C++vector容器类删除重复元素 s