2.1 基本內置類型
- 算術類型(arithmetictype)和空類型(void)在內的基本數據類型。其中算術類型包含了字符、整型數、布爾值和浮點數。空類型不對應具體的值,僅用于一些特殊的場合,例如最常見的是,當函數不返回任何值時使用空類型作為返回類型。
2.1.1算術類型
- 算術類型分為兩類:整型(integraltype,包括字符和布爾類型在內)和浮點型。算術類型的尺寸(也就是該類型數據所占的比特數)在不同機器上有所差別。表2.1列出了C++標準規定的尺寸的最小值,同時允許編譯器賦予這些類型更大的尺寸。某一類型所占的比特數不同,它所能表示的數據范圍也不一樣
- 類型char和類型signed char并不一樣。盡管字符型有三種,但 是字符的表現形式卻只有兩種:帶符號的和無符號的。類型char實際上會表現為上述兩 種形式中的一種,具體是哪種由編譯器決定。
- 無符號類型中所有比特都用來存儲值,例如,8比特的unsigned? char可以表示0至255區間內的值。C++標準并沒有規定帶符號類型應如何表示,但是約定了在表示范圍內正值和負值的
量應該平衡。因此,8比特的signed char理論上應該可以表示-127至127區間內的值,大多數現代計算機將實際的表示范圍定為-128至127。
如何選擇類型
- ?當明確知曉數值不可能為負時,選用無符號類型。
- 使用int執行整數運算。在實際應用中,short常常顯得太小而long-般和int有一樣的尺寸。如果你的數值超過了int的表示范圍,選用longlong。
- 在算術表達式中不要使用char或bool,只有在存放字符或布爾值時才使用它們。因為類型char在一些機器上是有符號的,而在另一些機器上又是無符號的,所以如果使用char進行運算特別容易出問題。如果你需要使用一個不大的整數,那么明確指定它的類型是signed char或者unsigned char。
- 執行浮點數運算選用double,這是因為float通常精度不夠而且雙精度浮點數和單精度浮點數的計算代價相差無幾。事實上,對于某些機器來說,雙精度運算甚至比單精度還快。long double提供的精度在一般情況下是沒有必要的,況且它帶來的運行時消耗也不容忽視。
類型轉化
- 當我們把一個非布爾類型的算術值賦給布爾類型時,初始值為0則結果為false,否則結果為true
- 當我們把一個布爾值賦給非布爾類型時,初始值為false則結果為0,初始值為true則結果為1
- 當我們把一個浮點數賦給整數類型時,進行了近似處理。結果值將僅保留浮點數中小數點之前的部分。
- 當我們把一個整數值賦給浮點類型時,小數部分記為0。如果該整數所占的空間超過了浮點類型的容量,精度可能有損失。
- 我們賦給無符號類型一個超出它表示范圍的值時,結果是初始值對無符號類型表示數值總數取模后的余數。例如,8比特大小的unsigned char可以表示0至255區間內的值,如果我們賦了一個區間以外的值,則實際的結果是該值對256取模后所得的余數。因此,把-1賦給8比特大小的unsigned char所得的結果是255。
- 當我們賦給帶符號類型一個超出它表示范圍的值時,結果是未定義的(undefined)此時,程序可能繼續工作、可能崩潰,也可能生成垃圾數據。
2.1.3字而值常量
- 一個形如42的值被稱作字面值常量(literal),這樣的值一望而知。每個字面值常量都對應一種數據類型,字面值常量的形式和值決定了它的數據類型。
- 注意,如果反斜線'后面跟著的八進制數字超過3個,只有前3個數字與'構成轉義序列。
- 例如,"\1234"表示2個字符,即八進制數123對應的字符以及字符4。相反,\x要用到后面跟著的所有數字,例如,"\xl234"表示一個16位的字符,該字符由這4個十六進制數所對應的比特唯一確定。因為大多數機器的char型數據占8位,所以上面這個例子可能會報錯。一般來說,超過8位的十六進制字符都是與表2.2中某個前綴作為開頭的擴展字符集一起使用的。
- 對于一個整型字面值來說,我們能分別指定它是否帶符號以及占用多少空間。如果后綴中有U,則該字面值屬于無符號類型,也就是說,以U為后綴的十進制數、八進制數或十六進缶I擻都將從unsigned int、unsigned long和unsigned long long中選擇能匹配的空間最小的一個作為其數據類型。如果后綴中有L,則字面值的類型至少是long;
- 如果后綴中有LL,則字面值的類型將是long long和unsigned long long中的一種。
- 顯然我們可以將U與L或LL合在一起使用。例如,以UL為后綴的字面值的數據類型將根據具體數值情況或者取unsigned long,或者取unsigned long long
2.2 變量
- 變量定義的基本形式是:首先是類型說明符(typespecifier),隨后緊跟由一個或多個變量名組成的列表,其中變量名以逗號分隔,最后以分號結束。列表中每個變量名的類型都由類型說明符指定,定義時還可以為一個或多個變量賦初值:
- 初始化不是賦值,初始化的含義是創建變量時賦予其一個初始值,而賦值的含篇點義是把對象的當前值擦除,而以一個新值來替代
列表初始化
- C++語言定義了初始化的好幾種不同形式,這也是初始化問題復雜性體現。例如,要想定義一個名為units_sold的int變量并初始化為0,以下的4條語句都可以做到這一點:
- int units_sold = 0;
- int units_sold = {0};
- int units_sold{0};
- int units_sold(0);
- 用花括號來初始化變量得到了全面應用,而在此之前,這種初始化的形式僅在某些受限的場合下才能使用,這種方式叫做列表初始化
- 無論是初始化對象還是某些時候為對象賦新值,都可以使用這樣一組由花括號括起來的初始值
默認初始化
- 如果定義變量時沒有指定初值,則變量被默認初始化(defaultinitialized),此時變量被賦予了“默認值”。默認值到底是什么由變量類型決定,同時定義變量的位置也會對此有影響。
- 定義于函數體內的內置類型的對象如果沒有初始化,則其值未定義。類的對象如果沒有顯式地初始化,則其值由類確定:
2 .2 .2 變量聲明和定義的關系
- 聲明(declaration)使得名字為程序所知,一個文件如果想使用別處定義的名字則必須包含對那個名字的聲明。而定義(definition)負責創建與名字關聯的實體。
- 變量聲明規定了變量的類型和名字,在這一點上定義與之相同。但是除此之外,定義還申請存儲空間,也可能會為變量賦一個初始值。
- 如果想聲明一個變量而非定義它,就在變量名前添加關鍵字extern,而且不要顯式地初始化變量:
- extern int i; / / 聲 明 i 而非定義i
- int j; / / 聲明并定義j
- 任何包含了顯式初始化的聲明即成為定義。我們能給由extern 關鍵字標記的變量賦 一個初始值,但是這么做也就抵消了extern 的作用。extern語句如果包含初始值就不再是聲明,而變成定義了:
- extern double pi = 3.1416; // 定義? 在函數體內部,如果試圖初始化一個由extern關鍵字標記的變量,將引發錯誤
- 變量能且只能被定義一次,但是可以被多次聲明
- 聲明和定義的區別看起來也許微不足道,但實際上卻非常重要。如果要在多個文件中使用同一個變量,就必須將聲明和定義分離。此時,變量的定義必須出現在且只能出現在一個文件中,而其他用到該變量的文件必須對其進行聲明,卻絕對不能重復定義。
2 .2 .3 標識符
- C++的標識符(identifier)由字母、數字和下畫線組成,其中必須以字母或下畫線開 頭。標識符的長度沒有限制,但是對大小寫字母敏感:
- 用戶自定義的標識符中不能連續出現兩個下畫線,也不能以下畫線緊連大寫字母開頭。
- 此外,定義在函數體外的標識符不能以下畫線開頭。
變量命名規范
2 .2 .4 名字的作用域
- 不論是在程序的什么位置,使用到的每個名字都會指向一個特定的實體:變量、函數類型等。然而,同一個名字如果出現在程序的不同位置,也可能指向的是不同實體
- 作用域(scope)是程序的一部分,在其中名字有其特定的含義。C++語言中大多數作 用域都以花括號分隔。
- 同一個名字在不同的作用域中可能指向不同的實體。名字的有效區域始于名字的聲明語句,以聲明語句所在的作用域末端為結束。
- 作用域能彼此包含,被包含(或者說被嵌套)的作用域稱為內層作用域(innerscope), 包含著別的作用域的作用域稱為外層作用域 (outer scope)
- 使用作用域操作符(參見1.2節,第7頁)來覆蓋默認的作用域規則,因為全局作用域本身并沒有名字,所以當作用域操作符的左側為空時,向全局作用域發出請求獲取作用域操作符右側名字對應的變量。結果是,第三條輸出語句使用全局變量reused,輸出420。
2.3 復合類型
- 復合類型(compound type)是指基于其他類型定義的類型。C++語言有幾種復合類型, 本章將介紹其中的兩種:引用和指針
- 一條聲明語句由一個基本數據類型(basetype)和緊隨其后的一個聲明符(declarator)列表組成。每個聲明符命名了一個變量并指定該變量為與基本數據類型有關的某種類型。
2 .3 .1 引用
- 引 用 (reference)為對象起了另外一個名字,引用類型引用(refers to )另外一種類型。 通過將聲明符寫成&d的形式來定義引用類型,其中d 是聲明的變量名:
- int ival = 1024;
- int &refVal = ival;?// refVal指 向 ival (是 ival的另一個名字)?
- int &refVal2; / / 報錯:引用必須被初始化
- 在初始化變量時,初始值會被拷貝到新建的對象中。然而定義引用時,程序把引用和它的初始值綁定(bind)在一起,而不是將初始值拷貝給引用。一旦初始化完成,引用將 和它的初始值對象一直綁定在一起。因為無法令引用重新綁定到另外一個對象,因此引用必須初始化。
- 引用并非對象,相反的,它只是為一個已經存在的對象所起的另外一個名字
2.3.2 指針
- 指針(pointer)是“指向(pointto)”另外一種類型的復合類型。與引用類似,指針也實現了對其他對象的間接訪問。然而指針與引用相比又有很多不同點。
- 其一,指針本身就是一個對象,允許對指針賦值和拷貝,而且在指針的生命周期內它可以先后指向幾個不同的對象。其二,指針無須在定義時賦初值。和其他內置類型一樣,在塊作用域內定義的指針如果沒有被初始化,也將擁有一個不確定的值。
- 指針存放某個對象的地址,要想獲取該地址,需要使用取地址符(操作符&)
空指針
- 空指針(null pointer) 不指向任何對象,在試圖使用一個指針之前代碼可以首先檢查 它是否為空。以下列出幾個生成空指針的方法:
- int *pl =?nullptr; / / 等 價 于 int *pl =0;
- int *p2 =0; / / 直接將p2 初始化為字面常量0
- int *p3 = NULL; / / 等 價 于 int *p3 = o;?//需 要 首 先 #include cstdlib
- 得到空指針最直接的辦法就是用字面值nullptor來初始化指針,這也是C++11新標準剛 剛引入的一種方法。nullptor是一種特殊類型的字面值,它可以被轉換成任意其他的指針類型。另一種辦法就如對p2 的定義一樣,也可以通過將指針初始化為字面值0 來生成空指針
- 預處理器是運行于編譯過程之前的一段程序就可以了。預處理變量不屬于命名空間s td ,它由預處理 器負責管理,因此我們可以直接使用預處理變量而無須在前面加上s td ::
- 當用到一個預處理變量時,預處理器會自動地將它替換為實際值,因此用NULL初始化指針和用0 初始化指針是一樣的。在新標準下,現在的C++程序最好使用nullptor, 同時盡量避免使用NULL。把 i n t 變量直接賦給指針是錯誤的操作,即使i n t 變量的值恰好等于0 也不行
- 有時候要想搞清楚一條賦值語句到底是改變了指針的值還是改變了指針所指對象的值不太容易,最好的辦法就是記住賦值永遠改變的是等號左側的對象。當寫出如下語句時,
- pi = &ival; // pi的值被改變,現在 pi指向了 ival,意思是為pi賦一個新的值,也就是改變了那個存放在pi內的地址值。相反的,如果寫出如下語句
- *pi = 0; // ival的值被改變,指 針 pi并沒有改變,則*pi(也就是指針pi指向的那個對象)發生改變。
void* 指針
- void*是一種特殊的指針類型,可用于存放任意對象的地址。一個void*指針存放著 一個地址,這一點和其他指針類似。不同的是,我們對該地址中到底是個什么類型的對象并不了解:
- 利用void*指針能做的事兒比較有限:拿它和別的指針比較、作為函數的輸入或輸出,或 者賦給另外一個void*指針。不能直接操作void*指針所指的對象,因為我們并不知道 這個對象到底是什么類型,也就無法確定能在這個對象上做哪些操作。
2.3.3理解復合類型的聲明擊
- 如前所述,變量的定義包括一個基本數據類型(base type)和一組聲明符。在同一條定義語句中,雖然基本數據類型只有一個,但是聲明符的形式卻可以不同。也就是說,一條定義語句可能定義出不同類型的變量;
- int i=1024,*p=&i,&r=i;??//i是一個int型的數,p是一個int型指針,r是一個int型引用
指向指針的指針
- 一般來說,聲明符中修飾符的個數并沒有限制。當有多個修飾符連寫在一起時,按照其邏輯關系詳加解釋即可。以指針為例,指針是內存中的對象,像其他對象一樣也有自己的地址,因此允許把指針的地址再存放到另一個指針當中。
指向指針的引用
- 引用本身不是一個對象,因此不能定義指向引用的指針。但指針是對象,所以存在對指針的引用:
- 要理解r 的類型到底是什么,最簡單的辦法是從右向左閱讀r 的定義。離變量名最近的符 號(此例中是& r 的符號&)對變量的類型有最直接的影響,因此r 是一個引用。聲明符的 其余部分用以確定r 引用的類型是什么,此例中的符號*說明r 引用的是一個指針。最后, 聲明的基本數據類型部分指出r 引用的是一個int指針
2.4 const限定符
- 有時我們希望定義這樣一種變量,它的值不能被改變。例如,用一個變量來表示緩沖區的大小。使用變量的好處是當我們覺得緩沖區大小不再合適時,很容易對其進行調整。另一方面,也應隨時警惕防止程序一不小心改變了這個值。為了滿足這一要求,可以用關鍵字const對變量的類型加以限定:
- const int bufSize = 512; // 輸入緩沖區大小
- 這樣就把bufSize定義成了一個常量。任何試圖為bufSize賦值的行為都將引發錯誤:
- bufSize = 512; / / 錯誤:試 圖 向 const對象寫值,因為const對象一旦創建后其值就不能再改變,所以const對象必須初始化。一如既往, 初始值可以是任意復雜的表達式:
初始化和const
- 正如之前反復提到的,對象的類型決定了其上的操作。與非const類型所能參與的操作相比,const類型的對象能完成其中大部分,但也不是所有的操作都適合。主要的限制就是只能在const類型的對象上執行不改變其內容的操作。例如,const int和普通的int一樣都能參與算術運算,也都能轉換成一個布爾值,等等。在不改變const對象的操作中還有一種是初始化,如果利用一個對象去初始化另外一個對象,則它們是不是const都無關緊要:
- int i=42;const int ci=i;int j=ci;//正確:i的值被拷貝給了ci//正確:ci的值被拷貝給了j
- 盡管c i 是整型常量,但無論如何c i 中的值還是一個整型數。c i 的常量特征僅僅在執行 改變c i 的操作時才會發揮作用。當用c i 去初始化j 時,根本無須在意ci 是不是一個常 量。拷貝一個對象的值并不會改變它,一旦拷貝完成,新的對象就和原來的對象沒什么關系了。
- 默認狀態下,const對象僅在文件內有效,當以編譯時初始化的方式定義一個const對象時,就如對bufSize的定義一樣:
- const對象被設定為僅在文件內有效。當 多個文件中出現了同名的const變量時,其實等同于在不同文件中分別定義了獨立的變量。?
- 某些時候有這樣一種const變量,它的初始值不是一個常量表達式,但又確實有必 要在文件間共享。這種情況下,我們不希望編譯器為每個文件分別生成獨立的變量。相反,我們想讓這類const對象像其他(非常量)對象一樣工作,也就是說,只在一個文件中 定義const.而在其他多個文件中聲明并使用它。 解決的辦法是,對于const變量不管是聲明還是定義都添加extern關鍵字,這樣 只需定義一次就可以了:
2.4.1 const 的引用
- 可以把引用綁定到const對象上,就像綁定到其他對象上一樣,我們稱之為對常量的引用(reference to const)。與普通引用不同的是,對常量的引用不能被用作修改它所綁定的對象:
- const int ci = 1024;
- const int &r 1 = ci; / / 正確:引用及其對應的對象都是常量
- rl = 42;??/ / 錯誤:rl是對常量的引用?
- int &r2 = ci;/ 錯誤:試圖讓一個非常量引用指向一個常量對象
- 因為不允許直接為c i 賦值,當然也就不能通過引用去改變c i。因此,對 r2 的初始化是錯誤的。假設該初始化合法,則可以通過r2 來改變它引用對象的值,這顯然是不正確的。
初始化和對const的引用
- 2.3.1節(第46頁)提到,引用的類型必須與其所引用對象的類型一致,但是有兩個例外。第一種例外情況就是在初始化常量引用時允許用任意表達式作為初始值,只要該表達式的結果能轉換成(參見2.1.2節,第32頁)引用的類型即可。尤其,允許為一個常量引用綁定非常量的對象、字面值,甚至是個一般表達式:
- int i=42;
- const int &r1 = i;//允許將const int&綁定到一個普通int對象上
- const int &r2 = 42;//正確:r2是一個常量引用
- const int &r3 = rl*2;://正確 r3是一個常量引用
- in t&r4 = rl*2;? //錯誤:r4是一個普通的非常量引用
- 要想理解這種例外情況的原因,最簡單的辦法是弄清楚當一個常量引用被綁定到另外一種類型上時到底發生了什么:
- double dval=3.14; const int &ri=dval;此處ri引用了一個int型的數。對ri的操作應該是整數運算,但dval卻是一個雙精度浮點數而非整數。因此為了確保讓ri綁定一個整數,編譯器把上述代碼變成了如下形式:const int temp=dval;//由雙精度浮點數生成一個臨時的整型常量? ? const int &ri=temp;//讓ri綁定這個臨時量
- 在這種情況下,ri綁定了一個臨時量(temporary)對象。所謂臨時量對象就是當編譯器需要一個空間來暫存表達式的求值結果時臨時創建的一個未命名的對象。
- C++程序員們常常把臨時量對象簡稱為臨時量。
- 接下來探討當ri不是常量時,如果執行了類似于上面的初始化過程將帶來什么樣的后果。如果ri不是常量,就允許對ri賦值,這樣就會改變ri所引用對象的值。注意,此時綁定的對象是一個臨時量而非dval。程序員既然讓ri引用dval,就肯定想通過ri改變dval的值,否則干什么要給ri賦值呢?如此看來,既然大家基本上不會想著把引用綁定到臨時量上,C++語言也就把這種行為歸為非法。
對const的引用可能引用一個并非const的對象
- 必須認識到,常量引用僅對引用可參與的操作做出了限定,對于引用的對象本身是不是一個常量未作限定。因為對象也可能是個非常量,所以允許通過其他途徑改變它的值:
- r2綁定(非常量)整數i是合法的行為。然而,不允許通過r2修改i的值。盡管如此,i的值仍然允許通過其他途徑修改,既可以直接給i賦值,也可以通過像r1一樣綁定到i的其他引用來修改。
2.4.2指針和const
- 與引用一樣,也可以令指針指向常量或非常量。類似于常量引用(參見2.4.1節,第54頁),指向常量的指針(pointertoconst)不能用于改變其所指對象的值。要想存放常量對象的地址,只能使用指向常量的指針:
- 2.3.2節 (第 47頁)提到,指針的類型必須與其所指對象的類型一致,但是有兩個例外。第一種例外情況是允許令一個指向常量的指針指向一個非常量對象:
- double dval = 3.14; // dval是一個雙精度浮點數,它的值可以改變
- cptr = &dval; / / 正確:但是不能通過cptr改變 dval的值
- 和常量引用一樣,指向常量的指針也沒有規定其所指的對象必須是一個常量。所謂指向常量的指針僅僅要求不能通過該指針改變對象的值,而沒有規定那個對象的值不能通過其他途徑改變。所謂指向常量的指針或引用,不過是指針或引用“自作多情”罷了,它們覺得自己指向了常量,所以自覺地不去改變所指對象的值。
const指針
- 指針是對象而引用不是,因此就像其他對象類型一樣,允許把指針本身定為常量。常量指針(constpointer)必須初始化,而且一旦初始化完成,則它的值(也就是存放在指針中的那個地址)就不能再改變了。把*放在const關鍵字之前用以說明指針是一個常量,這樣的書寫形式隱含著一層意味,即不變的是指針本身的值而非指向的那個值:也就是我指向了你,一輩子就認定了你,但是你會變,變得很陌生
- 如同2.3.3節(第52頁)所講的,要想弄清楚這些聲明的含義最行之有效的辦法是從右向左閱讀。此例中,離curErr最近的符號是const,意味著curErr本身是一個常量對象,對象的類型由聲明符的其余部分確定。聲明符中的下一個符號是*,意思是curErr是一個常量指針。最后,該聲明語句的基本數據類型部分確定了常量指針指向的是一個int對象。與之相似,我們也能推斷出,pip是一個常量指針,它指向的對象是一個雙精度浮點型常量。
- 指針本身是一個常量并不意味著不能通過指針修改其所指對象的值,能否這樣做完全依賴于所指對象的類型。例如,pip是一個指向常量的常量指針,則不論是pip所指的對象值還是pip自己存儲的那個地址都不能改變。相反的,curErr指向的是一個一般的非常量整數,那么就完全可以用curErr去修改errNumb的值
2.4.3頂層const
- 如前所述,指針本身是一個對象,它又可以指向另外一個對象。因此,指針本身是不是常量以及指針所指的是不是一個常量就是兩個相互獨立的問題。用名詞頂層const表示指針本身是個常量,而用名詞底層const表示指針所指的對象是一個常量。
- 更一般的,頂層const可以表示任意的對象是常量,這一點對任何數據類型都適用,如算術類型、類、指針等。底層const則與指針和引用等復合類型的基本類型部分有關。比較特殊的是,指針類型既可以是頂層const也可以是底層const,這一點和其他類型相比區別明顯:
- const int *p2 = &ci;? const int 表示我要存儲的變量的類型是const int類型的,因此可以指向ci,ci是const int類型的
- const int * const p3 = p2; / / 靠右的 const 是頂層 const,靠左的是底層
- const const int &r = ci; / / 用于聲明引用的const都是底層const
- 當執行對象的拷貝操作時,常量是頂層const還是底層const區別明顯。其中,頂 層const不受什么影響:
- i = ci; / / 正確:拷貝 ci的值,ci是一個頂層const, 對此操作無影響? ?單純拷貝數值
- p2 = p3; / / 正確:p2和 p3指向的對象類型相同,p3頂層 const的部分不影響
- 執行拷貝操作并不會改變被拷貝對象的值,因此,拷入和拷出的對象是否是常量都沒什么影響。
- 另一方面,底層const的限制卻不能忽視。當執行對象的拷貝操作時,拷入和拷出的對象必須具有相同的底層const資格,或者兩個對象的數據類型必須能夠轉換。一般來說,非常量可以轉換成常量,反之則不行:
- p3既是頂層const也是底層const,拷貝p3時可以不在乎它是一個頂層const,但是必須清楚它指向的對象得是一個常量。因此,不能用p3去初始化p,因為p指向的是一個普通的(非常量)整數。另一方面,p3的值可以賦給p2,是因為這兩個指針都是底層const,盡管p3同時也是一個常量指針(頂層const),僅就這次賦值而言不會有什么影響
2.4.4 constexpr和常量表達式
- 常量表達式(const expression)是指值不會改變并且在編譯過程就能得到計算結果的表達式。顯然,字面值屬于常量表達式,用常量表達式初始化的const對象也是常量表達式。后面將會提到,C++語言中有幾種情況下是要用到常量表達式的。
- 一個對象(或表達式)是不是常量表達式由它的數據類型和初始值共同決定,例如
- const int max_files = 20; // max_files 是常量表達式
- const int limit = max_files + 1; // limit 是常量表達式
- int staff_size = 27; // staff_size 不是常量表達式
- const int sz = get_size () ; // sz 不是常量表達式
- 盡管staff_size的初始值是個字面值常量,但由于它的數據類型只是一個普通int而非const int,所以它不屬于常量表達式。另一方面,盡管sz本身是一個常量,但它的具體值直到運行時才能獲取到,所以也不是常量表達式。
constexpr變量
- 在一個復雜系統中,很難(幾乎肯定不能)分辨一個初始值到底是不是常量表達式。
- 當然可以定義一個const變量并把它的初始值設為我們認為的某個常量表達式,但在實際使用時,盡管要求如此卻常常發現初始值并非常量表達式的情況。可以這么說,在此種情況下,對象的定義和使用根本就是兩回事兒。
- C++11新標準規定,允許將變量聲明為constexpr類型以便由編譯器來驗證變量的值是否是一個常量表達式。聲明為constexpr的變量一定是一個常量,而且必須用常量表達式初始化:
- constexpr int mf = 20; // 20 是常量表達式
- constexpr int limit = mf + 1; // mf + 1 是常量表達式
- constexpr int sz = size () ; // 只有當 size 是一個 constexpr 函數時,才是一條正確的聲明語句
- 盡管不能使用普通函數作為constexpr變量的初始值,但是正如6.5.2節(第214頁)將要介紹的,新標準允許定義一種特殊的constexpr函數。這種函數應該足夠簡單以使得編譯時就可以計算其結果,這樣就能用constexpr函數去初始化constexpr變量了。
- 一般來說, 如果你認定變量是一個常量表達式,那就把它聲明成constexpr類型
字面值類型
- 常量表達式的值需要在編譯時就得到計算,因此對聲明constexpr時用到的類型必須有所限制。因為這些類型一般比較簡單,值也顯而易見、容易得到,就把它們稱為“字面值類型
- 到目前為止接觸過的數據類型中,算術類型、引用和指針都屬于字面值類型。自定義類Sales_item、IO庫、string類型則不屬于字面值類型,也就不能被定義成constexpr。其他一些字面值類型將在7.5.6節(第267頁)和19.3節(第736頁)介紹。盡管指針和引用都能定義成constexpr,但它們的初始值卻受到嚴格限制。一個constexpr指針的初始值必須是nullptr或者0,或者是存儲于某個固定地址中的對象。6.1.1節(第184頁)將要提到,函數體內定義的變量一般來說并非存放在固定地址中,
- 因此constexpr指針不能指向這樣的變量。相反的,定義于所有函數體之外的對象其地址固定不變,能用來初始化constexpr指針。同樣是在6.1.1節(第185頁)中還將提到,允許函數定義一類有效范圍超出函數本身的變量,這類變量和定義在函數體之外的變量一樣也有固定地址。因此,constexpr引用能綁定到這樣的變量上,constexpr指針也能指向這樣的變量。
指針和constexpr
- 必須明確一點,在constexpr聲明中如果定義了一個指針,限定符constexpr僅對指針有效,與指針所指的對象無關
- const int *p = nullptr; // p 是一個指向整型常量的指針
- constexpr int *q = nullptr; // q 是一個指向整數的常量指針
- p 和 q 的類型相差甚遠,p 是一個指向常量的指針,而 q 是一個常量指針,其中的關鍵在 于 constexpr把它所定義的對象置為了頂層const (參見2.4.3節,第 57頁)。 與其他常量指針類似,constexpr指針既可以指向常量也可以指向一個非常量
2 . 5 處理類型
- 隨著程序越來越復雜,程序中用到的類型也越來越復雜,這種復雜性體現在兩個方面。一是一些類型難于“拼寫",它們的名字既難記又容易寫錯,還無法明確體現其真實目的和含義。二是有時候根本搞不清到底需要的類型是什么,程序員不得不回過頭去從程序的上下文中尋求幫助。
2 .5 .1 類型別名
- 類型別名(type alias)是一個名字,它是某種類型的同義詞。使用類型別名有很多好處,它讓復雜的類型名字變得簡單明了、易于理解和使用,還有助于程序員清楚地知道使用該類型的真實目的。
- 有兩種方法可用于定義類型別名。傳統的方法是使用關鍵字typedef:
- type def double wages; //wages 是 double 的同義詞
- typedef wages base, *p; //base 是 double 的同義詞,p 是 double*的同義詞
- 其中,關鍵字typedef作為聲明語句中的基本數據類型(參見2.3節,第 45頁)的一部分出現。含有typedef的聲明語句定義的不再是變量而是類型別名。和以前的聲明語句一樣,這里的聲明符也可以包含類型修飾,從而也能由基本數據類型構造出復合類型來。
- 新標準規定了一種新的方法,使用別名聲明(alias declaration)來定義類型的別名:
- using SI = Sales_item; // SI 是 Sales_item的同義詞
- 這種方法用關鍵字using作為別名聲明的開始,其后緊跟別名和等號,其作用是把等號 左側的名字規定成等號右側類型的別名。類型別名和類型的名字等價,只要是類型的名字能出現的地方,就能使用類型別名:
- wages hourly, weekly; // 等價于 double hourly、weekly;
- SI item; // 等價于 Sales_item item
指針、常量和類型別名
- 如果某個類型別名指代的是復合類型或常量,那么把它用到聲明語句里就會產生意想不到的后果。例如下面的聲明語句用到了類型pstring ,它實際上是類型char*的別名:
- typedef char *pstring; const pstring cstr = 0; // cstr是指向 char 的常量指針
- const pstring *ps; // ps是一個指針,它的對象是指向char的常量指針
- 上述兩條聲明語句的基本數據類型都是const pstring ,和過去一樣,const是對給定類型的修飾。pstring實際上是指向char的指針,因此,const pstring就是指向char的常量指針,而非指向常量字符的指針。 遇到一條使用了類型別名的聲明語句時,人們往往會錯誤地嘗試把類型別名替換成它本來的樣子,以理解該語句的含義:
- const char *cstr = 0; // 是對 const pstring cstr 的錯誤理解
- 再強調一遍:這種理解是錯誤的。聲明語句中用到pstring時,其基本數據類型是指針。 可是用char*重寫了聲明語句后,數據類型就變成了 char, *成為了聲明符的一部分。 這樣改寫的結果是,const char成了基本數據類型。前后兩種聲明含義截然不同,前者聲明了一個指向char的常量指針,改寫后的形式則聲明了一個指向const char的指針。
2.5.2 auto類型說明符?
- 編程時常常需要把表達式的值賦給變量,這就要求在聲明變量的時候清楚地知道表達式的類型。然而要做到這一點并非那么容易,有時甚至根本做不到。為了解決這個問題,C++11新標準引入了auto類型說明符,用它就能讓編譯器替我們去分析表達式所屬的類型。和原來那些只對應一種特定類型的說明符(比如double) 不同,auto 讓編譯器通過初始值來推算變量的類型。顯然,auto 定義的變量必須有初始值:
- / / 由 vail和 val2 相加的結果可以推斷出item的類型
- auto item = vail + val2; // item初始化為vail和 val2 相加的結果
- 此處編譯器將根據vail和val2相加的結果來推斷item的類型。如果vail和val2是類Sales_ item (參 見 1.5節,第 17頁)的對象,則 ite m 的類型就是Sales_ item ;
- 如果這兩個變量的類型是double ,則item的類型就是double ,以此類推。 使用auto。也能在一條語句中聲明多個變量。因為一條聲明語句只能有一個基本數據類型,所以該語句中所有變量的初始基本數據類型都必須一樣:
- auto i = 0, *p = &i; / / 正確:i 是整數、p 是整型指針 auto sz = 0, pi = 3.14; // 錯誤:sz 和 pi 的類型不一致
復合類型、常量和auto
- 編譯器推斷出來的auto。類型有時候和初始值的類型并不完全一樣,編譯器會適當地改變結果類型使其更符合初始化規則。首先,正如我們所熟知的,使用引用其實是使用引用的對象,特別是當引用被用作初始值時,真正參與初始化的其實是引用對象的值。此時編譯器以引用對象的類型作為auto 的類型:
2.5.3 decltype類型指示符
- 有時會遇到這種情況:希望從表達式的類型推斷出要定義的變量的類型,但是不想用該表達式的值初始化變量。為了滿足這一要求,C++11新標準引入了第二種類型說明符
- decltype,它的作用是選擇并返回操作數的數據類型。在此過程中,編譯器分析表達式 并得到它的類型,卻不實際計算表達式的值:
- decltype(f()) sum // sum的類型就是函數f 的返回類型
- 編譯器并不實際調用函數f,而是使用當調用發生時f的返回值類型作為sum的類型。換句話說,編譯器為sum指定的類型是什么呢?就是假如f 被調用的話將會返回的那個類型。
- decltype處理頂層const和引用的方式與auto有些許不同。如果decltype使用的表達式是一個變量,則decltype返回該變量的類型(包括頂層const和引用在內)
- 因為cj是一個引用,decltype (cj)的結果就是引用類型,因此作為引用的z必須被初始化。
- 需要指出的是,引用從來都作為其所指對象的同義詞出現,只有用在decltype處是一個例外。
decltype和引用
- 如果decltype使用的表達式不是一個變量,則decltype返回表達式結果對應的類型。如 4.1.1節 (第 120頁)將要介紹的,有些表達式將向decltype返回一個引用類型。 -般來說當這種情況發生時,意味著該表達式的結果對象能作為一條賦值語句的左值:
- 因為r是一個引用,因此decltype (r)的結果是引用類型。如果想讓結果類型是r所指的類型,可以把r作為表達式的一部分,如 r+0,顯然這個表達式的結果將是一個具體值而非一個引用。
- 另一方面,如果表達式的內容是解引用操作,則decltype將得到引用類型。正如我們所熟悉的那樣,解引用指針可以得到指針所指的對象,而且還能給這個對象賦值。因此,decltype (*p)的結果類型就是int &,而非int。
- decltype和 auto的另一處重要區別是,decltype的結果類型與表達式形式密切相關。有一種情況需要特別注意:對于 decltype所用的表達式來說,如果變量名加上了一對括號,則得到的類型與不加括號時會有不同。如果 decltype使用的是一個不加括號的變量,則得到的結果就是該變量的類型:如果給變量加上了一層或多層括號,編譯器就會把它當成是一個表達式。變量是一種可以作為賦值語句左值的特殊表達式,所以這樣的decltype就會得到引用類型:
補充知識
預處理器概述
- 確保頭文件多次包含仍能安全工作的常用技術是預處理器(preprocessor),它由C++語言從C語言繼承而來。預處理器是在編譯之前執行的一段程序,可以部分地改變我們所寫的程序。之前己經用到了一項預處理功能#include,當預處理器看到#include標記時就會用指定的頭文件的內容代替#include。C++程序還會用到的一項預處理功能是頭文件保護符(headerguard),頭文件保護符依賴于預處理變量(參見2.3.2節,第48頁)。預處理變量有兩種狀態:已定義和未定義。
- #define指令把一個名字設定為預處理變量,另外兩個指令則分別檢查某個指定的預處理變量是否已經定義:#ifdef當且僅當變量已定義時為真,#ifndef當且僅當變量未定義時為真。一旦檢查結果為真,則執行后續操作直至遇到#endif指令為止。
- 使用這些功能就能有效地防止重復包含的發生:
- 第一次包含Sales_data. h 時,#ifndef的檢查結果為真,預處理器將順序執行后面的操作直至遇到#endif為止。此時,預處理變量SALES_DATA_H的值將變為已定義,而且Sales_data.h也會被拷貝到我們的程序中來。后面如果再一次包含Sales_data . h,則#ifndef的檢查結果將為假,編譯器將忽略#ifndef到#endif之間的部分
- 預處理變量無視C++語言中關于作用域的規則" .
- 還可以使用 #pragma once? ?參考鏈接
- 整個程序中的預處理變量包括頭文件保護符必須唯一,通常的做法是基于頭文件中類的名字來構建保護符的名字,以確保其唯一性。為了避免與程序中的其他實體發生名字沖突,一般把預處理變量的名字全部大寫。
小 結?
- 類型是C++編程的基礎。
- 類型規定了其對象的存儲要求和所能執行的操作。C+ +語言提供了一套基礎內置類型,如 int和 char等,這些類型與實現它們的機器硬件密切相關。類型分為非常量和常量,一個常量對象必須初始化,而且一旦初始化其值就不能再改變。此外,還可以定義復合類型,如指針和引用等。復合類型的定義以其他類型為基礎。
- C++語言允許用戶以類的形式自定義類型。C++庫通過類提供了一套高級抽象類型,如輸入輸出和string等。
術語表
- 地址(address)是一個數字,根據它可以找到內存中的一個字節。
- 別名聲明(aliasdeclaration)為另外一種類型定義一個同義詞:使用“名字=類型”的格式將名字作為該類型的同義詞。
- 算術類型(arithmetictype)布爾值、字符、整數、浮點數等內置類型。
- 數組(array)是一種數據結構,存放著一組未命名的對象,可以通過索引來訪問這些對象。3.5節將詳細介紹數組的知識。
- auto是一個類型說明符,通過變量的初始值來推斷變量的類型。
- 基本類型(basetype)是類型說明符,可用const修飾,在聲明語句中位于聲明符之前。基本類型提供了最常見的數據類型,以此為基礎構建聲明符。
- 綁定(bind)令某個名字與給定的實體關聯在一起,使用該名字也就是使用該實體。例如,引用就是將某個名字與某個對象綁定在一起。參考鏈接
- 字節(byte)內存中可尋址的最小單元,大多數機器的字節占8位。
- 類成員(classmember)類的組成部分。復合類型(compoundtype)是一種類型,它的定義以其他類型為基礎。
- const是一種類型修飾符,用于說明永不改變的對象。const對象一旦定義就無法再賦新值,所以必須初始化。
- 常量指針(constpointer)是一種指針,它的值永不改變。
- 常量引用(constreference)是一種習慣叫法,含義是指向常量的引用。
- 常量表達式(constexpression)能在編譯時計算并獲取結果的表達式。constexpr是一種函數,用于代表一條常量表達式。6.5.2節(第214頁)將介紹constexpr函數。
- 轉換(conversion)-種類型的值轉變成另外一種類型值的過程。C++語言支持內置類型之間的轉換。
- 數據成員(datamember)組成對象的數據元素,類的每個對象都有類的數據成員的一份拷貝。數據成員可以在類內部聲明的同時初始化。
- 聲明(declaration)聲稱存在一個變量、函數或是別處定義的類型。名字必須在定義或聲明之后才能使用。
- 聲明符(declarator)是聲明的一部分,包括被定義的名字和類型修飾符,其中類型修飾符可以有也可以沒有。
- decltype是一個類型說明符,從變量或表達式推斷得到類型。默認初始化(defaultinitialization)當對象未被顯式地賦予初始值時執行的初始化行為。由類本身負責執行的類對象的初始化
行為。全局作用域的內置類型對象初始化為 0;局部作用域的對象未被初始化即擁有未定義的值。 - 定義 (definition ) 為某一特定類型的變量申請存儲空間,可以選擇初始化該變量。名字必須在定義或聲明之后才能使用。
- 轉義序列 (escape sequence) 字符特別是那些不可打印字符的替代形式。轉義以反斜線開頭,后面緊跟個字符,或者不多于3 個八進制數字,或者字母x 加上 1 個 I-六進制數。
- 全局作用域(global scope)位于其他所有作用域之外的作用域。
- 頭文件保護符(header guard)使用頂處理變量以防止頭文件被某個文件重復包含。
- 標識符 (identifier)組成名字的字符序列, 標識符對大小寫敏感。
- 類內初始值(in-class initializer)在聲明類的數據成員時同時提供的初始值,必須置等號右側或花括號內"
- 在作用域內(in scope) 名字在當前作用域內可見。
- 被初始化 (initialized) 變量在定義的同時被賦予初始值,變量一般都應該被初始化
- 內層作用域(inner scope)嵌套在其他作用域之內的作用域。
- 整 型 (integral type) 參見算術類型。 列表初始化(listinitialization)利用花括號把一個或多個初始值放在一起的初始化形式。
- 字 面 值 (literal)是一個不能改變的值,如 數字、字符、字符串等。單引號內的是字符字知值,雙引號內的是字符串字面值。
- 局部作用域(local scope) 是塊作用域的習慣叫法。
- 底層 const (low-level const) —個不屬頂層的const,類型如果由底層常量定義,則不能被忽略。
- 成 員 (member)類的組成部分。
- 不可打印字符(nonprintable character)不具有可見形式的字符,如控制符、退格、換行符等。
- 空 指 針 (null pointer)值為0 的指針,空指針合法但是不指向任何對象。nullptr是表示空指針的字面值常星”
- 對 象 (object)是內存的塊區域,具有某種類型,變量是命名了的對象。
- 外層作用域( outer scope) 嵌套著別的作用域的作用域。
- 指 針 (pointer)是一個對象,存放著某個對象的地址,或皆某個對象存儲區域之后的下一地址,或者0。
- 指向常量的指針(pointer to const)是一個指針,存放著某個常量對象的地址。指向常量的指針不能用來改變它所指對象的值。
- 預 處 理 器 (preprocessor)在 C++編譯過程中執行的-段程序。
- 預處理變量(preprocessor variable) 由預處理器管理的變量。在程序編譯之前,預處理器負責將程序的預處理變量替換成它的真實值
- 引 用 (reference)是某個對象的別名。?
- 對常量的引用(reference to const)是個引用,不能用來改變它所綁定對象的值。對常量的引用可以綁定常量對象,或者非常量對象,或者表達式的結果。
- 作用域(scope) 是程序的-部分,在其中某些名字有意義。C++有凡級作用域:
- 全 局 (global)----- 名字定義在所有其他作用域之外。
- 類 (class)-----名字定義在類內部。
- 命 名空間(namespace)----- 名字定義在命名空間內部。
- 塊 (block)----- 名字定義在塊內部。名字從聲叫位置開始育至聲明語句所在的作用域末端為止都是可用的。
- 分離式編譯< separate compilation) 把程序分割為多個單獨文件的能力。
- 帶符號類型(signed)保存正數、負數或0的整型。
- 字符串(string)是一種庫類型,表示可變長字符序列。
- struct是個關鍵宇,用于定義類。
- 臨時值(temporary)編譯器在計算表.達式結果時創建的無名對象。為某表達式創建了 -個臨時值,則此臨時值將一直存在直到包含何該表達式的最大的表達式計算完成為止。
- 頂層 const ( top-level const) 是一個const,規定某對象的值不能改變。
- 類型別名(type alias)是 個名字,是另外個類型的同義詞 ,通過關鍵字typedef或別名聲明語句來定義。
- 類型檢查(type checking )是一個過程. 編譯器檢查給定類型對象的方式與該類型的定義是否一致。
- 類型說明符< type specifier) 類型的名字。
- typedef為某類型足義一個別名。當關鍵字 typedef作為聲明的基本類型出現時,聲明中定義的名字就是類型名。
- 未 定 義 (undefined) 即 C ++沒有明確規定的情況。不論是否有意為之,未定義行為都能引發難以追蹤的運行時錯誤、安全問題利可移植性問題。
- 未初始化(uninitialized)變量己定義但沒被賦予初始值.一般來說,試圖訪問未初始化變量的值將引發未定義行為
- 無符號類型(unsigned)保存大于等于0的整型.
- 變量(variable)命名的對象或引用.C++語言要求變量要先聲明后使用。
- void* 以指向任以非常量的指針類型,不能執行解引用操作。
- void類型是-種有特殊用處的類型,既無操作也無值.不能定義-個void類型的變量
- 字(word)在指定機器上進行整數運算:的自然單位。一般來說,字的空間足夠字放地址.32位機器上的字通常占據4個字節
- &運算符(&operator)取地址運算符。
- *運算符(*operator)解引用運算符。解引用-個指針將返回該指針所指的對象,為解引用的結果賦值也就是為指針所指的對象賦值。
- #define是-條預處理指令,用于定義一個預處理變量
- #endif是一條預處理指令,用于結束一個#ifdef或#ifndef區域。
- #ifdef是-條預處理指令,用于判斷給定的變量是否已經定義.
- #ifndef是-條預處理指令,用于判斷給定的變量是否尚未定義’