【C/C++語言入門篇】-- 深刻指針

 再上一篇,咱們介紹了基本調試。以前也說了,之因此把調試放在前面講是由於後面的文章基本都會用到調試。觀察咱們的程序到底發生了什麼。讓咱們可以直接明瞭的看清楚問題的本質。本篇將深刻一點介紹指針這個讓無數初學者畏懼的東西。但願你們再看完本篇以後能對指針有新的認識,以後再也不害怕它。以爲它就那麼回事。那下面咱們就努力攻克這個令咱們「害怕」的東西。程序員

 

咱們可能進入大學讀計算機相關專業,基本第一門編程語言就是C語言。可能老師們也喜歡跟學生總結整本書難點在什麼地方。那麼指針必然是老師提到的難點之一。我我的以爲這樣的總結還不如不總結,緣由很簡單,由於這樣會給學生心理負擔,學到指針的時候那根弦都崩的很緊。從骨子裏就認定了它有難度,初學者脆弱的心靈所以而感到害怕。換個角度,爲何咱們不能以爲指針也就那麼回事?沒有什麼特別的嘛,哪裏難了嘛!這樣不是既有信心又有興趣去搞定它?說了這麼多,只想強調一點,什麼東西都報懷疑態度未必是件壞事。指針不是老師說的那麼恐怖。好了,下面咱們就係統的從幾個角度去理解指針。編程

 

概念上理解 所謂指針,沒學過編程語言的可能會以爲是指南針或者鼠標的指針。呵呵,這種說法雖然差之千里,可是也不是毫無道理。爲何呢?好比指南針,以C語言指針的角度去思考,那麼指南針之因此叫指南針由於它始終是指向南方的。對!南方,頓時恍然大悟。聯繫起來能夠想象成:指南針就是指針變量,它指向南方。南方便是指南針這個變量的值。那麼 指南針(指針) == 南方(這裏的==能夠理解成if( a == 100 )裏面的比較運算,下文同理)。此時咱們又發現南方有座大山,大山在南方。哇,又恍然大悟。那這麼說來大山就生在南方,假如咱們想象南方就是內存的某個地址單元。大山就是這個地址單元的值。所以又有等式:*指南針 == 大山。數組

問題一:這裏多了個星號是爲何?(看完後面我但願你能答出這個問題)編程語言

 

再來,咱們就傻瓜的認爲指針就是咱們經常使用的鼠標在桌面熟悉的那個箭頭。咱們的箭頭在咱們的控制下,咱們想點哪兒就點哪兒。哈哈,如此神奇。例如咱們想點桌面的「記事本」圖標。因而咱們將箭頭指向那個圖標,而後雙擊。便打開了咱們之前留下的一些記事。咱們就能看到了。從這個簡單的操做又可讓咱們產生聯想了。箭頭就比如咱們程序裏面的指針,咱們在想要打開記事本的時候,就箭頭指向它。在這個時候,箭頭指向了記事本。箭頭(指針)== 記事本。在雙擊打開記事本以後,裏面有內容,好比是:「我愛你!」。內容在記事本里面。那麼這些內容就能夠理解成是記事本里面存放的值,只不過這些值是以字符串的形式存儲在裏面的。所以有表達式:*箭頭 == 「我愛你」。ide

 

 

上面舉了兩個比較形象一點的例子,相信你們腦子裏有一個基本的關係鏈了吧。就是 指針----->地方----->東西。也就是某個地方有個東西。這個地方被一張藏寶圖記錄下來放到一個隱祕的地方了。這張藏寶圖就是所謂的指針,指明咱們想要找的寶貝的那個地方。清楚了形象的意思,再來句專業的形象一點的概念:指針就是指向某個內存地址的一個變量,這個變量的值就是存放的這個內存地址。這個內存地址裏面又存放了咱們的數據。這樣就構成了一個關係。咱們能夠方便的經過指針找到該地址而且向這個地址寫入數據或者讀取該地址的值。比較直接的讀寫方式就是賦值。函數

 

 

 

用法上理解 在前面瞭解了指針的概念,相信你們已經火燒眉毛想看到直接的代碼了。很正常,程序員就喜歡最直接了當貼出代碼。可是假如咱們沒有理解到他的意義,貼出的代碼可能你也只是粗略的一看。認爲本身已經清楚不已,這樣每每會漏掉很多細節。也會少不少精彩。google

 

