本篇是《》系列的附篇。因友人一再認為《C++從零開始》系列中對指針的闡述太過簡略,而提出的各個概念又雜七混八,且關於指針這一C++中的重要概念的運用少之又少,故本篇重點說明在《C++從零開始》系列中提出的數字、地址、指針等基礎概念,並給出指針的語義,說明指針和數組的關系,闡述多級指針、多維數組、函數指針、數組指針、成員指針的語義及各自的運用。
數字、操作符、類型、類型修飾符
在《C++從零開始(三)》中已經說明,其實CPU連二進制數都不認識,其只能處理狀態,而它能處理的狀態恰好能用二進制數表示,故稱CPU只認識二進制數。應注意由於CPU認識二進制數是認識其所表示的狀態,並不是數學意義上的二進制數,因此CPU並不認識十進制數20.不過將20按數學規則轉成二進制數10100後,運氣極好地CPU的設計人員將加法指令定義成狀態10100和狀態10100相加將得到狀態101000,而這個二進制數按數學規則轉成十進制數正好是40,即CPU連加減乘除都不會,只會以不同的方式改變狀態,而CPU的設計人員專門將那些狀態的改變方式定義成和數學上的加減乘除一樣進而使CPU表現得好像會加減乘除。
所以,為了讓CPU執行一條指令,則那條指令所涉及的東西只能是二進制數,就必須有規則將給出的數學意義上的數轉換成二進制數。如前面的十進制轉二進制的規則,在《C++從零開始(二)》中提到的原碼、補碼、IEEE real*4等。而要編寫C++代碼,則一定要在代碼中能體現上述的轉換規則,並且要能在代碼上體現欲被轉換的數學意義上的數,這樣編譯器才能根據我們書寫的代碼來決定CPU要操作的東西的二進制表示。對此,C++中用類型表現前者,用數字體現後者,用操作符表示CPU的指令,即CPU狀態的變換方式。
因此,為了讓CPU執行加法指令,代碼上書寫加法指令對應的操作符——“+”,“+”的兩側需要接兩個數字,而數字的類型決定了如何將數字所表示的數學上的數轉換成二進制數。應注意數字是編譯級的概念,不是代碼級的概念,即無法在代碼上表現數字,而只能通過操作符的計算的返回來獲得數字。因為任何操作符都要返回數字(不返回數字的操作符也可以通過返回類型為void的數字來表示以滿足這一說法),而最常見的一種得到數字的操作符就是通常被稱作常數的東西,如6.3、5.2f、0772等。我在《C++從零開始(二)》中將其稱作數字的確引起概念混淆,在此特澄清。
應注意只要是返回數字的東西就是操作符,故前面的常量也是一種操作符。對於變量、成員變量及函數,在《C++從零開始》系列中已多次強調它們都是映射元素,直接書寫變量名、成員變量名和函數名將分別返回各自所映射的數字,即變量名函數名等也都是操作符。
數字是具有類型的,C++提供了自定義類型struct、class等來自定義復雜的類型,但不僅如此,C++還提供了更值得稱贊的東西——類型修飾符。在《C++從零開始(五)》中已經說明,類型修飾符就是修飾類型用的,即按某種規則改變被修飾類型(稱作原類型)所表征的數字轉換規則。如豬血羊血和豬肉羊肉,這裡的“血”和“肉”都是類型修飾符,改變其各自的原類型——“豬”和“羊”。上面感覺更像後者修飾前者而非前者修飾後者,如豬血中的“血”是主語而“豬”是定語。即類型修飾符其實是以原類型的信息來修改其自身所表征的那個數字轉換規則。這就如稱“血”、“肉”是一種東西一樣,也說某類型是指針類型、引用類型、數組類型、函數類型等。
在《C++從零開始》系列中共提出下面幾種類型修飾符——引用“&”、指針“*”、數組“[]”、函數“()”、函數調用規則“__stdcall”、偏移“<自定義類型名>::”、常量“const”和地址類型修飾符。其中的地址類型修飾符是最混亂的。
在《C++從零開始(三)》中已經說明地址在32位操作系統中就是一個數,這個數經常以32位長的二進制數表示,以唯一標識一特定內存單元。而一個數字的類型是地址類型時(因為有地址類型修飾符,就好像一個數字是數組類型時),就將這個數字所代表的數學意義上的數用二進制表示,以標識出一個內存單元,然後按照原類型的規則來解釋那塊內存單元及其後續單元的內容(類型的長度可能不止一個字節,而地址類型是類型修飾符,故一定有原類型)。由於變量映射的數實際是地址,故變量所映射的數字就是地址類型的。如long a;,假設a映射的是3006,當書寫a = 3;時,由於a是變量名,故返回a所映射的數字3006,類型是long類型的地址類型。由於是地址類型,“=”操作符的語法檢查成功(這是類型的另一個用處——語法檢查,就好像動名形容詞一樣),執行“=”操作符的計算。
應注意C++並未提出地址類型修飾符這個概念,只是我出於語法上的完備而提出的,否則要涉及更多的無謂概念和規則,如*( p + 1 ) = 20; a[2] = 3;等的解釋將復雜化,故在《C++從零開始》系列中提出地址類型的數字這個概念,旨在以盡量少的概念解釋盡量多的語法。
最常用的類型修飾符——指針和數組
在《C++從零開始(五)》中已說明指針只是一種類型修飾符。一個數字是指針類型時,將這個數字所代表的數學意義上的數用二進制表示並返回。前面已說過數字的用處就是轉換其代表的數為二進制數,其類型僅說明如何轉換,而指針類型所代表的規則就是按數學規則變成二進制數,而不管其原類型是何種類型。由於不受原類型的影響,故指針類型總是固定長度(對成員指針則不一定),在32位操作系統上是四個字節。
如long a; long *p = &a;。假設a映射的是3006,p映射的是3010.對於*p = 3;,p這個操作符返回類型為long的指針類型的地址類型的數字3010,即這個數字的類型被兩個類型修飾符兩次修飾,由於最後是被地址修飾,故3010是地址類型的數字,而其原類型是long的指針類型。故*p返回類型為long類型的地址類型的數字3006,然後執行“=”操作符的計算。
這裡請注意兩個操作符——取內容操作符“*”和取地址操作符“&”。在《C++從零開始(五)》中也強調過,它們其實名不副實,應該叫類型轉換操作符才對。即前者後接指針類型的數字,返回的數字原封不動,僅將其類型變為地址類型;後者後接地址類型的數字,返回的數字原封不動,僅將其類型變為指針類型。這有點耍小聰明的感覺,但請注意:long *p1 = 0; long *p2 = &*p1;如果“*”的操作是取內容,則&*p1將先取地址為0的內存單元的內容,這將引起內存訪問違規,但實際並不會,因為“*”僅轉換類型而非取內容(取內容是由地址類型的數字的計算來實現的)。
前面已說明,在指針類型的數字返回二進制數時,並不需要原類型的參與,即類型為long*的數字3006和類型為char*的數字3006返回的二進制數都一樣,都是3006對應的二進制數。那麼為什麼要讓指針是類型修飾符以帶個不用的原類型?根據前面即可看出指針類型的原類型是給取內容操作符“*”用的。但它還有個用處,因為數組類型修飾符的加入,使得指針多了一個所謂的運算功能,在此先看看數組類型。
在《C++從零開始(五)》中已詳細說明數組的修飾功能是將原類型的元素重復多個連續存放以此形成一個新的類型。如long a[10];,a的類型就是long[10],長度為10*sizeof(long)=40個字節,而char[7]類型所對應的長度就是7*sizeof(char)=7個字節。一個數字的類型是數組類型時,因這個數字的長度可一個字節,可一萬個字節,故這個數字一定被存放在某塊內存中,而數組類型的數字返回的二進制數就是其被存放的內存的首地址。所以前面提到的常數就不能返回一個數組類型的數字,因其沒有給出一塊內存來存放數組類型的數字。
這裡有點混亂,注意數字不一定非要被內存所存儲。對於long a[3] = { 45, 45, 45 };,假設a映射的數字是3000,則表示以long[3]的規則解釋內存單元3000所記錄的數字,這個數字的長度是3*sizeof(long)=12個字節,它的值由於數組類型是長度可變的而決定使用3000(記錄它的內存的地址)來代表它,實際是3個值為45的數。所以a;將先返回long[3]類型的地址類型的數字3000,然後計算此地址類型的數字而返回其原類型的數字,由於原類型是long[3],而這個數字存放在3000所標識的內存處,故最後返回3000所對應的二進制數。
容易發現指針返回的是一個地址,數組也是一個地址,當它們原類型相同時,後者可以隱式類型轉換為前者,但反之不行,因為數組還具備元素個數這個信息,即long[2]和long[3]的原類型相同,但類型不同。因此有:long a[3]; long *p = a;。這裡沒任何問題,假設a映射的是3000,則p的值就是3000.因此*p = 3;就是將3放到3000處存放的數組類型的數字的第0個元素中。為了放到第1個和第2個元素中,C++提供了一個所謂的指針運算功能,如下:*( p + 1 ) = 4; *( p + 2 ) = 5;。這裡就把4放到第1個元素,5放到第2個元素中。對*( p + 1 ) = 4;,p返回一個long*的數字3000,而p + 1返回long*的數字3004,然後繼續後續計算。同理,p + 2返回類型為long*的數字3000+2*sizeof(long)=3008.即指針只能進行整數加減,如:char *p1 = 0; p1++; p1 = p1 + 5 * 8 - 1; short *p2 = 0; p2 += 11; p2——;上面p1的值為40,p2的值也為40,因為p1的原類型是char而p2的是short.因此為了獲得數組的第2個元素的值,需*( p + 2 );,這很明顯地不便於閱讀,為此C++專門提供了一個下標操作符“[]”,其前面接指針類型的數字,方括號中放一整型數字,將指針類型的數字換成地址類型,再將值按前面提到的指針運算規則變換,返回。如long a[4]; long *p = a;,假設a映射的是3000.則a[2] = 1;等效於*( p + 2 ) = 1;,a[2]前面接的是long*類型的數字3000(隱式類型轉換,從long[4]轉成long*),加2*sizeof(long),返回3008,類型則簡單地變成long類型的地址類型。由於“[]”僅僅只是前面說的指針運算的簡化易讀版本,故也可a[-1] = 3;,其等效於*( p - 1 ) = 3;。由於“[]”前接指針,故也可p[-1] = 3;,等效於a[-1] = 3;。
算規則變換,返回。如long a[4]; long *p = a;,假設a映射的是3000.則a[2] = 1;等效於*( p + 2 ) = 1;,a[2]前面接的是long*類型的數字3000(隱式類型轉換,從long[4]轉成long*),加2*sizeof(long),返回3008,類型則簡單地變成long類型的地址類型。由於“[]”僅僅只是前面說的指針運算的簡化易讀版本,故也可a[-1] = 3;,其等效於*( p - 1 ) = 3;。由於“[]”前接指針,故也可p[-1] = 3;,等效於a[-1] = 3;。