1. 程式人生 > >C++ Primer Plus--複合型別(四)

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強調的是在執行階段進行決策。執行階段指的是程式正在執行,編譯階段指的是編譯器將程式組合起來。執行階段是做決策,程式應該如何執行,而編譯階段是安全預先設定的程式執行。

執行階段決策提供了靈活性,可以根據當時的情況進行調整。比如:考慮為陣列分配記憶體的情況。

一種特殊的變數–指標用於儲存地址的值。\ast運算子被稱為間接值或解除引用。

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

結果:\astp_updates: 6 p_updates: 0x7ffc6c803fa4 \astp_updates + 1: 7

從中可知,p_updates表示地址,使用\ast號運算子來獲得值。\astp_updates和updates完全等價。可以像int變數一樣使用\astp_updates。

4.7.1 宣告和初始化指標

宣告指標,計算機需要跟蹤指標指向的值的型別。比如:char的地址和double的地址,看上去一樣,但char和double使用的位元組數不同,它們儲存值得內部格式不同。

int * p_updates;
或 int *p_updates;
或 int* p_updates; //int* 是一種型別--指向int的指標。
或 int*p_updates;

這表明,\astupdates的型別為int。由於\ast運算子被用於指標,因此p_updates變數本身必須是指標。

int* p1, p2;

注意上面的語句是建立一個指標(p1)和一個int變數(p2)。對於每個指標變數名,都需要使用一個\ast

double * tax;
char* str;

將tax宣告為一個指向double的指標,編譯器知道\asttax是一個double型別的值。即\asttax是一個以浮點數格式儲存的值,這個值佔據8個位元組(不同系統可能不同)。指標變數不僅僅是指標,而且是指向特定型別的指標。雖然,tax和str指向兩種不同長度的叔叔型別,但這兩個變數本身的長度是相同的,即char的地址和double的地址的長度相同。

可以在宣告語句中初始化。

int h = 5;
int *ph = &h;

被初始化的是指標,而不是它指向的值,即pt的值設為&h;而不是\astpt的值。

4.7.2 指標的危險

在C++中建立地址時,計算機分配用來儲存地址的記憶體,但不會分配用來儲存指標所指向的資料的記憶體。為資料提供空間是一個獨立的步驟,忽略這一步是錯誤的,如下:

long * fellow;
*fellow = 222;

fellow確實是一個指標。上述程式碼沒有將地址賦給fellow,那麼222將被存放在哪裡?由於fellow沒有被初始化,它可能有任何值。不管值是什麼,程式都將它解釋為儲存222的地址。

注意:一定要在對指標應用解除引用運算子(\ast)之前,將指標初始化為一個確定的、適當的地址。

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是地址,\astpn儲存那裡的值。將這種方法於將變數的地址賦給指標進行對比:

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和\astpw的值,前者是地址,後者是儲存在該地址的值。pw加1,數字地址值增加8(double型別)這樣pw指向陣列中第二個元素。而對於ps(short型別),ps+1,其地址值將增加2。

注意:將指標變數加1後,其增加的值等於指向的型別所佔用的位元組數。

stacks[1]和\ast(stacks+1)等價,\ast(stacks+1意味著先計算陣列第2個元素的地址,然後找到儲存在那裡的值。(運算子優先順序要求使用括號,如果不使用將給\aststacks的值加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指標(\astshort),而&tell是一個指向包含20個元素的short陣列(short(\ast)[20])的指標。

short (*pas)[20] = &tell;

pas的型別為short(\ast)[20],由於pas被設定為&tell,因此*pas於tell等價,即(\astpas)[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的指標,則\astpt是指向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 \astbird = “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是指向結構的指標,則\astps就是被指向的值—結構本身。由於\astps是一個結構,因此(\ast