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

作者: masefee  來源: CSDN  發布時間: 2010-11-02 15:26  閱讀: 1335 次  推薦: 6   原文鏈接   [收藏]  
摘要:介紹C中的指針,這是C的難點,作者通過提出問題的形式讓指針的講解變得簡單

  再上一篇,我們介紹了基本調試。之前也說了,之所以把調試放在前面講是因為后面的文章基本都會用到調試。觀察我們的程序到底發生了什么。讓我們能夠直接明了的看清楚問題的本質。本篇將深入一點介紹指針這個讓無數初學者畏懼的東西。希望大家再看完本篇之后能對指針有新的認識,之后不再懼怕它。覺得它就那么回事。那下面我們就努力攻克這個令我們“懼怕”的東西。

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

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

  問題一:這里多了個星號是為什么?(看完后面我希望你能答出這個問題)

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

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

  用法上理解在前面了解了指針的概念,相信大家已經迫不及待想看到直接的代碼了。很正常,程序員就喜歡最直接了當貼出代碼。但是假如我們沒有理解到他的意義,貼出的代碼可能你也只是粗略的一看。認為自己已經清楚不已,這樣往往會漏掉不少細節。也會少很多精彩。

  指針也是變量(大家不可能不知道什么是變量吧,如果不知道自己拍自己磚頭,然后去google)。不要因為它多了個星號就覺得它很特別,它就是個變量而已。因為是變量,那么指針也就有類型了,這個類型可以理解成就是指針的類型。它是某個類型就只能在語法上賦值為這種類型的值。也可以指針的類型理解成是他指向內存的地址里面存放的數據的類型,比如:

 

 
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有幾個?該怎么獲取?

6
0
 
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()