1. 程式人生 > >第04章 複合型別

第04章 複合型別

本章內容包括:

  • 建立和使用陣列
  • 建立和使用C-風格字串
  • 建立和使用string類字串.
  • 使用方法getline()和get()讀取字串.
  • 混合輸入字串和數字.
  • 建立和使用結構.
  • 建立和使用共用體.
  • 建立和使用指標.
  • 使用new和delete管理動態記憶體.
  • 建立動態陣列.
  • 建立動態結構.
  • 自動儲存,靜態儲存和動態儲存.
  • vector和array類簡介.

4.1 陣列

  • 宣告陣列的通用格式:typeName arrayName[arraySize];
  • C++陣列從0開始編號.
  • 有效下標值的重要性:編譯器不會檢查使用的下標是否有效.

4.1.1 程式說明:程式清單4.1 arrayone.cpp 
4.1.2 陣列的初始化規則

  • 只有在定義陣列時才能使用初始化,此後就不能使用了,也不能將一個數組賦給另一個數組.
  • 通常,讓編譯器計算元素個數是種很糟的做法,因為其計數可能與您想象的不一樣.

4.1.3 C++11陣列初始化方法

  • 首先,初始化陣列時,可省略等號(=);
  • 其次,可不在大括號內包含任何東西,這將把所有元素都設定為零. 
    列表初始化禁止縮窄轉換.

4.2 字串

  • 讓陣列比字串長沒有什麼害處,只是會浪費一些空間而已.這是因為處理字串的函式根據空字元的位置,而不是陣列長度來進行處理.
  • C++對字串長度沒有限制.
  • 警告:在確定儲存字串所需的最短陣列時,別忘了將結尾的空字元計算在內.

4.2.1 拼接字串常量 
4.2.2 在陣列中使用字串

  • sizeof運算子指出整個陣列的長度:xx個位元組,但strlen()函式返回的是儲存在陣列中的字串的長度,而不是陣列本身的長度.另外,strlen()只計算可見的字元,而不把空字元計算在內.

4.2.3 字串輸入 
4.2.4 每次讀取一行字串輸入

  • 1.面向行的輸入:getline()
  • 2.面向行的輸入:get()
  • 為什麼要使用get(),而不是getline()呢?首先,老式實現沒有getline().其次,get()使輸入更仔細.總之,getline()使用起來簡單一些,但get()使得檢查錯誤更簡單些.
  • 3.空行和其他問題

4.2.5 混合輸入字串和數字

  • 程式清單4.6 numstr.cpp
  • C++程式常使用指標(而不是陣列)來處理字串.

4.3 string類簡介

  • ISO/ANSI C++98標準通過新增string類擴充套件了C++庫,因此現在可以string型別的變數(使用C++的話說是物件)而不是字元陣列來儲存字串.
  • 程式清單4.7 strtype1.cpp
  • 在很多方面,使用string物件的方式與使用字元陣列相同. 
    • 可以使用C風格字串來初始化string物件
    • 可以使用cin來將鍵盤輸入儲存到string物件中.
    • 可以使用cout來現實string物件
    • 可以使用陣列表示法來訪問儲存在string物件中的字元.
  • string物件和字元陣列之間的主要區別是,可以將string物件宣告為簡單變數,而不是陣列.

4.3.1 C++11字串初始化

  • C++11也允許將列表初始化用於C風格字串和string物件.

4.3.2 賦值,拼接和附加

  • 程式清單4.8 strtype2.cpp

4.3.3 string類的其他操作

  • 程式清單4.9 strtype3.cpp

4.3.4 string類I/O

  • 程式清單4.10 strtype4.cpp 
    • 首先,為初始化的陣列的內容是未定義的;對於未被初始化的資料,第一個空字元的出現位置是隨機的.
    • 其次,函式strlen()從陣列的第一個元素開始計算位元組數,直到遇到空字元.
  • 為何一個getline()是istream的類方法?在引入string類之前很久,C++就有istream類.因此istream的設計考慮到了注入double和int等基本C++資料型別,但沒有考慮string型別,所以istream類中,有處理double,int和其他基本型別的類方法,但沒有處理string物件的類方法.

