從頭讀《C++ Primer Plus》(3)

第四章 複合類型

小目錄

  • 創建並使用數組
  • 創建並使用C式字符串
  • 創建並使用string類字符串
  • 使用getline和get方法讀入字符串
  • 混合輸入字符與數字
  • 創建並使用結構(structure)
  • 創建並使用聯合(union)
  • 創建並使用枚舉(enumeration)
  • 創建並使用指針(pointer)
  • 使用new和delete動態管理內存
  • 創建動態數組
  • 創建動態結構
  • 自動,靜態,和動態存儲
  • vector和array類

數組介紹

聲明數組需要三個要素:

  1. 每個元素的類型
  2. 數組的名字
  3. 數組的元素個數

格式:typename arrayname[arraysize];

舉例:short months[12];

數組長度必須是一個整數常量,即數字或一個const值,又或者一個常量表達式比如8*sizeof(int).這個值必須是在這一句聲明之前便已知的。特別地,數組長度不能是一個程序執行中賦值的變量。不過,本章遲些會介紹到如何使用new來突破這個限制。

數組中的每個元素可以被單獨訪問,方法是數組名加上下標。

格式:arrayname[subscripts]

舉例:months[0]  //注意,數組下標從0開始

需要注意的是,編譯器不會對超出範圍的下標發出警告,比如months[111]。但對這個不存在的數組元素操作會導致數據或代碼崩潰,導致不可知的結果。

數組初始化規則

C++有幾條數組初始化的規則。

初始化形式只能用於聲明語句,而不能在之後使用。同樣,你不能將一個數組整個賦值給另一個:

int cards[4] = {3, 6, 8, 1};      //行

int hand[4];                           //行

hand[4] = {3, 6, 8, 1};           //不行

hand = cards;                       //不行

不過你可以使用下標對單個元素進行賦值。

在初始化時,允許提供少於數組長度的值,比如:

float height[5] = {1.0, 5.6};

這樣編譯器會把剩下的元素都初始化爲0.所以要把數組所有元素初始化爲0可以這樣寫:

long o[500] = {0};

注意,如果你把0改成1,只是0位是1,剩餘依然是0。

如果你不寫上數組長度,編譯器會幫你數你傳入的元素個數,比如:

int stuff[] = {1, 3, 5, 7};

編譯器會把stuff長度設爲4。

C++11數組初始化

C++11有幾種新初始化格式。

  1. 省略等號:int a[4] {1, 2, 3, 4};
  2. 使用空的大括號將所有元素設爲0:int a[4] {};
  3. 列表初始化會阻止變量類型縮窄:long p[] = {25, 36, 33.0};     //double→long不行

字符串

字符串就是在存儲中連續地byte裏存儲的一系列字符。C++有兩種字符串處理,一種是C式字符串,一種是string類。

在連續byte中存儲的一系列字符這個概念讓你可以把string存到char的數組裏,每個字符一個元素。C式字符串有一個特性:最後一個字符是一個空字符(null character)。這個字符寫作\0,ASCII碼爲0,專用於標記字符串的結尾。比如以下兩個char數組:

char dog[] = {'d', 'a', 'v', 'i', 'd'};     //不是字符串

char cat[] = {'s', 't', 'e', 'v', 'e', '\0'};  //字符串

這兩個都是char數組,但只有第二個是字符串。C++有很多處理字符串的方法,包括cout使用的。它們都是從字符串的頭逐個字符處理直到讀到空字符。如果你向這些方法傳入一個沒有空字符的char數組,方法會一直逐個byte往後讀下去,直到在某個其他數據的存儲區內讀到空字符。

當然C++還有更好的字符串初始化方法,那就是使用被雙引號括起來的字符串常量:

char pig[] = "sicca";

被雙引號括起的字符串常量總是會隱式地包括空字符,所以不需要特地寫出來。同樣,多種C++輸入工具在讀入字符串時也會自動地在末尾加上空字符。

要注意讓數組足夠大以放下整個字符串,包括空字符,所以最好還是使用字符串常量進行初始化並讓編譯器幫你數。另外,讓數組比字符串長除了浪費空間之外是無害的。因爲字符串的長度取決於空字符的位置,和數組長度沒有關係。

字符串常量連接

有時一個字符串太長了,一行寫不下,C++允許將其拆成兩個雙引號括起的字符串常量並通過空白格(空格、縮進、回車)進行連接。也就是你可以這樣寫:

cout << "this string is too long "

"to be written in one line."

注意連接不會在兩個字符串之間添加任何字符,第一個字符串的空字符會被第二個字符串的第一個字符替代。

