第3篇:「來啊,造作吧,反正有大把的內存」

調查

一直希望課程能實際幫助到初學者動手實踐,而不是停留在看看有趣而已。只有收到經過實踐真實反饋,我才好知道如何課程。因此,第3篇開始前想提請大家配合幫忙:凡是按第2篇所提到而安裝了「Code::Blocks」 + GCC,並且完成「hello world」,也看到了數據小『a』的地址、尺寸的知友,能否在留言中說明一下。

複習

這是第2篇最後的代碼,複習一下:

#include <iostream>nnusing namespace std;nnint main()n{n int a(9);nn009 cout << a << endl;n010 cout << &a << endl;n011 cout << sizeof(a) << endl;nn return 0;n}n

其中「009」、「010」和「011」是為了後續說明方便而加的行號,在實際代碼中不應該有它們。

009行輸出a的值,010行輸出a的址,011行輸出a的尺寸。

重要!

請閉上眼睛,在腦海中回想第2篇提到的那張你必須隨時可以在腦海中浮現的圖。

如果想不起來或者有些模糊,請自行完整複習第2篇。因為我們馬上要給小a添加兩個鄰居。場面會變得更加熱鬧。如果你看著「int a(9)」這樣一個數據,無法「透過現象看本質」地看到它的名字、類型、值、地址、尺寸等信息;那一旦鄰居出現了,就容易出現學習上的崩盤。

小a和它的鄰居們不得不說的二三事

我們為小a安排的鄰居不是大A也不是老王,而是小b和小c,這就確定了他們之間的二三事一定會有健康的主調:

#include <iostream>nnusing namespace std;nnint main()n{n int a(9);n int b(10);n int c(11);n}n

對比之前的代碼,有關小a信息輸出的內容都刪除了。加了值為10的小b和值為11的小c的定義。

小提示:main()中的「return」語句可以不關心

相信心細而眼尖的同學會發現:原有自動生成的內容中,有一行 「return 0;」 也被我們刪除了,「return 0;」的作用是什麼?為什麼在此處可以刪除,這些都先別理,只要知道兩點結論:一、刪除它以後以上代碼仍然是合法的C++程序;二、刪除之後,我們的注意力可以更加集中在這三個鄰居身上。

我們的第一個問題是:小a、小b、小c誰年紀大?誰年紀小?

直接給答案:小a年紀最大,小c年紀最小。簡略而不嚴謹的說明:因為在同一代碼塊(什麼叫代碼塊,以後再詳解,此處大家就理解為它們位於同一對 { }之中即可)。小a最先被定義(相當於出生),小b次之,小c最晚。

重要:事關你合不合適學習C/C++的一個小問題

算是問題二:將來,小a、小b、小c三人,誰最長壽(活得最長)?

答:還是小a最長壽,而可憐的小c死得最早。為什麼?現在不說明。因為,我們已經不知不覺碰觸到了C/C++有關數據的另一個重要屬性:生命周期。

現在我們關注的是:你覺得這些有點意思嗎?如果有,那麼你多了一個自己其實比較適合學習C/C++語言的重要證據(這是我十數年教學經歷中的一些小經驗,而不是為了討你歡心。)。

相反,如果你覺得關心一個數據的類型、名字、值、址、尺寸就已經很無聊了,怎麼還要多出一個 「壽命長知」,這樣太無聊了!……好吧,一定是我講得不好。

回到主題,我們把a、b、c三個數據定義在同一代碼塊中,然後我們就稱呼它仨是鄰居,這個說法嚴謹嗎?不嚴謹。假設我們把「代碼塊」比成一個小區,那麼這三個數據在內存小區里很可能一個住東頭,一個住西頭,一個住南頭;各自隔了好些個位元組,因此算不上精確意義上的鄰居。

我們對內存中的數據是不是鄰居的精確定義是什麼呢?從字面解釋,確實同一個小區的住戶,甚至相鄰小區的住房都可以自稱「鄰居」。所以我們換個說法:「在內存中相鄰」。

