複合類型介紹
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;
}
結果:
[root@localhost ~]# ./a.out</br>
Enter your name: </br>
zxp</br>
Enter your favorite dessert:</br>
kk</br>
zxp like kk</br>
[root@localhost ~]# ./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;
}
結果:
[root@localhost ~]# ./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是一個結構,因此(ps).price是該結果的price成員。C++的運算符優先級規則要求使用括號。
newstrct.cpp
#include <iostream>
struct inflatable
{
char name[20];
float volume;
double price;
};
int main()
{
using namespace std;
inflatable *ps = new inflatable;
cout << "Enter name of infaltable item: ";
cin.get(ps->name, 20);
cout << "Enter volume of inflatable item: ";
cin >> (*ps).volume;
cout << "Enter price of inflatable item: ";
cin >> ps->price;
cout << "Name: " << (*ps).name << endl;
cout << "Volume: " << ps->volume << endl;
cout << "Price: " << ps->price << endl;
return 0;
}
結果:
Enter name of infaltable item: Frodo
Enter volume of inflatable item: 1.4
Enter price of inflatable item: 27.99
Name: Frodo
Volume: 1.4
Price: 27.99
1. 一個使用new和delete的示例
delete.cpp定義了一個函數getname(),該函數返回輸入字符串的指針。該函數將輸入讀入到一個大型臨時數組中,然後使用new[]來創建一個剛好存儲該輸入字符串的內存塊,並返回一個指向該內存塊的指針。對於讀取大量字符串的程序,這種方法可以大大節省內存。
假設程序需要讀取100個字符串,其中最大的字符串包含79個字符,而大多數字符串都短得多。如果用char數組來存儲這些字符串,則需要100個數組,其中每個數組的長度爲80個字符,總共需要8000多個字節,而其中大部分內存沒有被使用。
另一種方法,創建一個數組,它包含100個指向char的指針,然後使用new根據字符串的需要分配相應數量的內存。還可以使用new根據需要的指針數量來分配空間。
delete.cpp
#include <iostream>
#include <cstring>
using namespace std;
char * getname(void);
int main()
{
char *name;
name = getname();
cout << name << " at " << (int*)name << endl;
delete [] name;
name = getname();
cout << name << " at " << (int*)name << endl;
delete [] name;
return 0;
}
char * getname()
{
char temp[80];
cout << "Enter last name: ";
cin >> temp;
char *pn = new char [strlen(temp)+1];
strcpy(pn, temp);
return pn;
}
結果:
Enter last name: Fred
Fred at 0x1c76010
Enter last name: Zll
Zll at 0x1c76010
2. 程序說明
getname()函數,使用cin將輸入的字符串放到temp數組中,然後使用new分配行內存,以存儲該字符串。程序需要strlen(temp)+1個字符(包括空字符)來存儲該字符串,將這個值提供給new。獲得空間後,getname()使用標準庫函數strcpy()將temp中的字符串複製到新的內存塊中。最後返回pn,這是字符串副本的地址。
在main()中,返回值(地址)被賦值給name。該指針是在main()中定義的,但它指向getname()函數中分配的內存塊。接下來,在釋放name指向的內存快後,main()再次調用getname()。C++不保證新釋放的內存就是下一次使用new時選擇的內存,但該例子中剛好是的。
在這個例子中,getname()分配內存,而main()釋放內存,將new和delete放在不同的函數通常不是一個好辦法,因爲這樣容易忘記使用delete。不過這樣做確實可以。
4.8.5 自動存儲、靜態存儲和動態存儲
根據用戶分配內存的方法,C++有3種管理數據內存的方式:自動存儲、靜態存儲和動態存儲(有時也叫作自由存儲空間或堆)。在存儲時間的長短方面,以這3種方式分配的數據對象各不相同。
1、自動存儲
在函數內存定義的常規變量使用自動存儲空間,被稱爲自動變量。即它們在所屬的函數被調用時自動產生,在該函數結束時消亡。例如delete.cpp中的temp數組僅當getname()函數活動時存在。當程序控制權回到main()時,temp使用的內存自動釋放。
實際上,自動變量是一個局部變量,其作用域爲包含它的代碼塊。自動變量通常存儲在棧中,這意味着執行代碼塊時,其中的變量將依次加入棧中,而在離開代碼時,將按照相反的順序釋放這些變量。這被稱爲後進先出(LIFO)。
2、靜態存儲
靜態存儲是整個程序執行期間都存在的存儲方式。使變量成爲靜態存儲的方式有兩種:
-
在函數外面定義它
-
在聲明使用關鍵字static
static double fee = 44.3;
自動存儲和靜態存儲的關鍵在於:這些方法嚴格限制了變量的壽命。變量可能存在於程序的整個生命週期(靜態變量),也可能只是特定函數被執行時存在(自動變量)。
3、動態存儲
new和delete運算符提供了一種比自動變量和靜態變量更靈活的方法。它們管理了一個內存池,這在C++被稱爲自由存儲空間或堆。該內存池同用於靜態變量和自動變量的內存是分開的。
數據的生命週期不完全受程序或函數的生存控制。與常規的變量相比,使用new和delete讓程序員對程序如何使用內存有更大的控制權。在棧中,自動添加和刪除機制使得佔用的內存是連續,但new和delete的相互影響可能導致佔用的自由存儲區不連續,這使得跟蹤內存的位置更困難。
內存泄漏
如果使用new運算符在自由存儲空間(堆)上創建變量後,沒有調用delete,則即使包好指針的內存由於作用域和對象生命週期的原因而被釋放,在自由存儲空間上動態分配的變量或結構也將繼續存在。實際上,將會無法訪問自由存儲空間的結構,因爲指向這些內存的指針無效。這將導致內存泄漏。泄漏的內存,將在程序整個生命週期內不可使用,這些內存被分配出去,無法回收。極端情況,內存泄漏可能導致程序崩潰。
4.9 類型組合
struct antarctica_year_end
{
int year;
};
可以創建這種類型的變量:
antarctica_year_end s01, s02, s03;
然後,使用成員運算符來訪問其成員:
s01.year = 1998;
可以創建指向這種結構的指針:
antarctica_year_end *pa = &s02;
將指針設置爲有效地址後,就可以使用間接成員運算符來訪問成員:
pa->year = 1999;
可創建結構數組:
antarctica_year_end trio[3];
然後,使用成員運算符來訪問元素的成員:
trio[0].year = 2003;
由於數組名是一個指針,因此可以使用間接成員運算符:
(trio+1)->year = 2004;
可創建指針數組:
const antarctica_year_end *arp[3] = {&s01,&s02,&s03};
arp是一個指針數組,arp[1]就是一個指針,可將間接運算符應用於它:
cout << arp[1]->year << endl;
可創建指向上述數組的指針:
const antarctica_year_end **ppa = arp;
其中arp是一個數組名,因此它是第一個元素的地址。但其第一元素爲指針,因此ppa是一個指針,指向const antarctica_year_end的指針。這樣容易出錯,比如忘記const,搞錯順序或結構類型,C++11提供了auto,編譯器知道arp的類型能過正確地推斷出ppb的類型:
auto ppb = arp;
ppa是一個指向結構指針的指針,因此*ppa是一個結構指針,可間接成員運算符應用於它:
cout << (*ppa)->year << endl;
cout << (*(ppa+1))->year << endl;
mixtype.cpp
#include <iostream>
struct antarctica_year_end
{
int year;
};
int main()
{
using namespace std;
antarctica_year_end s01, s02, s03;
s01.year = 1998;
antarctica_year_end *pa = &s02;
pa->year = 1999;
antarctica_year_end trio[3];
trio[0].year = 2003;
cout << trio->year << endl;
const antarctica_year_end *arp[3] = {&s01,&s02,&s03};
cout << arp[1]->year << endl;
const antarctica_year_end **ppa = arp;
cout << (*ppa)->year << endl;
cout << (*(ppa+1))->year << endl;
return 0;
}
結果:
2003
1999
1998
1999
4.10 數組的替代品
4.10.1 模板類vector
模板類vector類類似於string類,也是一種動態數組。可以運行階段設置vector對象的長度,可在末尾附加新數據,還可以在中間插入新數據。實際上,vector使用new和delete來管理內存,但這種工作是自動完成的。
首先,使用vector類,必須包含頭文件vector;其次,vector包含在命名空間std中,因此可以使用using編譯指令、using聲明或std::vector;然後,模板使用不同的語法來指出它存儲的數據類型;最後,vector類使用不同的語法來指定元素數。
#include <vector>
using namespace std;
vector<int> vi; //創建一個int類型空數組
int n;
cin >> n;
vector<double> vd(n); //創建一個double類型n個元素的數組
其中vi是一個vector對象,vd是一個vector對象。由於vector對象在插入或添加值時自動調整長度,因此可以在將vi的長度初始化爲0。但要調整長度,需要使用vector包中的各種方法。
一般而言,創建一個名爲vt的vector對象,它可存儲n_elem個類型typeName的元素:
vector<typeName> vt(n_elem);
4.10.2 模板類array(C++11)
vector類的功能比數組強大,但付出的代價是效率低。如果需要的是固定長度的數組,使用數組是更佳的選擇,但代價是不那麼方便。鑑於此,C++新增了模板類array,它也位於命名空間std中。與數組一樣,array對象的長度也是固定,也使用棧(靜態內存分配),而不是自由存儲區,因此效率與數組相同,但更方便,更安全。需要包含頭文件array。創建arry對象的語法:
#include <array>
using namespace std;
array <int,5> ai;
array<double,4> ad = {1.2, 1.3, 2.2, 2.3};
創建一個名爲arr的array對象,它包含n_elem個類型爲typeName的元素:
array <typeName, n_elem> arr;
4.10.3 數組、vector對象、array對象比較
choices.cpp
#include <iostream>
#include <vector>
#include <array>
int main()
{
using namespace std;
double a1[4] = {1.2, 1.4, 2.2, 2.3};
vector<double> a2(4);
a2[0] = 1.0/3.0;
a2[1] = 1.0/5.0;
a2[2] = 1.0/7.0;
a2[3] = 1.0/9.0;
array<double, 4> a3 = {3.14, 2.72, 1.62, 1.41};
array<double, 4> a4;
a4 = a3;
cout << "a1[2]:" << a1[2] << " at " << &a1[2] << endl;
cout << "a2[2]:" << a2[2] << " at " << &a2[2] << endl;
cout << "a3[2]:" << a3[2] << " at " << &a3[2] << endl;
cout << "a4[2]:" << a4[2] << " at " << &a4[2] << endl;
a1[-2] = 20.2;
cout << "a1[-2]:" << a1[-2] << " at " << &a1[-2] << endl;
cout << "a3[2]:" << a3[2] << " at " << &a3[2] << endl;
cout << "a4[2]:" << a4[2] << " at " << &a4[2] << endl;
return 0;
}
Linux下使用C++11版本運行:g++ -std=c++11 choices.cpp
結果:
a1[2]:2.2 at 0x7ffdefd049f0
a2[2]:0.142857 at 0x16f2020
a3[2]:1.62 at 0x7ffdefd04a10
a4[2]:1.62 at 0x7ffdefd04a30
a1[-2]:20.2 at 0x7ffdefd049d0
a3[2]:1.62 at 0x7ffdefd04a10
a4[2]:1.62 at 0x7ffdefd04a30
程序說明:
首先,注意到無論數組、vector對象還是array對象,都可使用標準數組表示法來訪問各個元素。從地址可知,array對象和數組存儲在相同的內存區域(即棧)中,而vector對象,存儲在另一個區域(自由存儲區域或堆)中。
a1[-2] = 20.2;
索引-2,找出a1指向的地方,向前移兩個double元素,並將20.2存儲到目的地。即將信息存儲到數組的外面,與C語言一樣,C++也不檢查這種越界錯誤。vector和array對象仍可以編寫這種不安全的代碼。
對於vector和array對象還可使用at()成員函數:
a2.at(1) = 2.3;
使用at()時,將在運行期間捕獲非法索引,而程序默認將終端。這種額外檢查的代價是運行的時間更長,這就是C++允許使用任何一種表示法的原因。另外,這些類還能夠降低意外超界錯誤的概率,例如,它們包含成員函數begin()和end(),能夠確定邊界,以免無意超界。
4.11 總結
數組、結構和指針是C++的3種複合類型。
字符串
共用體
枚舉
指針
new和delete
vector和array對象