在數組使用字符串

兩種把字符串放入數組的方式,一是用字符串常量初始化數組,二是從文件或輸入讀入到數組。看下面的程序:

#include <iostream>
#include <cstring>  //包含strlen方法
int main()
{
	using namespace std;
	const int Size = 15;
	char name1[Size];
	char name2[Size] = "walawala";
	
	cout << "I am " << name2 <<". And you are?\n";
	cin >> name1;
	cout << "Hello " << name1 << ". Your name is " << strlen(name1) << "characters long.\n";
	name2[3] = '\0';
	cout << "The first 3 characters of my name is:" << name2 << endl;
 } 

這段程序有幾個點:

  1. 用const值Size初始化了兩個char數組的長。
  2. 通過cin從鍵盤讀入了字符串並存入了name1
  3. 通過將name2第4位(下標3)賦值爲空字符,讓cout只輸出前三位

字符串輸入

由於你沒法通過鍵盤輸入空字符,所以cin是靠空白格(空格、縮進,回車)來分割每一個字符串。這就意味着你無法從鍵盤輸入空格。看以下的程序:

#include <iostream>
int main()
{
	using namespace std;
	char c1[15];
	char c2[15];
	
	cout << "enter first string.\n";
	cin >> c1;
	cout << "enter second string.\n";
	cin >> c2;
	cout << "c1:" << c1 << " c2:" << c2;
 } 

輸出:

enter first string.
taylor fall
enter second string.
c1:taylor c2:fall

粗體的是輸入。可以看到,一次輸入被分成了兩個字符串。第一次cin讀了taylor,第二次讀了fall。第一次讀完taylor之後,fall放在了讀入隊列中。當程序輸出了第二句後,再次調用cin讀入,就從隊列中讀入了fall。

每次讀入一行

如果需要一次輸入一整行,包括其中的空格,就需要面向行的方法getline和get。這兩個方法都會一直讀到換行符,不同在於,getline讀入結束後會將換行丟棄,而get會把換行符留在讀入隊列中。

getline()

調用方法:cin.getline(name, 20);

name是讀入的字符串要存入的數組的名字,20是讀入的字符串的長度上限。

get()

get方法的調用和getline相同,也是數組名字與上限。但要注意的是,上文提過,get方法不會丟棄它所遇到的換行符。也即是說,第一個get方法讀入了一行後,讀入隊列的頭位便是一個換行符,後續的get方法將全都會在這個換行符面前停下,無法讀到任何字符。

這個情況的解決方法是使用沒有參數的get方法:cin.get();這個方法會讀入單個字符,即便是換行符也會讀入。所以我們可以靠它來越過換行符:

cin.get(str1, 12);

cin.get();

cin.get(str2, 12);

而由於get和getline都會將調用它們的cin對象實例作爲返回值返回,所以我們可以直接採用如下寫法:

cin.get(str1, 12).get();

cin.getline(str1, 12).getline(str2, 11);

空行和其他問題

如果getline和get讀入了空行會怎麼樣?現在實行的是,當get(不是getline)讀入了一個空行,它會這個一個失敗元(failbit),這會把之後的輸入全部阻隔。要恢復輸入就要使用以下命令:

cin.clear();

另一個潛在問題是輸入的字符串可能長於分配的空間。如果輸入的字符串比參數裏的輸入上限長,get和getline都會把多於的字符留在輸入隊列。但getline會增加一個失敗元(failbit)阻隔後續輸入。

字符串與數字混合輸入

使用面向行的字符串讀入方法進行數字和字符串混合讀入可能會導致問題。比如:

int year;
cin >> year;
char name[4];
cin.getline(name, 4);

你先輸入年份1996,然後回車,你會發現輸入就結束了。year值爲1996,而name則什麼都沒有。

這是因爲輸入1996後的回車在cin第一次讀入1996這個整數後並沒有作處理。所以在getline方法調用時,讀入序列頭位就是換行符。就和上文提到的get方法導致的情況一樣,這次同樣可以靠cin.get()來解決。

或者,你也可以採用連續的調用。>>操作符和小數點調用一樣,也會返回cin對象實例,所以可以採用如下寫法:

(cin >> year).get();


string類

先看一段程序:

#include <iostream>
#include <string> 
int main()
{
	using namespace std;
	string str1;
	string str2 = "string2";
	
	cout << "enter str1:";
	cin >> str1;
	cout << "str2:" << str2 << endl;
	cout << "the third character of str2: " << str2[2];
 } 

從以上程序可以看出:

  • 可以用C式字符串來初始化string對象
  • 可以用cin將輸入的字符串存入string對象
  • 可以用cout來輸出string對象
  • 可以用數組下標來單獨訪問特定位置的字符