4.3.5 其他形式的字串字面值

  • C++11還支援Unicode字元編碼方案UTF-8.在這種方案中,根據編碼的數字值,字元可能儲存為1~4個八位組.C++使用字首u8來表示這種型別的字串字面值.
  • C++11新增的另一種型別是原始(raw)字串.在原始字串中,字元表示的就是自己.原始字串將”(“和”)”用作定界符,冰食用字首R來標識原始字串.輸入原始字串時,按回車鍵不僅會移到下一行,還將在原始字串中添加回車字元.
  • 如果要在原始字串中包含)”,該如何辦呢?使用R”+(標識原始字元攢的開頭時,必須使用)+“標識原始字串的結尾.總之,這使用”+(和)+“替代了預設定界符”(和)”.

4.4 結構簡介

  • 結構也是C++ OOP堡壘(類)的基石.
  • C++允許在宣告結構變數時省略關鍵字struct.

4.4.1 在程式中使用結構

  • 結構宣告的位置很重要.
  • 外部宣告可以被其後面的任何函式使用,而內部宣告只能被該宣告所數的函式使用.通常應使用外部宣告,這樣所有函式都可以使用這種型別的結構.
  • 變數也可以在函式內部和外部定義,外部變數由所有的函式共享.C++不提倡使用外部變數,但提倡使用外部結構宣告.
  • 另外,在外部宣告符號常量通常更合理.

4.4.2 C++11結構初始化

  • 與陣列一樣,C++11也支援將列表初始化用於結構,且等號(=)是可選的.最後,不允許縮窄轉換.

4.4.3 結構可以將string類作為成員嗎?可以 
4.4.4 其他結構屬性

  • C++使使用者定義的型別與內建型別儘可能相似.
  • 還可以使用賦值運算子(=)將結構賦給另一個同類型的結構,這樣結構中每個成員都將被設定為另一個結構中相應成員的值,即使成員是陣列.這種賦值被稱為成員賦值.
  • 程式清單4.12 assgn_st.cpp

4.4.5 結構陣列

  • 程式清單4.13 arrstruct.cpp

4.4.6 結構中的位欄位

  • 與C語言一樣,C++也允許制定佔用特定位數的結構成員,這使得建立與某個硬體裝置上的暫存器對應的資料結構非常方便.欄位的型別應為整型或列舉,接下來是冒號,冒號後面是一個數字,它制定了使用的位數.可以使用沒有名稱的欄位來提供間距.每個成員都被稱為位欄位.
  • 位欄位通常用在低階程式設計中.

4.5 共用體

  • 共用體union是一種資料格式,它能夠儲存不同的資料型別,但只能同時儲存其中的一種型別.
  • 共用體的長度為其最大成員的長度.
  • 共用體常用語(但並非只能用於)節省記憶體.

4.6 列舉*

  • C++的enum工具提供了另一種建立符號常量的方式,這種方式可以代替const.
  • 對於列舉,制定已了賦值運算子.具體地說,沒有為列舉定義算術運算.
  • 如果打算只適用常量,而不建立列舉型別的變數,則可以省略列舉型別的名稱.

4.6.1 設定列舉量的值

  • 在C++早期的版本種,只能將int值(或提升為int的值)賦給列舉量,但這種限制取消了,因此可以使用long甚至long long型別的值

4.6.2 列舉的取值範圍

  • 取值範圍的定義: 
    • 首先,要找出上限,需要指導列舉量的最大值.
    • 找到大於這個最大值的,最小的2的冪,將它減去1,得到的便是取值範圍的上限.
  • 選擇用多少空間來儲存列舉由編譯器決定.
  • C++11擴充套件了列舉,增加了作用域內列舉.

4.7 指標和自由儲存空間

  • 使用常規變數時,值是指定的量,而地址為派生量.
  • 指標與C++基本原理 
    • 面向物件程式設計與傳統的過程性程式設計的區別在於,OOP強調的是在執行階段(而不是編譯階段)進行決策.執行階段指的是程式正在執行時,編譯階段指的是編譯器將程式組合起來時.執行階段決策就好比度假時,選擇參觀哪些景點取決於天氣和當時的心情;而編譯階段決策更像不管在什麼條件下,都堅持預先設定的日程安排.
    • 執行階段決策提供了靈活性,可以根據當時的情況進行調整.例如,考慮為陣列分配記憶體的情況.傳統的方法是宣告一個數組.要在C++中宣告陣列,必須制定陣列的長度.因此,陣列長度在程式編譯時就設定好了;這就是編譯階段決策.您可能認為,在80%的情況下,一個包含20個元素的陣列足夠了,但程式有時需要處理200個元素.為了安全起見,使用了一個包含200個元素的陣列.這樣,程式在大多數情況下都浪費了記憶體.OOP通過將這樣的決策推遲到執行階段進行,使程式更靈活.在程式執行後,可以這次高速它只需要20個元素,而還可以下次高速它需要205個元素.
    • 總之,使用OOP時,您可能在執行階段確定陣列的長度.為使用這種方法,語言必須允許在程式執行時建立陣列.稍後您會看到,C++採用的方法是,使用關鍵字new情況正確數量的記憶體以及使用指標來跟蹤新分配的記憶體的位置.
    • 在執行階段做決策並非OOP獨有的,但使用C++編寫這樣的程式碼比使用C語言簡單.
  • 程式清單4.15 pointer.cpp