已經知道一個數據至少有六個屬性:1類型、2名、3值、4址、5尺寸、6生命周期。兩個數據是否在內存中相鄰,和這六大屬性中「址」與「尺寸」有緊密關聯。比如,a的內存地址是5,且a的尺寸是4個位元組,那麼住在5+4,也就是9號內存中的那個數據,就和a相鄰。

問題三:上例代碼中的a、b、c的內存,是不是緊密相鄰呢?

回憶一下我們在第2篇說過:「C++是一門現實主義的語言」,所以我們猜測C++應該不會規定在代碼緊密相鄰的數據,就一定要在內存中也緊密相挨——因為現實中人們對事物的排列也也沒有這個硬性要求。有時候可以緊挨著,比如公交車來了,大家拚命地往上擠;但有時候也不能緊挨著,比如去銀行辦事,小丁看排號排在前面的A女士長得漂亮,非要上去堅挨著,就容易產生問題。

丁小明將一直是我們的調侃對象,但是他的心態特別好;從不在意,也從不受影響。你看,現在他就一直在認真思考課程的知識。他提出一個問題:「老師,如果我在代碼中,將a,b,c隔得遠一些,是不是就可以保證它們在內存鐵定不相鄰呢?」。以下是他出示的,將a,b,c隔得遠一些的代碼:

……nint a(9);nnnint b(10);nnnint c(11);n……n

「小丁,你的態度很好,但是你的智商……智商……」。唉!我不知道該怎麼說……但是也讓我意識到剛剛犯了一個表達上的錯誤。我不應該說在代碼塊中 「緊密相鄰的數據」,而應該換成在代碼塊中 「前後出現的數據」。

C++確實沒有規定在同一代碼塊中前後出現的數據,一定要在內存中緊挨,意思就是,它們可以在內存緊挨著存在,也可以在內存不緊挨著存在(完全廢話)。結論有了,但這節課要是就這樣結束那就太沒意思了。讓我們先做作業吧。沒錯,這就是《白話C++》專欄版的第一個課堂作業。

(強調:專欄只是點心。更有加正宗、有趣、系統、完整、深入的C++課程,是17年11月北航出版社將要出版紙質書《白話C++版》,屆時京東有售)。

課堂作業:如何眼見為實,真實看到a,b,c在內存是否相鄰?

就是讓你馬上動手寫代碼,親眼看到這三個數據的內存地址,然後觀察它們彼此之間是否相差4個位元組(即int類型在本課程使用的編譯環境下的尺寸)。

答案的關鍵三行代碼如下:

……ncout << &a << endl;ncout << &b << endl;ncout << &c << endl;n……n

F9 ,運行結果如下:

(a,b,c三個內存是否相鄰)

我是在Linux上使用GCC編譯的程序,該環境使用十六進位表達內存地址。如果你在Windows + mingw 環境下編譯後運行,看到的是地址很可能就是三個十進位整數。那倒好辦了。請將相鄰的兩個變數地址兩兩相減,看到差值是不是4。

如果你也在Linux下寫示常式序,請先看上例圖中的c和b。一個是xxxxxe0,另一個是xxxxxe4,雖然我們還不知道『e』代表什麼,但以我們在初中學到的「代數」思想,可以指導我們輕鬆計算出 xxxxxe4 - xxxxxe0 = 4。

小提示:

在16進位中,字母a表示10,b表示11,c表示11……f表示15,f再往上,就要高位進1了,於是又回到0。根據這個方法,大家計算一下 dc + 4 是不是就是 e0 ?

所以,剛剛,我們(加了引號的)的「眼見為實」就是:在某年某月某日某時某分某秒某毫秒某微秒的瞬間,我們編譯一段定義了三個相鄰數據a,b,c的代碼,並運行,然後它們剛好在內存中也是a和b緊密相挨,b和c緊密相挨。

為什麼要加引號?又為什麼要刻意強調這是只某一瞬間的現象,又為什麼要使用「剛好」?因為在前面說過了:這個現象和結果,它不是標準規範,不是鐵定的規律。

一大隊的正統,道學的C/C++老師已經在前來譴責我的路上。他們說:「你在第二篇教學生什麼「對數據赤祼祼窺視」,我們忍了,想不到在這第3篇你越發過份,居然一次性窺視三個數據的內存地址!這跟組織學生偷窺女澡堂有何本質區別?!」