string類和字符數組最主要的區別就是,你可以將string類聲明爲一個簡單的變量,而不是一個數組。

string類的設計允許它自動重定義大小。比如string str1將str1長度初始化爲0.在用cin對其輸入時,程序會自動將str1重定義爲合適的長度。所以string類是更方便且安全的。

C++11的string初始化

C++11的大括號初始化同樣適用於string類:

string str1 = {"wow."};
string str2 {"yeah!"};

賦值、連接和添加

string有很多方面都比字符數組方便。

首先,你可以直接將一個string賦值給另一個string:

string str1;
string str2 = "hey.";
str1 = str2;

string類還簡化了字符串合併。你可以直接用加號把兩個string合併,也可以用+=把一個數組加到另一個的後面:

string str3;
str3 = str1 + str2;
str1 += str2;

更多的string類操作

ctring頭文件裏其實也定義了用於操作字符數組的方法:

#include <string>
#include <cstring>
int main()
{
    using namespace std;
    char c1[10];
    char c2[10] = "aaaaaa";
    string str1;
    string str2 = "bbb";
    
    //將第二個賦值給第一個
    str1 = str2;
    strcpy(c1, c2);

    //將第二個加在第一個之後
    str1 += str2;
    strcat(c1, c2);

    //獲得長度
    int len1 = str1.size();
    int len2 = strlen(c1);
}

但顯然,string類的方法用起來要方便得多。並且,字符數組的操作也存在潛在的問題。比如strcat(c1, c2),把c2加在c1之後再存入c1.c1原本長度爲6,c2長度也是6,二者相加長度爲12.而c1的空間只有10,這個方法就會導致內存泄漏。strcpy等方法也有同樣的問題。相對地,string類因爲可以自動重定義自身長度,它的方法就不會出現這種問題。C的庫其實也提供了strncat和strncpy這種安全的方法,但寫起來就更麻煩了。

更多的string類I/O

先前提過面向行的讀入方法cin.get和cin.getline。注意,這兩個方法也可以用於string類但用法不完全相同:

cin.getline(c1, 20);   //讀入到字符數組
getline(cin, str1);    //讀入到string類

從代碼可以看出,第二個getline並不是cin的成員方法,所以它需要cin對象作爲參數來傳入。同時參數裏去掉了讀入長度上限,因爲string類的長度是可以重定義的。

爲什麼第一個getline是istream裏的方法而第二個不是?因爲在string類出現之前,istream就已經是C++的一部分了。所以istream只能處理C++的基本類型比如int,double之類的,而無法處理string。

但爲什麼cin>>str1這種寫法又是可以的?因爲這是調用了cin的方法,而cin轉爲調用了來自string類的友鄰方法(friend function)。友鄰方法這個概念將會在11章講到。


結構(structure)介紹

結構(structure)是一個用戶自定義類型,其結構聲明定義了類型的數據屬性。在定義類型後,你就可以創建一個這個類型的變量。所以創建一個結構需要兩步。首先先寫出結構的定義,然後創建一個結構變量。寫法如下:

struct player
{
    char name[10];
    int point;
};

player david;

就像對象調用成員方法一樣,結構變量通過小數點單獨訪問它的成員數據,比如david.name;

在程序中使用結構

看如下程序:

#include <iostream>

struct player 
{
	char name[10];
	int point;
 };
 
int main()
{
	using namespace std;
	player player1 = 
	{
		"david",  //name值
		300       //point值 
	};
	
	cout << "player1's name:" << player1.name << endl;
	cout << "player1's point:" << player1.point << endl;
 } 

以上程序寫明瞭結構的定義以及結構變量的初始化格式,還有訪問結構的成員數據的方法。

C++11結構初始化

出現了大括號初始化,你就懂了,C++11當然也是對它作了更改,讓你可以省掉等號。還有你可以讓大括號內爲空,讓編譯器把結構內所有成員數據初始化爲0:

player player2 {"jack", 500};
player player3 {};

當然還有,數據類型窄化是不行的。

其他結構屬性

C++讓用戶自定義類型儘量和自有類型的相似。比如你可以將結構作爲參數傳給方法,也可以將其作爲返回值。同樣你也可以用等號將一個結構變量賦值給另一個同類型的結構變量。

你可以將結構定義和變量聲明寫到一起:

struct player
{
    char[] name;
    int point;
}player1, player2;

甚至可以直接把變量初始化寫上:

struct player
{
    char[] name;
    int point;
}player1 =
{
    "david",
    100
}

