程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> C++從零開始之指針及其語義和運用

C++從零開始之指針及其語義和運用

編輯:關於C++

本篇是《C++從零開始》系列的附篇。因友人一再認為《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;。

類型修飾符的重復修飾——多級指針和多維數組

前面提到的類型修飾符中,只有指針、數組和偏移三個類型修飾符是可以連續重復修飾類型的。偏移重復修飾以表示類型嵌套,指針重復修飾被稱作多級指針,數組重復則稱作多維數組。這看起來好像變復雜了,實際完全沒有,只用記住一點:類型修飾符修飾某一原類型後形成一種新的可被認為是原類型的類型。

long **p;。p映射的數字是一long**的地址類型的數字,則p;返回的數字的類型就是long**.對於*p;就可將p返回的數字的類型看作是long*的指針類型,原類型為long*,故*p返回的數字的類型是long*的地址類型的數字,而**p;則返回long的地址類型的數字。

對於long a[2][3];也是同樣。由於其原類型是long[3],故可以long ( *p )[3] = a;,進而a[1]返回的是long[3]的地址類型的數字,而a[1][2]返回的就是long的地址類型的數字。

注意為什麼a的原類型不是long[2]而是long[3].在《C++從零開始(五)》中已經說明,當多個類型修飾符同時修飾時,修飾順序是從左到右,而相同連續修飾符的修飾則從右到左以符合人的習慣,而“()”則降低修飾優先度。如long*[2][3]是long的指針類型的數組[3]類型的數組[2]類型,即它是一個數組類型,有兩個元素,原類型是long*[3].也就是“[2]”最後修飾,優先級最低(從其所修飾的數字的類型方面來看,也可認為其優先級最高)。而long*[3]*[2][3]卻是一個錯誤的類型,雖然按照前面所說的從左到右進行修飾沒有任何問題,但C++規定變量定義時,指針類型修飾符必須在變量名的左側,數組類型修飾符必須在右側以符合人的閱讀習慣,也因此才會在類型中出現“()”以降低修飾優先度。所以long*[3]*[2][3]應寫成long *( *[2][3] )[3],欲定義變量則long *( *p[2][3] )[3];,其中最後修飾的是“[2]”(不是“[3]”,因相同連續修飾符修飾時是從右到左而非從左到右)。故p是一個有兩個元素的數組,原類型為long *( *[3] )[3],而long *( *( *pp )[2][3] )[3] = &p;沒有任何問題,因為最後修飾的是“*”,而原類型為long *( *[2][3] )[3].

除此以外還應注意一件事——不管什麼多級指針,其長度都為4字節(這是對於32位操作系統,而成員指針可能不止4個字節),但數組的維數越多,類型的長度就越長(當然,如果元素個數為1則長度沒有變化)。如long ***p;,此時只分配4個字節內存空間,而long a[2][3][5];則分配2*3*5*sizeof(long)=120個字節的內存空間。如下:

long a = 0; long *p = &a, **pp = &p; long b[2][3][4];

假設上面a映射的是3000,則p就映射3004,pp就映射3008,而b映射3012.如上賦值後,a的值為0,類型為long;p的值為3000,類型為long*;pp的值為3004,類型為long**;b的值為3012,類型為long[2][3][4].

對於*( *( pp + 1 ) ) = 5;,pp返回類型為long**的數字3004,而原類型long*的長度是4個字節,故pp + 1返回類型為long**的數字3008,而*( pp + 1 )僅轉換類型,返回類型為long*的地址類型的數字3008,返回類型為long*的數字3004,故*( *( pp + 1 ) )返回類型為long的地址類型的數字3004,而*( *( pp + 1 ) ) = 5;則將5按照long的存放規則放到3004所標識的內存中,結果p的值變為5而不再是3000(運氣極好地5是正數,此時long類型的數字轉換規則和long*一樣),進而如果再繼續*p = 1;將錯誤(應注意上面是假設編譯器順序安放a、p和pp,進而使pp的地址較p多4.不同的編譯設置和編譯器將不一定如上順序安放局部變量,則*( *( pp + 1 ) ) = 5;將有可能失敗)。