偷窺女澡堂這麼沒品的事,我可沒幹過;但小時候好奇鄰居小姐姐,於是掀起她的裙子,這事我承認做過。我覺得年幼時這種事扯不上道德大義,相反,如果一個小時候沒有掀過女生的裙子的男生,長大成年學習編程——估計 學習別知識也一樣——極大可能存在四個嚴重的能力缺陷: 一、缺少對美好事物的審美能力;二、缺少對未知事物必要的好奇心力;三、缺少為追求真理而不怕挨打的膽量和氣魄;四、缺少解決問題的實際動手能力。

丁小明又問題了,他一邊問我一邊看著班花:「老師,我現在補救還來得及吧?」

……有突發事件,大家等等……

好,繼續。

人不輕狂枉少年。各位我不管你們是10幾歲,20幾歲,30幾歲,甚至40幾歲,因為《白話C++》就是寫給小白的課程。所以我一視同仁假設你們全是「C++學習」上的小白。所以你們現在全是可愛的小朋友;所以我會偶爾帶大家做一些C++編程上的輕狂事;以及你們年幼的心靈里對世界必然的天生的破壞欲。但是,一旦你學習了《白話++》20節課(以專欄版為主)以後,還在做這些輕狂事,我肯定要表示驚訝並阻止。大家都看到我嘴角的血了?因為我剛才很拚命地制止丁小明同學掀班花裙子這一件事的發生。

單單只是看了三個變數的地址,這算什麼輕狂呢?讓我們進入下一小節。把指針叫出來,一起狂,一起造,一起作。

讓指針出場

指針出場前,讓欠對「指針」再做一些介紹:

  1. 指針也是數據——有不理解這句話的嗎?如果不理解,請回看第一篇《不談世界,我們談家》;
  2. 指針也是數據——是數據就有「地址」屬性,因為是數據就得有「家」住 → 所以指針也要佔用一塊內存 → 所以指針也有一個內存地址;
  3. 指針也是數據——是數據就有「值」的屬性。關鍵在於指針數據家中存放的值,通常是另一個數據的內存地址。(第二次提到了,既是本節重點,也是將來理解C/C++很多事情的重點!後面會展開說明);
  4. 指針也是數據——是數據就有尺寸的屬性。由於不管哪個指針,它的值,也就是指針所佔用的那塊內存中存放的東西,統統是一個地址(不管有效、無效);而程序中的地址就是一個數字,尺寸固定。所以只要是指針的尺寸也是固定的。至於是多大的一個固定值,這和編譯環境(編譯器、操作系統、機器等)有關,比如常說的16位、32位、64位操作系統等,指的就是內存地址的寬度(1個位元組有8位寬度);
  5. 指針也是數據——所以它當然也要有類型。

我們從最後一點說起:指針的類型。請注意第3點說到:指針數據家中存放的值是一個地址,而地址不管是用十六進位還是十進位表達,它就是一個數值;所以指針數據的類型,難道不應該就是一個整數(int類型)嗎?

這個說法確實說到點子上了。能這樣的思考的同學前途無量啊。

現實生活中的地址很亂,所以更像是字元串類型,比如「福建省廈門市思明區XX路78號」或「林中村村口小賣鋪邊上的養雞場」等等。內存地址當然不會這麼亂,它就是一個整數。所以指針的值,本質上確就是一個整數的值。

但是,我們不能將指針的類型簡單地歸成 「int」。整數通常用來做加減乘除等四則運算,比如: 55 + 21等於76。但拿兩個地址相加往往沒有意義。老師就不會布置這樣的作業: 假設你家住55號,你心儀的姑娘住21號,二者相加得到的76號是老王家地址。請寫一篇不少於400字的文章分析論述這三者間的關係。

當然,指針之間也不是完全不能計算,比如有些時候兩個地址相減還是有意義的:55-21得到34,表示你和她家之間還是有些距離的。

不管怎樣,「地址」是一種特殊的數值數據,不能簡單地將它和普通整數值混用。而C++又是非常強調「出身論」的語言,為各種指針數據進一步細分類型,就成了必然的事情。要怎麼細分類型呢?不急。讓我們先來理解在C/C++語言中,為什麼要有指針?