不過分開寫還是更清楚易讀性更高。

另外你還可以定義一種沒有名字的結構類型,就直接去除定義時的名字就好:

struct 
{
    char[] name;
    int point;
}player1;

這個結構變量叫player1,可以用來訪問其成員數據。但這個結構類型是沒有名字的,所以你無法再創建另一個這個結構類型的變量。

結構數組

player players[10]{
    {"david", 100},
    {"jack", 30}
};
cin >> players[2].name;
cout << players[4].point;

無需細說。

結構裏的bit域

C++和C一樣,允許你指定每個變量佔據的bit數。這一般是爲了創建與某些硬件註冊信息相對應的數據結構。域的類型應爲整形或枚舉類型(枚舉在本章稍後說到), 冒號之後跟着的數字就是要用的bit數。可以通過未命名的域名來創建空的區域。舉例:

struct player
{
    int point : 4;  //point值用4個bit
    int : 4;        //4個bit的空區間
    bool alive : 1; //alive用1個bit
}

這種操作一般還是用於底層編程。


聯合(unions)

聯合是一種可以容納多種數據類型但每次只能使用其中之一的數據格式。也即是說,比如一個結構可以容納一個int 和 一個long 和 一個double,相對的一個聯合可以容納一個int 或 一個long 或 一個double。看如下代碼:

union one4all
{
    int i;
    long l;
    double d;
}

one4all o;
o.i = 15;  //存入一個int
cout << o.i;
o.l = 13;  //存入一個long,之前的int丟失
cout << o.l;

因爲聯合每次只會通納其中一種類型的值,所以它的大小隻要只夠容納它最大的成員類型就行了。

聯合的用處是當一個數據可能會用到多種類型但永遠不會同時用到不同類型時,聯合可以節省空間。比如玩家的id,有的id是數字,有的是字符串,這時就可以將id定義爲一個聯合:

struct player
{
    char name[10];   
    union id
    {
        long id_l;
        char id_c[20];
    } player_id;
};
...
player player1;
...
cout << player1.player_id.id_c;

匿名聯合不需要名字,它的成員會變成共享一個地址的多個變量。當然,特定時刻這幾個變量只存在其中之一。舉例:

struct player
{
    union
    {
        long id_l;
        char id_c[10];
    };
};
...
player p1;
...
cout << p1.id_l;

因爲聯合是匿名的,所以id_l和id_c相當於是player的兩個共享地址的成員變量。這樣在訪問時就不需要再寫上聯合的名字了。


枚舉(enumeration)

C++enum是const之外的另一種創建符號常量的方式。看下面這一句:

enum weekday {monday, tuesday, wednesday, thursday, friday, saturday, sunday};

上面這句做了兩件事:

  • 它將weekday定義爲了一個新的類型;weekday稱爲一個枚舉類型(enumeration)。
  • 它定義了monday到sunday共7個符號常量對應整數值0-7.這些常量稱爲enumerator。

默認情況下,enumerator的值從0開始往後一一對應。但你也可以給他們賦特定的整型值,這個遲些說。

你可以用枚舉類型的名字來聲明一個枚舉變量:

weekday today;

不發生強轉直接賦值給枚舉變量的值只能是枚舉類型定義中出現過的enumerator值。比如:

today = monday;  //可以,monday是一個enumerator
today = 100;     //不行,100不是一個enumerator

也就是,一個weekday變量的取值只有7種。有些編譯器會在你試圖給它賦無效值,而另一些則發個警告。爲了最大的可移植性,這種無效賦值應被當做error來對待。

enumeration只定義了等號操作,加號等運算符是不適用的。

enumerator是整數類型,可以被升級爲int類型,但int不能自動轉爲枚舉類型:

int day = monday; //可以

day = monday + 2;//可以

weekay today = 1; //不行

你還可以用int來初始化一個枚舉變量,但這個值要存在於這個枚舉類型:

today = weekday(0);

設定enumerator值

enum month {jan, feb = 0, may = 5, jun};

上面這句有幾個要點:

  • jan是默認取值,爲0;
  • feb指定了取值爲0,就是說enumerator的值可以相同
  • may指定了取值5,jun在其後,值是上一個值+1,所以jun = 6

只能給enumerator指定整數值。早先只能指定int類型值,但現在指定long和long long也行。

enumeration值範圍

本來枚舉變量取值只能是枚舉類型中存在的值,但C++靠強轉擴展了這個範圍。現在每個枚舉類型都有一個範圍,在範圍內的整型都可以賦值給枚舉變量,即便enumerator中沒有這個值。比如:

enum weekday {monday = 1, friday = 5};
weekday today;

