日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

《C++ Primer 5th》笔记(2 / 19):变量和基本类型

發布時間:2023/12/13 c/c++ 32 豆豆
生活随笔 收集整理的這篇文章主要介紹了 《C++ Primer 5th》笔记(2 / 19):变量和基本类型 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

文章目錄

    • 基本內置類型
      • 算術類型
        • 內置類型的機器實現(類型在物理層面上的說明)
        • 建議:如何選擇類型
      • 類型轉換
        • 建議:避免無法預知和依賴于實現環境的行為
        • 算術表達式里使用布爾值
        • 含有無符號類型的表達式(帶符號數會自動地轉換成無符號數)
        • 提示:切勿混用帶符號類型和無符號類型
      • 字面值常量
        • 整型和浮點型字面值
        • 字符和字符串字面值
        • 轉義序列
        • 通過前后綴指定字面值的類型
        • 布爾字面值和指針字面值
    • 變量
      • 變量定義
        • 術語:何為對象?
        • 初始值
        • 列表初始化
        • 默認初始化
        • 提示:未初始化變量引發運行時故障
      • 變量聲明和定義的關系
        • 關鍵概念:靜態類型
      • 標識符
      • 名字的作用域
        • 建議:當你第一次使用變量時再定義它
        • 嵌套的作用域
    • 復合類型
      • 引用
        • 引用即別名
        • 引用的定義
      • 指針
        • 獲取對象的地址
        • 指針值
        • 利用指針訪問對象
        • 關鍵概念:一符多義(& 與 *)
        • 空指針
        • 建議:初始化所有指針
        • 賦值和指針
        • 其他指針操作
        • void* 指針
      • 理解復合類型的聲明
        • 定義多個變量
        • 指向指針的指針
        • 指向指針的引用(指針的別名)(從右向左閱讀理解)
    • const限定符
      • 概述
        • 初始化和const
        • 默認狀態下,const對象僅在文件內有效(const 常量在多文件中使用方法)
      • const的引用
        • 術語:常量引用是對const的引用
        • 初始化和對const的引用
        • 對const的引用可能引用一個并非const的對象
      • 指針和const
        • const指針
      • 頂層const
      • constexpr和常量表達式
        • constexpr變量
        • 字面值類型
        • 指針和constexpr
    • 處理類型
      • 類型別名
        • 指針、常量和類型別名
      • auto類型說明符
        • 復合類型、常量和 auto
      • decltype類型指示符
        • decltype和引用
    • 自定義數據結構
      • 定義Sales_data類型
        • 類數據成員
      • 使用Sales_data類
        • Sales_data對象讀入數據
        • 輸出兩個Sales_data對象的和
      • 編寫自己的頭文件
        • 預處理器

數據類型決定了程序中數據操作的意義。如下所示的語句:

i = i + j;

其含義依賴于i和j的數據類型。如果i和j都是整型數,那么這條語句執行的就是最普通的加法運算。然而,如果i和j是Sales_item類型(上一章內容)的數據則上述語句把這兩個對象的成分相加。

基本內置類型

C++定義基本數據類型:

  • 算術類型(arithmetic type)
    • 字符
    • 整型數
    • 布爾值
    • 浮點數
  • 空類型(void)
    • 空類型不對應具體的值,僅用于一些特殊的場合,例如最常見的是,當函數不返回任何值時使用空類型作為返回類型。

算術類型

算術類型分為:

  • 整型(integral type,包括字符和布爾類型在內)
  • 浮點型

算術類型的尺寸(也就是該類型數據所占的比特數)在不同機器上有所差別。

下表列出了C++標準規定的尺寸的最小值,同時允許編譯器賦予這些類型更大的尺寸。某一類型所占的比特數不同,它所能表示的數據范圍也不一樣。

類型含義最小尺寸
bool布爾類型未定義
char字符8位
wchar_t寬字符16位
char16_tUnicode字符16位
char32_tUnicode字符32位
short短整型16位
int整型16位
long長整型32位
long long長整型64位
float單精度浮點數6位有效數字
double雙精度浮點數10位有效數字
long double擴展精度浮點數10位有效數字

bool

布爾類型(bool)的取值是真(true)或者假(false)。

char

C++提供了幾種字符類型,其中多數支持國際化。基本的字符類型是char,一個 char的空間應確保可以存放機器基本字符集中任意字符對應的數字值。也就是說,一個char的大小和一個機器字節一樣。

其他字符類型用于擴展字符集,如 wchar_t、char16_t、char32_t。wchar_t類型用于確??梢源娣艡C器最大擴展字符集中的任意一個字符,類型 charl6_t和char32_t則為Unicode字符集服務(Unicode是用于表示所有自然語言中字符的標準)。

int

除字符和布爾類型之外,其他整型用于表示(可能)不同尺寸的整數。C++語言規定一個int至少和一個short一樣大,一個long至少和一個int一樣大,一個long long至少和一個long一樣大。其中,數據類型long long是在C++11中新定義的。

float

浮點型可表示單精度、雙精度和擴展精度值。C++標準指定了一個浮點數有效位數的最小值,然而大多數編譯器都實現了更高的精度。通常,float 以1個字(32比特)來表示,double 以2個字(64比特)來表示,long double 以3或4個字(96或128比特)來表示。一般來說,類型float和 double分別有7和16個有效位;類型long double則常常被用于有特殊浮點需求的硬件,它的具體實現不同,精度也各不相同。


帶符號類型和無符號類型