為什麼要有指針?又來了,「C++是一門現實主義的語言」,所以它的很多設計約定(包括它要兼容C),都可以在生活中找到廣泛的理論基礎或實踐經驗。大家都當過少先隊員吧? 有沒有在出門上學前,滿世界找不到紅領巾,急得像熱鍋上的螞蟻的經歷?

眼看再找下去就會遲到,在這樣一個萬分焦急的時刻……努力回憶一下,通常你們會怎麼做?

「 媽,我的紅領巾呢?」

沒錯,小學生們都會在這個時候找媽。找媽就能找到紅領巾,請把這句話讀三遍,一是為了感謝在我們年少時強大近乎萬能的媽媽,二是因為這句話事關指針作用的本質。

我們每天早晨上學前都想訪問到我們的紅領巾,但是每一天早上紅領巾就像長了腳和我們玩捉迷藏遊戲一樣,有可能在沙發,有可能在床上,有可能在晾衣架,有可能在你某雙今天不穿的運動鞋裡,有可能在書包的某一隔層里……紅領巾是一個數據。它一有名字(某某同學的第三條紅領巾)、二有類型(中國少先隊員第三版紅領巾)、三有值(剛帶了一周,絲綢做的,貴)、四有尺寸(統一大小)、五有生命周期(如果不丟的話,應該要用到你小學畢業)……頁此時的關鍵就是:它有地址,但你不知道它們在哪裡。

於是你一付快哭出來的樣子,叫著「媽,我的紅領巾呢?」。媽媽過來拉拉你的衣領,說:「小傻瓜,紅領巾就在你的脖子啊。它繞到你後背上去了。你昨晚寫作業後直接上床,就沒有解下過它。」

多麼溫馨的回憶!重點是,大家理解指針的最基本的用處了嗎?指針就像媽媽一樣,它幫程序記住許多後面要訪問的其它數據的地址。現在再來看這個說法:指針也是一種數據,它的值是其它數據的地址,是不是覺得好理解了一點?

當然,媽媽的偉大和強大何止在於記憶一個紅領巾的地址。媽媽們可以記憶我們的書包、我們的作業本,我們的玩具,我們的公交卡,我們自為為偷偷藏著的隔壁班級校花的照片都在哪裡放著;也能知曉爸爸的領帶,爸爸的煙灰缸,爸爸的私房錢藏在哪裡……

現實生活中,人類大量地利用了藉助A來找到B的的手段。媽媽只是一個比喻。事實上在第一節課《不談世界,我們談家》里,我們費了半天口舌,那麼啰嗦地扯了半天,所有得到的結論之一就是:地址不是家,但通過地址可以找到家。其它類似的還有:銀行卡不是錢,但通過銀行卡可以取到錢。通訊錄不是人,但是通過通訊錄可以找到某個具體的人。剎車踏板也是一個例子:你腳踩的是一塊鋼板,但這塊板輾轉傳遞出去的動作和力量,抱緊了車輪——這些例子都不僅僅是讓A幫我們記住B的地址的作用了,實際都有通過A幫我們更好地、更靈活地、更安全地訪問B的作用在裡頭。

當然,在生活中,我們仍然有極大數量的一部分操作,必須是直接訪問數據,不容他人或他物經手,哪怕是媽媽。比如喝可樂,比如親嘴等等。既然C++是一門入世的語言,一門現實主義的語言(又提了一次,別煩,很快就不會再提了……),所以在C++程序中,既有大量直接訪問數據的用法,也有大量通過指針來間接訪問數據的用法,甚至可以通過「指針的指針」來轉手多次訪問某個數據……

小提示:語言設計的差別

一個東西,你可以直接訪問,也可以間接通過他人或他物訪問,聽起來很自然?不過也有一些語言的設計思路不一樣,它們認為程序員最好永遠不要直接訪問到內存——事實上永遠禁止直接訪問物理內存,這件事已經會有操作系統來做保障,但有的語言仍不放心。所以在這些語言中,程序每次訪問數據,事實上都有個「媽」夾在中間轉手——這樣的語言就沒有特別的「指針」的概念——因為基本所有數據都是指針,自然就不用再特別提指針。這樣的設計有它的好處和壞處。我們拿來做對比,只是為了幫助更好地理解C++的設計「哲學」:入世。

通過指針數據訪問另一個數據,在這一過程中指針是訪問手段,最終的數據才是訪問目標,所以,指針的類型設定就應當能體現最終數據的類型。意思就是,當我們需要他人來幫助時,C++程序認為應該為你配備多種類型的秘書(媽媽?保姆?)。當我們想找紅領巾,哦不,都有秘書了,找什麼紅領巾啊。當我們需要找領帶,有專門管理領帶的秘書,當我們需要訂餐,有專門訂餐的秘書……嗯,汽車的設計也是這樣的,當我們要剎車,就有剎車踏板,當我們要加油,就有油門踏板。若是將這兩者混為一個踏板,這樣的設計表面上很強大,但其實會害死人。

結論是,如果要間接訪問「int/整數」數據,那我們就需要「int/整數」類型的指針,如果要間接訪問一個字元串,那麼我們就需要字元串類型的指針。出於簡化和直觀,通常稱「整數類型的指針」為「指向整數的指針」;稱「字元串類型的指針」為「指向字元串的指針」。代碼上怎麼表達呢?在類型之後加一個『*字元就可以了。我們先複習一個整數類型數據的定義語法:

int a(9);n

然後複習如何解讀它:數據類型int,名字a,初始值 9。

接著定義一個指針,我們希望這個指針指向a,並為它取名p。所以它的類型是「指向整數的指針」,初始值則是a的地址。 a的地址如何取?還是『&符號

定義一個符合上述要求的指針,代碼如下:

int* p(&a);n

嘗試解度:「int」 表示p最終指向的數據是整數類型;而其後的『*』字元,表示『p』是一個指針。至於(&a),表示這個指針被初始化為a的地址——直觀的表達是:p指向了a,客觀的結果是:p就是a的地址,或者說,p的值就是&a。對比普通數據與指針數據的定義,重點是如何設定初值:

/* 普通數據(整數) | 指針數據(指向整數的指針) */n int a(9) | int* p(&a);n cout << a << endl; | cout << p << endl;n

如果代碼運行,那麼左邊將輸出a,值是9,因為初始化代碼是 a(9);右邊輸出p的值將工a的地址,因為初始化代碼是 p(&a)。馬上測試:

#include <iostream>nnusing namespace std;nnint main()n{n int a(9);n int b(10);n int c(11);nn int* p(&a);n cout << &a << endl;n cout << p << endl;n}n

結果:

(指針存的是某一數據的地址)

確實,此刻p就是&a。

又一個課堂作業來了。

課堂作業:理解指針的值,和指針自己的址:

請問,針對以上示例中的p,&p 是什麼? p 是什麼?

答案:p是另外一個數據(在本例是a)的地址,而&p是p自己的內存地址。就這麼簡單,媽媽的腦海里知道我們的紅領巾一直在我們的脖子上,但媽媽不在我們的脖子上。這是課後作業:請以a和p為例,寫代碼證明「媽媽知道紅領巾在我們脖子上,但媽媽不在我們的脖子上」。

答案也很簡單,完整代碼在這裡:

#include <iostream>nnusing namespace std;nnint main()n{n int a(9);n int b(10);n int c(11);nn int* p(&a);n cout << "紅領巾的地址:" << &a << endl;n cout << "媽媽腦海中紅領巾的地址:" << p << endl;n cout << "媽媽自己的地址:" << &p << endl;n}n

「老師你為什麼要把答案直接給出來?你這樣不是讓我們喪失了獨立思考的機會了嗎?」

「你一個小學一年級的學生,讓你開口跟著老師讀『a』、『o』、e,你不讀,一直在那邊托著腮思考為什麼a的發音是『啊』,『o』的發音是『哦』……你是在幹嘛呢! 」

就階段而言,此時的編程學習,動手是首要的。應該在動手之後,包括確保代碼無誤,然後觀察運行結果,再作思考。