4.7.1 宣告和初始化指標

  • 指標宣告必須制定指標指向的資料的型別.
  • *運算子兩邊的空格是可選的.傳統上,C程式設計師使用這種格式:int ptr;這強調*ptr是一個int型別的值.而很多C++程式設計師使用這種格式:int ptr;這強調的是:int*是一種型別–指向int的指標.
  • int* p1,p2;宣告建立一個指標p1和一個int變數;對每個指標變數名,都需要使用一個*
  • 注意:在C++中,int*是一種符合型別,是指向int的指標.
  • 和陣列一樣,指標都是基於其他型別的.
  • 程式清單4.16 init_ptr.cpp

4.7.2 指標的危險

  • 極其重要的一點是:在C++中建立指標時,計算機將分配用來儲存地址的記憶體,但不會分配用來儲存指標所指向的資料的記憶體.為資料提供空間是一個獨立的步驟,忽略這一步無疑是自找麻煩.
  • 警告:一定要在對指標應用接觸引用運算子(*)之前,將指標初始化為一個確定的,適當的地址.這是關於使用指標的金科玉律.

4.7.3 指標和數字 
4.7.4 使用new來分配記憶體

  • 指標真正的用武之地在於,在執行階段分配未命名的記憶體以儲存值.在這種情況下,只能通過指標來訪問記憶體.
  • 在C語言中,可以用庫函式malloc()來分配記憶體;在C++中仍然可以這樣做,但C++還有更好的方法-new運算子.
  • 為一個數據物件(可以是結構,也可以是基本型別)獲得並制定分配記憶體的通用格式:typeName * pointer_name = new typeName;
  • 程式清單4.17 use_new.cpp 
    • 對於指標,需要指出的是,new分配的記憶體塊通常與常規變數宣告分配的記憶體塊不同.變數nights和pd的值都儲存在被稱為棧stack的記憶體區域中,而new從被稱為堆heap或自由儲存區free store的記憶體區域分配記憶體.
    • 記憶體被耗盡?計算機可能會由於沒有足夠的記憶體而無法滿足new的請求.在這種情況下,new通常會引發異常;而在較老的實現中,new將返回0.在C++中,值為0的指標被稱為空指標null pointer.C++確保空指標不會指向有效的資料,因此它常被用來表示運算子或函式失敗(如果成功,他們將返回一個有用的指標).就目前而言,只需如下要點:C++提供了檢測並處理記憶體分配失敗的工具.

4.7.5 使用delete釋放記憶體

  • 一定要配對地使用new和delete;否則將發生記憶體洩漏.
  • 不要嘗試釋放已經釋放的記憶體快,C++標準指出,這樣做的結果將是不確定的,這意味著什麼情況都可能發生.另外,不能使用delete來釋放宣告變數所獲得的記憶體.
  • 警告:只能用delete來釋放使用new分配的記憶體.然而,對空指標使用delete是安全的.
  • 注意,使用delete的關鍵在於,將它用於new分配的記憶體.這並不意味著要使用用於new的指標,而是用於new的地址.
  • 一般來說,不要建立兩個指向同一個記憶體塊的指標,因為這將增加錯誤地刪除同一個記憶體塊兩次的可能性.

4.7.6 使用new來建立動態陣列

  • 通常,對於大型資料(如陣列,字串和結構),應使用new,這正是new的用武之地.
  • 關於動態陣列的兩個基本問題:如何使用C++的new運算子建立陣列以及如何使用指標訪問陣列元素. 
    • 1.使用new建立動態陣列 
      • C++早起版本無法識別方括號表示法.然而,對於ANSI/ISO標準來說,new與delete的格式不匹配導致的後果是不確定的,這意味著程式設計師不能依賴於某種特定的行為.
      • 使用new和delete時,應遵守以下規則: 
        • 不要使用delete來釋放不是new分配的記憶體.
        • 不要使用delete釋放同一個記憶體塊兩次
        • 如果使用new[]為陣列分配記憶體,則應使用delete[]來釋放.
        • 如果使用new[]為一個實體分配記憶體,則應使用delete(沒有方括號)來釋放.
        • 對空指標應用delete是安全的.
      • 為陣列分配記憶體的通用格式如下:type_name*pointer_name = new type_name [num_elements];
    • 2.使用動態陣列 
      • C和C++內部都使用指標來處理陣列.陣列和指標基本等價是C和C++的優點之一(這在有時候也是個問題,但這是另一碼事).
      • 程式清單4.18 arraynew.cpp 
        • 不能修改陣列名的值.但指標是變數,因此可以修改它的值.

