C++基礎(chǔ)之指針的詳細(xì)介紹(一)
在介紹C++中的指針開始之前,我們一定先要了解數(shù)組的概念以及用法,大家可以看看這篇文章,《淺析C++中的動(dòng)態(tài)多維數(shù)組》,供參考。
數(shù)組
在C++中是通過(guò)變量來(lái)對(duì)內(nèi)存進(jìn)行訪問(wèn)的,但根據(jù)前面的說(shuō)明,C++中只能通過(guò)變量來(lái)操作內(nèi)存,也就是說(shuō)要操作某塊內(nèi)存,就必須先將這塊內(nèi)存的首地址和一個(gè)變量名綁定起來(lái),這是很糟糕的。
比如有100塊內(nèi)存用以記錄100個(gè)工人的工資,現(xiàn)在要將每個(gè)工人的工資增加5%,為了知道各個(gè)工人增加了后的工資為多少,就定義一個(gè)變量float a1;,用其記錄第1個(gè)工人的工資,然后執(zhí)行語(yǔ)句a1 += a1 * 0.05f;,則a1里就是增加后的工資。由于是100個(gè)工人,所以就必須有100個(gè)變量,分別記錄100個(gè)工資。因此上面的賦值語(yǔ)句就需要有100條,每條僅僅變量名不一樣。
上面需要手工重復(fù)書寫變量定義語(yǔ)句float a1;100遍(每次變一個(gè)變量名),無(wú)謂的工作。因此想到一次向操作系統(tǒng)申請(qǐng)100*4=400個(gè)字節(jié)的連續(xù)內(nèi)存,那么要給第i個(gè)工人修改工資,只需從首地址開始加上4*i個(gè)字節(jié)就行了(因?yàn)閒loat占用4個(gè)字節(jié))。
為了提供這個(gè)功能,C++提出了一種類型——數(shù)組。數(shù)組即一組數(shù)字,其中的各個(gè)數(shù)字稱作相應(yīng)數(shù)組的元素,各元素的大小一定相等(因?yàn)閿?shù)組中的元素是靠固定的偏移來(lái)標(biāo)識(shí)的),即數(shù)組表示一組相同類型的數(shù)字,其在內(nèi)存中一定是連續(xù)存放的。在定義變量時(shí),要表示某個(gè)變量是數(shù)組類型時(shí),在變量名的后面加上方括號(hào),在方括號(hào)中指明欲申請(qǐng)的數(shù)組元素個(gè)數(shù),以分號(hào)結(jié)束。因此上面的記錄100個(gè)工資的變量,即可如下定義成數(shù)組類型的變量:
- float a[100];
上面定義了一個(gè)變量a,分配了100*4=400個(gè)字節(jié)的連續(xù)內(nèi)存(因?yàn)橐粋€(gè)float元素占用4個(gè)字節(jié)),然后將其首地址和變量名a相綁定。而變量a的類型就被稱作具有100個(gè)float類型元素的數(shù)組。即將如下解釋變量a所對(duì)應(yīng)內(nèi)存中的內(nèi)容(類型就是如何解釋內(nèi)存的內(nèi)容):a所對(duì)應(yīng)的地址標(biāo)識(shí)的內(nèi)存是一塊連續(xù)內(nèi)存的首地址,這塊連續(xù)內(nèi)存的大小剛好能容納下100個(gè)float類型的數(shù)字。
因此可以將前面的float b;這種定義看成是定義了一個(gè)元素的float數(shù)組變量b.而為了能夠訪問(wèn)數(shù)組中的某個(gè)元素,在變量名后接方括號(hào),方括號(hào)中放一數(shù)字,數(shù)字必須是非浮點(diǎn)數(shù),即使用二進(jìn)制原碼或補(bǔ)碼進(jìn)行表示的數(shù)字。如a[ 5 + 3 ] += 32;就是數(shù)組變量a的第5 + 3個(gè)元素的值增加32.又:
- long c = 23;
- float b = a[ ( c – 3 ) / 5 ] + 10, d = a[ c – 23 ];
上面的b的值就為數(shù)組變量a的第4個(gè)元素的值加10,而d的值就為數(shù)組變量a的第0個(gè)元素的值。即C++的數(shù)組中的元素是以0為基本序號(hào)來(lái)記數(shù)的,即 a[0]實(shí)際代表的是數(shù)組變量a中的第一個(gè)元素的值,而之所以是0,表示a所對(duì)應(yīng)的地址加上0*4后得到的地址就為第一個(gè)元素的地址。
應(yīng)該注意不能這樣寫:
- long a[0];
定義0個(gè)元素的數(shù)組是無(wú)意義的,編譯器將報(bào)錯(cuò),不過(guò)在結(jié)構(gòu)或類或聯(lián)合中符合某些規(guī)則后可以這樣寫,那是C語(yǔ)言時(shí)代提出的一種實(shí)現(xiàn)結(jié)構(gòu)類型的長(zhǎng)度可變的技術(shù)。
還應(yīng)注意上面在定義數(shù)組時(shí)不能在方括號(hào)內(nèi)寫變量,即
- long b = 10;
- float a[ b ];//是錯(cuò)誤的
因?yàn)榫幾g此代碼時(shí),無(wú)法知道變量b的值為多少,進(jìn)而無(wú)法分配內(nèi)存??墒乔懊婷髅饕呀?jīng)寫了b = 10;,為什么還說(shuō)不知道b的值?那是因?yàn)闊o(wú)法知道b所對(duì)應(yīng)的地址是多少。
因?yàn)榫幾g器編譯時(shí)只是將b和一個(gè)偏移進(jìn)行了綁定,并不是真正的地址,即b所對(duì)應(yīng)的可能是Base - 54,而其中的Base就是在程序一開始執(zhí)行時(shí)動(dòng)態(tài)向操作系統(tǒng)申請(qǐng)的大塊內(nèi)存的尾地址,因?yàn)槠淇赡茏兓?,故無(wú)法得知b實(shí)際對(duì)應(yīng)的地址(實(shí)際在 Windows平臺(tái)下,由于虛擬地址空間的運(yùn)用,是可以得到實(shí)際對(duì)應(yīng)的虛擬地址,但依舊不是實(shí)際地址,故無(wú)法編譯時(shí)期知道某變量的值)。
但是編譯器仍然可以根據(jù)前面的long b = 10;而推出Base - 54的值為10啊?重點(diǎn)就是編譯器看到long b = 10;時(shí),只是知道要生成一條指令,此指令將10放入Base - 54的內(nèi)存中,其它將不再過(guò)問(wèn)(也沒(méi)必要過(guò)問(wèn)),故即使才寫了long b = 10;編譯器也無(wú)法得知b的值。
上面說(shuō)數(shù)組是一種類型,其實(shí)并不準(zhǔn)確,實(shí)際應(yīng)為——數(shù)組是一種類型修飾符,其定義了一種類型修飾規(guī)則。關(guān)于類型修飾符,后面將詳述。
字符串
要查某個(gè)字符對(duì)應(yīng)的ASCII碼,通過(guò)在這個(gè)字符的兩側(cè)加上單引號(hào),如'A'就等同于65.而要表示多個(gè)字符時(shí),就使用雙引號(hào)括起來(lái),如:"ABC".而為了記錄字符,就需要記錄下其對(duì)應(yīng)的ASCII碼,而ASCII碼的數(shù)值在-128到127以內(nèi),因此使用一個(gè) char變量就可以記錄一個(gè)ASCII碼,而為了記錄"ABC",就很正常地使用一個(gè)char的數(shù)組來(lái)記錄。如下:
程序無(wú)論執(zhí)行多少遍,在申請(qǐng)內(nèi)存時(shí)總是申請(qǐng)固定大小的內(nèi)存,則稱此內(nèi)存是靜態(tài)分配的。前面提出的定義變量時(shí),編譯器幫我們從棧上分配的內(nèi)存就屬于靜態(tài)分配。每次執(zhí)行程序,根據(jù)用戶輸入的不同而可能申請(qǐng)不同大小的內(nèi)存時(shí),則稱此內(nèi)存是動(dòng)態(tài)分配的,后面說(shuō)的從堆上分配就屬于動(dòng)態(tài)分配。
很明顯,動(dòng)態(tài)比靜態(tài)的效率高(發(fā)票長(zhǎng)度的利用率高),但要求更高——需要電腦和打印機(jī),且需要收銀員的素質(zhì)較高(能操作電腦),而靜態(tài)的要求就較低,只需要已經(jīng)印好的發(fā)票聯(lián),且也只需收銀員會(huì)寫字即可。
同樣,靜態(tài)分配的內(nèi)存利用率不高或運(yùn)用不夠靈活,但代碼容易編寫且運(yùn)行速度較快;動(dòng)態(tài)分配的內(nèi)存利用率高,不過(guò)編寫代碼時(shí)要復(fù)雜些,需自己處理內(nèi)存的管理(分配和釋放)且由于這種管理的介入而運(yùn)行速度較慢并代碼長(zhǎng)度增加。
靜態(tài)和動(dòng)態(tài)的意義不僅僅如此,其有很多的深化,如硬編碼和軟編碼、緊耦合和松耦合,都是靜態(tài)和動(dòng)態(tài)的深化。
地址
前面說(shuō)過(guò)“地址就是一個(gè)數(shù)字,用以唯一標(biāo)識(shí)某一特定內(nèi)存單元”,而后又說(shuō)“而地址就和長(zhǎng)整型、單精度浮點(diǎn)數(shù)這類一樣,是數(shù)字的一種類型”,那地址既是數(shù)字又是數(shù)字的類型?不是有點(diǎn)矛盾嗎?
如下:浮點(diǎn)數(shù)是一種數(shù)——小數(shù)——又是一種數(shù)字類型。即前面的前者是地址實(shí)際中的運(yùn)用,而后者是由于電腦只認(rèn)識(shí)狀態(tài),但是給出的狀態(tài)要如何處理就必須通過(guò)類型來(lái)說(shuō)明,所以地址這種類型就是用來(lái)告訴編譯器以內(nèi)存單元的標(biāo)識(shí)來(lái)處理對(duì)應(yīng)的狀態(tài)。
指針
已經(jīng)了解到動(dòng)態(tài)分配內(nèi)存和靜態(tài)分配內(nèi)存的不同,現(xiàn)在要記錄用戶輸入的定單數(shù)據(jù),用戶一次輸入的定單數(shù)量不定,故選擇在堆上分配內(nèi)存。假設(shè)現(xiàn)在根據(jù)用戶的輸入,需申請(qǐng)1M的內(nèi)存以對(duì)用戶輸入的數(shù)據(jù)進(jìn)行臨時(shí)記錄,則為了操作這1M的連續(xù)內(nèi)存,需記錄其首地址,但又由于此內(nèi)存是動(dòng)態(tài)分配的,即其不是由編譯器分配(而是程序的代碼動(dòng)態(tài)分配的),故未能建立一變量來(lái)映射此首地址,因此必須自己來(lái)記錄此首地址。
因?yàn)槿魏我粋€(gè)地址都是4個(gè)字節(jié)長(zhǎng)的二進(jìn)制數(shù)(對(duì)32位操作系統(tǒng)),故靜態(tài)分配一塊4字節(jié)內(nèi)存來(lái)記錄此首地址。檢查前面,可以將首地址這個(gè)數(shù)據(jù)存在unsigned long類型的變量a中,然后為了讀取此1M內(nèi)存中的第4個(gè)字節(jié)處的4字節(jié)長(zhǎng)內(nèi)存的內(nèi)容,通過(guò)將a的值加上4即可獲得相應(yīng)的地址,然后取出其后連續(xù)的4個(gè)字節(jié)內(nèi)存的內(nèi)容。但是如何編寫取某地址對(duì)應(yīng)內(nèi)存的內(nèi)容的代碼呢?
前面說(shuō)了,只要返回地址類型的數(shù)字,由于是地址類型,則其會(huì)自動(dòng)取相應(yīng)內(nèi)容的。但如果直接寫:a + 4,由于a是unsigned long,則a + 4返回的是unsigned long類型,不是地址類型,怎么辦?
C++對(duì)此提出了一個(gè)操作符——“*”,叫做取內(nèi)容操作符(實(shí)際這個(gè)叫法并不準(zhǔn)確)。其和乘號(hào)操作符一樣,但是它只在右側(cè)接數(shù)字,即*( a + 4 )。此表達(dá)式返回的就是把a(bǔ)的值加上4后的unsigned long數(shù)字轉(zhuǎn)成地址類型的數(shù)字。
但是有個(gè)問(wèn)題:a + 4所表示的內(nèi)存的內(nèi)容如何解釋?即取1個(gè)字節(jié)還是2個(gè)字節(jié)?以什么格式來(lái)解釋取出的內(nèi)容?如果自己編寫匯編代碼,這就不是問(wèn)題了,但現(xiàn)在是編譯器代我們編寫匯編代碼,因此必須通過(guò)一種手段告訴編譯器如何解釋給定的地址所對(duì)內(nèi)存的內(nèi)容。
C++對(duì)此提出了指針,其和上面的數(shù)組一樣,是一種類型修飾符。在定義變量時(shí),在變量名的前面加上“*”即表示相應(yīng)變量是指針類型(就如在變量名后接“[]”表示相應(yīng)變量是數(shù)組類型一樣),其大小固定為4字節(jié)。如:
- unsigned long *pA;
也就是說(shuō),某個(gè)地址的類型為指針時(shí),表示此地址對(duì)應(yīng)的內(nèi)存中的內(nèi)容,應(yīng)該被編譯器解釋成一個(gè)地址。
因?yàn)樽兞烤褪堑刂返挠成?,每個(gè)變量都有個(gè)對(duì)應(yīng)的地址,為此C++又提供了一個(gè)操作符來(lái)取某個(gè)變量的地址——“&”,稱作取地址操作符。其與“數(shù)字與”操作符一樣,不過(guò)它總是在右側(cè)接數(shù)字(而不是兩側(cè)接數(shù)字)。
“&”的右側(cè)只能接地址類型的數(shù)字,它的計(jì)算(Evaluate)就是將右側(cè)的地址類型的數(shù)字簡(jiǎn)單的類型轉(zhuǎn)換成指針類型并進(jìn)而返回一個(gè)指針類型的數(shù)字,正好和取內(nèi)容操作符“*”相反。
上面正常情況下應(yīng)該會(huì)讓你很暈,下面釋疑。
- unsigned long a = 10, b, *pA;
- pA = &a;
- b = *pA;
- ( *pA )++;
上面的第一句通過(guò)“*pA”定義了一個(gè)指針類型的變量pA,即編譯器幫我們?cè)跅I戏峙淞艘粔K4字節(jié)的內(nèi)存,并將首地址和pA綁定(即形成映射)。然后“&a”由于a是一個(gè)變量,等同于地址,所以“&a”進(jìn)行計(jì)算,返回一個(gè)類型為unsigned long*(即unsigned long的指針)的數(shù)字。
應(yīng)該注意上面返回的數(shù)字雖然是指針類型,但是其值和a對(duì)應(yīng)的地址相同,但為什么不直接說(shuō)是unsigned long的地址的數(shù)字,而又多一個(gè)指針類型在其中攪和?因?yàn)橹羔橆愋偷臄?shù)字是直接返回其二進(jìn)制數(shù)值,而地址類型的數(shù)字是返回其二進(jìn)制數(shù)值對(duì)應(yīng)的內(nèi)存的內(nèi)容。因此假設(shè)上面的變量a所對(duì)應(yīng)的地址為2000,則a;將返回10,而&a;將返回2000.
再來(lái)看取內(nèi)容操作符“*”,其右接的數(shù)字類型是指針類型或數(shù)組類型,它的計(jì)算就是將此指針類型的數(shù)字直接轉(zhuǎn)換成地址類型的數(shù)字而已(因?yàn)橹羔橆愋偷臄?shù)字和地址類型的數(shù)字在數(shù)值上是相同的,僅僅計(jì)算規(guī)則不同)。
所以:
- b = *pA;
返回pA對(duì)應(yīng)的地址,計(jì)算此地址的值,返回類型為unsigned long*的數(shù)字2000,然后“*pA”返回類型unsigned long的地址類型的數(shù)字2000,然后計(jì)算此地址類型的數(shù)字的值,返回10,然后就只是簡(jiǎn)單地賦值操作了。同理,對(duì)于++( *pA )(由于“*”的優(yōu)先級(jí)低于前綴++,所以加“()”),先計(jì)算“*pA”而返回unsigned long的地址類型的數(shù)字2000,然后計(jì)算前綴++,最后返回unsigned long的地址類型的數(shù)字2000.
如果你還是未能理解地址類型和指針類型的區(qū)別,希望下面這句能夠有用:地址類型的數(shù)字是在編譯時(shí)期給編譯器用的,指針類型的數(shù)字是在運(yùn)行時(shí)期給代碼用的。如果還是不甚理解,在看過(guò)后面的類型修飾符一節(jié)后希望能有所幫助。