學到這個時候,我們仍然只是知道可以通過指針間接訪問到某個數據,但仍然不知如何實際操作。比如,現在a的值是9,那我們要怎麼通過p來得到這個9呢?還是『*』這個符號。在定義時,『*』表示p是一個指針,在訪問時,『*p』 表示指針所指向的數據的值。所以『*』在此處被稱為「取值」操作符。和&(取址操作符)隱約有一種對應關係。&可以取一個數據的地址,而*可以取一個指針所存儲的地址那塊內存的上值……很繞,所以直觀地叫做:一個指所指向的數據的值。讓我們加兩行代碼:

……n cout << "直接訪問紅領巾的值:" << a << endl;n cout << "通過媽媽間接訪問紅領巾的值:" << *p << endl;n……n

現在才是思考、整理、歸納和理解的時間:如果p是一個指針,請理解三者含義的不同:p、&p 、*p。在這一問題上的理解,必須做到永遠永遠不含混。

又是一天清晨,陽光正好。你揉著迷糊的眼神,又叫了一聲「媽,我的紅領巾呢?」。如果這一次紅領巾又在你的脖子上,那我們就沒有故事可講了。根據劇情,這一次的你紅領巾,在鄰居小b那裡。指針可以在定義之後,改變它的指向——「改變指針的指向」,這種高大上的說辭,其實就是:改變指針的值。改變指針的值使用我們在小學就學過的 『=』等號,不過精確的術語是『賦值操作符』。比如,下面的代碼讓指針p在初始化為a的地址,但後面改為指向b:

……n int a(9);n int b(10);n int c(11);nn int* p(&a);n cout << *p << endl;n p = &b;n cout << *p << endl;n……n

同樣是輸出『*p』,但第一次是9,第二次是10。

課堂作業

讓p在指向b之後,改為指向c,並輸出新指向之後的*p值。

關於指針,其實也包括普通數據,其實還有大量知識點還沒有講。但根據課程的安排《白話C++》專欄版這前面幾節課,根本就不是正課(正課哪有寫得這麼啰嗦的)。沒有哪個人一定要學編程啊!也不是所有人就真的能很有興趣的堅持學下去啊!就算一定要學習編程,並且一定能排除萬難堅持下去,也不一定要學習C++呀!

插一點並不廢的廢話:通常,你不應直接扎進某一門知識的學習

一個人跑過來問你該如何做酸菜魚,你回答他:「這樣吧,你先去超市裡把酸菜魚需要的料,都買回來,晚上我教你。」 這樣的要求雖然可以完成,但其實是不太合理的,因為很可能那人並不清楚知道酸菜魚的需要的所有材料啊! 這只是做魚呢!如果是做滿漢全席呢?這只是做滿漢全席呢!如果我們是要學習軟體開發呢!哦,不是如果,我們就是要學習軟體開發啊!軟體開發的知識點之繁雜、之旁路叢生、之優勝劣汰,根本就是一個恐怖的存在。任何一個人若可以拍著胸膛說:「我已經明確知道自己要學習以及適合學習什麼」,那他就基本不算是編程的小白了。任何一個人若可以拍著胸膛說:「我已經清楚地規劃好了我三年內的學習路徑」,那他的學習計劃其實只餘下兩年了。

「老師你扯半天,其實你是不是只會C++所以才教我們C++啊?」。這問題好尖銳啊。不過正是我想要的。

再插一點並不廢的廢話:為什麼是《白話 C++》?

終於不那麼生硬地扯出了一個話題,讓我可以自我介紹一下。

我在大學課堂上學過BASIC、Pascal、C。C++和Java都是在1996年自學。上班前幾年C/C++用得比較多。後面用到Java、C#、Delphi(Object Pascal)和PHP。大概代碼量是C/C++會有數十萬行,Java、C#、PHP應該都是十數萬行左右。沒有實際統計,說的只是一個比例關係。創業時又開始正式地使用Node JS 及Python。

現在教C++課程,有這麼幾個理由:

一、確實用得最多,也比較有站在「教課」的角度來來思考的一門語言。只是,C++肯定不是我最熟悉的語言,這麼語言的神奇和可怕之處,你們上網搜索一下就懂了。倒過來,C++肯定是我現在還有在實際使用中的,最容易讓我犯錯的那門語言。