對於*( *( *( b + 1 ) + 1 ) + 1 ) = 5;,b返回類型為long[2][3][4]的數字3012,原類型為long[3][4],則b + 1將先進行隱式類型轉換以將3012轉換為long(*)[3][4],而sizeof(long[3][4])=48字節,則b + 1將返回類型為long(*)[3][4]的數字3012+48=3060,而*( b + 1 )返回類型為long[3][4]的地址類型的數字3060,再返回類型為long[3][4]的數字3060.則*( b + 1 ) + 1返回類型為long(*)[4]的數字3060+sizeof(long[4])=3076,同理,*( *( *( b + 1 ) + 1 ) + 1 )返回long類型的地址類型的數字3076+sizeof(long)=3080,將5放在3080所標識的內存中。由前面對“[]”的說明可知*( *( *( b + 1 ) + 1 ) + 1 ) = 5;等效於b[1][1][1] = 5;,可如上自行推驗。應注意雖然b是多維數組,但它仍是一塊連續的內存空間。

為什麼要有多級指針和多維數組?long a[3][4];和long a[12];都是分配一塊連續的48字節內存空間,它們有什麼區別?何時用前者何時用後者?在《C++從零開始》系列中強調要按語義編寫程序,因此只要明確指針和數組的語義就能有條理地使用它們了。

數組的語義及運用——矢量和容器

一種類型,由幾個相同類型的元素共同構成,這就是數組類型修飾符的語義。這正好可以用來映射線性代數中的矢量,如二維平面上的點坐標就是二維矢量。假設用double來記錄點坐標的分量,則double a[2];就可以認為定義了一個二維點坐標,如果想要更具可讀性,可typedef double POINT_2D[2]; POINT_2D a;。

實際中,很容易就發現數組其實實現了一個集合,即可用來作為容器以記錄多個同一類型的數字。此時數組在類型上表現的語義——矢量——已被忽略,重點是定義出來的數組變量,即重點不再是POINT_2D,而是a.如:double container[300];就通過變量名而不再是類型來體現語義了。作為容器光寫個double container[300];是不夠的,因為無法知道哪些元素有效哪些無效,因此還需其它的變量介入以共同完成一個容器的基本功能。對此,實際中常編寫一個類來隱藏上面實現容器的基本功能的細節,這個類映射的語義是容器,這種類一般被稱作容器類或集合類,如STL中的容器類vector.這種類也被稱作是對數組container的封裝類。

那多維數組的語義是什麼?有何意義?這其實很簡單,因為數組是類型修飾符,不用去管它一維還是多維,只用知道它是多個原類型的元素構成的一個類型。比如long a[2][3];就可以映射為一個二維矢量,只不過每個分量又是一個三維矢量罷了,這正好可映射線性代數中的矩陣。因此typedef MATRIX_23[2][3]; MATRIX_23 a;。同理可只注重定義的數組變量而忽略其類型所表征的語義,如double a[300][300];。這裡的a和double b[90000];定義的b有什麼區別?最終所操作的內存沒有區別,僅僅語義上的差別——前者所代表的容器需要兩個關鍵字(key)才能定位一個元素(value),而後者只用一個關鍵字就可以定位元素(a[2][3] = 3;等同於b[603] = 3;,但前者效率更低)。前者如電子表格中的表格,給出橫縱兩個坐標才能定位欲寫入數據的位置;後者則是一般的連續容器。還可以既注重數組的類型語義——矢量,又注重數組的實例語義——容器,如:POINT_2D PContainer[300];,PContainer的類型實際是double[300][2].

引用和間接

欲說明指針的語義及其運用,最好是先了解一下引用和間接。這裡的引用不是指C++中的引用類型修飾符“&”,而是一個語言無關的概念,在《C++從零開始(八)》中已詳細闡明,在此再說明一下:引用表示了一個連接關系,以至於原來需要某種手段才能達到的目的,現在另一種手段也能達到,這種手段往往比原來的手段在某方面有優勢。如某人的手機號碼就是和某人談話的引用;圖書館裡藏書的編號就是書的引用;某人的名字就是某人的引用。引用有什麼用?其本身是沒用的,一定有一些手段來實現其“相當於”的目的。如某人的手機沒帶或沒電,則“手機號碼”無法完成相當於“和某人談話”的功能;沒有正確的排放書籍,書的編號毫無意義;沒有完備的搜查系統,給出人的名字也無法找到人。這是因為引用是“間接”的一部分,在《C++從零開始(十一)下篇》中說明了何謂間接,指出其三大優點——簡化操作、提高效率(從某方面)和增加靈活性,下面說明。

