1、第一部分第十二課:指針一出,誰與爭鋒
2、第一部分第十三課預告:第一部分小測驗
上一課《【C++探索之旅】第一部分第十一課:小練習,猜單詞》中,我們用一個小游戲來總結了之前幾課學習的知識點。
現在,終於來到第一部分的最後一個知識點了,也是C++的基礎部分的最後一個講題。之後進入第二部分,就會開始面向對象之旅。因此,這一課也注定不平凡。系好安全帶吧,因為馬力要加足了!
指針這個C系語言的難點(著名的C語言裡也有指針),令無數英雄"盡折腰",也是這個系列課程裡最難的課之一。不過不用怕,只要耐心地跟著小編走,哪能不濕鞋... 噢,不是,是肯定能治好您的"腰間盤突出"!
只要腰間盤一康復,你會發現,很多其他的知識點都會變得異常清晰和容易。而且,腰不酸了,腿不疼了,一口氣都能上五樓了... 小編你夠了...
指針在C++的程序中被廣泛使用。在之前的課程中我們已經在使用了,只是你沒意識到罷了。只要學好了指針,對於內存的掌控水准就又上了一個台階。
好了,不吊胃口了。我們出發吧~
內存地址的問題
你是否還記得之前講到內存的那一課?也就是《【C++探索之旅】第一部分第四課:內存,變量和引用》。希望你能夠再溫習一下那一課,特別是那幾張內存的圖示。
那一課中說到,當我們聲明一個變量時,操作系統會把內存的一小塊空間借給我們使用,並且給這一塊空間起一個名字(變量的名字)。就好像借給我們一個抽屜使用,並且在抽屜外面貼一個標簽。例如:
intmain() { intuserAge(16); return0; }
我們可以用內存圖示來說明上面的代碼:
上圖既簡單又清楚,不是嗎?不過,上圖簡化了不少事情。
你會慢慢發現,在電腦中,所有都是井然有序,符合邏輯的。我們之前把內存中的地址空間比喻為一個個的抽屜,而這些抽屜上會被貼標簽(如果被分配給了變量的話),也就是變量的名字。實際的情況可比這個復雜。
電腦的內存確實是由多個"抽屜"組成的,在這一點上我們之前並沒有說錯。而且,現代的電腦的內存裡有多達數十億個"抽屜"!
因此,我們需要一個體系來幫助我們在茫茫抽屜海中找到心儀的那個抽屜。內存中的每一個抽屜都有一個獨一無二的編號,也就是內存地址。如下圖所示:
上圖中,我們看到了在內存中的那些"抽屜",每個抽屜對應一個內存地址。
我們之前的程序只使用了其中的一個"抽屜",它的內存地址是53768,這個抽屜裡存放了我們的變量userAge。
注意:每一個變量都有唯一的內存地址,每個內存地址上一次也只能存放一個變量。
在我們之前的課程中,為了訪問一個變量,我們使用變量的名稱,例如:
userAge=18;//用變量名來訪問變量,對變量重新賦值
但是,我們也可以用變量的地址作為訪問變量的媒介。例如,我們可以對電腦"說":乖,給我顯示內存地址53768上的內容。
也許你會說:既然有了第一種用變量名來訪問變量的方法,既簡單又有效率,那為什麼還要用第二種方法呢?
確實,不過我們馬上會看到,有時候利用內存地址來訪問變量的方法是必要的。
在這之前,我們先來學習如何得知變量的內存地址。
顯示內存地址
在C++中,和C語言一樣,用於獲取一個變量的內存地址,我們需要用到&這個符號。&也被稱為"取地址符"。
假如我想要獲取變量userAge的內存地址,很簡單,我只要這樣寫:
&userAge
寫一個程序試一下:
#includeusingnamespacestd; intmain() { intuserAge(16); cout<<"變量userAge的內存地址是"<<&userAge<
運行後,在我的電腦上顯示的是:
變量userAge的內存地址是 0x22ff0c
你運行自己的程序時,顯示的內存地址一般來說和我的不一樣,畢竟這個內存地址是操作系統分配的。
雖然上面顯示的內存地址中包含了字母,但其實它是一個數字。
因為內存地址打印出來的時候默認是以十六進制的格式來顯示的(開頭的0x表示後面的數字是十六進制的,因此其實是十六進制數22ff0c)。我們很熟悉十進制,也就是我們一般用的數制,電腦最底層其實只知道0和1組成的二進制。
不過十進制,二進制,十六進制之間都是可以相互轉換的。至於如何轉換,一般學校裡都教過了。如果沒有,那百度一下也不是難事。或者你用電腦裡自帶的計算器程序就可以實現進制間的轉換了。
例如,上面的這個十六進制的數(0x22ff0c),轉化成十進制是2293516,轉換成二進制是1000101111111100001100 。
我們的&符號,在這裡是用於顯示變量地址。之前講到引用時,我們也用到了&符號。因此,不要搞混了兩種用法。
接下來,我們就來看看拿這些地址能干什麼。
指針,指哪打哪
內存地址其實就是以數字來表示的。
我們已經知道,在C++中,有多種數據類型可以儲存數字:int,unsigned int,double,等。因此,我們可以將內存地址儲存在變量中嗎?
回答是肯定的。
不過,要儲存內存地址,我們不使用之前見過的那些普通變量類型,而要用一種特殊的類型:指針。
指針,簡而言之就是:儲存別的變量的內存地址的一種變量。
請記住這句話。
聲明一個指針
聲明一個指針,就和以前我們聲明一個普通變量是類似的,需要兩樣東西:
類型
名字
至於名字,就和以前一樣,只要符合規則,隨便你取什麼名字都行。
不過,指針的類型卻有些特別。這個類型須要指明我們要存儲的地址上的變量的類型,還要加上一個*號。例如:
int*pointer;
看上去有點特別是嗎?比之前我們聲明一個int型的變量時多加了一個星號(*)。正是這個*號標明了這是一個指針變量。
上面的代碼聲明了一個指針,名字是pointer,其內容是int型變量的內存地址。也可以說成:pointer指向一個int型的變量(之所以叫"指針"的原因)。
我們也可以寫成:
int*pointer;
這次我們讓*和int相鄰,之前我們是將*緊鄰pointer來寫的。這兩種寫法的效果是一樣的,但是我們推薦前一種。
為什麼呢?因為如果是寫成int* pointer這樣的形式,很容易讓我們認為int*是一個整體,如果我們在一行上同時聲明多個指針,很容易就會這麼寫:
int*pointer1,pointer2,pointer3;
我們的初衷是:聲明三個指針變量pointer1, pointer2, pointer3,都指向int型變量。但事實上,只有pointer1成功地被聲明為了指針變量,後面的pointer2和pointer3只是int型變量!
這是初學需要注意的。因此,我們一般這麼寫:
int*pointer1,*pointer2,*pointer3;//聲明三個指針變量
同樣地,我們可以聲明指向其他變量類型的指針。例如:
double*pointerA; //聲明一個指針,其中可以儲存double類型的變量的地址 unsignedint*pointerB; //聲明一個指針,其中可以儲存unsignedint類型的變量的地址 string*pointerC; //聲明一個指針,其中可以儲存string類型的變量的地址 vector*pointerD; //聲明一個指針,其中可以儲存vector 類型的變量的地址 intconst*pointerE; //聲明一個指針,其中可以儲存intconst類型的變量的地址
注意:指針是一種變量類型,這種變量類型在每一個操作系統上的大小是固定的,就好像int型,double型這樣。不要認為指針可以儲存int類型的變量的地址,這個指針就是int型指針,這樣的說法是不准確的。
你可以測試一下,用sizeof操作符來獲取指針類型所占的字節數。
例如:
cout<<"指針的大小是"<
在我的電腦上,打印的結果是
指針的大小是 4
也就是說指針的大小是4個字節,是不隨其所指向的內存地址上的變量的類型而改變的。
暫時,上面那些指針還只是聲明了,沒有被賦值。
這是很危險的!
因為此時指針裡面包含的可以是任意的內存地址。假如你使用這樣未賦值的指針的話,你並不知道自己在操作內存中的哪塊地址。有可能這塊內存地址上保存著極為重要的數據。
因此,永遠記得:聲明指針之後,在使用前一定要先對其賦值。
因此,一個不錯的習慣就是聲明的同時給指針賦初值,就算是初始化啦。通常習慣賦0,例如:
int*pointer(0); double*pointerA(0); unsignedint*pointerB(0); string*pointerC(0); vector*pointerD(0); intconst*pointerE(0);
還記得這一課的開始處我們給的一幅內存圖嗎?內存的第一個可用的抽屜的標號是1而不是0,地址為0的內存空間一般不可用。
因此,當我們為一個指針變量賦初始值0時,意味著它不指向任何內存地址。就好像用一根缰繩把一匹會亂跑的悍馬栓在0這個木樁上("套馬的漢子,你威武雄壯...")。
因此,假如你聲明一個指針變量時還未決定讓其指向什麼地址,那麼給其賦初值0是很必要的。
儲存一個內存地址
現在我們已經會聲明指針變量了,接下來要學習的就是如何把另一個變量的內存地址儲存到這個指針變量中。
我們已經知道,要獲得變量的內存地址,需要用到&符號。那麼就很簡單了,例如:
intmain() { intuserAge(16);//一個int型的變量 int*ptr(0);//一個指針變量,其中可以儲存一個int型變量的內存地址 ptr=&userAge;//把int型變量userAge的地址存放到ptr這個指針變量裡 return0; }
以上程序中,最關鍵的一句就是
ptr=&userAge;
執行完這句指令之後,指針變量ptr裡面的內容就變成了userAge這個變量的地址,我們說:ptr指向userAge。
用一張內存圖示來說明:
上圖中,我們見到了我們的老朋友userAge,此變量存放在內存地址53768處,變量的值是16。
新增的內容當然就是指針啦。在內存地址14566上(當然這些內存地址都是舉個例子)存放著一個指針變量,名字是ptr,注意看:ptr的值就是53768,也就是我們的userAge的地址。
好了,現在你差不多理解了吧。
當然了,也許你還是有疑問:為什麼要把一個變量的內存地址存放到另一個變量裡呢?有什麼好處呢?
相信我,你會"守得雲開見月明"的。
假如你理解了上圖的話,那麼是時候深入學習咯。
顯示內存地址
指針也是一種變量類型,因此,我們可以顯示其值。
寫個程序:
#includeusingnamespacestd; intmain() { intuserAge(16); int*ptr(0); ptr=&userAge; cout<<"變量userAge的內存地址是"<<&userAge<
運行以上程序,顯示:
變量userAge的內存地址是 0x22ff0c
指針變量ptr的值是 0x22ff0c
看到了嗎?我們的userAge變量的內存地址和指針變量ptr的值是一樣的。
訪問指針指向的內容
還記得指針的作用嗎?它使我們可以不通過變量名就訪問一個變量。
那麼怎麼做呢?需要使用*號,它可以獲取指針指向的變量的值。
例如:
intmain() { intuserAge(16); int*ptr(0); ptr=&userAge; cout<<"指針變量ptr所指向的變量的值是"<<*ptr<
程序執行到 cout << *ptr 時依次做了以下的操作:
找到名字是ptr的內存地址空間
讀取ptr中的內容
"跟著"指針指向的地址(也就是ptr的值),找到所在地址的內存空間
讀取其中的內容(變量userAge的值)
打印這個值(此處是16)
此處我們又一次使用了星號(*),術語稱為:"解引用"一個指針。
還記得之前我們用星號來聲明指針變量嗎?因此,同一個符號在不同情況下作用也許不一樣。
符號小結
我承認,這些符號是有點讓人頭暈。目前為止,星號(*)有兩個作用,而且&在這一課之前是用於引用的。
這可不能怪我,你們說對吧,要怪也得怪C++之父。
好吧,我們來小結一下:
假如我們有如下代碼:
intnumber=16; int*pointer=&number;
那麼:
對於int型變量 number 來說
number:number的值
&number:number的內存地址
對於指針 pointer 來說
pointer:指針的值,也就是其所指向的另一個變量的內存地址。也就是number的地址,即&number
*pointer:指針所指向的內存的值,也就是number的值
如果不太理解,可以畫一些圖來幫助掌握。
動態分配
你想要知道指針到底可以用來做什麼嗎?那好,我們先來學習第一種用法:動態分配。
內存的自動管理
在我們以前關於變量的課程裡,我們已經學過:當變量被定義時,大致說來程序其實做了兩步:
程序請求操作系統分配給它一塊內存空間。用專業術語說就是:內存分配。
用一個數值來填充這塊內存空間,用術語說就是:變量的初始化。
上面的兩個步驟是自動完成的,程序會替我們打理。而且,當我們的程序執行到函數的末尾時,就會把操作系統分配的內存空間自動歸還。專業術語叫做:內存釋放。在這種情況下,內存釋放也是自動的。
我們現在就來學習不是自動的方式,也就是手動的。是的,聰明如你應該猜到了,我們要使用指針。
分配內存空間
為了手動申請內存空間,我們需要使用運算符new。
new在英語中時"新的"的意思。
new會申請一個內存空間,如果成功,則返回指向這塊內存空間的指針。所以嘛,就輪到我們指針上場啦。例如:
int*pointer(0); pointer=newint;
上面的兩行程序中的第二行向操作系統申請一塊內存地址,為了儲存int型變量。這塊內存地址的地址值將儲存在pointer這個指針裡。原理如下圖所示:
從上圖中可以看到,我們一共使用了兩個內存空間:
第一塊內存空間的地址是14563,其中存放的是一個還沒被賦初值的int型變量,而且此變量也沒有名字。
第二塊內存空間的地址是53771,其中存放的是我們的指針pointer,指針的值是14563。
記得:在內存地址14563上的變量是沒有變量名的,只能通過指針pointer來訪問。
因此,如果我們修改指針pointer的值,我們就失去了唯一訪問那塊沒有標簽的內存的機會。你將不能再使用那塊內存,也不能刪掉它。這塊內存就好像迷失了一般,不過又占用著內存,術語稱為:內存洩漏。因此必須當心!
一旦手動分配成功,這個變量就和平時的變量一樣使用。只不過我們是通過指向這個變量的指針來操作它的,因為我們並沒有給這個變量起名字。需要用到解引用(*)。
int *pointer(0); pointer = new int; *pointer = 2; //通過指針訪問內存,以改寫其中的內容
那塊沒有標簽的內存空間(相當於沒有變量名的變量)現在被填充了數值,是2。因此,內存裡的情況如下:
使用完此內存地址後,我們需要向操作系統歸還這塊內存地址(指針指向的那塊內存地址)。
釋放內存
我們用delete運算符來釋放內存。
delete在英語中是"刪除,清除"的意思。例如:
int *pointer(0); pointer = new int; delete pointer; //釋放內存。注意:是釋放了指針指向的那塊內存
執行上面的操作後,指針pointer所指向的那塊內存就被歸還給操作系統了。不過,內存卻還一直存在,而且還是指向那塊內存,不過,我們沒有權利再使用這個指針了。如下圖所示:
上圖還是比較形象的。如果我們循著指針的所指的箭頭找去,我們會達到一塊已經不屬於我們的內存。因此我們不能再使用這塊內存了。
"伊人已嫁,吾將何去何從?何以解憂,唯有稀粥"
因此,為了避免我們之後又誤操作這塊內存,須要在delete操作之後,再刪去這個指向已不屬於我們的內存的箭頭。聰慧如你應該想到了,就是將指針的值置為0(無效的內存地址)。如果沒有置0這一步,那麼程序往往會奔潰,即使一時沒奔潰,也是存在隱患的。
int *pointer(0); pointer = new int; delete pointer; //釋放內存 pointer = 0; //把指針指向一個無效的地址
記得:手動分配了內存之後,使用完一定要釋放這塊內存。不然,你的內存會越來越少。一旦內存不夠用了,你的程序就會奔潰。
一個完整的例子
我們用一個完整的例子來結束這一小節吧:詢問用戶的年齡,並借助指針來顯示年齡
#includeusing namespace std; int main() { int* pointer(0); pointer = new int; cout << "您的年齡是 ? "; cin >> *pointer; //借助指針變量pointer,我們改寫pointer指向的內存地址上的內容 cout << "您 " << *pointer << " 歲了." << endl; //再次使用pointer指針 ? ?delete pointer; //別忘記釋放內存 ?pointer = 0; //並且把指針指向一個無效的地址 ? ?return 0; }
通過這個例子,我們掌握了如何通過指針進行內存的分配和釋放。
之後,我們會學習使用Qt來開發圖形界面的程序。我們會經常使用new和delete這對組合,例如在創建窗體和銷毀窗體的時候。
到底啥時用指針好呢?
到了這一課的最後一小節,我們需要解釋一下:何時使用指針呢?
通常在以下三種情況下應該使用指針:
想要自己控制內存空間的分配和釋放
在多個代碼塊中共享一個變量
在多個元素間選擇
其他的情況,我們可以不必使用指針。
第一種情況我們已經學習了。來看看後兩種吧。
共享一個變量
對於指針的這種用法,暫時我還不給你完整的代碼示例。當之後第二部分講到面向對象的編程時,我們自然就會有實例了。不過,我會給出一個更加視覺化的例子。
你玩過策略游戲嗎?如果沒玩過,也許聽說過吧。
舉個例子,這類游戲中有一個很著名的游戲:Warcraft III,暴雪公司的作品。截圖如下:
要編寫這樣一個大型的RPG游戲,是非常復雜的。我們此處不討論游戲,而是借此來思考一下一些用例。
在上圖中,我們可以看到紅色的人族和藍色的獸族在交戰。游戲中的每一個角色都有一個攻擊目標。
例如,獸族的幾乎所有兵力都在攻擊被暗影獵手變成小動物的山丘之王(就是鼠標點中的那個"小動物")。人族的兵力在攻擊劍聖。
那麼,在C++中,如何指明紅色的人族的攻擊目標呢?當然了,暫時你還不知道怎麼用代碼實現,但是你應該有點主意了吧?回想一下這一課的主題。
是的,就是用指針。游戲中每一個角色都有一個指針,指向他們攻擊的目標。這樣,每個角色才會知道在某一刻該攻擊誰。例如,我們可以寫這樣的代碼:
Personage *target; //指向攻擊目標的一個指針
當雙方沒有交戰之前,target這個指針指向地址0,也就是沒有攻擊目標。一旦兩軍兵刃相接,target指針就指向攻擊的目標,而且隨著游戲進行,target指向的目標是可以切換的。
因此,在這樣的游戲中,一個指針就可以將一個角色和他的攻擊目標聯系起來了。
之後我們會學習怎麼寫這樣的代碼。而且在第二部分我們也可以寫一個小型的角色扮演游戲(RPG游戲)。
在多個元素間選擇
指針的第三種用途是可以依據用戶的不同選擇來作出不同的反應。
舉個例子,我們給用戶出一個多項選擇題,有三個選項。一旦用戶選擇答案之後,我們用指針來顯示他選擇了哪個答案。代碼如下:
#include#include using namespace std; int main() { string responseA, responseB, responseC; responseA = "幽閉空間恐懼症"; responseB = "老年癡呆症"; responseC = "妄想症"; cout << "阿爾茨海默病是指什麼 ? " << endl; //提問題 //顯示答案 cout << "A) " << responseA << endl; cout << "B) " << responseB << endl; cout << "C) " << responseC << endl; char response; cout << "您的答案是 (填寫A, B 或 C) : "; cin >> response; //提取用戶的答案 string *userResponse(0); //指向所選答案的指針 switch(response) { case 'A': userResponse = &responseA; break; case 'B': userResponse = &responseB; break; case 'C': userResponse = &responseC; break; } //用指針來顯示我們選擇的答案 cout << "您選擇了答案 " << *userResponse << endl; return 0; }
當然了,指針還有很多用處,以後我們會慢慢學習的。
總結
每一個變量是存儲在內存的不同地址空間上。
每一個地址上一次只能存放一個變量。
可以借助&符號來獲得一個變量的地址,用法是:&variable
指針是一種特殊的變量類型,這種變量中存儲的是另一個變量的地址。
一個指針是這樣聲明的:int *pointer; (這種情況下這個指針指向的是一個int型的變量)
如果我們打印一個指針的內容,那麼得到的是其中儲存的內存地址。如果我們用*符號來獲得指針指向的內存(*pointer),那麼打印的是指針指向的地址上存放的變量值:
我們可以手動申請一塊內存地址,用new關鍵字。如果用了new關鍵字進行了所謂動態內存分配,之後不用此變量了,我們需要用delete關鍵字來手動釋放這塊內存(因為不會被自動釋放)。
初學指針可能會覺得比較難理解。不過不必擔心,大可再讀一遍這一課。讀書百遍,其義自見。不過也要配合練習。不用怕,以後我們會經常使用指針,所以你會慢慢熟練的。
第一部分第十三課預告
今天的課就到這裡,一起加油吧!
下一課我們學習:第一部分小測驗