二、從和出版社簽合同開始,現最後完稿,寫了快十年的《白話C++》課程(紙質書,內容和這裡的相差比較大),所以要寫專欄時,我當然寫C++。

三、最重要的是:我自己的實際經歷是學習了C/C++之後再學習其它語言,感覺是順的,並且對理解其它語言大有幫助。然後我因為一直比較好為人師,所以在一線研發工作也好,當技術總監之後也好,都一直堅持有在做一些半義務的編程教育(實際當過C++JavaPHP的培訓老師),發現從Java轉C++學習是有些卡。所以我堅持或推薦很多人先學習C++。當然,這完全是我的個人經驗,不具有代表性,但人總是要教自己經歷、熟悉而有歸納和思考的東西,這對被教的人也是好事。

四、我也不是只教C++。我只是先教C++而已。我的白話Python和白話NodeJS(暫定名稱)也在準備的進度中。

廢話結束,我們要開始作了。

帶著指針一起作

通過將某個數據的地址直接賦值給指針,可以改變指針的指向。不過指針還可以自己「摸索」著在內存中前進或後退。說到「摸索」,大家想像的可能是在一個迷宮或黑屋裡到處走動。這很形象,但內存是線性(一維、連續)的,所以指針所「摸索」也就是前進或後退而已。還是來一個比喻吧。

我把車停在車庫裡,然後你來向我借車,我給你鑰匙,然後說:「在78號車位」。為了更加接近內存的編址方式,我們假設這個車庫的所有車位就是一條直線排列。你去了以後卻發現78號車位上的車並不是我的,或者根本沒車。你打電話過來問我怎麼回事。我回你「哎呀,就在附近,你前後找找吧!」。 現在,你就是一個指針,你的腦子裡先是保存著我給你的初始化值:78號。你往後再找,就是將78加1,變成查看79號位;你往前找,那就是將腦海里的號減1,比如從78號變成77號。

為了方便這樣前進和後退,C/C++的指針支持兩個操作符:「++」和「--」操作。比如 ++p,就是讓p前進一步,--p,就讓p後退一步。

事情沒這麼簡單。你知道,因為我當了這麼多年程序員,所以我肯有很多很多的錢啊!因為我很有錢,所以我肯定和馬雲等人臨近的富人區啊!因為我住在富人區所以肯定要處處顯示我很富有啊。所以我這片小區賣停車位的要求時一買必須四個車位。所以車雖然不大,但每輛車都是停在四個車位上。問題來了,知道這個真相之後,你為了找車,每次前進或後退是一個車位還是四個車位?當然是四個車位啦。

示例中的「p」指針指向的int類型,而int類型的數據佔用四個位元組(在我們當前環境下),所以++p也是一次前進4個位元組,即,直接進入下一個整數。p--也類似。如圖所示:

(指針前進、後退的單位:目標數據類型的尺寸)

我們已經知道:代碼中定義了三個連續的整數數據,然後它們湊巧緊密相鄰了——重要的事說三遍:這只是湊巧、湊巧、湊巧——所以我們準備讓p冒險一下,在它先指向a的情況下,使用「++」操作前進到b,再「++」一次,前進到c,然後再前進一次,前進到不知是什麼地方的內存,然後再前進、前進、前進……反正你的電腦內存肯定不會只有能存放三個整數這麼小,肯定還有大把的未知內存供我們通過這個指針去探索……

一個C/C++程序要訪問內存,合法的方法是定義一個數據,讓這個數據存有一塊內存,然後我們通過這個數據合法而安全地訪問(讀取或修改)它所佔有一畝三分地。我們可以將已經定義了數據的內存,認作是「經合法開墾的土地」,那未經定義數據的內存,就是未經開墾之地。但是,通過讓指針修改自己的值以改變指向,然後藉由指針間接訪問,那就會訪問到非法的「土地」。我們之所以可以這樣做,是因為指針本身居住的地方是合法的,指針自己開墾的。