用手機和某人談話是通過手機間接和那人談話;看風景照是通過照片間接觀看風景;商品賣出是通過銷售員間接賣出去的(不是老板親自賣的)。即有一個“原始手段”可以達到目的,有一個“高級手段”能操控那個“原始手段”,則通過使用“高級手段”來達到目的稱作間接,而“高級手段”的配置信息(說明如何使用“高級手段”)就是引用。所謂的手段就是某種方法或功能,而方法的配置信息用於完善方法的運用。如講話是個方法,而講話的人就是引用,即人引用了講話,表示只要擁有個人,就可以實現講話。“聲音傳到人的耳朵裡”是“原始手段”,“手機能傳遞聲音並使其傳到人的耳朵裡”是“高級手段”,“手機號碼”決定手機如何傳遞聲音(傳給哪個手機):“風景反射的光線進入眼睛”是“原始手段”,“照片反射的光線進入眼睛”是“高級手段”,“照片”決定如何反射光線:“銷售員能把商品賣出”是“原始手段”,“銷售員能被命令賣出商品”是“高級手段”,“銷售員”決定了如何命令銷售員賣商品(命令哪個)。“手機號碼”引用了“和某人談話”:“照片”引用了“風景”:“銷售員”引用了“賣出商品”。

應注意引用自身也是一種資源,其可以被操作,當它被修改時,對於同一個“高級手段”的執行,將得到不同的結果,因此間接能提高靈活性。即之所以能提高靈活性,是因為“高級手段”的可配置,如果不可配置則間接無法提高靈活性。

還應注意“原始手段”可能是另一個間接中的“高級手段”,如A向B要錢,B命令C賣商品以得到錢,即間接的間接。如果兩級間接的“高級手段”都可配置,則此二級間接的靈活性將可能高於一級的間接(只是可能,要視“可配置”這個功能是否被運用)。

間接一定降低效率。因為原來一個“原始手段”即可達到目的,現在卻要施行“高級手段”和“原始手段”兩個手段才能達到目的。那要間接有何意義?必定因為“高級手段”在某方面優於“原始手段”。如施行方面以達到簡化操作,如MFC中的窗口包裝類CWnd;使用方面通過“高級手段”的可配置以提高靈活性;無法施行“原始手段”而需借助“高級手段”,如A沒時間而派B去跟客戶洽談;管理方面為“高級手段”賦於一定的意義以簡化管理,等同於歸類分層,如營銷部賣商品而不是銷售員賣商品。

指針的語義——引用

上面說那麼多,到底關指針什麼事?指針的語義就是引用。在《C++從零開始》系列中已說明,程序即方法的描述,方法即說明如何操作資源,在C++中能被操作的資源只有數字,數字以其類型所決定的規則存放在一塊內存中,內存塊是通過其首地址來標識的。而前面已說地址類型的數字的計算將進行取內容操作,指針類型的數字的計算則不取內容,直接返回地址,即指針類型的數字就是一個地址,而地址是C++中唯一能操作的資源——數字——被存放的位置,即其是某塊內存塊的引用。

指針既然被稱作引用也就帶入了間接。數字的計算(即按數字的類型定義的規則來返回相應的二進制數)是“原始手段”,指針是引用,而“高級手段”則是取內容(通過取內容操作符“*”將指針類型換成地址類型以實現)。應注意引用也可以不使用取內容這個“高級手段”,其他的也可以,但需要保證此“高級手段”能控制(或執行)“原始手段”——數字的計算(即C++語法中所有可以放表達式的地方,指針類型轉地址類型正好是通過操作符的形式實現,即可放於表達式中而達到執行“原始手段”的目的),如類型轉換。現已了解指針所引入的間接,下面就來看如何運用這個間接。

“高級手段”取內容有什麼方面比“原始手段”計算數字優越呢?取內容具有一個唯一的可配置信息——取內容的位置,即地址。前面已說,“高級手段”的可配置帶來靈活性,如下:long a1 = 1, a2 = 2, a3 = 3, *p = 0; /* 一些操作以決定p的值 */ *p += 5;上面的靈活性體現在多次執行*p += 5;這條語句,可能會將不同內存中的值增5.但如果a2 += 5;,則無論執行多少遍都是將a2對應的地址所標識的內存中的值增5.之所以能這樣是因為指針是地址類型的數字的引用。

應注意地址類型也帶入一個間接,其是數字的引用。數字的計算是“原始計算”,取內容是“高級手段”(感覺和指針一樣,但後者需要取內容操作符的轉換,而前者不用),地址類型的數字就是引用。這也就導致a3 = a1 + a1;也具有靈活性,因為不是a3 = 1 + 1;,而是由地址間接獲得long類型的數字。對於a3 = *p + *p;則先間接獲得地址,再間接獲得放在內存中的數字,這是一個二級間接,其靈活性要高於a3 = a1 + a1;,因此指針也可以說是一個數字的引用。

