C++從零開始(五)——何謂指針
原始出處:網絡
本篇說明C++中的重中又重的關鍵——指針類型,並說明兩個很有意義的概念——靜態和動態。
數組
前面說了在C++中是通過變量來對內存進行訪問的,但根據前面的說明,C++中只能通過變量來操作內存,也就是說要操作某塊內存,就必須先將這塊內存的首地址和一個變量名綁定起來,這是很糟糕的。比如有100塊內存用以記錄100個工人的工資,現在要將每個工人的工資增加5%,為了知道各個工人增加了後的工資為多少,就定義一個變量float a1;,用其記錄第1個工人的工資,然後執行語句a1 += a1 * 0.05f;,則a1裡就是增加後的工資。由於是100個工人,所以就必須有100個變量,分別記錄100個工資。因此上面的賦值語句就需要有100條,每條僅僅變量名不一樣。
上面需要手工重復書寫變量定義語句float a1;100遍(每次變一個變量名),無謂的工作。因此想到一次向操作系統申請100*4=400個字節的連續內存,那麼要給第i個工人修改工資,只需從首地址開始加上4*i個字節就行了(因為float占用4個字節)。
為了提供這個功能,C++提出了一種類型——數組。數組即一組數字,其中的各個數字稱作相應數組的元素,各元素的大小一定相等(因為數組中的元素是靠固定的偏移來標識的),即數組表示一組相同類型的數字,其在內存中一定是連續存放的。在定義變量時,要表示某個變量是數組類型時,在變量名的後面加上方括號,在方括號中指明欲申請的數組元素個數,以分號結束。因此上面的記錄100個工資的變量,即可如下定義成數組類型的變量:
float a[100];
上面定義了一個變量a,分配了100*4=400個字節的連續內存(因為一個float元素占用4個字節),然後將其首地址和變量名a相綁定。而變量a的類型就被稱作具有100個float類型元素的數組。即將如下解釋變量a所對應內存中的內容(類型就是如何解釋內存的內容):a所對應的地址標識的內存是一塊連續內存的首地址,這塊連續內存的大小剛好能容納下100個float類型的數字。
因此可以將前面的float b;這種定義看成是定義了一個元素的float數組變量b。而為了能夠訪問數組中的某個元素,在變量名後接方括號,方括號中放一數字,數字必須是非浮點數,即使用二進制原碼或補碼進行表示的數字。如a[ 5 + 3 ] += 32;就是數組變量a的第5 + 3個元素的值增加32。又:
long c = 23; float b = a[ ( c – 3 ) / 5 ] + 10, d = a[ c – 23 ];
上面的b的值就為數組變量a的第4個元素的值加10,而d的值就為數組變量a的第0個元素的值。即C++的數組中的元素是以0為基本序號來記數的,即a[0]實際代表的是數組變量a中的第一個元素的值,而之所以是0,表示a所對應的地址加上0*4後得到的地址就為第一個元素的地址。
應該注意不能這樣寫:long a[0];,定義0個元素的數組是無意義的,編譯器將報錯,不過在結構或類或聯合中符合某些規則後可以這樣寫,那是C語言時代提出的一種實現結構類型的長度可變的技術,在《C++從零開始(九)》中將說明。
還應注意上面在定義數組時不能在方括號內寫變量,即long b = 10; float a[ b ];是錯誤的,因為編譯此代碼時,無法知道變量b的值為多少,進而無法分配內存。可是前面明明已經寫了b = 10;,為什麼還說不知道b的值?那是因為無法知道b所對應的地址是多少。因為編譯器編譯時只是將b和一個偏移進行了綁定,並不是真正的地址,即b所對應的可能是Base - 54,而其中的Base就是在程序一開始執行時動態向操作系統申請的大塊內存的尾地址,因為其可能變化,故無法得知b實際對應的地址(實際在Windows平台下,由於虛擬地址空間的運用,是可以得到實際對應的虛擬地址,但依舊不是實際地址,故無法編譯時期知道某變量的值)。
但是編譯器仍然可以根據前面的long b = 10;而推出Base - 54的值為10啊?重點就是編譯器看到long b = 10;時,只是知道要生成一條指令,此指令將10放入Base - 54的內存中,其它將不再過問(也沒必要過問),故即使才寫了long b = 10;編譯器也無法得知b的值。
上面說數組是一種類型,其實並不准確,實際應為——數組是一種類型修飾符,其定義了一種類型修飾規則。關於類型修飾符,後面將詳述。
字符串
在《C++從零開始(二)》中已經說過,要查某個字符對應的ASCII碼,通過在這個字符的兩側加上單引號,如A就等同於65。而要表示多個字符時,就使用雙引號括起來,如:"ABC"。而為了記錄字符,就需要記錄下其對應的ASCII碼,而ASCII碼的數值在-128到127以內,因此使用一個char變量就可以記錄一個ASCII碼,而為了記錄"ABC",就很正常地使用一個char的數組來記錄。如下:
char a = A; char b[10]; b[0] = A; b[1] = B; b[2] = C;
上面a的值為65,b[0]的值為65,b[1]為66,b[2]為67。因為b為一個10元素的數組,在這其記錄了一個3個字符長度的字符串,但是當得到b的地址時,如何知道其第幾個元素才是有效的字符?如上面的b[4]就沒有賦值,那如何知道b[4]不應該被解釋為字符?可以如下,從第0個元素開始依次檢查每個char元素的值,直到遇到某個char元素的值為0(因為ASCII碼表中0沒有對應的字符),則其前面的所有的元素都認為是應該用ASCII碼表來解釋的字符。故還應b[3] = 0;以表示字符串的結束。
上面的規則被廣泛運用,C運行時期庫中提供的所有有關字符串的操作都是基於上面的規則來解釋字符串的(關於C運行時期庫,可參考《C++從零開始(十九)》)。但上面為了記錄一個字符串,顯得煩瑣了點,字符串有多長就需要寫幾個賦值語句,而且還需要將末尾的元素賦值為0,如果搞忘則問題嚴重。對於此,C++強制提供了一種簡寫方式,如下:
char b[10] = "ABC";
上面就等效於前面所做的所有工作,其中的"ABC"是一個地址類型的數字(准確的說是一初始化表達式,在《C++從零開始(九)》中說明),其類型為char[4],即一個4個元素的char數組,多了一個末尾元素用於放0來標識字符串的結束。應當注意,由於b為char[10],而"ABC"返回的是char[4],類型並不匹配,需要隱式類型轉換,但實際沒有進行轉換,而是做了一系列的賦值操作(就如前面所做的工作),這是C++硬性規定的,稱為初始化,且僅僅對於數組定義時進行初始化有效,即如下是錯誤的:
char b[10]; b = "ABC";
而即使是char b[4]; b = "ABC";也依舊錯誤,因為b的類型是數組,表示的是多個元素,而對多個元素賦值是未定義的,即:float d[4]; float dd[4] = d;也是錯誤的,因為沒定義d中的元素是依次順序放到dd中的相應各元素,還是倒序放到,所以是不能對一個數組類型的變量進行賦值的。
由於現在字符的增多(原來只用英文字母,現在需要能表示中文、日文等多種字符),原來使用char類型來表示字符,最多也只能表示255種字符(0用來表示字符串結束),所以出現了所謂的多字節字符串(MultiByte),用這種表示方式記錄的文本文件稱為是MBCS格式的,而原來使用char類型進行表示的字符串稱為單字節字符串(SingleByte),用這種表示方式記錄的文本文件稱為是ANSI格式的。
由於char類型可以表示負數,則當從字符串中提取字符時,如果所得元素的數值是負的,則將此元素和下一個char元素聯合起來形成一short類型的數字,再按照Unicode編碼規則(一種編碼規則,等同於前面提過的ASCII碼表)來解釋這個short類型的數字以得到相應的字符。
而上面的"ABC"返回的就是以多字節格式表示的字符串,因為沒有漢字或特殊符號,故好象是用單字節格式表示的,但如果:char b[10] = "AB漢C";,則b[2]為-70,b[5]為0,而不是想象的由於4個字符故b[4]為0,因為“漢”這個字符占用了兩個字節。
上面的多字節格式的壞處是每個字符的長度不固定,如果想取字符串中的第3個字符的值,則必須從頭開始依次檢查每個元素的值而不能是3乘上某個固定長度,降低了字符串的處理速度,且在顯示字符串時由於需要比較檢查當前字符的值是否小於零而降低效率,故又推出了第三種字符表示格式:寬字節字符串(WideChar),用這種表示方式記錄的文本文件稱為是Unicode格式的。其與多字節的區別就是不管這個字符是否能夠用ASCII表示出來,都用一個short類型的數字來表示,即每個字符的長度固定為2字節,C++對此提供了支持。
short b[10] = L"AB漢C";
在雙引號的前面加上“L”(必須是大寫的,不能小寫)即告訴編譯器此雙引號內的字符要使用Unicode格式來編碼,故上面的b數組就是使用Unicode來記錄字符串的。同樣,也有:short c = LA;,其中的c為65。
如果上面看得不是很明白,不要緊,在以後舉出的例子中將會逐漸了解字符串的使用的。
靜態和動態
上面依然沒有解決根本問題——C++依舊只能通過變量這個映射元素來訪問內存,在訪問某塊內存前,一定要先建立相應的映射,即定義變量。有什麼壞處?讓我們先來了解靜態和動態是什麼意思。
收銀員開發票,手動,則每次開發票時,都用已經印好的發票聯給客人開發票,發票聯上只印了4個格子用以記錄商品的名稱,當客人一次買的商品超過4種以上時,就必須開兩張或多張發票。這裡發票聯上的格子的數量就被稱作靜態的,即無論任何時候任何客人買東西,開發票時發票聯上都印著4個記錄商品名稱用的格子。
超市的收銀員開發票,將商品名稱及數量等輸入電腦,然後即時打印出一張發票給客人,則不同的客人,打印出的發票的長度可能不同(有的客人買得多而有的少),此時發票的長度就稱為動態的,即不同時間不同客人買東西,開出的發票長度可能不同。
程序無論執行多少遍,在申請內存時總是申請固定大小的內存,則稱此內存是靜態分配的。前面提出的定義變量時,編譯器幫我們從棧上分配的內存就屬於靜態分配。每次執行程序,根據用戶輸入的不同而可能申請不同大小的內存時,則稱此內存是動態分配的,後面說的從堆上分配就屬於動態分配。
很明顯,動態比靜態的效率高(發票長度的利用率高),但要求更高——需要電腦和打