範圍定義:

  • 上界。先找到最大的enumerator值,然後找比這個值大的最小的二次冪,這個二次冪-1即是上界
  • 下界。先找到最小的enumerator值,如果這個值大於等於0,則下界爲0.如果最小值是負數,則其下界確定方法和上界類似。(如最小值是-6,則比6大的最小的二次冪爲8,這個二次冪-1爲7.得到的下界爲-7.)

指針(pointer)和解放內存

指針是一個存儲了值的地址的變量。先看看如何找到一個變量的地址。那就是使用&操作符。看以下代碼:

#include <iostream>
int main
{
    using namespace std;
    int a = 6;
    int b = 7;

    cout << "value:" << a;
    cout << "address:" << &a << endl;
    cout << "value:" << b;
    cout << "address:" << &b << endl;
}

輸出:

value:6address:0x6ffe3c
value:7address:0x6ffe38

cout在輸出地址的時候會特別地使用十六進制,因爲存儲地址通常都是使用十六進制的。可以看到變量b的存儲地址比變量a小,二者的差爲0x6ffe3c - 0x6ffe38 = 4。這是合理的因爲a是int,一個int佔4個byte。不同的系統,給出的地址是不一樣的,甚至變量存儲順序都不一樣。比如原書上是先存a後存b,而我的編譯器上是先存b的。

指針和C++哲學

面向對象編程和傳統面向過程編程不同在於,面型對象編程着重在運行過程中(runtime)進行選擇,而非在編譯時選擇。運行時選擇就像是在假期的時候,根據天氣和心情選擇要去的景點,而編譯時選擇則是事先排好時間表,而不管當時的環境。

運行時選擇提供了一種適應當前環境的靈活度。比如,給一個數組分配空間。傳統的方法是聲明一個數組。在C++裏聲明數組,你必須要給出一個特定的大小。故而在編譯完成時,數組的大小就是固定的了,這是編譯時選擇。也許你覺得20個元素的數組可以滿足80%的情況了,但特定情況下會需要處理200個元素。爲了安全,你就要聲明一個200個元素的數組。這就會使得你的程序大大地浪費空間。面向對象編程會通過將選擇延遲到運行時來讓程序更靈活。在程序開始運行後,你就可以知道你究竟是需要20元素的數組還是200元素的了。

簡單來說,通過面向對象編程你可以在運行的時候決定數組的大小。爲了實現這個,這門語言允許你在運行時創建一個數組——或其他類似的對象。C++的方法同樣,包含了使用關鍵詞new來請求合適的存儲空間和使用指針來定位新分配的存儲的位置。

在運行時選擇不是面向對象編程獨有的。但C++使得編寫代碼比C更直接一些。

一種特別的變量類型——指針——誕生了,專用於存儲值的地址。所以指針的名字就代表着地址。使用*操作符來訪問地址內存儲的值。(對的這個*就是乘號的那個*。C++會通過上下文來判斷這是乘號還是指針操作符。)看以下程序:

#include <iostream>
int main()
{
    using namespace std;
    int a = 6;
    int * p_a = &a;
	
	cout << "a:" << a << endl;
	cout << "*p_a" << *p_a << endl;
	
	cout << "&a:" << &a << endl;
	cout << "p_a:" << p_a << endl;
	
	*p_a += 1;
	cout << "a:" << a << endl; 
}

輸出:

a:6
*p_a6
&a:0x6ffe34
p_a:0x6ffe34
a:7

聲明和初始化指針

上面的程序中的聲明語句:

int * p_a;

這個說明了*p_a這個組合是一個int。而p_a自身則是一個指針。我們說法是p_a這個指針指向了一個int類型。p_a的類型是一個指向int的指針,或者更直接一些:int*。重複:p_a是一個指針,而*p_a是一個int。

在*兩邊的空格是可選的。傳統上,C程序員會這麼寫:

int *p_a;

這個是順應了*p_a是一個int這個概念。相對應地,許多C++程序員會這麼寫:

int* p_a;

這個則強調p_a是一個int*類型。甚至,你還可以把空格都去掉:

int*p_a;

不過注意的是,如下寫法會創建一個指針p1和一個普通int類型p2:

int* p1,p2;

你需要給每個指針都加一個*。

還有一點是對於一個指向double的指針和一個指向char數組的指針,它們指向的類型的大小是不同的,但它們本身大小是一樣的。因爲它們是存儲地址的,而地址的大小都是一樣的。就好像一棟別墅的門牌號是四位的,一棟辦公樓的門牌號同樣也是四位的。所以指針的大小並不會告訴你它指向的是什麼類型。一般來說一個指針大小在2到4個byte之間,取決於你的系統。

指針危險

隨意地使用指針會導致危險。最最重要的一點是如果你在C++中創建一個指針,計算機會分配內存來存儲一個地址,但它不會分配內存來存儲存了這個地址的地址。爲數據申請空間需要一個步驟,而如果省略了這個步驟,比如以下代碼,就會導致危險:

long * fellow;
*fellow = 2233;

fellow是一個指針,但它指向哪裏?這段代碼沒有寫明fellow指向的地址,那2233要存到哪裏去?這就會導致計算機產生不可預料的結果。

指針和數字

指針不是整數類型,儘管計算機一般地址地址當成整數來處理。概念上,指針是區別於整數的一個類型。數字是你可以用來運算的,但地址不是。把兩個地址加起來或乘起來是不知所謂的舉動。同樣,概念上,你不能向指針賦值一個數值:

int * pt = 0xb8000000; //不行,類型不符合

你可能知道這個0xb8000000是你係統裏面的某個地址,但程序並不能識別出來這是一個地址。如果你想要用數值來給指針賦值,你就要使用到強轉:

int * pt = (int8)0xb8000000;

使用new來分配內存

到目前,我們都是用一個變量的地址來初始化指針。指針看起來僅僅是訪問變量地址的第二種方式。但實際上,真正的指針應該是你在運行中分配未命名空間用於容納值時用到的。在這個情況下,指針就成了唯一可以定位那塊空間的手段。在C語言裏,你可以用malloc來分配空間,現在C++也可以,但C++還有更好的方法:用new操作符。

int * pt = new int;

new int這個部分說明了你想要一塊內存來容納一個int。new操作符用類型來得出需要多少的byte。然後它會找到一塊空間並將其返回。然後,你就可以將這個返回的空間賦值給指針pt。

用delete解放內存

用new申請內存只是C++內存管理包的一半,另一半則是delete操作符,用於將申請的內存在使用結束後歸還給內存池(memory pool)。這是高效使用內存非常關鍵的一步。範例如下:

int* pt = new int;
...
delete pt;

delete會將pt指向的內存塊擦除,但並不會刪除pt本身。你可以再給pt分配一塊新的內存塊。要注意平衡new和delete的數量,否則就可能會發生內存泄漏(memory leak)。已分配的內存可能無法被使用,導致程序不斷申請新的內存空間直到崩潰。

同時你也不能解放一塊已解放的內存空間。其結果是未定義的。

用new創建動態數組

int * ps = new int[10];

delete [] ps;

上面兩句是用new創建一個動態數組然後將其刪除。new操作符會返回數組第一個元素的地址,賦值給你定義的指針。delete和指針名之間的中括號意思是不要只擦除指針指向的地址,而是將整個數組都擦除。

以下是幾條使用new和delete的規則:

  • 不要用delete解放還未分配的內存
  • 不要用delete解放同一個內存塊兩次
  • 要用delete[]如果你是用的new[],如果沒用中括號new就不要中括號delete
  • 對空指針使用delete是安全的,什麼都不會發生

現在回到動態數組。注意ps指針是指向數組首位元素,也即是一個普通int,而非整個數組。所以記錄數組長度需要自己動手。雖然實際上程序有記錄數組分配到的空間以用於刪除用,但這對我們是不可用的。我們無法通過sizeof來獲得數組長度。

動態數組使用

#include <iostream>
int main()
{
    using namespace std;
    double * p3 = new double[3];
    p3[0] = 0.1;
    p3[1] = 0.2;
    p3[2] = 0.3;
    
    cout << "p3[0]:" << p3[0] << endl;
    p3 += 1;
    cout << "p3[0]:" << p3[0] << endl;
    p3 -= 1;
    delete [] p3;
}

輸出:

p3[0]:0.1
p3[0]:0.2

注意看,p3指針指向了new出來的數組第一位,和普通數組一樣,用下標可以單獨訪問元素。但這個下標代表的是從p3指針指向的元素數起的元素。當p3加一後,p3指向了數組第二位,這時的p3[0]就會訪問到原本的p3[1]。最後刪除數組時,刪除也是從p3指向的位置開始,所以在delete之前要把p3指向的地址移回原處。

另外注意的是加一這個運算可以用在指針身上因爲它是個變量,但這是不能用於一個數組的名稱的。


指針,數組和指針運算

指針和數組名稱的近似等價源於指針算法以及C ++如何在內部處理數組。先來看看算法。給整數變量加一就是讓其值加一,但給指針加一則是讓其值加它所指向的類型用到的byte的數量。給指向double的指針加一就是讓其值加8(假設該系統內一個double是8bytes)。