4.8 指標,陣列和指標算術

  • 指標和陣列基本等價的原因在於指標算術pointer arithmetic和C++內部處理陣列的方式.
  • C++將陣列名解釋為地址.
  • 程式清單4.19 addpntrs.cpp

4.8.1 程式說明

  • 注意:將指標變數加1後,其增加的值等於指向的型別佔用的位元組數.
  • 對陣列應用sizeof運算子得到的是陣列的長度,而對指標應用sizeof得到的是指標的長度,即使指標指向的是一個數組.
  • 陣列的地址:對陣列取地址時,陣列名也不會被解釋為其地址.等等,陣列名難道不被解釋為陣列的地址嗎?不完全如此:陣列名被解釋為其第一個元素的地址,而對陣列名應用地址運算子時,得到的是整個陣列的地址.
  • 總之,使用new來建立陣列以及使用指標來訪問不同的元素很簡單.只要把指標當做陣列名對待即可.然而,要理解為何可以這樣做,將是一種挑戰.

4.8.2 指標小結

  1. 宣告指標 
    • 要宣告指向特定型別的指標,格式:typeName * pointerName;
  2. 給指標賦值 
    • 應將記憶體地址賦給指標.
    • 可以對變數名應用&運算子,來獲得被命名的記憶體的地址,new運算子返回未命名的記憶體的地址.
  3. 對指標解除引用 
    • 對指標解除應用意味著獲得指標指向的值.對指標應用解除引用或間接值運算子(*)來解除應用.
    • 另一種對指標解除引用的方法是使用陣列表示法,例如,pn[0]與*pn是一樣的.絕不要對未被初始化為適當地址的指標解除引用.
  4. 區分指標和指標所指向的值
  5. 陣列名 
    • 在多數情況下,C++將陣列名視為陣列的第一個元素的地址.一種例外情況是:將sizeof運算子用於陣列名用時,此時將返回整個陣列的長度(單位為位元組).
  6. 指標算術
  7. 陣列的動態聯編和靜態聯編 
    • 使用陣列宣告來建立陣列時,將採用靜態聯編,即陣列的長度在編譯時設定
    • 使用new[]運算子建立陣列時,將採用動態聯編(動態陣列),即將在執行時為陣列分配空間,其長度也將在執行時設定.使用完這種陣列後,應使用delete[]釋放其佔用的記憶體.
  8. 陣列表示法和指標表示法 
    • 使用方括號陣列表示法等同於對指標解除引用.

4.8.3 指標和字串

  • 陣列和指標的特殊關係可以擴充套件到C風格字串.
  • 在C++中,用引號括起的字串像陣列名一樣,也是第一個元素的地址.
  • 注意:在cout和多數C++表示式中,char陣列名,char指標以及用引號括起的字串常量都被解釋為字串第一個字元的地址.
  • 程式清單4.20 ptrstr.cpp 
    • 警告:在將字串讀入程式時,應使用已分配的記憶體地址.該地址可以是陣列名,也可以四使用new初始化過的指標.
    • 警告:應使用strcpy()或strncpy(),而不是賦值運算子來將字串賦給陣列.

4.8.4 使用new建立動態結構

  • 建立動態結構時,不能將成員運算子句點用於結構名,因為這種結構沒有名稱,知識指導它的地址.C++專門為這種情況提供了一個運算子:箭頭成員運算子(->).
  • 提示:有時,C++新手在制定結構成員時,搞不清楚何時應使用句點運算子,何時應使用箭頭運算子.規則非常簡單.如果結構識別符號是結構名,則使用句點運算子;如果識別符號是指向結構的指標,則使用箭頭運算子.
  • C++的運算子優先規則要求使用括號,如:(*ps).price
  • 程式清單4.21 newstrct.cpp
  • 1.一個使用new和delete的示例 
    • 程式清單4.22 delete.cpp
  • 2.程式說明 
    • 將new和delete放在不同的函式中通常並不是個好方法,因為這樣很容易忘記使用delete.