指針也是變量(你們不可能不知道什麼是變量吧,若是不知道本身拍本身磚頭,而後去google)。不要由於它多了個星號就以爲它很特別,它就是個變量而已。由於是變量,那麼指針也就有類型了,這個類型能夠理解成就是指針的類型。它是某個類型就只能在語法上賦值爲這種類型的值。也能夠指針的類型理解成是他指向內存的地址裏面存放的數據的類型,好比:spa

int* p;  // 它就表示指向的內存地址裏面存放的是int類型的值。指針

int   a = 100;調試

p = &a; //  知道了吧,簡單的賦值操做就讓p指向了a所在的內存地址,在CC++語言裏用&去某個變量的內存地址。簡單的語句,我們幹了很多事,咱們將變量a的內存地址取了出來,而後賦值給了指針p。是否是咱們的對應關係:

指南針        南方              大山

鼠標箭頭    記事本        存放的記事

藏寶圖        大山              寶貝

  指針        內存地址        數據值

   p             &a           a(100金幣)

 

 

 

根據上面的對應關係你們更清楚了吧,既然指針有類型,那麼咱們就多看一些類型:

char*     pName = "masefee";  // 這一句,是一個字符類型的指針。既然咱們說的指針是存放的內存數據的地址,那麼這裏的就是後面"masefee"字符串的首地址,何爲首地址?首先咱們知道這個字符串確定是存放在內存裏面的,而後咱們又知道內存的最小存儲單元是字節,既然是字節,那麼這個字符串總共佔用的字節數就是8字節。7個字母加一個結束符'/0'一共8個。你們又會問了,爲何須要結束符?緣由很簡單,咱們這個pName指針只指向了這個字符串的起始地址(首地址),它並不知道這個串有多長。假如沒有結束符,咱們取這個字符串的時候怎麼知道取到哪裏爲止呢?所以'/0'就專門用來結束,這個也是個字符,在內存裏面存放的值就是數字0.意思就是說'/0'字符的ASCII碼就是0。扯遠了,前面說了一共佔8字節,內存中不可能在同一地址下存放8個字節喲。一個內存地址只能存放一個字節。因此這個"masefee"就佔用了8個內存地址,首地址就是m字符的地址。你們能夠將pName選中拖放到內存窗口的地址欄觀察。形如:

0x0012fed4    m  首地址,pName的值就等於0x0012fed4這個地址值。

0x0012fed5    a

0x0012fed6    s

0x0012fed7    e

0x0012fed8    f

0x0012fed9    e

0x0012feda    e

0x0012fedb    0

上面就是數據在內存裏面存放的位置關係,逐字節存儲。這裏注意,在內存裏面一般咱們看到的是16進制。這裏只是爲了更形象直接寫成字符了。

 

 

 

short      level = 2500;

short*    pLevel = &level;

這兩句相信你們也不難理解了,pLevel指向的就是level所在的內存地址。先看看在內存中的存放關係:

0x0012fed4   0xC4   

0x0012fed5   0x09

這裏只有2行是由於short只佔2字節(16位)。那爲何奇怪的變成0xc4  0x09呢? 再觀察0x0012fed4比0x0012fed5小,咱們知道2500的十六進制數十0x09C4。這裏爲何倒過來存放的呢? 高位存放在了後面,低位存放在了前面?緣由是由於CPU,有的CPU是順着存放有的是倒過來存放的。這裏咱們不追究,記住就能夠了。這樣一來,pLevel的值究竟是哪一個地址呢?答案很簡單,小的那個。也能夠理解成首地址。從小的地址往大的地址讀取是人之常情撒。那爲何沒有向字符串那樣有結束符呢?緣由也很簡單,short咱們是知道長度的,不須要結束符。這裏記住一點,咱們將一個大於1字節的基本數據類型變量(int  short等)的地址賦值給指針後,該指針指向的地址咱們能夠將該變量理解成只在一個字節上(地址上)。該指針指的內存地址裏面的值就是該變量值。固然你也能夠就根據他的存儲佔用字節,理解該指針就是指向的這幾個字節的首字節。另外,若是level的值不夠佔用2字節,另一個字節就會被自動填充0。這裏咱們要取pLevel指向的地址裏面的值能夠用語句:

short lv = *pLevel;  // *就表示間接訪問該指針所指向的內存裏面的值,這是CC++語法,就是這麼規定的,後臺處理就別管了。知道取指針所指向的內存地址裏面的值的方式就是在前面搞一個星號,稱之爲間接訪問。這樣取出來後,lv的值就等於level的值:2500.

 

 

 

char       className[ 5 ] = { 'M', 'a', 's', 'e', '/0' };

char*     pClassName = className;

問題二: pClassName所指向的地址是數組裏面哪個字符的地址?這裏爲何直接賦值沒有加&符號?

 

int          array[ 3 ] = { 1, 2, 3 };

int*        pArray = array;

這兩句在問題二解決以後天然就會了,這裏須要注意的是,int是4字節,佔用的內存地址將有4個,假如該int變量值很小佔用不到4個字節,剩餘字節將填充爲0.

 

 

 

 

本質上理解 談到本質,指針能夠說就是地址。爲何?由於他的值就是某個變量的所在內存地址。由於咱們一般使用的電腦是32位機,那麼咱們每一個字節的地址就佔用4個字節,地址是16進制數,是整數。因此任何指針存放的值都是一個整數。因此沒有特殊的狀況(這種狀況咱們基本碰不到,這裏就不說了)咱們任何一種類型的指針都佔用4字節,由於它存放的是32位整數。所以前面咱們定義聲明指針如:

int* p; char* p; 這裏的int,char並非表明指針自己的類型,而是指向的地址裏面的值的類型。這裏的p存放的就是一個32位的整數,你能夠大膽的認爲它就是一無符號整數。只不過這個無符號的整數是具備地址信息。

 

那麼有的同窗就會疑問了,既然是存放的一個整數,那麼這個指針又是被存放在哪兒的呢?前面不是說了嗎?咱們指針也是變量,那麼指針也就有它本身的內存地址,也就是用來存放這個指針的。好比:

int* p = &a;

int** pp = &p;   

這裏就不得不說說二級指針了,其實也沒有什麼特別的,二級指針名義上就是指針的指針。二級指針存放的是存放一級指針的那個內存地址。就比如:個人抽屜裏---->藏寶圖--->大山---->寶貝。這裏的抽屜就是二級指針。藏寶圖也仍是要放到一個地方藏起來撒。否則都發財了。對吧!

 

談到本質,指針既然是存放的整數,那麼咱們能夠大膽的強制類型轉換:

int a =100;

int* p = &a;

unsigned int addr = ( unsigned int )p;  // 將變量p強制類型轉換成無符號整數,此時它就是一個實實在在的整數了,這個整數就是變量a的內存地址了。是否是更加以爲指針也就那麼回事了?呵呵,咱們繼續。

 

說到這裏,咱們不得不說說咱們前面提到的void類型了,以前咱們將void一般用來表示函數沒有返回值。在這裏,咱們將瞭解到它的另外一面。那就是:

void* p; // 無明確指向類型的指針,意思就是p並不知道他所指向的內存地址裏面的數據是什麼類型。看具體用法:

int* pInt;

p = ( void* )pInt;

這裏的int型指針被強制轉換成無類型的,既然是無類型,那麼就不可能使用:

int var = *p;

這樣加上星號,咱們前面已經說了是間接訪問p所指向的內存地址下面的值,這裏p是存放了內存地址沒錯。可是這裏是無類型的,咱們的p就不知道到底該讀取多少個字節到var變量裏。可能有的朋友又會說那假若有結束符呢? 呵呵,也是不行的,既然是無類型,怎麼能亂下結論必定是有結束符的數據類型呢?所以在C++語法上面是不容許間接訪問void*指針的。 若是想讀取這個地址裏面的值,那麼此時就至關靈活了。你能夠把這個void型指針強制類型轉換成任何類型的指針。而後賦值過去。這樣作是危險的,可是有時也是必須的。你在這樣作的時候得保證這個void指針強制賦值過去後,你的數據是沒有問題的。

假如:

int a = 0x7fffffff;

int* pA = &a;

void* p = ( void* )pA;

short b = *( short* )p;  // 這裏是先強制類型轉換,而後再間接訪問值,因此是*( short* ).

這時b的值將會把a的值進行截斷,由於short的範圍比int的範圍小。這樣數據就會出問題。這裏清楚了吧。

 