大部分情況下,C++將數組的名字理解爲其首位元素的地址。所以下列代碼:

double wages[3];
double * pw = wages;

讓指針pw指向了數組wages的首位。也即是我們有:wages = &wages[0] = 數組首位元素地址。

對於指針pw,pw[0]對系統來說和*(pw + 1)是完全一樣的。第二種寫法的意思是計算出需要訪問位置的地址,然後讀取該地址的元素。也就是說,系統會把所有的 數組名[下標] 轉爲 *(數組名 + 下標)。指針名也同樣。所以某些方面來說,你可以把指針名和數組名當成同樣的東西來用。但有一個不同的是,指針值可以變,但數組是一個常量:

pointername = pointername + 1;  //行
arrayname = arrayname + 1;      //不行

第二個不同是,對數組用sizeof會返回數組的長度,但對指針用sizeof則會返回指針的大小。

指針用法總結

  • 聲明:typeName * pointerName;
  • 賦值:pointerName = &varName(new typeName);
  • 訪問:*pointerName;
  • 數組:typeName arrayName[arrayLength];  (arrayName = &arrayName[0])
  • 指針算法:pointerName += integer;
  • 數組的動態與靜態綁定:
    typeName arrayName[size];                       //靜態
    typeName * pointerName = new typeName[size];    //動態
    delete [] pointerName;                          //歸還內存
  • 數組下標和指針下標:arrayName[notation] = *(arrayName + notation)    pointerName[notation] = *(pointerName + notation)

指針和字符串

數組和指針的特殊關係擴展到了C式字符串。看如下程序:

char pet[10] = "cat";
cout << pet << endl;

數組名代表了其首位的元素地址,所以cout語句中的pet是存有字符'c'的元素地址。cout對象假定這個char是一個字符串的開始,cout將會從該字符開始一直向後逐位輸出直到遇到字符串的結束——空字符\0.

這當中關鍵在於pet不是一個數組名而是作爲一個字符的地址。這意味着你可以向cout輸入一個指向char的指針,因爲它也是一個char的地址。

使用new創建動態結構

struct s
{
    int a;
    int b;
};

s * ps = new s;
cin >> ps->a;
cin >> (*ps).b;
delete ps;

上面程序中涉及了兩種通過結構指針訪問結構內元素的方法。一是通過指針名和箭頭(->)操作符訪問。二是*和指針名組合,代表指針指向的結構變量,然後通過小數點操作符訪問成員元素。

自動存儲,靜態存儲,動態存儲

自動存儲:

正常在方法裏定義的變量就是自動存儲,稱爲自動變量(automatic variables)。這意爲這些變量會在包含它們的方法被調用時自動出現,然後在方法結束時被丟棄。實際上,自動變量屬於區塊(block)自有。區塊就是大括號之間的代碼稱爲一個區塊。到目前一個區塊就是一個方法。但下一章會講到方法內的區塊。

自動變量一般存在棧(stack)內。變量會順序存入棧內,然後反序取出。也即是先進後出。先定義,後歸還。

靜態存儲:

靜態存儲會貫穿整個程序的執行過程。有兩種使變量靜態的方法,一是在方法外將其定義。而是使用關鍵詞static:

static double d = 3.0;

K&R C語言中,只允許初始化靜態的數組和結構,但C++2.0和ANSI允許初始化自動數組和結構。但,某些C++接口並未實現自動數組和結構。

第九章將會詳細討論靜態存儲。

動態存儲:

new和delete提供了更靈活的方法。它們管理的內存池在C++意爲堆(heap)和自由內存(free store)。這個池和用於靜態和自動變量的內存是分開的。new和delete讓你可以更好地控制程序使用的內存。但同時也複雜化了內存管理。在棧裏,使用中的內存永遠都是連續的,但使用new和delete管理的內存則有可能出現空擋,這就讓分配內存複雜了。


數組替代方案

本章早些提到了vactor和array模板類,作爲基礎數組的替代。

vector模板類

vector的大小可以在運行時定義,還可以往內添加新的元素。基本上這就是使用new創建動態數組的一個替代。事實上,在vector內部就是用new和delete來管理內存的。

vector範例:

#include <vector>
...
using namespace std;
vector<int> vi;        //創建一個長爲0的int數組
int n;
cin >> n;
vector<double> vd(n);  //創建一個n個double的數組

 vector的大小可以是整數變量或常量。

array模板類(C++11)

vector比基礎array類型有更大的容量,但這會略微地降低效率。如果你只想要一個固定大小的數組,更好的還是用C++自有類型。但那並不方便也不安全。C++11爲此新加了array模板類,作爲std命名空間的一員。就像自有類型一樣,array對象有固定的大小且使用棧而非自由存儲。所以它和自有類型效率一樣。而它又有着更方便的使用和更高的安全度。

範例:

#include <array>
...
using namespace std;
array<int, 5> ai;
array<double, 4> ad = {1.2, 2.1, 1.0, 3.3};

array的大小隻能是常量。

數組,vector對象,array對象比較

#include <iostream>
#include <vector>
#include <array>  
int main()
{
    using namespace std;
    // C,原版C++ 
    double a1[4] = {1.0, 1.1, 1.2, 1.3};
    // C++98 STL
    vector<double> a2(4);  //創建有4個元素的vector
	// 98裏沒有方便的初始化方法 
	a2[0] = 1.0;
	a2[1] = 1.1;
	...
	// C++11 創建並初始化array對象
	array<double, 4> a3 = {1.0, 1.1, 1.2, 1.3}; 
	array<double, 4> a4;
	a4 = a3;     //同大小的可以相互賦值
	//使用數組下標
	cout << "a[1]:" << a[1] << "at" << &a[1] << endl;
	//越界
	a1[-2] = 2.0;
	cout << "a1[-2]:" << a1[-2] << "at" << &a1[-2]  << endl;
	cout << "a3[2]:" << a3[2] << endl;
}

注意到最後的下表越界。a1[-2]其實是*(a1 - 2),也就是數組首位元素地址往前兩個double的地址。這其實就不在數組範圍內了,但C++並不對這種行爲識別爲錯誤(error)。

那麼vector和array類有針對這種情況的應對嗎?有的,那就是at方法:

a2.at(1);  //相當於a2[1]

調用at方法將會捕捉越界的下標,然後報錯並默認終止程序。


總結

  • 數組,結構和指針是C++的三種複合類型。數組可以在單個數據對象中容納相同類型的複數的值。使用索引或下標可以單獨訪問元素
  • 結構可以在單個數據結構容納複數的不同類型的值,然後使用小數點(.)來訪問其成員。使用結構第一步是創建一個結構的模型來定義結構容納的成員。結構的名字就成爲了一個新的類型標識。接着就可以使用這個名字來聲明一個這個類型的結構變量。
  • 一個聯合可以容納單個值,但這個值可以是多個類型,其成員名標識當前它是哪個類型。
  • 指針是一個設計用於容納地址的變量。我們說指針指向它容納的地址。指針聲明一定會包含其指向的對象的類型。對指針使用*操作符會顯示指針指向的地址內的值。
  • 字符串是空字符(null character)\0結尾的一連串字符。一個字符串可以是雙引號括起的字符串常量,在這個情況下結尾的空字符是隱含的。可以把字符串存入字符數組,然後通過將指針初始化爲指向字符串首位字符來讓這個指針代表這個字符串。strlen這個方法返回字符串的長度,不算空字符。strcpy方法將字符串從一個地址複製到另一個。當使用這兩個方法要include頭文件cstring或string.h
  • string頭文件支持的C++string類是一個對用戶更友好的處理字符串的替代方法。特別地,string類會自動重定義大小來適應需要存儲的字符串,且你可以用等號來複制一個字符串。
  • new操作符可以在程序運行中爲數據對象申請空間。這個操作符會返回申請到的空間的地址,然後可以將這個地址賦值給指針。唯一的訪問這個地址的方法就是通過這個指針。如果數據對象是一個簡單的變量,可以用*操作符到指代它的值。如果數據對象是一個數組,你可以把指針當做數組名來訪問其元素。如果數據對象是一個結構,你可以用箭頭(->)操作符來訪問機構的成員。
  • 指針和數組是高度相關的。如果ar是一個數組的名字,那麼ar[i]等價於*(ar + i),數組的名字等價於數組首位元素的地址。所以數組名的作用和指針是相同的。同樣的,你可以用指針名和數組下標來訪問new分配的數組的元素。
  • new和delete操作符讓你可以顯式地管理數據對象的空間的分配與歸還至內存池。自動變量,是在方法內聲明的變量。靜態變量是定義在方法之外的變量或用關鍵字static定義的變量,這靈活度較低。自動變量在包含它的區塊(一般是方法定義)開始時出現,然後在區塊結束時失效。靜態變量則持續整個程序的運行過程。
  • 標準模板庫(STL)是在C++98標準添加的,提供了vector模板類,這模板類提供了自定義動態數組的替代。C++11提供了array模板類,則是一個定長數組的替代。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章