4.8.5 自動儲存,靜態儲存和動態儲存

  • 根據用於分配記憶體的方法,C++有3種管理資料記憶體的方式:自動儲存,靜態儲存和動態儲存(有時也叫作自由儲存空間或堆).(C++11新增了第四種類型—執行緒儲存).
  • 1.自動儲存 
    • 在函式內部定義的常規變數使用自動儲存空間,被稱為自動變數,這意味著它們在所屬的函式被呼叫時自動產生,在該函式結束時消亡.
    • 實際上,自動變數是一個區域性變數,其作用域為包含它的程式碼塊.
    • 自動變數通常儲存在棧中.這意味著執行程式碼塊時,其中的變數將依次加入到棧中,而在離開程式碼塊時,將按相反的順序釋放這些變數,這被稱為後進先出LIFO.因此,在程式執行過程中,棧將不斷的增大和縮小.
  • 2.靜態儲存 
    • 使變數稱為靜態的方式有兩種:一種是在函式外面定義它;另一種是在宣告變數時使用關鍵字static.
  • 3.動態儲存 
    • 棧,堆和記憶體洩漏:如果使用new運算子在自由儲存空間(或堆)上建立變數後,沒有呼叫delete,將發生什麼情況呢?如果沒有呼叫delete,則即使包含指標的記憶體由於作用域規則和物件宣告週期的原因而被釋放,在自由儲存空間上動態分配的變數或結構也將繼續存在.實際上,將會無法訪問自由儲存空間中的結構,因為指向這些記憶體的指標無效.這將導致記憶體洩漏.被洩漏的記憶體將在程式的整個證明週期內都不可使用;這些記憶體被分配出去,但無法收回.極端情況(不過不常見)是,記憶體洩漏可能會非常嚴重,以致於應用程式可用的記憶體被耗盡,出現記憶體耗盡錯誤,導致程式崩潰.另外,這種洩漏還會給一些作業系統或在相同的記憶體空間中執行的應用程式帶來負面影響,導致他們崩潰.即使是最好的程式設計師和軟體公司,也可能導致記憶體洩漏.要避免記憶體洩漏,最好是養成這樣一種習慣,即同時使用new和delete運算子,在自由儲存空間上動態分配記憶體,隨後便釋放它.C++智慧指標有助於自動完成這種任務.
    • 注意:指標是功能最強大的C++工具之一,但也最危險,因為他們與女婿執行對計算機不友好的操作,如使用未經初始化的指標來訪問記憶體或者試圖釋放同一個記憶體塊兩次.另外,在通過實踐習慣指標表示法和指標概念之前,指標是容易引起迷惑的.

4.9 型別組合

  • 程式清單4.23 mixtypes.cpp

4.10 陣列的替代品

  • 模板類vector和array是陣列的替代品.

4.10.1 模板類vector

  • 模板類vector類似於string類,也是一種動態陣列.基本上,它是使用new建立動態陣列的替代品.實際上,vector類確實使用new和delete來管理記憶體,但這種工作是自動完成的.
  • 一般而言,下面的宣告建立一個名為vt的vector物件,它可儲存n_elem個型別為typeName的元素:C++ vector<typeName> vt(n_elem);其中引數n_elem可以是整型常量,也可以是整型變數.

4.10.2 模板類array(C++11)

  • vector類的功能比陣列強大,但付出的代價是效率稍低.如果需要的是長度固定的陣列,使用陣列是更加的選擇,但代價是不那麼方便和安全.
  • 宣告建立一個名為arr的array物件,它包含n_elem個型別為typename的元素:C++ array<ttypeName>,n_elem> arr;與建立vector物件不同的是,n_elem不能是變數.
  • 在C++11中,可將列表初始化用於vector和array物件,但在C++98中,不能對vector物件這樣做.

4.10.3 比較陣列,vector物件和array物件

  • 程式清單4.24 choices.cpp
  • array物件和陣列儲存在相同的記憶體區域(即棧)中,而vector物件儲存在另一個區域(自由儲存區或堆)中.
  • 將一個array物件賦給另一個array物件;而對於陣列,必須逐元素賦值資料.
  • 與C語言一樣,C++也不檢查陣列下標越(超)界的錯誤.
  • vector和array物件的成員函式at();

4.11 總結

  • C++98新增的標準模板庫STL提供了模板類vector,它是動態陣列的替代品.C++11提供了模板類array,它是定長陣列的替代品.

4.12 複習題 
4.13 程式設計練習

附件:本章原始碼下載地址