C++ Primer Plus--複合型別(四)
複合型別介紹
4.1 陣列
陣列是一種資料格式,能過儲存多個同類型的值。例如,陣列可以儲存60個int型別的值。
建立陣列,可使用宣告語句,宣告輸入應指出以下三點:
- 儲存在每個元素種的值的型別
- 陣列名
- 陣列種的元素數
C++中,可以通過修改簡單變數的宣告,新增中括號來完成陣列宣告。例如,下面的宣告建立一個名為months的陣列,該陣列有12個元素,每個元素都可以儲存一個short型別的值:
short months[12]
事實上,可以將陣列中的每個元素看作一個簡單變數。宣告陣列的通用格式如下:
typename arrayName[arraySize];
表示式arraySize指定元素數目,它必須是整型或const值,也可以是常量表達式(如:8*sizeof(int)),即其中所有的值在編譯時都是已知的。具體說,arraySize不能時變數,變數的值時程式執行時設定。稍後,介紹如何使用new運算子來避免這種限制。
陣列之所以稱之為複合型別,是因為它是可以使用其他型別來建立的。不能僅僅將某種東西宣告為陣列,它必須是特定型別的陣列。沒有通用的陣列型別,當存在很多特定的陣列型別,如char型別或long陣列。例如:
float loans[20];
loans的型別不是“陣列”,而是“float陣列”。這強調loans陣列是使用float型別建立的。
陣列的很多用途都是基於這樣一個事實:可以單獨訪問陣列的元素。方法是使用下標或索引來對元素進行編號。C++陣列從0開始編號。C++使用帶索引的括號表示法來指定陣列的元素。例如,months[0]是months陣列的第一個元素,months[11]是最後一個元素。注意最後一個元素的索引比陣列長度小1。因此,陣列宣告能過使用一個宣告建立大量的變數,然後便可以用索引來標識和訪問各個元素。
有效下標的重要性:編譯器不會檢查使用的下標是否有效。但是程式執行後,這種賦值可能引發問題,他可能破壞資料或程式碼,也可能導致程式異常終止。
陣列初始化:
int y[3] = {0,1,2};
sizeof運算子返回型別或資料物件的長度(單位為位元組)。
sizeof(y);
4.1.1 陣列初始化
只有定義陣列時才能初始化,此後就不能使用了,也不能將一個數組賦給另一個數組:
int cards[5] = {3,6,8,10}; int hand[4]; hand[4] = {5,6,7,9}; //錯誤做法,hand[4] = 5;這樣是替換低4個元素的值為5 hand = cards; //錯誤語法
然而,可以使用下標分別給陣列中的元素賦值。
初始化陣列時,提供的值可以少於陣列的元素數目。例如,下面的語句只初始化hotelTips的前兩個元素:
float hotelTips[5] = {5.0, 2.5};
如果只對陣列的一部分進行初始化,則編譯器將把其他元素設定為0。將陣列中所有的元素初始化為0非常簡單,只要顯式地將第一個元素初始化0。然後編譯器將其他元素都初始化為0即可:
long totals[500] = {0};
如果初始化陣列方括號內([])為空,C++編譯器將計算元素個數,例如,對於下面的宣告:
short things[] = {1,5,3,8};
編譯器將使things陣列包含4個元素。
4.1.2 C++11陣列初始化方法
初始化時可以省略等號:
double earning[3] {100.1, 112.2,133.2};
可以在花括號中不包括任何內容,這將把所有元素都設定為零:
float blances[100] {};
列表初始化禁止縮窄轉換:
long plifs[] = {25, 3.0}; //錯誤語法,3.0為float無法轉換為long
char slifs[] = {'h', 'i', 1122011}; //最後一個元素太大,char無法容納,char變數的長度為8位
char tilfs[4] = {'h', 'i', 24}; //正確做法
4.2 字串
字串是儲存在記憶體的連續位元組中的一系列字元。C++處理字串的方式有兩種:
- 來自C語言,常被稱為C-風格字串
- 基於string類庫的方法
儲存在連續位元組中的一系列字元意味著可以將字串儲存在char陣列中,其中每個字元都位於自己的陣列元素中。C-風格字元具有一種特殊的性質:以空字元結尾,用字元被寫作\0,其ASCII碼為0,用來標記字串的結尾。例如:
char dog[4] = {'b','e','a','x'}; //不是一個字串
char cat[4] = {'f','s','a','\0'}; //是一個字串
這兩個陣列都是char陣列,但只有第二個陣列是字串。空字元對C-風格字串而言至關重要。例如,C++有很多處理字串的函式,其中包括cout使用的那些函式,它們都逐個地字串中的字元,直到到達一個空字元為止。如果使用cout顯式cat這樣的字串,則將前3個字元,發現空字元後停止。如果使用cout顯式上面的dog陣列(他不是字串),cout將打印出陣列中的4個字元,並接著將記憶體中隨後的各個位元組解釋為要列印的字元,直到遇到控制符為止。由於空字元(實際上是被設定為0的位元組)在記憶體中是常見的,因此這一過程很快就停止。
一種更好的、將字元陣列陣列初始化為字串的方法–只需使用一個引號括起來的字元即可,這種字串被稱為字串常量或字串字面值,如下:
char bird[11] = "Mr. Cheeps";
char fish[] = "Bubbles";
用引號括起的字串隱式地包括結尾的空字元,因此不用顯式地包括它。
C++輸入工具通過鍵盤輸入,將字串讀入到char陣列中時,將自動加上末尾的空字元。當然應確保陣列足夠大,能過儲存字串中的所有字元—包括空字元。
注意:在確定儲存字串所需的最短陣列時,別忘了將結尾的空字元計算在內。
字串常量(使用雙引號)不能與字元常量(使用單引號)互換。字元常量(如‘S’)是字串編碼的簡寫表示。在ASCII系統上,‘S’只是83的另一種寫法,因此,下面的語句將83賦給shirt_size:
char shirt_size = 'S';
但"S"不是字元常量,它表示的是兩個字元(字元S和\0)組成的字串。更糟糕的是,"S"實際上表示的是字串所在的記憶體地址。因此,下面的語句試圖將一個記憶體地址賦給shirt_size:
char shirt_size = "S";//錯誤的做法
由於地址在C++中是一種獨立的型別,因此編譯器不允許這種不合理的做法。
4.2.1 拼接字串常量
但字串很長無法放到一行時,C++允許拼接字串字面值,即將兩個用引號括起來的字串合併為一個。 事實上,任何兩個有空白(空格、製表符、換行符)分割的字串常量都將自動拼接成一個。下面語句等價:
cout << "I'd give my right arm to ba " "a great violinist.\n";
cout << "I'd give my right arm to ba a great violinist.\n";
cout << "I'd give my right ar"
"m to ba a great violinist.\n"
注意,拼接時不會再被連線的字串之間新增空格,第二個字串的第一個字元將緊接在第一個字串的最後一個字元(不考慮\0)後面。第一個字串中的\0會被第二個字串中的第一個字元取代。
4.2.2 在陣列中使用字串
將字串存段陣列中,常用的兩種方法:
- 將陣列初始化為字串常量
- 將鍵盤或檔案輸入讀入到陣列中
string.cpp
#include <cstring>
#include <iostream>
using namespace std;
int main()
{
const int Size = 15;
char name1[Size];
char name2[Size] = "C++owboy";
cout << "Howdy! I'm " << name2;
cout << "! What's your name? \n";
cin >> name1;
cout << "Well, " << name1 << endl;
cout << "Your name has " << strlen(name1) << " letters and is stored" << endl;
cout << "Your name has " << sizeof(name1) << " bytes. \n" ;
name2[3] = '\0'; //第四個字元設定為空字元,列印字串時,到此結束
cout << "First 3 characters of my name: " << name2 << endl;
return 0;
}
結果:Howdy! I’m C++owboy! What’s your name? zxphello Well, zxphello Your name has 8 letters and is stored Your name has 15 bytes. First 3 characters of my name: C++
sizeof運算子指出整個陣列的長度:15位元組;但strlen()函式返回的是儲存在陣列中的字串的長度,而不是陣列本身的長度。另外,strlen()只計算可見的字元,空字元不計算在內。如果儲存字串cosmic,陣列的長度不能短於strlen(comisc) + 1。
注意:使用符合常量表示陣列長度,當修改程式以使用不同陣列長度時,工作變得非常簡單。
4.2.3 字串輸入
程式inst1.cpp
#include <iostream>
using namespace std;
int main()
{
const int Size = 20;
char name[Size];
char dessert[Size];
cout << "Enter your name: \n";
cin >> name;
cout << "Enter your favorite dessert: \n";
cin >> dessert;
cout << name << " like "<< dessert << endl;
return 0;
}
結果:
[[email protected] ~]# ./a.out</br>
Enter your name: </br>
zxp</br>
Enter your favorite dessert:</br>
kk</br>
zxp like kk</br>
[[email protected] ~]# ./a.out</br>
Enter your name: </br>
zxp zxp1</br>
Enter your favorite dessert: </br>
zxp like zxp1</br>
對於第二種情況,我們還沒有對“輸入甜點的提示”做出反應,程式便他把顯示出來。
由於不能通過鍵盤輸入空字元,因此cin需要用別的方法來字串的結尾位置。cin使用空白(空格、製表符和換行符)來確定字串的結束位置,這意味著cin在獲取字元陣列輸入時只讀取一個單詞。讀取該單詞後,cin將該字串放到陣列中,並自動在結尾新增空字元。
這個例子的實際結果是,cin把zxp作為第一個字串,並將它放到name陣列中。把zxp1留在佇列中,當cin在輸入佇列中搜索使用者喜歡的甜點時,它發現了zxp1,因此cin讀取zxp1,並將它放到dessert陣列中。
4.2.4 每次讀取一行字串輸入
當程式要求使用者輸入城市名,使用者輸入New York,希望完整的儲存城市名,而不僅僅是New。
istream中的類(如cin)提供了面向行的類成員:getline()和get()。這兩個函式都讀取一行輸入,直到到達換行符。然而,隨後getline()將丟棄換行符,而get()將換行符保留在輸入序列中。
1、getline()
使用cin.getline()呼叫。該函式有兩個引數:
- 用來儲存輸入行的陣列的名稱引數
- 要讀取字元數的引數,getline()成員函式,在讀取指定數目的字元或遇到換行符時停止讀取。
例如,使用getline()將姓名讀取到一個包含20個元素的name陣列中:
cin.getline(name, 20);
如果一行讀入不超過19個字元,將全部讀取到name陣列中。
將instr1.cpp程式修改為使用cin.getline(),而不是簡單的cin。
#include <iostream>
using namespace std;
int main()
{
const int Size = 20;
char name[Size];
char dessert[Size];
cout << "Enter your name: \n";
cin.getline(name, Size);
cout << "Enter your favorite dessert: \n";
cin.getline(dessert, Size);
cout << name << " like "<< dessert << endl;
return 0;
}
結果:
[[email protected] ~]# ./a.out
Enter your name:
zxp zxp1
Enter your favorite dessert:
kk dd
zxp zxp1 like kk dd
該程式可以讀取完整的姓名和使用者喜歡的甜點。getline()函式每次讀取一行,通過換行符來確定行尾,但不儲存換行符。
2、get()
get()函式的引數跟getline()相同,解釋引數的方式也相同,並且都讀取到行尾。但get()不丟棄換行符,而是將其留在輸入佇列中。假設,連續兩次呼叫get():
cin.get(name, Arsize);
cin.get(dessert, Arsize);
由於第一次呼叫後,換行符還在輸入佇列中,因此第二次呼叫時看到的第一個字元便是換行符。因此get()預設已到達行尾,而沒有讀取到任何內容。
一種方法是通過使用get()的變體,使用不帶引數的cin.get()呼叫可讀取下一個字元(即使換行符),因此可以用它來處理換行符:
cin.get(name, Arsize);
cin.get()
cin.get(dessert, Arsize);
另一種使用get()的方式是將兩個類成員函式拼接起來(合併):
cin.get(name, Arsize).get();
這樣做,是由於cin.get(name, Arsize)返回一個cin物件,該物件隨後呼叫get()函式。
下面語句跟兩次呼叫getline()效果相同:
cin.getline(name1, Arsize).getline(name1,Arsize);
使用get()使輸入更仔細。例如,假設用get()將一行讀入到陣列中,如何直到停止的原因是由於已經讀取了整行,而不是由於陣列已填滿。檢視下一個輸入字元,如果是換行符,說面已讀取了整行,否則,說明該行中還有其他輸入。
3、空行和其他問題
當getline()和get()讀取空行時,最初的做法,下一條輸入語句將在前一條getline()或get()結束讀取的位置開始讀取。當前做法,當get()(而不是getline())讀取空行後,將設定失效位(failbit),這意味著接下來的輸入被阻斷,但可以使用如下命令來恢復輸入:
cin.clear()
另一個問題是:輸入字串可能比分配的空間長。如果輸入行包含的字元數比指定的多,則getline()和get()將把餘下的字元保留在佇列中,而getline()還會設定失效位,並關閉後面的輸入。
4.2.5 混合輸入字串和數字
numstr.cpp
#include <iostream>
using namespace std;
int main()
{
int year;
char address[80];
cout << "Enter year:\n" ;
cin >> year;
cout << "Enter address:\n";
// cin >> address;
cin.getline(address,80);
cout << "Year: " << year <<endl;
cout << "Address: " << address << endl;
return 0;
}
結果: Enter year: 1991 Enter address: Year: 1991 Address:
使用者根本沒有輸入地址。問題在於cin讀取年份,將回車生成的換行符留在的輸入佇列中。後面的cin.getline()看到換行符後,將認為是一個空行,並將一個空字串賦給address陣列。解決的辦法是:在讀取地址之前先讀取並丟棄換行符。具體方法是:
cin.get();//呼叫一個沒有引數的get()
或者 cin.get(ch); //呼叫一個接受引數的get()
或者 (cin >> year).get();
或者 (cin >> year).get(ch);
4.3 string類簡介
string型別的變數可以儲存字串,不是使用字元陣列的方式儲存。string類使用起來比陣列簡單,同時提供了將字串作為一種資料型別的表達方式。
使用string類,必須在程式中包含標頭檔案string。string類位於名稱空間std中,因此必須通告一條using編譯指令,或者使用std::string來引用它。string來定義隱藏了字串的陣列特性。
strtype1.cpp
#include <iostream>
#include <string>
using namespace std;
int main()
{
char ch1[20];
char ch2[20] = "jaguar";
string str1;
string str2 = "panther";
cout << "Enter a king of feline:\n";
cin >> ch1;
cout << "Enter another king of faline: \n";
cin >> str1;
cout << "ch1: " << ch1 << endl;
cout << "ch2: " << ch2 << endl;
cout << "str1: " << str1 << endl;
cout << "str2: " << str2 << endl;
return 0;
}
結果:Enter a king of feline: ocelot Enter another king of faline: tiger ch1: ocelot ch2: jaguar str1: tiger str2: panther
string類設計讓程式能夠處理string的大小。例如:str1的宣告建立一個長度為0的string物件,當程式將輸入讀取到str1中時,將自動調整str1的長度。這跟陣列相比,使用string更安全方便。
char陣列視為一組用於儲存一個字串的char儲存單元,而string類變數是一個表示字串的實體。
C++11字串初始化
C++11也允許將列表初始化用於字串和string物件:
char ch1[] = {"aaaa, 11"};
string str1 = {"bbb, 22"}
賦值、拼接和附加
使用string類時,一些操作比陣列更簡單。比如:不能將一個數組賦給另一個數組,但可以將一個string物件賦給另一個string物件。
char ch1[20];
char ch2[20] = "jaguar";
string str1;
string str2 = "panther";
ch1 = ch2;
str1 = str2;
#include <iostream>
#include <string>
using namespace std;
int main()
{
char ch1[20];
char ch2[20] = "jaguar";
string str1;
string str2 = "panther";
ch1 = ch2;//語法錯誤
str1 = str2;
cout << "ch1: " << ch1 << endl;
cout << "ch2: " << ch2 << endl;
cout << "str1: " << str1 << endl;
cout << "str2: " << str2 << endl;
return 0;
}
報錯: strtype1.cpp: 在函式‘int main()’中: strtype1.cpp:14:6: 錯誤:無效的陣列賦值 ch1 = ch2;
string類簡化了字串的合併操作。可以使用+運算子將兩個string物件合併,還可以將字串附加到string物件的末尾。即:
string str3;
str3 = str1 + str2;
str1 += str2;
string類的其他操作
C-風格字串一些常用函式(包含在cstring標頭檔案中):
- 字串賦值到字元陣列中:使用strcpy()函式
- 將字串附加到字元陣列末尾:使用strcat()函式
- 獲取陣列字串長度:strlen(ch1)
獲取string類字串長度: str1.size(),size()是一個類方法,只能通過所屬類的物件進行呼叫。
strtype3.cpp
#include <iostream>
#include <string>
#include <cstring>
using namespace std;
int main()
{
char ch1[20];
char ch2[20] = "jaguar";
int len1 = strlen(ch2);
string str1 = "12345";
int len2 = str1.size();
strcpy(ch1, ch2 );
strcat(ch1, "ccc");
cout << "ch1: " << ch1 << endl;
cout << "ch2: " << ch2 << endl;
cout << "len1: " << len1 << endl;
cout << "len2: " << len2 << endl;
return 0;
}
結果:
ch1: jaguarccc
ch2: jaguar
len1: 6
len2: 5
4.3.4 string類I/O
strtype2.cpp
#include <iostream>
#include <string>
#include <cstring>
using namespace std;
int main()
{
char ch1[20];
string str1;
cout << "ch1's length before input: " << strlen(ch1) << endl;
cout << "str's length before input: " << str1.size() << endl;
cout << "Enter a line of text: \n";
cin.getline(ch1,20);
cout << "Enter another line of text: \n";
getline(cin,str1);
cout << "ch1: " << ch1 << " it's length: " << strlen(ch1) << endl;
cout << "str1: " << str1 << " it's length: " << str1.size() << endl;
return 0;
}
結果:
ch1's length before input: 6
str's length before input: 0
Enter a line of text:
abc
Enter another line of text:
kkkk
ch1: abc it's length: 3
str1: kkkk it's length: 4
使用者輸入之前,指定了ch1的長度為20,而輸出為6,這是因為:
- 初始化的陣列的內容未定義
- 函式strlen()從陣列的第一個元素開始計算位元組數,直到遇到空字元
在本例中,在陣列中第6個位元組遇到空字元。對於未被初始化的資料,第一個空字元出現的位置是隨機的,也可能出現數組規定位元組外,這樣陣列的長度大於20。
getline(ch1, 20)是一個istream類的一個類方法,第一個引數為目標陣列,第二引數為陣列長度。
getline(cin, str1)表明getline()不是類方法(類方法使用句點表示法),它將cin作為引數,指出從哪裡查詢輸入。另外,沒有規定字串的長度,string物件會根據字串的長度自動調整大小。
getline()一個是istream的類方法,而另一個不是。在引入string之前,C++就有istream類,因此istrem設計考慮了int、double等資料型別,但沒有考慮string型別,所以沒有處理string物件的方法。但處理string物件的程式碼使用string類的一個友元函式。
4.3.5 其他形式的字串字面值
除char型別外,C++還有型別wchar_t,C++新增了char16_t和char32_t。可建立這些型別的陣列和這些型別的字串字面值。對於這些型別的字串字面值,C++分佈使用字首L、u和U來表示,如下:
wchar_t a[] = L"aaaa";
char16_t b[] = u"bbbb";
char32_t c[] = U"cccc";
C++11還支援Unicode字元編碼方案UTF-8。在這種方案中,根據編碼的數字值,字元可能儲存為1~4個八位組。C++使用字首u8來表示這種型別的字串字面值。
C++11還增加了另一種新型別是原始(raw)字串。在原始字串中,字元表示的就是自己。例如:\n不表示換行符,而是兩個常規的字元–斜槓和n。還有在字元中使用",不用"來表示。原始字串使用"(和)"來做定界符,並使用字首R來標識。
cout << R"(Jim "King" \n instead of endl.)" << '\n';
輸出為:
Jim "King" \n instead of endl.
原始字串的界定符還可以自己設定,比如:有時候需要在字串中輸入"(或者)",這是需要自定義界定符。可以在"和(之間新增任意符號,這樣在字串結尾的)和"之間也要新增這些字元。比如:使用R"+#(標識字串的開頭,必須使用)(+#"作為原始字串的結尾。因此由:
cout << R"+#(Jim Keing"(hello world)")+=" << endl;
4.4 結構簡介
結構是一種比較靈活的資料格式,同一個結構中可以儲存多種型別的資料。結構也是C++面向物件(類)的基石。結構是使用者自定義的型別,而結構宣告定義了這種型別的資料屬性。定義了型別後,便可以建立這種型別的變數。建立一個結構包括兩步:定義結構描述;按描述建立結構變數。
結構描述如下:
struct inflatable
{
char name[20];
float volume;
double price;
}
其中關鍵字struct表明,這些程式碼定義的是一個結構的佈局。識別符號inflatable是這種資料格式的名稱,因此新型別的名稱為inflatable。定義結構後,便可以建立這種型別的變數:
inflatable hat;
inflatable mainframe;
C中要求新增struct關鍵字,如下:
struct inflatable hat;
因為hat的型別為inflatable,因此可以是一個.操作符訪問各個成員。比如:hat.volume指的是結構得volume成員。
4.4.1 程式中使用結構體
structur.cpp
#include <iostream>
using namespace std;
struct inflatable
{
char name[20];
float volume;
double price;
};
int main()
{
inflatable guest = {"gloria", 1.88, 29.99};
inflatable pal = {"Arthur", 3.12, 32.99};
cout << "Guest: " << guest.name << " " << guest.volume
<< " " << guest.price << endl;
cout << pal.price + guest.price << endl;
return 0;
}
結果:Guest: gloria 1.88 29.99 62.98
結果宣告宣告得位置有兩種選擇,第一,放在main()函式中;第二,放在main()函式的前面,其他函式也可以訪問。變數也可以在函式內部和外部定義,外部定義由所有函式共享。
C++結構初始化
與陣列踹壞,C++也支援列表初始化用於結構,且等號是可選的:
inflatable duck {"Daphe", 0.12, 9,89};
其次,如果大括號內為空,各個成員初始化為零。如下:
inflatable mayor {};
最後,不允許縮窄轉換。
結構可以使用string類o成員
struct inflatable
{
std::string name;
float volume;
double price;
};
其他結構屬性
- 結構變數之間可以使用賦值運算子;
- 結構可以作為引數傳遞給函式,也可以讓函式返回一個結構;
assgn_st.cpp
#include <iostream>
using namespace std;
struct inflatable
{
char name[20];
float volume;
double price;
};
int main()
{
inflatable bou = {"sun", 0.2, 12.49};
inflatable choice;
choice = bou;
cout << "choice: " << choice.price << endl;
return 0;
}
結果: choice: 12.49
從中可見成員賦值是有效的,choice結構中的成員值與bouquet結構中儲存的值相同。
可以同時完成定義結構和建立結果的工作,如下:
struct perks
{
int key_num;
char car[12];
}mr_smith, ms_jones;
甚至可以初始化以這種方式建立的變數:
struct perks
{
int key_num;
char car[12];
}mr_smith ={7, "Packard"};
還可以宣告沒有名稱的結構體,這樣以後無法建立這種型別的變數,如下:
struct
{
int key_num;
char car[12];
}mr_smith;
建立了一個mr_smith變數,可以訪問其中的成員。
4.4.5 結構陣列
可以建立結構陣列,比如,建立一個包含100個inflatable結構的陣列,如下:
inflatable gifts[100];
這樣gifts是一個inflatable陣列,其中的每個元素(如gifts[0])都是inflatable物件,可以與成員運算子一起使用:
cin >> gifts[0].volume;
gifts本身是一個數組,不是一個結構。因此gifts.price是無效的。
初始化結構陣列,可以結合使用初始化陣列的規則,具體如下:
inflatable guests[2] = {
{"zzz", 1.2, 33.4},
{"ddd", 0.4, 33,2}
};
4.4.6 結構中的位欄位
C++允許指定佔用特定位數的結構成員,這使得建立與某個硬體裝置上的暫存器對應的資料結構非常方便。欄位的型別應為整型或列舉,接下來是冒號,冒號後面是一個數字,它指定了使用的位數。可以使用沒有名稱的欄位來提供間距。每個成員都被稱為位欄位(bit field)。下面是一個例子:
struct torgle_register
{
unsigned int SN : 4;
ussigned int :4 ;
bool goodIn : 1;
bool goodTorgle : 1;
}
可以先通常那樣初始化這些欄位,還可以使用標準的結構表示法來訪問位欄位:
torgle_register tr = {14, true, false};
cout << tr.goodIn;
位欄位一般使用在低階程式設計中。
4.5 共用體
共用體(union)是一種資料格式,它能夠儲存不同的資料格式,但只能同時儲存其中的一種型別。即,結構體可以同時儲存int、long和double,共用體只能儲存int、long或double。共用體的句法與結構體相似,但含義不同。
union one4all
{
int int_val;
long long_val;
double double_val;
}
可以使用one4all變數來儲存int、long或double,條件是在不同的時間進行:
one4all pail;
pail.int_val = 15;
cout << pail.int_val;
pail.double_val = 2.2; //int_val的值丟失
cout << pail.double_val;
因此,pail有時可以是int變數,而有時是double型別的變數。通用體每次只能儲存一個值,因此必須有足夠大的空間來儲存最大的成員,所以共用體的長度為其最大成員的長度。
共用體的用處之一是,但資料項使用兩種或多種格式時,可節省空間。例如:管理一個小商品目錄,其中一些商品的ID為整型,而另一些為字串。在這種情況可以如下:
struct widget
{
char brand[20];
int tyep;
union id
{
long id_num;
char id_char[20];
} id_val;
};
widget prize;
if (prize.type == 1)
cin >> prize.id_val.id_num;
else
cin >> prize.id_val.id_char;
匿名共用體沒有名稱,其成員將成為位於相同地址出的變數,顯然,每次只有一個成員是當前的成員:
struct widget
{
char brand[20];
int tyep;
union
{
long id_num;
char id_char[20];
} ;
};
widget prize;
if (prize.type == 1)
cin >> prize.id_num;
else
cin >> prize..id_char;
由於共用體是匿名的,因此id_num和id_char被視為prize的兩個成員,它們的地址相同,所以不需要中間識別符號id_val。共用體用於節省記憶體。但C++用於嵌入式程式設計,如控制烤箱或火星漫步者的處理器,記憶體非常寶貴。
4.6 列舉
C++的enum工具提供了另一種建立符號常量的方式,這種方式可以替代const。它還允許定義新型別,但必須按照嚴格的限制進行。使用enum句法與使用結構相似。例如:
enum spectrum {red, orange, yellow, green, blue, violet, indigo ultraviolet};
該語句完成了兩個工作:
- 讓spectrum成為新型別的名稱:spectrum被稱為列舉(enumeration)
- 將red、orange等作為符號常量,它們對應整數值0~7,這些常量叫做列舉量。
利用列舉型別來宣告這種型別的變數:
spectrrm band;
對於列舉型別,只定義了賦值運算子,具體說,沒有為列舉定義算術運算:
band = orange;
++band; //非法
band = orange + yellow; //非法
band = 2000; //非法,2000不是一個列舉型別
列舉量是整型,可被提升為int型別,但int型別不能自動轉換為列舉型別:
int color = bule;
band = 3; //非法
color = 3 + red;
如果int值是有效的,則可以通過強制型別轉換,將它賦值給列舉變數:
band = spectrum(3);
如果試圖對一個適當的值進行強制型別轉換,結果是不確定的,不會報錯:
band = spectrum(5000);
如果只使用常量,而不建立列舉型別的變數,則可以省略列舉型別的名稱,如下:
enum {red, orange, yellow, green, blue, violet, indigo ultraviolet};
4.6.1 設定列舉量的值
可以使用賦值運算子來顯示地設定列舉量的值:
enum bits {one = 1, two = 2, four = 4, eight = 8};
指定的值必須是整數,也可以只顯示地定義其中一些列舉量地值:
enum bigstep {first, second = 100, third};
這裡,first在預設情況下為0,後面沒有被初始化地列舉量地值將比其前面的列舉量大1.因此third的值為101。
最火,可以建立多個值相同的列舉量:
enum {zero, null = 0, one, numero_nuo = 1};
其中,zero,null的值都沒零,one和numero_nuo都為1。在早期,只能將int值賦給列舉型別,但這種限制取消了,因此可以使用long甚至long long型別的值。
4.6.2 列舉的取值範圍
對於列舉來說,只有宣告中指出的那些值是有效的。然而,C++現在通過強制型別轉換,增加了可賦給列舉變數的合法值。每個列舉都要取值範圍,通過強制型別轉換,可以將取值範圍中的任何整數賦值給列舉變數,即使這個值不是列舉型別,如下:
enum bits{one=1, two=2, four=4, eight=8};
bits myflag;
下面的程式碼合理:
myflag = bits(6);
其中6不是列舉型別,但它位於列舉定義的取值範圍內。
取值的範圍定義如下:首先,找出上限,需要知道列舉的最大值。找出大於這個最大值的、最小的2的冪,將它減去1,得到的便是取值範圍的上限。計算下限,需要知道列舉量的最小值,如果它不小於0,則取值範圍的下限為0;否則採用與尋找上限方式相同的方式,但加上負號。
例如:前面定義的bigstep的最大列舉值是101。在2的冪中,比這個數大的最小值是128,因此取值範圍的上限為127。對於下限,如果最小的列舉量為-6,而比它小的、最大的2的冪是-8,因此下限為-7。
4.7 指標和自由儲存空間
使用&地址運算子,獲取變數的地址。例如,如果home是一個變數,則&home是它的地址。
#include <iostream>
int main()
{
using namespace std;
int donuts = 6;
double cups = 4.5;
cout << "donuts's addresss: " << &donuts << endl;
cout << "cups's addresss: " << &cups << endl;
return 0;
}
結果:donuts’s addresss: 0x7ffe74cc89b8 cups’s addresss: 0x7ffe74cc89b4
顯示地址時,該實現的cout使用十六進位制表示法,因為這是常用於描述記憶體的表示法。兩個地址的差為:0x7ffe74cc89b8-0x7ffe74cc89b4(即4),在實現中,donuts的儲存位置比cups低,而這種型別使用4個位元組。當然,不同的系統,儲存的順序以及位元組大小都不同。
指標與C++基本原理:
面向物件程式設計與傳統的過程性程式設計的區別在於,OOP強調的是在執行階段進行決策。執行階段指的是程式正在執行,編譯階段指的是編譯器將程式組合起來。執行階段是做決策,程式應該如何執行,而編譯階段是安全預先設定的程式執行。
執行階段決策提供了靈活性,可以根據當時的情況進行調整。比如:考慮為陣列分配記憶體的情況。
一種特殊的變數–指標用於儲存地址的值。運算子被稱為間接值或解除引用。
pointer.cpp
#include <iostream>
int main()
{
using namespace std;
int updates = 6;
int *p_updates;
p_updates = &updates;
cout << "*p_updates: " << *p_updates << endl;
cout << "p_updates: " << p_updates << endl;
*p_updates = 1 + *p_updates;
return 0;
}
結果:p_updates: 6 p_updates: 0x7ffc6c803fa4 p_updates + 1: 7
從中可知,p_updates表示地址,使用號運算子來獲得值。p_updates和updates完全等價。可以像int變數一樣使用p_updates。
4.7.1 宣告和初始化指標
宣告指標,計算機需要跟蹤指標指向的值的型別。比如:char的地址和double的地址,看上去一樣,但char和double使用的位元組數不同,它們儲存值得內部格式不同。
int * p_updates;
或 int *p_updates;
或 int* p_updates; //int* 是一種型別--指向int的指標。
或 int*p_updates;
這表明,updates的型別為int。由於運算子被用於指標,因此p_updates變數本身必須是指標。
int* p1, p2;
注意上面的語句是建立一個指標(p1)和一個int變數(p2)。對於每個指標變數名,都需要使用一個。
double * tax;
char* str;
將tax宣告為一個指向double的指標,編譯器知道tax是一個double型別的值。即tax是一個以浮點數格式儲存的值,這個值佔據8個位元組(不同系統可能不同)。指標變數不僅僅是指標,而且是指向特定型別的指標。雖然,tax和str指向兩種不同長度的叔叔型別,但這兩個變數本身的長度是相同的,即char的地址和double的地址的長度相同。
可以在宣告語句中初始化。
int h = 5;
int *ph = &h;
被初始化的是指標,而不是它指向的值,即pt的值設為&h;而不是pt的值。
4.7.2 指標的危險
在C++中建立地址時,計算機分配用來儲存地址的記憶體,但不會分配用來儲存指標所指向的資料的記憶體。為資料提供空間是一個獨立的步驟,忽略這一步是錯誤的,如下:
long * fellow;
*fellow = 222;
fellow確實是一個指標。上述程式碼沒有將地址賦給fellow,那麼222將被存放在哪裡?由於fellow沒有被初始化,它可能有任何值。不管值是什麼,程式都將它解釋為儲存222的地址。
注意:一定要在對指標應用解除引用運算子()之前,將指標初始化為一個確定的、適當的地址。
4.7.3 指標和數字
指標不是整型,索然計算機通常把地址當作整數處理。指標沒有加減乘除運算,指標描述的是位置。不能簡單的對將整數賦給地址:
int *pt;
pt = 0xB8000000;
在C++中,編譯器將顯示錯誤資訊,型別不匹配。要將數字值作為地址來使用,應通過強制型別轉換將數字轉換為適當的地址型別:
int *pt;
pt = (int*) 0xB8000000;
這樣,賦值語句兩邊都是整型的地址,因此賦值有效。pt是int值的地址,並不意味著pt本身的型別是int。
4.7.4 使用new來分配記憶體
在C語言中,可以使用庫函式malloc()來分配記憶體;而在C++讓可以這樣做,但C++提供了更好的方法—new運算子。
在執行階段為一個int值分配未命名的記憶體,並使用指標來訪問這個值:
int *pn = new int;
new int告訴程式,需要適合儲存int的記憶體。new運算子根據型別確定需要多少位元組的記憶體,然後找到這樣的記憶體,並返回其地址,並將地址賦給pn,pn是被宣告為指向int的指標。現在pn是地址,pn儲存那裡的值。將這種方法於將變數的地址賦給指標進行對比:
int h = 5;
int *ph = &h;
在這兩種情況下,都是將一個int變數的地址賦給了指標。在第二種情況,可以通過變數名了訪問該int值,而第一種情況只能通過指標進行訪問。
為資料物件(可以是結構,也可以是基本型別)獲得並指定分配記憶體的通用格式:
typeName *pointer_name = new typeName;
use_new.cpp
#include <iostream>
int main()
{
using namespace std;
int nights = 1001;
int *pt = new int;
*pt = 1001;
cout << "*pt: " << *pt << endl;
cout << "pt: " << pt << endl;
cout << "&nights: " << &nights << endl;
return 0;
}
結果:*pt: 1001 pt: 0x220c010 &nights: 0x7ffc241baf94
new為int資料物件分配記憶體,這是在程式執行時進行的。指標必須宣告所指向的型別的原因是:地址本身只指出了物件儲存的地址開始,而沒有指出其型別(使用的位元組數)。
對於指標,new分配的記憶體塊於常規變數宣告分配的記憶體塊不同。變數nights的值儲存在被稱為棧的記憶體區域中,new從被稱為堆或自由儲存區的記憶體區域分配記憶體。
4.7.5 使用delete釋放記憶體
需要記憶體時,使用new來請求。使用完記憶體後,使用delete運算子將其歸還給記憶體池。一定要配對使用new和delete,否則會發送記憶體洩漏,即被分配的記憶體再也無法使用。如果洩漏嚴重,則程式將由於不斷尋找更多記憶體而終止。
int *ps = new int;
delete ps;
不用使用delete釋放已經釋放的記憶體,這樣做結果是不確定的。另外,不要使用delete來釋放宣告變數所獲得的記憶體:
int jugs = 5;
int *pi = &jugs;
delete pi; //不允許,錯誤的做法
注意:只能用delete釋放使用new分配的記憶體,然後,對空指標使用delete是安全的。
使用delete的關鍵在於,將它用於new分配的地址,而不意味著要使用用於new的指標,而是使用者new的地址:
int *ps = new int;
int *pq = ps;
delete pq;
一般來說,不要建立兩個指向同一個記憶體塊的地址,因為這樣增加錯誤地刪除同一個記憶體塊兩次的可能性。但,對於返回指標的函式,使用另一個指標是有道理的。
4.7.6 使用new來建立動態陣列
在程式設計時給陣列分配記憶體被稱為靜態聯編,意味著陣列在編譯時加入到程式中的。但使用new時,如果在執行階段需要陣列,則建立它,如果不需要,則不建立。還可以在程式執行時選擇陣列的長度,這種被稱為動態聯編,意味著數是在程式執行時建立的。這種陣列叫作動態陣列。
1、使用new建立動態陣列
在C++中建立動態陣列:只要將陣列元素型別和元素數目告訴new即可。必須在型別名後加上方括號,其中包含元素的數目:
int *psome = new int[10];
delete [] psome;
建立了一個包含10個int元素的陣列。並使用delete對分配的記憶體進行釋放。釋放記憶體時,方括號告訴程式,應該釋放整個陣列,而不僅僅是指標指向的元素。
程式確實跟蹤了分配的記憶體量,以便以後使用delete []正確地釋放這些記憶體,但這種資訊是不公用的。例如:不能使用sizeof運算子來確定動態陣列分配的陣列包含的位元組數。
2、使用動態陣列
*psome是第1個元素的值,psome[0]同樣是第一個元素的值。psome[1]是第2個元素的值,以此類推。
arraynew.cpp
#include <iostream>
int main()
{
using namespace std;
double *p3 = new double [3];
p3[0] = 0.2;
p3[1] = 0.5;
p3[3] = 0.8;
cout << "p3[1]: " << p3[1] << endl;
p3 = p3 + 1;
cout << "p3+1,p3[0]: " << p3[0] << endl;
p3 = p3 - 1;
delete [] p3;
return 0;
}
結果:p3[1]: 0.5 p3+1,p3[0]: 0.5
從中可知,程式將指標p3當作陣列名來使用,p3[0]表示第1個元素,依次類推。不過指標和陣列名之間有差別的,不能更改陣列名的值,但指標是變數,因此可以修改它的值:
p3 = p3 + 1;
將p3加1的效果,是將p3[0]指向陣列中的第2個元素。將它減1後,指標將指向原來的值,這樣程式可以給delete[]提供正確的地址。
4.8 指標、陣列和指標算術
指標和陣列基本等級的原因在於指標運算子和C++內部處理陣列的方式。將指標加1後,增加的量等於它所指向的型別的位元組數。比如:將double型別的指標加1後,如果系統double使用8個位元組儲存,則數值將加8。另外,C++將陣列名解釋為地址。
addpntrs.cpp
#include <iostream>
int main()
{
using namespace std;
double wages[3] = {1000.0, 2000.0, 3000.0};
short stacks[3] = {3, 2, 1};
double *pw = wages;
short *ps = &stacks[0];
cout << "pw = " << pw << ", *pw = " << *pw << endl;
pw = pw + 1;
cout << "Add 1 to the pw pointer:\n";
cout << "pw = " << pw << ", *pw = " << *pw << endl;
cout << "ps = " << ps << ", *ps = " << *ps << endl;
ps = ps + 1;
cout << "Add 1 to the ps pointer:\n";
cout << "ps = " << ps << ", *ps = " << *ps << endl;
cout << "stacts[0] = " << stacks[0] << endl;
cout << "*(stacks + 1) = " << *(stacks+1) << endl;
cout << "Wages array size: " << sizeof(wages) << endl;
cout << "pw pointer size: " << sizeof(pw) << endl;
return 0;
}
結果:
pw = 0x7ffedfcf9060, *pw = 1000
Add 1 to the pw pointer:
pw = 0x7ffedfcf9068, *pw = 2000
ps = 0x7ffedfcf9050, *ps = 3
Add 1 to the ps pointer:
ps = 0x7ffedfcf9052, *ps = 2
stacts[0] = 3
*(stacks + 1) = 2
Wages array size: 24
pw pointer size: 8
4.8.1 程式說明
在多數情況下,陣列名解釋為陣列的第一個元素的地址。因此,下面語句將pw宣告為指向double型別的指標,然後將它初始化為wages—wages陣列中第一個元素的地址:
double *pw = wages;
和所有陣列一樣,有:
wages = &wages[0]; //第一個元素的地址
程式查看了pw和pw的值,前者是地址,後者是儲存在該地址的值。pw加1,數字地址值增加8(double型別)這樣pw指向陣列中第二個元素。而對於ps(short型別),ps+1,其地址值將增加2。
注意:將指標變數加1後,其增加的值等於指向的型別所佔用的位元組數。
stacks[1]和(stacks+1)等價,(stacks+1意味著先計算陣列第2個元素的地址,然後找到儲存在那裡的值。(運算子優先順序要求使用括號,如果不使用將給stacks的值加1)。
對於陣列和指標,c++可以執行下面的轉換:
arrayname[i]; -> *(arrayname+1);
pointername[i]; -> *(pointername+1);
陣列和指標的區別在於,陣列名是常量,而指標可以修改其值。如下:
arrayname = arrayname + 1;//錯誤
pointername = pointername + 1;
另一個區別,對於陣列應用sizeof運算子得到的是陣列的長度,而對指標應用sizeof運算子得到的指標的長度,即使指標指向一個數組。在上述程式中有體現。
陣列的地址 陣列名被解釋為其第一個元素的地址,而對陣列名應用地址運算子時,得到的是整個陣列的地址:
short tell[10];
cout << tell << endl; //第一個元素的地址
cout << &tell << endl; //整個陣列的地址
從數字上說,這兩個值是相等的;但概念上,tell(&tell[0])是一個2位元組記憶體塊的地址,而&tell是一個20位元組的記憶體塊地址。因此表示式tell+1將地址值加1,而表示式&tell+2將地址加20。即:tell是一個short指標(short),而&tell是一個指向包含20個元素的short陣列(short()[20])的指標。
short (*pas)[20] = &tell;
pas的型別為short()[20],由於pas被設定為&tell,因此*pas於tell等價,即(pas)[0]為tell陣列的第一個元素。其中括號不能少,否則,pas是一個short指標陣列,它包含20個元素。
4.8.2 指標小結
1、宣告指標
typeName * pointername;
double *pn;
char *pc;
2、給指標賦值
對變數使用&運算子,來獲取被命名的記憶體的地址,new運算子返回未命名的記憶體的地址。
double *pn;
char * pc;
couble * pa;
double bud = 2.33;
pn = &bud;
pc = new char;
pa = new double [10];
3、對指標解除引用
對指標解除引用意味著獲取指標指向的值。
cout << *pn;
*pc = "s";
pa[1] = 2.11;
決不要對未被初始化為適當地址的指標解除引用。
4、區分指標和指標指向的值
pt是指向int的指標,則pt是指向int型別的變數的值。
int *pt = new int;
*pt = 3;
5、陣列名
在多數情況下,C++將陣列名視為陣列第一個元素的地址。一種例外情況是,將sizeof運算子用於陣列名時,此時將返回整個陣列的長度。
6、指標算術
C++允許將指標和整數相加。還可以將一個指標減去另一個指標,獲得兩個指標的差,僅當兩個指標指向同一個陣列時,運算才有意義。
int tacos[10] = {2,3,4,5,6,8,9,1,0,7};
int *pt = tacos;
pt = pt + 1;
int *pe = &tacos[9];
pe = pe - 1;
int diff = pe - pt;
7、陣列的動態聯編和靜態聯編
使用陣列宣告來建立陣列時,採用靜態聯編,即陣列的長度在編譯時給定:
int tacos[10];
使用new[] 運算子建立陣列時,將採用動態聯編,即將在執行時為陣列分配空間,其長度也在執行時設定:
int size;
cin >> size;
int *pz = new int [size];
delete [] pz;
8、陣列表示法和指標表示法
tacos[0]; 等價於 *tacos;
tacos[3]; 等價於 *(tacos+3);
陣列名和指標變數都是如此,因此對於指標和陣列名,既可以使用指標表示法,也可以使用陣列表示法。
4.8.3 指標和字串
cout物件認為char的地址是字串的地址,因此它列印該地址處的地址,然後繼續列印後面的字元,知道遇到空字元(\0)為止。如果要獲取字串陣列的地址,需要進行強制轉換,如(int*)flower。而且,"are red"字串常量,為了保持輸出一致,這個引號括號起來的字串也是一個地址。
注意:在cout和多數C++表示式中,char陣列名、char指標以及引號括起來的字串常量都被解釋為字串第一個字元的地址。
ptrstr.cpp
#include <iostream>
#include <cstring>
int main()
{
using namespace std;
char animal[20] = "bear";
const char *bird = "wren";
char *ps;
cout << animal << " and " << bird << endl;
//cout << ps << endl;
cout << "Enter a kind of animal:";
cin >> animal;
ps = animal;
cout << ps << endl;
cout << "Before using strcpy():\n";
cout << animal << " at " << (int *)animal << endl;
cout << ps << " at " << (int*)ps << endl;
ps = new char[strlen(animal) + 1];
strcpy(ps, animal);
cout << "After using strcpy():\n";
cout << animal << " at " << (int *)animal << endl;
cout << ps << " at " << (int*)ps << endl;
return 0;
}
結果:
bear and wren
Enter a kind of animal:fox
fox
Before using strcpy():
fox at 0x7ffd1b868460
fox at 0x7ffd1b868460
After using strcpy():
fox at 0x7ffd1b868460
fox at 0xe91010
其中"wren"實際表示的是字串的地址,因此"const char bird = “wren”;"語句是將"wren"的地址賦給了bird指標。程式中將bird指標宣告為const,因此編譯器將禁止改變bird指向的位置中的內容。
獲得字串副本,首先,需要分配記憶體來儲存該字串,這可以通過宣告一個數組或使用new來完成。後一種方法使得能夠根據字串長度來指定所需的空間:
ps = new char[strlen(animal) + 1];
然後,需要將animal陣列中的字串複製到新分配的空間中。將animal賦給ps是不可行的,因為這樣只能修改儲存在ps中的地址,從而失去程式訪問新分配記憶體的唯一途徑,需要使用庫函式strcpy():
strcpy(ps, animal);
strcpy()函式接收兩個引數,第一個是目標地址,第二個是要賦值的字串的地址。通過使用new和strcpy(),將獲得"fox"兩個獨立的副本。
fox at 0x7ffd1b868460
fox at 0xe91010
經常需要將字串放到陣列中。初始化陣列時,使用"="運算子;否則使用strcpy()或strncpy()。
char food[20] = "carrots";
strcpy(food, "flan");
strcpy(food, "a picnic basket filled with many goodies");//導致問題,food陣列比字串小。
對於最後一種情況,函式將字串剩餘的部分複製到陣列後面的記憶體位元組中,這可能覆蓋程式正在使用的其他記憶體。要避免這種問題,使用strncpy()。該函式接收第3個引數–要複製的最大字元數。
strncpy(food, "a picnic basket filled with many goodies", 19);
food[19] = '\0';
這樣最多將19個字元複製到陣列中,然後最後一個元素設定為空字元。如果該字串少於19個字元,則strncpy()將在複製完成字串之後加上空字元,以標記字串的結尾。
4.8.4 使用new建立動態結構
在執行時建立陣列優於在編譯時建立陣列,對於結構也如此。對於new用於結構由兩步組成:建立結構和訪問其成員。建立結構,需要同時使用結構型別和new。如下:
inflatable *ps = new inflatable;
這樣把足以儲存inflatable結構的一塊可用記憶體的地址賦給ps。這種句法和C++內建型別完全相同。接下來是成員訪問,建立動態結構時,不能使用運算子句點用於結構,因為這種結構沒有名稱,只知道其地址。C++專門提供了箭頭成員運算子(->)。該運算子由連字元和大於號組成,可用於指向結構的指標,就像點運算子可用於結構名一樣。例如:ps->price。
另一種訪問結構的方法是,如果ps是指向結構的指標,則ps就是被指向的值—結構本身。由於ps是一個結構,因此(