做事要小心,更何況做壞事,所以我們先讓p加兩次就好,就是從a跳到b,再跳到c,並且每次都輸出所指向的值——注意,哪怕是這樣,也是在犯錯誤。因為a,b,c是三個獨立的數據,它們只是定義在一起,但C++並不保證它們的住址一定相鄰。

通過p從a跳到b,再跳到c的代碼:

#include <iostream>nnusing namespace std;nnint main()n{n int a(9);n int b(10);n int c(11);nn int* p(&a);n cout << *p << endl;n ++p;n cout << *p << endl;n ++p;n cout << *p << endl;n}n

輸出結果(左邊是關鍵代碼,右邊是對應輸出):

(「恰巧」通過p輸出了三個相鄰定義的整數的值)

嗯,得手了,然後我們更大膽一點,繼續往前探索(這內容怎麼越寫越有點怪怪的)……注意,再往前探索的內容,就完全脫離我們在程序中主動申請佔用的內存了……會怎樣呢?我們準備讓p再往前三步,也就是最遠將非法走出12個位元組……我的電腦有4G內存呢,4G是1024*1024*1024,也就1073741824個位元組呢!我只是伸手要了這12個位元組,不會被發現的!大膽點(天哪,這就是人會犯罪的一種僥倖心理嗎?)。

通過p大膽往前走12個位元組的代碼:

#include <iostream>nnusing namespace std;nnint main()n{n int a(9);n int b(10);n int c(11);nn int* p(&a);n cout << *p << endl;n ++p;n cout << *p << endl;n ++p;n cout << *p << endl;nn ++p;n cout << *p << endl;n ++p;n cout << *p << endl;n ++p;n cout << *p << endl;n}n

未開墾的地方就是可怕,居然值那麼巨大:

(p又非法前進三次,每次的值)

不過,最後一次居然是負數的,感覺虧了!要不繼續探索?不,這樣往前探索已經沒什麼新鮮感了。人的好奇心就是可怕,現在我們想直接訪問一下內存地址為0的地方……據說,在計算機的世界裡,內存地址為0的地方,那是整個世界萬事萬物的起源之處……我要去看看!

怎麼看呢?非常之方便而簡捷,只要將指針p初始為0就可以了嘛!這時候的「零」它所代表的,並不是一個簡單意義的整數零,而是代表編號為0內存地址。這個地址它是存在呢?如果存在,它的裡面的值會是什麼呢?

讓p試圖指向0號地址的代碼:

……nint* p(0);n……n

你是剛剛開始學習C/C++的人,你當然有這樣權利去做這件事,甚至你被鼓勵做這樣的探索,我知道,隔壁班他們學完一個「Hello world !」,並且都開始學變數、常量、簡單數據類型啦!不不不,我們白話班才不這樣,你們還小,沒有童年的人生那是一生的悲哀,並且無法彌補。小丁,你剛出來,你現在懂了嗎?

完整代碼:

#include <iostream>nnusing namespace std;nnint main()n{n int a(9);n int b(10);n int c(11);nn int* p(0);n cout << *p << endl;n}n

會怎樣?會怎樣?會怎樣?

對不起,我什麼都可以幫你,惟有,隔壁家小姐姐的裙子,還得你自己去掀!

下篇預告

《第4篇:我不會告訴你學習C++的性價比有多高》

很多人,我學習C++半天,就是在一個黑乎乎的控制台下搞一些簡單的輸入輸出……其實,任何一門成年人的語言,當你在學習語言本身時,基本是同樣的過程。很多時候你覺得學習C++就一直在面對一個黑乎乎小窗口,那只是因為C++的語法知識點太多,造成的。

但我們是《白話C++》。我們怎麼有可能讓學生學了三個月還做不了什麼有趣的事?聽說學Python的人都會在網上扒拉一些美女圖?這在C++也是小事啊。

不, 白話C++課程中會逼著大家能寫多有趣,實用的程序。

讓你性情大改,逼著你有趣,這是我可以透露的,學習C++最大的性價比。


推薦閱讀:

宏名不能隨便起
增長黑客們都在使用什麼編程語言?
Python 2 系列壽命還剩幾年?
王垠:如何掌握所有的程序語言
Python入門容易掉進的10個坑

TAG:CC | 自学编程 | 编程语言 |