說到這裏又不得不說說空指針和野指針了,空指針既然叫空指針,他的意思就是這個指針所存放的內存地址爲0,不如:

int* a = 0; 指針a所存的地址就是0x00000000,這個地址專門用於指針初始化或者清零,內存的這個地址是被保護起來的,既不能往裏面寫數據也不能讀裏面的數據。若是試圖如:

int* pNULL = 0;

int var = *pNULL;

這樣將出錯,出錯信息是:

什麼什麼.exe的什麼什麼地方 未處理的異常: 0xC0000005: 讀取位置 0x00000000 時發生訪問衝突 。

 

若是試着寫:

*pNULL = 100;

將出錯:

什麼什麼.exe的什麼什麼地方 未處理的異常: 0xC0000005: 寫入位置 0x00000000 時發生訪問衝突 。

因此,咱們不少時候不能保證某個指針是否正常不爲空指針時就得加以判斷:

if ( pNULL )

    *pNULL = 100;

這樣程序就不會給空指針賦值了,讀取一個道理。之後講函數的時候會進一步講解空指針的防範。

 

另外就是野指針,所謂野指針就跟野豬一個道理,處處亂跑。野指針就是指向了不應指向的內存地址,假如這個內存地址不可寫或者不可讀,咱們的程序將會崩潰。假如這個內存沒有被保護,讀寫均可以的話,這樣的錯誤將很難找到。數據將會出錯。會出現不少莫名其妙的現象;不少時候會由於越界恰好修改了某個指針的值,這樣指向的內存地址就有可能不是咱們想要的地址就形成了野指針。函數那一篇咱們也將詳細講解野指針的避免。由於在函數裏面講野指針將更具體直觀。仍是舉個野指針的例子吧:

int* p = &a;  // 正常

p = ( int* )0x12345678; // 這句不要奇怪,既然指針可以轉換爲整數,整數一樣能夠轉換爲指針,這裏轉換事後,p的值就等於0x12345678;這個內存地址並非咱們想要的,也極可能是非法的。這裏的p就野了。也就是野指針。

 

這裏再簡單說說數據拼接,假如我有一個short類型的數組:

short a[ 2 ] = { 0xffff, 0xeeee };

short* pA = a;

這樣一來,pA[ 0 ] 就是0xffff,pA[ 1 ]就是0xeeee。再有一個:

int* pInt = ( int* )pA; 將pA轉換爲int*(指針之間能夠隨便轉換,只要確保不出錯,前面已經說過),short*變成了int*。咱們知道2個short恰好等於一個int所佔的內存。而後咱們操做這個pInt:

int var = *pInt;  這裏也能夠直接使用:int var = *( int* )pA; 那麼此時var取出來的就是4個字節的內存數據,咱們知道short數組a有2個元素恰好佔用4個字節,並且數組是連續存放的。那麼var將是這兩個元素的組合值。

問題三:大家的電腦裏組合出來的var的值是什麼?

問題四:假如a數組有4個元素,而後轉化成int*的pInt,那麼var有幾個?該怎麼獲取?

 

好了,不知不覺寫了整整接近4小時了,這篇終於差很少了。累!這篇較之之前的文章要更深刻一點,因此篇幅比較大一些。但願不懂的朋友多讀幾遍,理解其中的意思。相信大家的悟性!呵呵。打算睡覺了。Good morning!

 

四個問題必定得好好思考喲!

 

 

【C++語言入門篇】系列:

【C++語言入門篇】-- HelloWorld思考

【C/C++語言入門篇】-- 基本數據類型

【C/C++語言入門篇】-- 調試基礎

【C/C++語言入門篇】-- 深刻指針

【C/C++語言入門篇】-- 數組與指針

【C/C++語言入門篇】-- 結構體

【C/C++語言入門篇】-- 深刻函數【上篇】

【C/C++語言入門篇】-- 深刻函數【下篇】

【C/C++語言入門篇】-- 位運算【上篇】

【C/C++語言入門篇】-- 位運算【下篇】

【C/C++語言入門篇】-- 剖析浮點數

【C/C++語言入門篇】-- 文件操做【上篇】

【C/C++語言入門篇】-- 文件操做【中篇】

【C/C++語言入門篇】-- 文件操做【下篇】

相關文章
相關標籤/搜索