除去布爾型和擴展的字符型之外,其他整型可以劃分為:

  • 帶符號的(signed),可以表示正數、負數或0。
  • 無符號的(unsigned),僅能表示大于等于0的值。
  • 類型int、short、long和 long long 都是帶符號的,通過在這些類型名前添加unsigned就可以得到無符號類型,例如unsigned long。類型unsigned int可以縮寫為unsigned。

    與其他整型不同,字符型被分為了三種:char、signed char和unsigned char。

    特別需要注意的是:類型char和類型signed char并不一樣。盡管字符型有三種,但是字符的表現形式卻只有兩種:帶符號的和無符號的。類型char實際上會表現為上述兩種形式中的一種,具體是哪種由編譯器決定。

    無符號類型中所有比特都用來存儲值,例如,8比特的unsigned char可以表示0至255區間內的值。

    C++標準并沒有規定帶符號類型應如何表示,但是約定了在表示范圍內正值和負值的量應該平衡。因此,8比特的signed char理論上應該可以表示-127至127區間內的值,大多數現代計算機將實際的表示范圍定為-128至127。

    內置類型的機器實現(類型在物理層面上的說明)

    計算機以比特序列存儲數據,每個比特非0即1,例如:

    00011011011100010110010000111011…

    大多數計算機以2的整數次冪個比特作為塊來處理內存,可尋址的最小內存塊稱為“字節(byte)”,存儲的基本單元稱為“字(word)”,它通常由幾個字節組成。在C++語言中,一個字節要至少能容納機器基本字符集中的字符。

    大多數機器的字節由8比特(bit)構成(1byte = 8bits),字則由32或64比特構成,也就是4或8字節(1 word = 4 bytes or 8 bytes)。大多數計算機將內存中的每個字節與一個數字(被稱為“地址(address)”)關聯起來。

    一個字節為8比特、字為32比特的機器上,我們可能看到一個字的內存區域如下所示:

    其中,左側是字節的地址右側是字節中8比特的具體內容。

    我們能夠使用某個地址來表示從這個地址開始的大小不同的比特串,例如,我們可能會說地址736424的那個字或者地址736427的那個字節。

    為了賦予內存中某個地址明確的含義必須首先知道存儲在該地址的數據的類型。類型決定了數據所占的比特數以及該如何解釋這些比特的內容

    • 如果位置736424處的對象類型是float,并且該機器中float以32比特存儲,那么我們就能知道這個對象的內容占滿了整個字。這個 float 數的實際值依賴于該機器是如何存儲浮點數的。
    • 如果位置736424處的對象類型是unsigned char,并且該機器使用ISO-Latin-1字符集,則該位置處的字節表示一個分號。

    Note:這里在物理層面說明一個變量的類型的作用。

    建議:如何選擇類型

    和C語言一樣,C的設計準則之一也是盡可能地接近硬件。C++的算術類型必須滿足各種硬件特質,所以它們常常顯得繁雜而令人不知所措。事實上,大多數程序員能夠(也應該)對數據類型的使用做出限定從而簡化選擇的過程。以下是選擇類型的一些經驗準則:

    • 當明確知曉數值不可能為負時,選用無符號類型。(Note:無負無符,嗚呼嗚呼)

    • 使用int執行整數運算。在實際應用中,short常常顯得太小而long一般和int有一樣的尺寸。如果你的數值超過了int 的表示范圍,選用long long。(Note:取中庸int,按需用short或long)

    • 在算術表達式中盡量不要使用 char或bool,而是只有在存放字符或布爾值時才使用它們。因為類型char在一些機器上是有符號的,而在另一些機器上又是無符號的,所以如果使用char進行運算特別容易出問題。如果你需要使用一個不大的整數,那么明確指定它的類型是signed char或者unsigned char(If you need a tiny integer, explicitly specify either signed char or unsigned char)。(Note:算數表達式盡量不用char或bool)

    • 執行浮點數運算選用 double,這是因為float通常精度不夠而且雙精度浮點數和單精度浮點數的計算代價相差無幾。事實上,對于某些機器來說,雙精度運算甚至比單精度還快。long double提供的精度在一般情況下是沒有必要的,況且它帶來的運行時消耗也不容忽視。(Note:浮點數直接上double)

    類型轉換

    對象的類型定義了對象能包含的數據和能參與的運算,其中一種運算被大多數類型支持,就是將對象從一種給定的類型轉換(convert)為另一種相關類型。

    當在程序的某處我們使用了一種類型而其實對象應該取另一種類型時,程序會自動進行類型轉換。

    將來第4章會有更詳細的介紹類型轉換:

    此處,有必要說明當給某種類型的對象強行賦了另一種類型的值時,到底會發生什么。

    bool b = 42; // b is true /* 當我們把一個非布爾類型的算術值賦給布爾類型時,初始值為О則結果為false,否則結果為true。 */int i = b; // i has value 1 /* 當我們把一個布爾值賦給非布爾類型時,初始值為false 則結果為0,初始值為true則結果為1。 */i = 3.14; // i has value 3 /* 當我們把一個浮點數賦給整數類型時,進行了近似處理。結果值將僅保留浮點數中小數點之前的部分(截斷取整數部分)。 */double pi = i; // pi has value 3.0 /* 當我們把一個整數值賦給浮點類型時,小數部分記為0。如果該整數所占的空間超過了浮點類型的容量,精度可能有損失。 */unsigned char c = -1; // assuming 8-bit chars, c has value 255 /* 當我們賦給無符號類型一個超出它表示范圍的值時,結果是初始值對無符號類型表示數值總數取模后的余數。例如,8比特大小的unsigned char可以表示0至255區間內的值,如果我們賦了一個區間以外的值,則實際的結果是該值對256取模后所得的余數。因此,把-1賦給8比特大小的unsigned char所得的結果是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個人認為以底層知識更容易理解這次轉換 計算機保存數值都以補碼的形式保存。-1的補碼是11111111,但char的類型是無符號整形數,編譯器就把11111111當作無符號整形來輸出。11111111當作正數時值就是255。 */signed char c2 = 256; // assuming 8-bit chars, the value of c2 is undefined /* 當我們賦給帶符號類型一個超出它表示范圍的值時,結果是未定義的(undefined)。此時,程序可能繼續工作、可能崩潰,也可能生成垃圾數據。 */

    類型所能表示的值的范圍決定了轉換的過程。

    建議:避免無法預知和依賴于實現環境的行為

    無法預知的行為源于編譯器無須(有時是不能)檢測的錯誤。即使代碼編譯通過了,如果程序執行了一條未定義的表達式,仍有可能產生錯誤。

    不幸的是,在某些情況和/或某些編譯器下,含有無法預知行為的程序也能正確執行。但是我們卻無法保證同樣一個程序在別的編譯器下能正常工作,甚至已經編譯通過的代碼再次執行也可能會出錯。此外,也不能認為這樣的程序對一組輸入有效,對另一組輸入就一定有效。

    程序也應該盡量避免依賴于實現環境的行為。如果我們把 int的尺寸看成是一個確定不變的已知值,那么這樣的程序就稱作不可移植的(nonportable)。當程序移植到別的機器上后,依賴于實現環境的程序就可能發生錯誤。要從過去的代碼中定位這類錯誤可不是一件輕松愉快的工作。

    算術表達式里使用布爾值

    當在程序的某處使用了一種算術類型的值而其實所需的是另一種類型的值時,編譯器同樣會執行上述的類型轉換。

    例如,如果我們使用了一個非布爾值作為條件,那么它會被自動地轉換成布爾值,這一做法和把非布爾值賦給布爾變量時的操作完全一樣:

    int i = 42; if (i) // if條件的值將為truei = 0;

    如果i的值為0,則條件的值為false;i的所有其他取值(非0)都將使條件為true。

    以此類推,如果我們把一個布爾值用在算術表達式里,則它的取值非0即1,所以一般不宜在算術表達式里使用布爾值。

    含有無符號類型的表達式(帶符號數會自動地轉換成無符號數)

    記住:無符號類型與有符號類型混合表達式中,帶符號數會自動地轉換成無符號數

    盡管我們不會故意給無符號對象賦一個負值,卻可能(特別容易)寫出這么做的代碼。

    例一:

    例如,當一個算術表達式中既有無符號數又有int 值時,那個int值就會轉換成無符號數。把int轉換成無符號數的過程和把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
  • 在第一個輸出表達式里,兩個(負)整數相加并得到了期望的結果。
  • 在第二、三個輸出表達式里,相加前首先把i的-42轉換成無符號數。把負數轉換成無符號數類似于直接給無符號數賦一個負值,結果等于這個負數加上無符號數的模。(有符號數與無符號數相加,先將有符號轉換成無符號)。
  • 例二:

    當從無符號數中減去一個值時,不管這個值是不是無符號數,我們都必須確保結果不能是一個負值:

    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

    例三:

    無符號數不會小于0這一事實同樣關系到循環的寫法。例如,寫一個循環,通過控制變量遞減的方式把從10到0的數字降序輸出。這個循環可能類似于下面的形式:

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

    可能你會覺得反正也不打算輸出負數,可以用無符號數來重寫這個循環。

    然而,這個不經意的改變卻意味著死循環

    // 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;

    來看看當u等于0時發生了什么,這次迭代輸出0,然后繼續執行for語句里的表達式。表達式–u從u當中減去1,得到的結果-1并不滿足無符號數的要求,此時像所有表示范圍之外的其他數字一樣,-1被自動地轉換成一個合法的無符號數。假設 int類型占32位,則當u等于0時,–u的結果將會是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; }

    改寫后的循環先執行對循環控制變量減1的操作,這樣最后一次迭代時,進入循環的u值為1。此時將其減1,則這次迭代輸出的數就是0:下一次再檢驗循環條件時,u的值等于0而無法再進入循環。

    因為我們要先做減1的操作,所以初始化u的值應該比要輸出的最大值大1。這里,u初始化為11,輸出的最大數是10。(也就預大一位)。

    提示:切勿混用帶符號類型和無符號類型

    如果表達式里既有帶符號類型又有無符號類型,當帶符號類型取值為負時會出現異常結果,這是因為帶符號數會自動地轉換成無符號數。

    例如,在一個形如a * b的式子中,如果a=-1,b=1,而且a和b都是int,則表達式的值顯然為-1。

    然而,如果a是int,而b是unsigned,則結果須視在當前機器上int所占位數而定。在我們的環境里,結果是4294967295。

    字面值常量

    一個形如42的值被稱作字面值常量(literal)。每個字面值常量都對應一種數據類型,字面值常量的形式和值決定了它的數據類型。

    整型和浮點型字面值

    我們可以將整型字面值寫作十進制數、八進制數或十六進制數的形式。

    以0開頭的整數代表八進制數,以0x或0x開頭的代表十六進制數。

    例如,我們能用下面的任意一種形式來表示數值20:

    • 20 十進制
    • 024 八進制
    • 0x14 十六進制

    整型字面值具體的數據類型由它的值和符號決定。默認情況下,十進制字面值是帶符號數,八進制和十六進制字面值既可能是帶符號的也可能是無符號的。十進制字面值的類型是int、long和 long long 中尺寸最小的那個(例如,三者當中最小是int),當然前提是這種類型要能容納下當前的值。(帶負號的八進制、十六進數少見)。

    八進制和十六進制字面值的類型是能容納其數值的int、unsigned int、long、unsigned long、long long和 unsigned long long中的尺寸最小者。如果一個字面值連與之關聯的最大的數據類型都放不下,將產生錯誤。類型short沒有對應的字面值。

    以U、L等后綴可以代表相應的字面值類型。

    盡管整型字面值可以存儲在帶符號數據類型中,但嚴格來說,十進制字面值不會是負數。如果我們使用了一個形如-42的負十進制字面值,那個負號并不在字面值之內,它的作用僅僅是對字面值取負值而已。


    浮點型字面值表現為一個小數或以科學計數法表示的指數,其中指數部分用E或e標識:

    • 3.14159
    • 3.14159E0
    • 0.
    • 0e0
    • .001

    默認的,浮點型字面值是一個double,我們可以用后綴F等來表示其他浮點型。

    字符和字符串字面值

    由單引號括起來的一個字符稱為char型字面值,雙引號括起來的零個或多個字符則構成字符串型字面值。

    • ‘a’:字符字面值
    • "Hello world! ":字符串字面值

    字符串字面值的類型實際上是由常量字符構成的數組(array)。編譯器在每個字符串的結尾處添加一個空字符(’\0’),因此,字符串字面值的實際長度要比它的內容多1。

    例如,

    • 字面值’A‘表示的就是單獨的字符A
    • 字符串"A"則代表了一個字符的數組,該數組包含兩個字符:一個是字母A、另一個是空字符(’\0’)。

    如果兩個字符串字面值位置緊鄰且僅由空格、縮進和換行符分隔,則它們實際上是一個整體。

    當書寫的字符串字面值比較長,寫在一行里不太合適時,就可以采取分開書寫的方式:

    //分多行書寫的字符串字面值 std::cout<< "a really, really long string literal ""that spans two lines" << std::endl; //與Java相比,不用+號

    轉義序列

    有兩類字符程序員不能直接使用:

  • 不可打印(nonprintable)的字符,如退格或其他控制字符,因為它們沒有可視的圖符;
  • 在C++語言中有特殊含義的字符(單引號、雙引號、問號、反斜線)。
  • 對于特殊含義的字符需要用到轉義序列(escape sequence),轉義序列均以反斜線作為開始,C++語言規定的轉義序列包括:

    • 換行符 \n

    • 縱向制表符 \v

    • 反斜線 \ \

    • 回車符 \r

    • 橫向制表符 \t

    • 退格符 \b

    • 問號 ?

    • 進紙符 \f

    • 報警(響鈴)符 \a

    • 雙引號 \ "

    • 單引號 \ ’

    在程序中,上述轉義序列被當作一個字符使用:

    std::cout << '\n'; // prints a newline std::cout << "\tHi!\n"; // prints a tab followd by "Hi!" and a newline

    偏僻語法,不能一見知意,少用)我們也可以使用泛化的轉義序列,其形式是\x后緊跟1個或多個十六進制數字,或者\后緊跟1個、2個或3個八進制數字,其中數字部分表示的是字符對應的數值。假設使用的是Latin-1字符集,以下是一些示例:

    • \7 (bell)
    • \12 (newline)
    • \40 (blank)
    • \0 (null)
    • \115 (‘M’)
    • \x4d (‘M’)

    我們可以像使用普通字符那樣使用C++語言定義的轉義序列:

    std::cout << "Hi \x4dO\115!\n"; // prints Hi MOM! followed by a newline std::cout << '\115' << '\n'; // prints M followed by a newline

    注意,如果反斜線\后面跟著的八進制數字超過3個,只有前3個數字與\構成轉義序列。

    例如,"\1234"表示2個字符,即八進制數123對應的字符以及字符4。

    相反,\x要用到后面跟著的所有數字,例如,"\x1234"表示一個16位的字符,該字符由這4個十六進制數所對應的比特唯一確定。

    因為大多數機器的char型數據占8位,所以上面這個例子可能會報錯。一般來說,超過8位的十六進制字符都是與U等前綴作為開頭的擴展字符集一起使用的。

    通過前后綴指定字面值的類型

    通過添加前綴和后綴,可以改變整型、浮點型和字符型字面值的默認類型。

    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作后綴)當使用一個長整型字面值時,請使用大寫字母L來標記,因為小寫字母l和數字1太容易混淆了。

    通過添加前綴或后綴指定字面值的類型

    字符和字符串字面值

    前綴含義類型
    uUnicode 16字符char16_t
    UUnicode 32字符char32_t
    L寬字符wchar_t
    u8UTF8(僅用于字符串字面常量)char

    整型字面值

    后綴最小匹配類型
    u or Uunsigned
    l or Llong
    ll or LLlong long

    浮點數字面值

    后綴類型
    f or Ffloat
    l or Llong double

    對于一個整型字面值來說,我們能分別指定它是否帶符號以及占用多少空間。如果后綴中有u,則該字面值屬于無符號類型,也就是說,以u為后綴的十進制數、八進制數或十六進制數都將從unsigned int、unsigned long和 unsigned long long中選擇能匹配的空間最小的一個作為其數據類型。

    如果后綴中有L,則字面值的類型至少是long; 如果后綴中有LL,則字面值的類型將是long long和unsigned long long 中的一種。顯然我們可以將u與工或LL合在一起使用。例如,以UL為后綴的字面值的數據類型將根據具體數值情況或者取unsigned long,或者取unsigned long long。

    布爾字面值和指針字面值

    true和false是布爾類型的字面值:

    bool test = false;

    nullptr是指針字面值。

    變量

    變量提供一個具名的、可供程序操作的存儲空間。

    C++中的每個變量都有其數據類型,數據類型決定著變量:

    • 所占內存空間的大小和布局方式、(內存大小)
    • 該空間能存儲的值的范圍,(范圍)
    • 以及變量能參與的運算。(運算)

    對C++程序員來說,“變量(variable)”和“對象(object)”一般可以互換使用。

    變量定義

    變量定義的基本形式是:首先是類型說明符(type specifier),隨后緊跟由一個或多個變量名組成的列表,其中變量名以逗號分隔,最后以分號結束。

    列表中每個變量名的類型都由類型說明符指定,定義時還可以為一個或多個變量賦初值:

    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 literal

    book的定義用到了庫類型std: :string,像iostream一樣,string 也是在命名空間std中定義的,我們將在第3章中對string類型做更詳細的介紹。眼下,只需了解string是一種表示可變長字符序列的數據類型即可。

    C++庫提供了幾種初始化string對象的方法,其中一種是把字面值拷貝給string對象,因此在上例中,book被初始化為0-201-78345-X。

    術語:何為對象?

    C++程序員們在很多場合都會使用對象(object)這個名詞。通常情況下,對象是指一塊能存儲數據并具有某種類型的內存空間

    • 一些人僅在與有關的場景下才使用“對象”這個詞。
    • 另一些人則把已命名的對象和未命名的對象區分開來,他們把命名了的對象叫做變量。
    • 還有一些人把對象和值區分開來,其中對象指能被程序修改的數據,而(value)指只讀的數據。

    本書遵循大多數人的習慣用法,即認為對象是具有某種數據類型的內存空間。我們在使用對象這個詞時,并不嚴格區分是類還是內置類型,也不區分是否命名或是否只讀。

    (記住:對象是具有某種數據類型的內存空間

    初始值

    當對象在創建時獲得了一個特定的值,我們說這個對象被初始化(initialized)了。

    用于初始化變量的值可以是任意復雜的表達式。

    當一次定義了兩個或多個變量時,對象的名字隨著定義也就馬上可以使用了。

    因此在同一條定義語句中,可以用先定義的變量值去初始化后定義的其他變量。

    // 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++語言中,初始化是一個異常復雜的問題,我們也將反復討論這個問題。

    很多程序員對于用等號=來初始化變量的方式倍感困惑,這種方式容易讓人認為初始化是賦值的一種。事實上在C++語言中,初始化和賦值是兩個完全不同的操作。然而在很多編程語言中二者的區別幾乎可以忽略不計,即使在C++語言中有時這種區別也無關緊要,所以人們特別容易把二者混為一談。

    需要強調的是,這個概念至關重要,我們也將在后面不止一次提及這一點。

    初始化不是賦值,初始化的含義是創建變量時賦予其一個初始值,而賦值的含義是把對象的當前值擦除,而以一個新值來替代。

    (Note: 初始化和賦值是兩碼事,即使它們很相似。當某一變量首次用=號的就是初始化,其他地方用=號的就是賦值

    列表初始化

    C++語言定義了初始化的好幾種不同形式,這也是初始化問題復雜性的一個體現。例如,要想定義一個名為units_sold的int變量并初始化為0,以下的4條語句都可以做到這一點:

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

    作為C++11新標準的一部分,用花括號來初始化變量得到了全面應用,而在此之前,這種初始化的形式僅在某些受限的場合下才能使用。出于3.3.1節將要介紹的原因,這種初始化的形式被稱為列表初始化(list initialization)。現在,無論是初始化對象還是某些時候為對象賦新值,都可以使用這樣一組由花括號括起來的初始值了。

    當用于內置類型的變量時,這種初始化形式有一個重要特點:如果我們使用列表初始化且初始值存在丟失信息的風險,則編譯器將報錯

    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變量時可能丟失數據,所以編譯器拒絕了a和b的初始化請求。

    其中,至少ld的小數部分會丟失掉,而且int也可能存不下ld的整數部分。

    (Note: 列表初始化變量轉型有數據丟失報錯功能???)

    默認初始化

    如果定義變量時沒有指定初值,則變量被默認初始化(default initialized),此時變量被賦予了“默認值”。默認值到底是什么由變量類型決定,同時定義變量的位置也會對此有影響。

    如果是內置類型的變量未被顯式初始化,它的值由定義的位置決定。定義于任何函數體之外的變量被初始化為0。

    • 一種例外情況是,定義在函數體內部的內置類型變量將不被初始化(uninitialized)。

    • 一個未被初始化的內置類型變量的值是未定義的,如果試圖拷貝或以其他形式訪問此類值將引發錯誤。

    每個類各自決定其初始化對象的方式。而且,是否允許不經初始化就定義對象也由類自己決定。如果類允許這種行為,它將決定對象的初始值到底是什么。

    絕大多數類都支持無須顯式初始化而定義對象,這樣的類提供了一個合適的默認值。例如,String類規定如果沒有指定初值則生成一個空串:

    std::string empty;//empty非顯式地初始化為一個空串 Sales_item item;//被默認初始化的sales_item對象

    一些類要求每個對象都顯式初始化,此時如果創建了一個該類的對象而未對其做明確的初始化操作,將引發錯誤。

    定義于函數體內的內置類型的對象如果沒有初始化,則其值未定義。類的對象如果沒有顯式地初始化,則其值由類確定。

    提示:未初始化變量引發運行時故障

    未初始化的變量含有一個不確定的值,使用未初始化變量的值是一種錯誤的編程行為并且很難調試。盡管大多數編譯器都能對一部分使用未初始化變量的行為提出警告,但嚴格來說,編譯器并未被要求檢查此類錯誤。

    使用未初始化的變量將帶來無法預計的后果。有時我們足夠幸運,一訪問此類對象程序就崩潰并報錯,此時只要找到崩潰的位置就很容易發現變量沒被初始化的問題。另外一些時候,程序會一直執行完并產生錯誤的結果。更糟糕的情況是,程序結果時對時錯、無法把握。而且,往無關的位置添加代碼還會導致我們誤以為程序對了,其實結果仍舊有錯。

    建議初始化每一個內置類型的變量。雖然并非必須這么做,但如果我們不能確保初始化后程序安全,那么這么做不失為一種簡單可靠的方法。

    (Note: 創建一個變量都初始化吧。保安全)

    變量聲明和定義的關系

    為了允許把程序拆分成多個邏輯部分來編寫,C++語言支持分離式編譯(separate compilation)機制,該機制允許將程序分割為若干個文件,每個文件可被獨立編譯。

    如果將程序分為多個文件,則需要有在文件間共享代碼的方法。例如,一個文件的代碼可能需要使用另一個文件中定義的變量。一個實際的例子是std: :cout和std::cin,它們定義于標準庫,卻能被我們寫的程序使用。

    為了支持分離式編譯,C++語言將聲明和定義區分開來。

    • 聲明(declaration)使得名字為程序所知,一個文件如果想使用別處定義的名字則必須包含對那個名字的聲明。
    • 定義(definition)負責創建與名字關聯的實體。

    變量聲明規定了變量的類型和名字,在這一點上定義與之相同。但是除此之外,定義還申請存儲空間,也可能會為變量賦一個初始值。

    如果想聲明一個變量而非定義它,就在變量名前添加關鍵字extern,而且不要顯式地初始化變量:

    extern int i; // declares but does not define i int j; // declares and defines j

    任何包含了顯式初始化的聲明即成為定義。我們能給由extern關鍵字標記的變量賦一個初始值,但是這么做也就抵消了extern的作用。extern語句如果包含初始值就不再是聲明,而變成定義了

    extern double pi = 3.1416; //定義

    在函數體內部,如果試圖初始化一個由extern關鍵字標記的變量,將引發錯誤。

    聲明和定義的區別看起來也許微不足道,但實際上卻非常重要。(重中之重)如果要在多個文件中使用同一個變量,就必須將聲明和定義分離。此時,變量的定義必須出現在且只能出現在一個文件中,而其他用到該變量的文件必須對其進行聲明,卻絕對不能重復定義。

    (Note: 變量能且只能被定義一次,但是可以被多次聲明(只為使用它)。)

    關于C++語言對分離式編譯的支持在將來做更詳細介紹。

    關鍵概念:靜態類型

    C++是一種靜態類型(statically typed)語言,其含義是在編譯階段檢查類型。其中,檢查類型的過程稱為類型檢查(type checking)。

    對象的類型決定了對象所能參與的運算。在C++語言中,編譯器負責檢查數據類型是否支持要執行的運算,如果試圖執行類型不支持的運算,編譯器將報錯并且不會生成可執行文件。

    程序越復雜,靜態類型檢查越有助于發現問題。然而,前提是編譯器必須知道每一個實體對象的類型,這就要求我們在使用某個變量之前必須聲明其類型。

    標識符

    C++的標識符(identifier)由字母、數字和下畫線組成,其中必須以字母或下畫線開頭。標識符的長度沒有限制,但是對大小寫字母敏感:

    // defines four different int variables int somename, someName, SomeName, SOMENAME;

    下面兩表所示,C++語言保留了一些名字供語言本身使用,這些名字不能被用作標識符。

    同時,C++也為標準庫保留了一些名字。用戶自定義的標識符中不能連續出現兩個下畫線,也不能以下畫線緊連大寫字母開頭。此外,定義在函數體外的標識符不能以下畫線開頭。

    C++關鍵字

    -----
    alignascontinuefriendregistertrue
    alignofdecltypegotoreinterpret_casttry
    asmdefaultifreturntypedef
    autodeleteinlineshorttypeid
    booldointsignedtypename
    breakdoublelongsizeofunion
    casedynamic_castmutablestaticunsigned
    catchelsenamespacestatic_assertusing
    charenumnewstatic_castvirtual
    char16_texplicitnoexceptstructvoid
    char32_texportnullptrswitchvolatile
    classexternoperatortemplatewchar_t
    constfalseprivatethiswhile
    constexprfloatprotectedthread_local
    const_castforpublicthrow

    C++操作符替代名

    ---
    andcomplor_eq
    and_eqnotxor
    bitandnot_eqxor_eq
    bitoror

    變量命名規范

    變量命名有許多約定俗成的規范,下面的這些規范能有效提高程序的可讀性:

    • 標識符要能體現實際含義。
    • 變量名一般用小寫字母,如 index,不要使用Index或INDEX。
    • 用戶自定義的類名一般以大寫字母開頭,如 Sales_item。
    • 如果標識符由多個單詞組成,則單詞間應有明顯區分,如 student_loan或studentLoan,不要使用studentloan。

    對于命名規范來說,若能堅持,必將有效

    (Note:使用Java駝峰命名法吧)

    名字的作用域

    不論是在程序的什么位置,使用到的每個名字都會指向一個特定的實體:變量、函數、類型等。然而,同一個名字如果出現在程序的不同位置,也可能指向的是不同實體。

    作用域(scope)是程序的一部分,在其中名字有其特定的含義。C++語言中大多數作用域都以花括號分隔。

    同一個名字在不同的作用域中可能指向不同的實體。名字的有效區域始于名字的聲明語句,以聲明語句所在的作用域末端為結束。

    #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定義于所有花括號之外,它和其他大多數定義在函數體之外的名字一樣擁有全局作用域(global scope)。一旦聲明之后,全局作用域內的名字在整個程序的范圍內都可使用。

    名字sum定義于main函數所限定的作用域之內,從聲明sum開始直到main函數結束為止都可以訪問它,但是出了main函數所在的塊就無法訪問了,因此說變量sum擁有塊作用域(block scope)。名字val定義于 for語句內,在for語句之內可以訪問val,但是在main函數的其他部分就不能訪問它了。

    建議:當你第一次使用變量時再定義它

    一般來說,在對象第一次被使用的地方附近定義它是一種好的選擇,因為這樣做有助于更容易地找到變量的定義。更重要的是,當變量的定義與它第一次被使用的地方很近時,我們也會賦給它一個比較合理的初始值。

    嵌套的作用域

    作用域能彼此包含,被包含(或者說被嵌套)的作用域稱為內層作用域(inner scope),包含著別的作用域的作用域稱為外層作用域(outer scope)。

    作用域中一旦聲明了某個名字,它所嵌套著的所有作用域中都能訪問該名字。同時,允許在內層作用域中重新定義外層作用域已有的名字:

    #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出現在局部變量 reused定義之前,因此這條語句使用全局作用域中定義的名字reused,輸出42 0。

    • 輸出#2發生在局部變量reused定義之后,此時局部變量reused正在作用域內(in scope),因此第二條輸出語句使用的是局部變量reused而非全局變量,輸出0 0。

    • 輸出#3使用作用域操作符來覆蓋默認的作用域規則,因為全局作用域本身并沒有名字,所以當作用域操作符的左側為空時,向全局作用域發出請求獲取作用域操作符右側名字對應的變量。結果是,第三條輸出語句使用全局變量reused,輸出42 0。

    如果函數有可能用到某全局變量,則不宜再定義一個同名的局部變量。(各個變量盡量在可控范圍有獨一名字)

    復合類型

    復合類型( compound type)是指基于其他類型定義的類型。C++語言有幾種復合類型,本章將介紹其中的兩種:引用和指針。

    與我們已經掌握的變量聲明相比,定義復合類型的變量要復雜很多。

    上一節提到,一條簡單的聲明語句由一個數據類型和緊隨其后的一個變量名列表組成。

    其實更通用的描述是,一條聲明語句由一個基本數據類型(base type)和緊隨其后的一個聲明符(declarator)列表組成。每個聲明符命名了一個變量并指定該變量為與基本數據類型有關的某種類型。

    (Note: ,1條聲明語句=1個基本數據類型+1個聲明符)

    目前為止,我們所接觸的聲明語句中,聲明符其實就是變量名,此時變量的類型也就是聲明的基本數據類型。其實還可能有更復雜的聲明符,它基于基本數據類型得到更復雜的類型,并把它指定給變量。

    (系好安全帶吧,少年!)

    引用

    C++11中新增了一種引用:所謂的“右值引用(rvalue reference)”,之后會做更詳細的介紹。這種引用主要用于內置類。

    嚴格來說,當我們使用術語“引用(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)在一起,而不是將初始值拷貝給引用。一旦初始化完成,引用將和它的初始值對象一直綁定在一起。因為無法令引用重新綁定到另外一個對象,因此引用必須初始化。(引用一次性的)

    引用即別名

    引用并非對象,相反的,它只是為一個已經存在的對象所起的另外一個名字。

    對象是具有某種數據類型的內存空間)(引用只是對象的別名)

    定義了一個引用之后,對其進行的所有操作都是在與之綁定的對象上進行的:

    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

    因為引用本身不是一個對象,所以不能定義引用的引用。

    引用的定義

    允許在一條語句中定義多個引用,其中每個引用標識符都必須以符號&開頭:

    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

    所有引用的類型都要和與之綁定的對象嚴格匹配。而且,引用只能綁定在對象上,而不能與字面值或某個表達式的計算結果綁定在一起:

    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)”另外一種類型的復合類型。與引用類似,指針也實現了對其他對象的間接訪問。

    然而指針與引用相比又有很多不同點

  • 指針本身就是一個對象,允許對指針賦值和拷貝,而且在指針的生命周期內它可以先后指向幾個不同的對象。(不像引用那樣一次性)
  • 指針無須在定義時賦初值。和其他內置類型一樣,在塊作用域內定義的指針如果沒有被初始化,也將擁有一個不確定的值。
  • 指針通常難以理解,即使是有經驗的程序員也常常因為調試指針引發的錯誤而被備受折磨。

    定義指針類型的方法將聲明符寫成*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: &取地址符 跟 引用&號是兩碼事,注意區分)

    其他所有指針的類型都要和它所指向的對象嚴格匹配:(有兩種例外情況,日后再介紹)

    對象是具有某種數據類型的內存空間

    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

    因為在聲明語句中指針的類型實際上被用于指定它所指向對象的類型,所以二者必須匹配。如果指針指向了一個其他類型的對象,對該對象的操作將發生錯誤。

    指針值

    指針的值(即地址)應屬下列4種狀態之一:

  • 指向一個對象。
  • 指向緊鄰對象所占空間的下一個位置。(用在數組)
  • 空指針,意味著指針沒有指向任何對象。
  • 無效指針,也就是上述情況之外的其他值。
  • 試圖拷貝或以其他方式訪問無效指針的值都將引發錯誤。編譯器并不負責檢查此類錯誤,這一點和試圖使用未經初始化的變量是一樣的。訪問無效指針的后果無法預計,因此程序員必須清楚任意給定的指針是否有效。

    盡管第2種和第3種形式的指針是有效的,但其使用同樣受到限制。顯然這些指針沒有指向任何具體對象,所以試圖訪問此類指針(假定的)對象的行為不被允許。如果這樣做了,后果也無法預計。

    利用指針訪問對象

    如果指針指向了一個對象,則允許使用解引用符 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: * & 位置不同傻傻分不清楚。)

    一條聲明語句由一個基本數據類型(base type)和緊隨其后的一個聲明符(declarator)列表組成

    int a = 1; int &aa = a;//這里&為引用聲明符(這名我自起的)int ival = 42;jie int *p = &ival;//這里*為指針聲明符(這名我自起的),&為取地址符 cout << *p;//這里*為解引用符

    對指針解引用會得出所指的對象,因此如果給解引用的結果賦值,實際上也就是給指針所指的對象賦值:

    *p = 0; // * yields the object; we assign a new value to ival through p,這里*是解引用符 cout << *p; // prints 0

    如上述程序所示,為*p賦值實際上是為p所指的對象賦值。

    解引用操作僅適用于那些確實指向了某個對象的有效指針。

    關鍵概念:一符多義(& 與 *)

    像&和*這樣的符號,既能用作表達式里的運算符(&按位與符取址符乘法符或解引用符*),也能作為聲明的一部分出現(&引用聲明符,*指針聲明符),符號的上下文決定了符號的意義:

    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
    • 在聲明語句中,&和*用于組成復合類型(我個人將它們分別稱為引用聲明符、指針聲明符)。

    • 在表達式中,&和*又轉變成運算符(操作數為1個時,分別稱為取值符、解引用符。操作數為2個時,分別稱為按位與符、乘法符)。

    在不同場景下出現的雖然是同一個符號,但是由于含義截然不同,所以我們完全可以把它當作不同的符號來看待。

    空指針

    空指針(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新標準剛剛引入的一種方法。nullptr是一種特殊類型的字面值,它可以被轉換成任意其他的指針類型。另一種辦法就如對p2的定義一樣,也可以通過將指針初始化為字面值0來生成空指針。

    過去的程序還會用到一個名為NULL的預處理變量(preprocessor variable)來給指針賦值,這個變量在頭文件cstdlib中定義,它的值就是0。

    稍微介紹一點關于預處理器的知識,現在只要知道預處理器是運行于編譯過程之前的一段程序就可以了。預處理變量不屬于命名空間std,它由預處理器負責管理,因此我們可以直接使用預處理變量而無須在前面加上std: :。

    當用到一個預處理變量時,預處理器會自動地將它替換為實際值,因此用NULL初始化指針和用0初始化指針是一樣的。在新標準下,現在的C++程序最好使用nullptr,同時盡量避免使用NULL。

    把int變量直接賦給指針是錯誤的操作,即使int變量的值恰好等于0也不行。

    int zero = 0; pi = zero; // error: cannot assign an int to a pointer

    建議:初始化所有指針

    使用未經初始化的指針是引發運行時錯誤的一大原因。(像Java的NullPointerException)

    和其他變量一樣,訪問未經初始化的指針所引發的后果也是無法預計的。通常這一行為將造成程序崩潰,而且一旦崩潰,要想定位到出錯位置將是特別棘手的問題。

    在大多數編譯器環境下,如果使用了未經初始化的指針,則該指針所占內存空間的當前內容將被看作一個地址值。訪問該指針,相當于去訪問一個本不存在的位置上的本不存在的對象。糟糕的是,如果指針所占內存空間中恰好有內容,而這些內容又被當作了某個地址,我們就很難分清它到底是合法的還是非法的了。(指針未初始化會出現的糟糕狀況)

    因此建議初始化所有的指針,并且在可能的情況下,盡量等定義了對象之后再定義指向它的指針。如果實在不清楚指針應該指向何處,就把它初始化為nullptr或者0,這樣程序就能檢測并知道它沒有指向任何具體的對象了。

    賦值和指針

    指針和引用都能提供對其他對象的間接訪問,然而在具體實現細節上二者有很大不同:

  • 其中最重要的一點就是引用本身并非一個對象。一旦定義了引用,就無法令其再綁定到另外的對象,之后每次使用這個引用都是訪問它最初綁定的那個對象。
  • 指針和它存放的地址之間就沒有這種限制了。和其他任何變量(只要不是引用)一樣,給指針賦值就是令它存放一個新的地址,從而指向一個新的對象:
  • int i = 42; int *pi = 0; // pi is initialized but addresses no object int *pi2 = &i; // pi2 initialized to hold the address of i int *pi3; // if pi3 is defined inside a block, pi3 is uninitialized pi3 = pi2; // pi3 and pi2 address the same object, e.g., i pi2 = 0; // pi2 now addresses no object

    有時候要想搞清楚一條賦值語句到底是改變了指針的值還是改變了指針所指對象的值不太容易,最好的辦法就是記住賦值永遠改變的是等號左側的對象。當寫出如下語句時,

    pi = &ival; // value in pi is changed; pi now points to ival

    意思是為 pi賦一個新的值,也就是改變了那個存放在pi內的地址值。相反的,如果寫出如下語句,

    *pi = 0; // value in ival is changed; pi is unchanged

    則*pi(也就是指針pi指向的那個對象)發生改變。

    其他指針操作

    只要指針擁有一個合法值,就能將它用在條件表達式中。和采用算術值作為條件遵循的規則類似,如果指針的值是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指針對應的條件值都是true。

    對于兩個類型相同的合法指針,可以用相等操作符(=)或不相等操作符(!=)來比較它們,比較的結果是布爾類型。如果兩個指針存放的地址值相同,則它們相等;反之它們不相等。

    這里兩個指針存放的地址值相同(兩個指針相等)有三種可能:

  • 它們都為空、
  • 它們都指向同一個對象,
  • 它們都指向了同一個對象的下一地址(???這情況不太懂)
  • 需要注意的是,一個指針指向某對象,同時另一個指針指向另外對象的下一地址,此時也有可能出現這兩個指針值相同的情況,即指針相等。

    因為上述操作要用到指針的值,所以不論是作為條件出現還是參與比較運算(再數組中用),都必須使用合法指針,使用非法指針作為條件或進行比較都會引發不可預計的后果。

    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*指針能做的事兒比較有限:

    • 拿它和別的指針比較、
    • 作為函數的輸入或輸出,
    • 賦給另外一個void*指針。

    不能直接操作void*指針所指的對象,因為我們并不知道這個對象到底是什么類型,也就無法確定能在這個對象上做哪些操作。

    概括說來,以void*的視角來看內存空間也就僅僅是內存空間,沒辦法訪問內存空間中所存的對象,不過,是有獲取void*指針所存地址的方法,日后介紹。

    理解復合類型的聲明

    如前所述,變量的定義包括一個基本數據類型(base type)和一組聲明符。在同一條定義語句中,雖然基本數據類型只有一個,但是聲明符的形式卻可以不同。也就是說,一條定義語句可能定義出不同類型的變量:

    // i is an int; p is a pointer to int; r is a reference to int int i = 1024, *p = &i, &r = i;

    很多程序員容易迷惑于基本數據類型和類型修飾符的關系,其實后者不過是聲明符的一部分罷了。

    定義多個變量

    經常有一種觀點會誤以為,在定義語句中,類型修飾符(*或s)作用于本次定義的全部變量。造成這種錯誤看法的原因有很多,其中之一是我們可以把空格寫在類型修飾符和變量名中間:

    int* p; // legal but might be misleading

    我們說這種寫法可能產生誤導是因為int*放在一起好像是這條語句中所有變量共同的類型一樣。其實恰恰相反,基本數據類型是int而非int*。*僅僅是修飾了p而已,對該聲明語句中的其他變量,它并不產生任何作用:

    int* p1, p2;// p1 is a pointer to int; p2 is an int

    涉及指針或引用的聲明,一般有兩種寫法。

    第一種把修飾符和變量標識符寫在一起:(推薦)

    int *p1, *p2; // both p1 and p2 are pointers to int

    這種形式著重強調變量具有的復合類型。

    第二種把修飾符和類型名寫在一起,并且每條語句只定義一個變量:

    int* p1; // p1 is a pointer to int int* p2; // p2 is a pointer to int

    這種形式著重強調本次聲明定義了一種復合類型

    上述兩種定義指針或引用的不同方法沒有孰對孰錯之分,關鍵是選擇并堅持其中的一種寫法,不要總是變來變去。

    我們接下都采用第一種寫法,將*(或是&)與變量名連在一起。(聲明符與變量)

    指向指針的指針

    一般來說,聲明符中修飾符的個數并沒有限制。當有多個修飾符連寫在一起時,按照其邏輯關系詳加解釋即可。以指針為例,指針是內存中的對象,像其他對象一樣也有自己的地址,因此允許把指針的地址再存放到另一個指針當中。

    通過*的個數可以區分指針的級別。也就是說,**表示指向指針的指針,***表示指向指針的指針的指針,以此類推:

    int ival = 1024; int *pi = &ival; // pi points to an int int **ppi = &pi; // ppi points to a pointer to an int

    此處pi是指向int型數的指針,而ppi是指向int型指針的指針,下圖描述了它們之間的關系。

    解引用int型指針會得到一個int型的數,同樣,解引用指向指針的指針會得到一個指針。此時為了訪問最原始的那個對象,需要對指針的指針做兩次解引用:

    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引用的是一個指針。最后,聲明的基本數據類型部分指出r引用的是一個int指針。

    面對一條比較復雜的指針或引用的聲明語句時,從右向左閱讀有助于弄清楚它的真實含義。


    我寫的小程序來解惑:

    #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; }

    輸出結果:

    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: 只讀變量

    例如,用一個變量來表示緩沖區的大小。使用變量的好處是當我們覺得緩沖區大小不再合適時,很容易對其進行調整。另一方面,也應隨時警惕防止程序一不小心改變了這個值。為了滿足這一要求,可以用關鍵字const對變量的類型加以限定:

    const int bufSize = 512; //輸入緩沖區大小

    這樣就把bufSize定義成了一個常量。任何試圖為bufSize賦值的行為都將引發錯誤:

    bufSize = 1024; //錯誤:試圖向const對象寫值

    因為const對象一旦創建后其值就不能再改變,所以const對象必須初始化。一如既往,初始值可以是任意復雜的表達式:

    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

    正如之前反復提到的,對象的類型決定了其上的操作。與非 const類型所能參與的操作相比,const類型的對象能完成其中大部分,但也不是所有的操作都適合。主要的限制就是只能在const類型的對象上執行不改變其內容的操作。

    例如,const int和普通的int一樣都能參與算術運算,也都能轉換成一個布爾值,等等。

    在不改變 const對象的操作中還有一種是初始化,如果利用一個對象去初始化另外一個對象,則它們是不是const都無關緊要:

    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中的值還是一個整型數。ci的常量特征僅僅在執行改變ci的操作時才會發揮作用。當用ci去初始化j時,根本無須在意ci是不是一個常量??截愐粋€對象的值并不會改變它,一旦拷貝完成,新的對象就和原來的對象沒什么關系了。

    默認狀態下,const對象僅在文件內有效(const 常量在多文件中使用方法)

    當以編譯時初始化的方式定義一個const對象時,就如對bufsize的定義一樣:

    const int bufSize = 512;//輸入緩沖區大小

    編譯器將在編譯過程中把用到該變量的地方都替換成對應的值。也就是說,編譯器會找到代碼中所有用到bufSize的地方,然后用512替換。

    為了執行上述替換,編譯器必須知道變量的初始值。如果程序包含多個文件,則每個用了const對象的文件都必須得能訪問到它的初始值才行。要做到這一點,就必須在每一個用到變量的文件中都有對它的定義。為了支持這一用法,同時避免對同一變量的重復定義,默認情況下,const對象被設定為僅在文件內有效。當多個文件中出現了同名的const變量時,其實等同于在不同文件中分別定義了獨立的變量。

    某些時候有這樣一種 const變量,它的初始值不是一個常量表達式,但又確實有必要在文件間共享。這種情況下,我們不希望編譯器為每個文件分別生成獨立的變量。相反,我們想讓這類const對象像其他(非常量)對象一樣工作,也就是說,只在一個文件中定義const,而在其他多個文件中聲明并使用它。

    解決的辦法是,對于const變量不管是聲明還是定義都添加extern關鍵字,這樣只需定義一次就可以了:

    //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并非本文件所獨有,它的定義將在別處出現。

    如果想在多個文件之間共享const對象,必須在變量的定義之前添加extern關鍵字。

    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賦值,當然也就不能通過引用去改變ci。因此,對r2的初始化是錯誤的。假設該初始化合法,則可以通過r2來改變它引用對象的值,這顯然是不正確的。

    術語:常量引用是對const的引用

    C++程序員們經常把詞組“對const的引用”簡稱為“常量引用”,這一簡稱還是挺靠譜的,不過前提是你得時刻記得這就是個簡稱而已。

    嚴格來說,并不存在常量引用。因為引用不是一個對象,所以我們沒法讓引用本身恒定不變。事實上,由于C+語言并不允許隨意改變引用所綁定的對象,所以從這層意義上理解所有的引用又都算是常量。引用的對象是常量還是非常量可以決定其所能參與的操作,卻無論如何都不會影響到引用和對象的綁定關系本身。

    初始化和對const的引用

    前文提到,引用的類型必須與其所引用對象的類型一致,但是有兩個例外。

    第一種例外情況就是在初始化常量引用時允許用任意表達式作為初始值,只要該表達式的結果能轉換成引用的類型即可。尤其,允許為一個常量引用綁定非常量的對象、字面值,甚至是個一般表達式:

    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

    要想理解這種例外情況的原因,最簡單的辦法是弄清楚當一個常量引用被綁定到另外一種類型上時到底發生了什么:

    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++語言也就把這種行為歸為非法。

    (Note: 第二種例外情況在哪???)

    對const的引用可能引用一個并非const的對象

    必須認識到,常量引用僅對引用可參與的操作做出了限定,對于引用的對象本身是不是一個常量未作限定。因為對象也可能是個非常量,所以允許通過其他途徑改變它的值:

    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 const

    r2綁定(非常量)整數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 = &pi; // error: ptr is a plain pointer const double *cptr = &pi; // 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

    和常量引用一樣,指向常量的指針也沒有規定其所指的對象必須是一個常量。所謂指向常量的指針僅僅要求不能通過該指針改變對象的值,而沒有規定那個對象的值不能通過其他途徑改變。(也就是說還有其他路子改變對象的值,可查看第一節)。

    試試這樣想吧:所謂指向常量的指針或引用,不過是指針或引用“自以為是”罷了,它們覺得自己指向了常量,所以自覺地不去改變所指對象的值。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指針

    (上一節講的是指向常量的指針,這節將常量指針)

    指針是對象而引用不是,因此就像其他對象類型一樣,允許把指針本身定為常量。常量指針(const pointer)必須初始化,而且一旦初始化完成,則它的值(也就是存放在指針中的那個地址)就不能再改變了。把*放在const關鍵字之前用以說明指針是一個常量,這樣的書寫形式隱含著一層意味,即不變的是指針本身的值而非指向的那個值:

    int errNumb = 0; int *const curErr = &errNumb; // curErr will always point to errNumb const double pi = 3.14159; const double *const pip = &pi; // pip is a const pointer to a const object

    前文提到,要想弄清楚這些聲明的含義最行之有效的辦法是從右向左閱讀。(Note:這是理解這些復雜申明語句的關鍵

    此例中,

  • 離curErr最近的符號是const,意味著curErr本身是一個常量對象,對象的類型由聲明符的其余部分確定。
  • 聲明符中的下一個符號是*,意思是curErr是一個常量指針
  • 最后,該聲明語句的基本數據類型部分確定了常量指針指向的是一個int對象。
  • 對象是指一塊能存儲數據并具有某種類型的內存空間

    與之相似,我們也能推斷出,pip是一個常量指針,它指向的對象是一個雙精度浮點型常量。

    指針本身是一個常量并不意味著不能通過指針修改其所指對象的值,能否這樣做完全依賴于所指對象的類型。例如,pip是一個指向常量的常量指針,則不論是 pip 所指的對象值還是pip自己存儲的那個地址都不能改變。

    相反的,curErr指向的是一個一般的非常量整數,那么就完全可以用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

    如前所述,指針本身是一個對象,它又可以指向另外一個對象。因此,指針本身是不是常量以及指針所指的是不是一個常量就是兩個相互獨立的問題。

    用名詞頂層const(top-level const)表示指針本身是個常量,而用名詞底層const (low-level const)表示指針所指的對象是一個常量

    更一般的:

    頂層const可以表示任意的對象是常量,這一點對任何數據類型都適用,如算術類型、類、指針等。

    底層const則與指針和引用等復合類型的基本類型部分有關。比較特殊的是,指針類型既可以是頂層const也可以是底層const,這一點和其他類型相比區別明顯:

    (助記:頂常底復)

    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:與指針和引用等復合類型的基本類型部分有關 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:根據const所在位置來判斷頂層const或底層const不管用)

    當執行對象的拷貝操作時,常量是頂層const還是底層const區別明顯。

    其中,頂層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

    執行拷貝操作并不會改變被拷貝對象的值,因此,拷入和拷出的對象是否是常量都沒什么影響。

    另一方面,底層 const 的限制卻不能忽視。當執行對象的拷貝操作時,拷入和拷出的對象必須具有相同的底層 const 資格,或者兩個對象的數據類型必須能夠轉換。一般來說,非常量可以轉換成常量,反之則不行:

    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 int

    p3既是頂層const也是底層const,拷貝p3時可以不在乎它是一個頂層const,但是必須清楚它指向的對象得是一個常量。因此,不能用p3去初始化p,因為p指向的是一個普通的(非常量)整數。另一方面,p3的值可以賦給p2,是因為這兩個指針都是底層const,盡管p3同時也是一個常量指針(頂層const),僅就這次賦值而言不會有什么影響。

    (Note:這節不好懂)

    constexpr和常量表達式

    常量表達式(const expression)是指值不會改變并且在編譯過程就能得到計算結果的表達式。顯然,字面值屬于常量表達式,用常量表達式初始化的const對象也是常量表達式。后面將會提到,C++語言中有幾種情況下是要用到常量表達式的。

    一個對象(或表達式)是不是常量表達式由它的數據類型和初始值共同決定,例如:

    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的初始值是個字面值常量,但由于它的數據類型只是一個普通int而非const int,所以它不屬于常量表達式。另一方面,盡管 sz本身是一個常量,但它的具體值直到運行時才能獲取到,所以也不是常量表達式。

    constexpr變量

    在一個復雜系統中,很難(幾乎肯定不能)分辨一個初始值到底是不是常量表達式。當然可以定義一個 const變量并把它的初始值設為我們認為的某個常量表達式,但在實際使用時,盡管要求如此卻常常發現初始值并非常量表達式的情況??梢赃@么說,在此種情況下,對象的定義和使用根本就是兩回事兒。

    C++11新標準規定,允許將變量聲明為constexpr類型以便由編譯器來驗證變量的值是否是一個常量表達式。聲明為constexpr的變量一定是一個常量,而且必須用常量表達式初始化:

    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

    盡管不能使用普通函數作為constexpr變量的初始值,但是,將要介紹的,新標準允許定義一種特殊的constexpr函數。這種函數應該足夠簡單以使得編譯時就可以計算其結果,這樣就能用constexpr函數去初始化 constexpr變量了。

    一般來說,如果你認定變量是一個常量表達式,那就把它聲明成 constexpr類型。

    字面值類型

    常量表達式的值需要在編譯時就得到計算,因此對聲明constexpr時用到的類型必須有所限制。因為這些類型一般比較簡單,值也顯而易見、容易得到,就把它們稱為“字面值類型”( literal type)。

    到目前為止接觸過的數據類型中,算術類型、引用和指針都屬于字面值類型。

    自定義類sales_item、IO 庫、string 類型則不屬于字面值類型,也就不能被定義成constexpr。

    盡管指針和引用都能定義成constexpr,但它們的初始值卻受到嚴格限制。一個constexpr指針的初始值必須是nullptr或者0,或者是存儲于某個固定地址中的對象。

    將要提到,函數體內定義的變量一般來說并非存放在固定地址中,因此constexpr指針不能指向這樣的變量。相反的,定義于所有函數體之外的對象其地址固定不變,能用來初始化constexpr指針。

    將提到,允許函數定義一類有效范圍超出函數本身的變量,這類變量和定義在函數體之外的變量一樣也有固定地址。因此,constexpr引用能綁定到這樣的變量上,constexpr 指針也能指向這樣的變量。

    (Note:哪些定義constexpr,哪些不能定義constexpr)

    指針和constexpr

    必須明確一點,在constexpr聲明中如果定義了一個指針,限定符constexpr僅對指針有效,與指針所指的對象無關:

    const int *p = nullptr; // p is a pointer to a const int constexpr int *q = nullptr; // q is a const pointer to int

    p和q的類型相差甚遠,p是一個指向常量的指針,而q是一個常量指針,其中的關鍵在于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

    處理類型

    隨著程序越來越復雜,程序中用到的類型也越來越復雜,這種復雜性體現在兩個方面。

  • 一些類型難于“拼寫”,它們的名字既難記又容易寫錯,還無法明確體現其真實目的和含義。
  • 有時候根本搞不清到底需要的類型是什么,程序員不得不回過頭去從程序的上下文中尋求幫助。
  • 類型別名

    類型別名(type alias)是一個名字,它是某種類型的同義詞。使用類型別名有很多好處,它讓復雜的類型名字變得簡單明了、易于理解和使用,還有助于程序員清楚地知道使用該類型的真實目的。

    有兩種方法可用于定義類型別名。傳統的方法是使用關鍵字typedef

    typedef double wages; // wages is a synonym for double typedef wages base, *p; // base is a synonym for double, p for double*

    其中,關鍵字typedef作為聲明語句中的基本數據類型的一部分出現。含有 typedef 的聲明語句定義的不再是變量而是類型別名。和以前的聲明語句一樣,這里的聲明符也可以包含類型修飾,從而也能由基本數據類型構造出復合類型來。

    C++11新標準規定了一種新的方法,使用別名聲明(alias declaration)來定義類型的別名:

    using SI = Sales_item; // SI is a synonym for Sales_item

    這種方法用關鍵字using 作為別名聲明的開始,其后緊跟別名和等號,其作用是把等號左側的名字規定成等號右側類型的別名。

    類型別名和類型的名字等價,只要是類型的名字能出現的地方,就能使用類型別名:

    wages hourly, weekly; // same as double hourly, weekly; SI item; // same as Sales_item item

    指針、常量和類型別名

    如果某個類型別名指代的是復合類型或常量,那么把它用到聲明語句里就會產生意想不到的后果。例如下面的聲明語句用到了類型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//從指向指針的指針

    上述兩條聲明語句的基本數據類型都是const pstring,和過去一樣,const是對給定類型的修飾。pstring 實際上是指向char 的指針,因此,const pstring 就是指向char的常量指針,而非指向常量字符的指針。

    遇到一條使用了類型別名的聲明語句時,人們往往會錯誤地嘗試把類型別名替換成它本來的樣子,以理解該語句的含義:(Note:不能像代數那樣代入

    const char *cstr = 0; // wrong interpretation of const pstring cstr

    再強調一遍:這種理解是錯誤的。

    聲明語句中用到pstring 時,其基本數據類型是指針??墒怯胏har*重寫了聲明語句后,數據類型就變成了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.

    這樣改寫的結果是,const char成了基本數據類型。前后兩種聲明含義截然不同,前者聲明了一個指向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新特性

    編程時常常需要把表達式的值賦給變量,這就要求在聲明變量的時候清楚地知道表達式的類型。然而要做到這一點并非那么容易,有時甚至根本做不到。為了解決這個問題,C++11新標準引入了auto類型說明符,用它就能讓編譯器替我們去分析表達式所屬的類型。和原來那些只對應一種特定類型的說明符(比如 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

    此處編譯器將根據val1和val2相加的結果來推斷item的類型。如果val1和val2是類Sales _item(具體查看上一章)的對象,則item的類型就是Sales_item;如果這兩個變量的類型是double,則item的類型就是double,以此類推。

    使用auto也能在一條語句中聲明多個變量。因為一條聲明語句只能有一個基本數據類型,所以該語句中所有變量的初始基本數據類型都必須一樣:

    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

    復合類型、常量和 auto

    編譯器推斷出來的auto類型有時候和初始值的類型并不完全一樣,編譯器會適當地改變結果類型使其更符合初始化規則

    首先,正如我們所熟知的,使用引用其實是使用引用的對象,特別是當引用被用作初始值時,真正參與初始化的其實是引用對象的值。此時編譯器以引用對象的類型作為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則會保留下來,比如當初始值是一個指向常量的指針時:

    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

    還可以將引用的類型設為auto,此時原來的初始化規則仍然適用:

    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

    設置一個類型為auto的引用時,初始值中的頂層常量屬性仍然保留。和往常一樣,如果我們給初始值綁定一個引用,則此時的常量就不是頂層常量了。

    要在一條語句中定義多個變量,切記,符號&和*只從屬于某個聲明符,而非基本數據類型的一部分,因此初始值必須是同一種類型:

    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:符號&和*只從屬于某個聲明符,而非基本數據類型的一部分,這一句很重要)。

    decltype類型指示符

    C++11新標準

    有時會遇到這種情況:希望從表達式的類型推斷出要定義的變量的類型,但是不想用該表達式的值初始化變量。為了滿足這一要求,C++11新標準引入了第二種類型說明符decltype,它的作用是選擇并返回操作數的數據類型。在此過程中,編譯器分析表達式并得到它的類型,卻不實際計算表達式的值:

    decltype(f()) sum = x; // sum has whatever type f returns

    編譯器并不實際調用函數f,而是使用當調用發生時f的返回值類型作為sum的類型。換句話說,編譯器為sum 指定的類型是什么呢?就是假如f被調用的話將會返回的那個類型。

    decltype處理頂層const和引用的方式與auto有些許不同。如果decltype使用的表達式是一個變量,則 decltype返回該變量的類型(包括頂層const和引用在內):

    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)的結果就是引用類型,因此作為引用的z必須被初始化。

    需要指出的是,引用從來都作為其所指對象的同義詞出現,只有用在 decltype 處是一個例外。

    decltype和引用

    如果decltype使用的表達式不是一個變量,則decltype返回表達式結果對應的類型。有些表達式將向decltype返回一個引用類型。一般來說當這種情況發生時,意味著該表達式的結果對象能作為一條賦值語句的左值:

    // 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?的結果是引用類型。如果想讓結果類型是r所指的類型,可以把r作為表達式的一部分,如r+0,顯然這個表達式的結果將是一個具體值而非一個引用。

    另一方面,如果表達式的內容是解引用操作,則decltype 將得到引用類型。正如我們所熟悉的那樣,解引用指針可以得到指針所指的對象,而且還能給這個對象賦值。因此,decltype (*p)的結果類型就是int&,而非int。

    decltype和 auto的另一處重要區別是,decltype的結果類型與表達式形式密切相關。有一種情況需要特別注意:對于 decltype所用的表達式來說,如果變量名加上了一對括號,則得到的類型與不加括號時會有不同。

    • 如果 decltype使用的是一個不加括號的變量,則得到的結果就是該變量的類型;
    • 如果給變量加上了一層或多層括號,編譯器就會把它當成是一個表達式。

    變量是一種可以作為賦值語句左值的特殊表達式,所以這樣的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))(注意是雙層括號)的結果永遠是引用,而decltype(variable)結果只有當 variable本身就是一個引用時才是引用。

    自定義數據結構

    從最基本的層面理解,數據結構是把一組相關的數據元素組織起來然后使用它們的策略和方法。

    舉一個例子,我們的Sales_item類把書本的ISBN編號、售出量及銷售收入等數據組織在了一起,并且提供諸如isbn函數、>>、<<、+、+=等運算在內的一系列操作,sales_item類就是一個數據結構。

    C++語言允許用戶以類的形式自定義數據類型,而庫類型string、 istream、ostream等也都是以類的形式定義的,就像上一章Sales_item類型一樣。

    定義Sales_data類型

    盡管我們還寫不出完整的Sales_item類,但是可以嘗試著把那些數據元素組織到一起形成一個簡單點兒的類。初步的想法是用戶能直接訪問其中的數據元素,也能實現一些基本的操作。

    既然我們籌劃的這個數據結構不帶有任何運算功能,不妨把它命名為 Sales_data以示與Sales_item的區別。Sales_data初步定義如下:

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

    我們的類以關鍵字struct開始,緊跟著類名和類體(其中類體部分可以為空)。類體由花括號包圍形成了一個新的作用域。類內部定義的名字必須唯一,但是可以與類外部定義的名字重復。

    類體右側的表示結束的花括號后必須寫一個分號,這是因為類體后面可以緊跟變量名以示對該類型對象的定義,所以分號必不可少:

    struct Sales_data { /* ... */ } accum, trans, *salesptr; // equivalent, but better way to define these objects struct Sales_data { /* ... */ }; Sales_data accum, trans, *salesptr;

    分號表示聲明符(通常為空)的結束。一般來說,最好不要把對象的定義和類的定義放在一起。這么做無異于把兩種不同實體的定義混在了一條語句里,一會兒定義類,一會兒又定義變量,顯然這是一種不被建議的行為。

    很多新手程序員經常忘了在類定義的最后加上分號。

    類數據成員

    類體定義類的成員,我們的類只有數據成員(data member)。類的數據成員定義了類的對象的具體內容,每個對象有自己的一份數據成員拷貝。修改一個對象的數據成員,不會影響其他Sales_data的對象。

    定義數據成員的方法和定義普通變量一樣:首先說明一個基本類型,隨后緊跟一個或多個聲明符。我們的類有3個數據成員:

  • 一個名為bookNo的string 成員、
  • 一個名為units_sold的unsigned 成員
  • 一個名為revenue的 double 成員。
  • 每個Sales_data的對象都將包括這3個數據成員。

    C++11新標準規定,可以為數據成員提供一個類內初始值(in-class initializer)。創建對象時,類內初始值將用于初始化數據成員。沒有初始值的成員將被默認初始化(函數體內的默認不初始化,函數體外的都默認初始化)。因此當定義Sales_data的對象時,units_sold和revenue都將初始化為0,bookNo將初始化為空字符串。

    用戶可以使用C++語言提供的另外一個關鍵字class來定義自己的數據結構,到時也將說明現在我們使用struct 的原因?,F在使用struct定義自己的數據類型。

    和Sales_item類不同的是,我們自定義的sales_data類沒有提供任何操作,sales_data類的使用者如果想執行什么操作就必須自己動手實現。例如,寫一段程序實現求兩次交易相加結果的功能。程序的輸入是下面這兩條交易記錄:

    0-201-78345-x 3 20.00 0-201-78345-x 2 25.00

    每筆交易記錄著圖書的ISBN編號、售出數量和售出單價。

    使用Sales_data類

    因為sales_data類沒有提供任何操作,所以我們必須自己編碼實現輸入、輸出和相加的功能。假設已知Sales_data類定義于Sales_data.h文件內。

    #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對象讀入數據

    在此之前,我們先了解一點兒關于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 }

    編寫自己的頭文件

    函數體內定義類(先了解一下),但是這樣的類畢競受到了一些限制。所以,類一般都不定義在函數體內。當在函數體外部定義類時,在各個指定的源文件中可能只有一處該類的定義。而且,如果要在不同文件中使用同一個類,類的定義就必須保持一致。

    為了確保各個文件中類的定義一致,類通常被定義在頭文件中,而且類所在頭文件的名字應與類的名字一樣。例如,庫類型string在名為string 的頭文件中定義。又如,我們應該把Sales_data類定義在名為sales_data.h 的頭文件中。

    頭文件通常包含那些只能被定義一次的實體,如類、const和 constexpr變量等。

    頭文件也經常用到其他頭文件的功能。

    例如,我們的Sales_data類包含有一個string 成員,所以Sales_data.h必須包含string.h頭文件。同時,使用sales_data類的程序為了能操作bookNo成員需要再一次包含string.h頭文件。

    這樣,事實上使用sales_data類的程序就先后兩次包含了string.h頭文件:一次是直接包含的,另有一次是隨著包含sales_data.h 被隱式地包含進來的。有必要在書寫頭文件時做適當處理,使其遇到多次包含的情況也能安全和正常地工作。

    頭文件一旦改變,相關的源文件必須重新編譯以獲取更新過的聲明。

    預處理器

    確保頭文件多次包含仍能安全工作的常用技術是預處理器(preprocessor),它由C++語言從C語言繼承而來。預處理器是在編譯之前執行的一段程序,可以部分地改變我們所寫的程序。之前已經用到了一項預處理功能#include,當預處理器看到#include標記時就會用指定的頭文件的內容代替#include。

    C++程序還會用到的一項預處理功能是頭文件保護符(header guard),頭文件保護符依賴于預處理變量。預處理變量有兩種狀態:已定義和未定義。

    • #define指令把一個名字設定為預處理變量,

    另外兩個指令則分別檢查某個指定的預處理變量是否已經定義:

    • #ifdef當且僅當變量已定義時為真,
    • #ifndef當且僅當變量未定義時為真。

    一旦檢查結果為真,則執行后續操作直至遇到 #endif 指令為止。

    使用這些功能就能有效地防止重復包含的發生

    #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的檢查結果為真,預處理器將順序執行后面的操作直至遇到#endif 為止。此時,預處理變量SALES_DATA_H的值將變為已定義,而且sales_data. h也會被拷貝到我們的程序中來。

    后面如果再一次包含sales_data.h,則#ifndef 的檢查結果將為假,編譯器將忽略#ifndef到#endif之間的部分。

    預處理變量無視C++語言中關于作用域的規則。

    整個程序中的預處理變量包括頭文件保護符必須唯一,通常的做法是基于頭文件中類的名字來構建保護符的名字,以確保其唯一性。為了避免與程序中的其他實體發生名字沖突,一般把預處理變量的名字全部大寫。

    頭文件即使(目前還)沒有被包含在任何其他頭文件中,也應該設置保護符。頭文件保護符很簡單,程序員只要習慣性地加上就可以了,沒必要太在乎你的程序到底需不需要。

    (Note:日后寫頭文件都要設置保護符

    總結

    以上是生活随笔為你收集整理的《C++ Primer 5th》笔记(2 / 19):变量和基本类型的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。