前面提到的間接的簡化操作等功用,由於需要“高級手段”的支持,而這裡一般性的只有取內容操作符,故指針主要用於增加靈活性。

注意引用一般是一個較小的資源,易於記錄才會被視為引用。如只用將B的手機號碼給A而不用將B帶到A面前就可以實現A和B談話。當引用的是對某種資源的操作時,給出較小的引用的代價要小於給出那種資源的一個實例,此時傳遞引用就比傳遞那種資源的實例更有效率,此即前面所謂的提高效率。這在C++中非常流行,這完全依賴於指針的二級間接以實現對數字的引用。即一個數字很大,如長度300字節(如類型為char[300]),而其引用——地址——只有4個字節,故欲將其傳遞給函數時應傳遞地址,而指針類型的數字就是地址,故函數的參數類型應是指針(char(*)[300])而不是原類型(char[300]),後者的傳遞費用更高。

指針的運用——間接

至此,根據上面已經可以得出指針的四個運用目的了——增加靈活性、引用內存塊、引用數字、語義需要,分述如下:增加靈活性  編寫一段代碼時,尤其是循環,循環體中是經常使用指針的地方,因為之所以能體現靈活性,就是相同的做法得到不同的結果,也因此多次循環執行相同的循環體卻得到不同的結果以達到簡化程序編寫的目的(否則需對每種結果書寫一段代碼)。如需增加100個分散的等間隔20字節的內存塊中的值,則可將第一個地址放在一個指針中,如下:long *p; /* 初始化p */ for( long i = 0; i < 100; i++, p += 5 ) *p++;因此對於不想或不能改變代碼的地方,為了增大其適用范圍,常利用指針來增加其靈活性。如函數體內、庫文件內(如動態連接庫)、模塊內(指程序結構上,如游戲邏輯計算模塊和繪圖模塊)等。

注意函數指針的存在。函數的一個原型代表了一種調用規則,即《C++從零開始(十八)》中說的接口,則函數指針是一個接口的引用,則兩個模塊(或稱組件)之間銜接時,如邏輯計算模塊調用void Draw();來繪圖,但繪圖模塊中(即函數Draw內部)可能根據當前繪圖數據的情況(如場景復雜與否等)而決定不同的繪圖方法,則可使用一全局函數指針void ( *g_pDraw )();,每次繪圖模塊會根據繪圖情況設置此變量,而邏輯計算模塊就變成( *g_pDraw )();而不再Draw();(此法不推薦,在此僅作為例子提出)。

對於成員指針,在《C++從零開始(九)》中已指出非靜態成員變量映射的是偏移而不是地址,即成員變量指針不是內存塊的引用,僅僅是偏移類型的數字的引用,而偏移類型的數字4個字節長,其指針反而可能8字節長,因此它不用於前面的傳遞的優化,也不能引用內存塊,僅只有增加靈活性和語義需要。如一個結構是學生成績,裡面有40門學科的成績,則欲計算平均分,如下:

 struct Student
    {
        float Course1, Course2, …, Course40;
        static float Student::*pCourse[40];
    };
    float Student::*Student::pCourse[40] = { &Student::Course1, …, &Student::Course40 };
 

則可書寫一循環來簡化計算,如:

Student a; float avg = 0.0f;

for( long i = 0; i < 40; i++ ) avg += a.*Student::pCourse[ i ];

avg /= 40.0f;

對於成員函數指針也是一樣的,只用按照前述的規則和語義就可了解各類指針的運用。

引用內存塊  這是指針的原始語義,當欲傳遞內存塊時,就只能靠傳遞指針來實現(內存塊是不能傳遞的),如:

bool GetPersonInfo( PersonInfo *pInfo );

PersonInfo temp; GetPersonInfo( &temp );

// 使用返回的放在temp中的個人信息

上面的參數pInfo就是傳遞一個內存塊給函數GetPersonInfo以讓它將個人信息填在給定的內存塊中以進行傳遞。如果PersonInfo* GetPersonInfo();,那麼返回的指針所引用的那塊內存塊需要被釋放,但使用GetPersonInfo的代碼無法知道那內存塊是如何分配的進而無法釋放,這屬於內存管理方面,在此不深入討論。再如:

bool GetMaxAndMin( long[10], long *max, long *min );

long a[10] = { 5, 3, 10 }, max, min; GetMaxAndMin( a, &max, &min );

引用數字  這是借助地址類型的數字所帶入的間接而形成的二級間接的運用,目的是被引用的數字的大小大於指針的大小時傳遞指針以減少傳遞成本。如上面的long[10]是40個字節,如果傳遞它的指針則只有4個字節,大大降低成本,如下:

bool GetMaxAndMin( long(*)[10], long*, long* );

long a[10] = { 5, 3, 10 }, max, min; GetMaxAndMin( &a, &max, &min );

這裡數組a的傳遞就比之前有效率得多。再如之前的PersonInfo* GetPersonInfo();而不PersonInfo GetPersonInfo();也是出於sizeof(PersonInfo*)<sizeof(PersonInfo),也因此這樣是愚蠢的:char Max( char*, char*, char* );。這裡欲返回最大值,但sizeof(char*)>sizeof(char),不僅沒提高反而降低了效率(對於32位編譯器,這裡將不是由於傳遞降低效率,而是間接引用數字降低效率)。

應注意指針能引用數字是由於二級間接,這導致多了一級無謂的引用——對內存塊的引用,本只想傳遞數字,結果連裝數字的內存塊也傳遞了,將引起問題。如前面的GetMaxAndMin( &a, &max, &min );,執行後,數組a的內容可能被改變,因為記錄a的內存塊也被傳過去了,函數GetMaxAndMin可任意修改傳進去的內存塊的內容,也就是數組a的內容。當函數GetMaxAndMin和調用它的代碼不是同一個人編的時候,則前者就可以肆意破壞a的內容而導致後者發生問題,這被稱作漏洞。所以當設計對外的接口時,應小心確定參數類型是否應為指針類型(注意對於對外的接口,即使寫成bool GetMaxAndMin( const long(*)[10], long*, long* );也是毫無意義的)。

語義需要  指針的語義是引用,當邏輯上需要引用時就使用指針來映射,如鏈表的結點中有個對下一個結點的引用,因此設計鏈表的結點時其就應該有一個指針類型的成員。

對於函數指針,函數就是一段程序,程序即方法的描述,即函數指針是方法的引用。如一個容器類具有一個排序方法的引用,這樣使用它時給出不同的排序方法就能實現不同的排序(這是“間接”的增加靈活性的表現),因此那個容器類應有一個成員變量是函數指針類型。

有,函數指針可實現回調。如搜索數據時,通過調用函數Search,其有一個參數以指明當搜索時間超時時應怎麼辦,所以Search需要一個函數的引用(代碼不能傳遞),也就是函數指針,如下:

 bool bContinue( unsigned long overSeconds )
    {
        if( /* 判斷是否繼續 */ )
        { /* 必要的處理 */ return true; }
        return false;
    }
    bool Search( bool(*)( unsigned long ), /* 其它參數 */ );
    Search( &bContinue, /* 其它參數 */ );

上面的Search中就可以每隔一定時間就調用bContinue以詢問是否還要繼續搜尋以給出一種手段來終止過長時間的搜索。

對於成員指針,同樣,是對成員的引用。在《C++從零開始(十二)》中已說明,成員變量就是相應資源的屬性和狀態,成員函數就是相應資源的功能,則成員指針就是資源的屬性和狀態的引用以及資源功能的引用。如狗具有走鋼絲、跳火圈、倒立三個功能,有一個方法是馬戲表演。為了增加靈活性,馬戲表演這個函數中就應該保持一個對狗的功能的引用,以不同的場次表演不同的馬戲。

從上可以看出,所謂語義需要,應是根據前面所說的間接的好處來設計算法,再從語義上表現出對引用的需要。但間接也有壞處,即它可能失敗,而直接就總是能成功。“原始手段”一定成功(因為是直接達到目的,如果是在多級間接中則要考慮其有效性),失敗是因為“高級手段”控制失敗。而“高級手段”之所以控制失敗是因為其配置信息無效,也就是引用是無效的。對於指針,則需要保證其值是有效的,一旦無效,將可能引起內存訪問違規。如果沒有適當處理措施,則程序可能崩潰。因此大多在使用指針前先檢測其有效性,檢測的方法就是判斷其所引用的地址所標識的內存當前是否可訪問(操作系統一般都提供了這樣的接口),但借助操作系統效率較低,一般認為只要不為零就是有效的,因此一般如下:

if( p ) { /* 使用指針p */ }

所以一般都將指針初始化為0,如long *p = 0;。對此一般都會建立一個宏或常量來表現這個語義,在MFC中,就定義了NULL這個宏以代表0,則就可以:long *p = NULL;,NULL表示空。

至此已說明了指針及其運用,指針的運用是建立在間接的基礎上的,明確了間接的優點和缺點將有助於更好地運用指針。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved