指針,大概是c語言中最難了解的概念之一了。指針這個東西是c語言中的一個基本概念,c99中對于指針的定義是:
1. 指針的類型是derived from其它類型,也就是說指針的類型是由它指向的類型決定的;
2. 指針是一種reference類型,即引用類型;
指針這個詞出現在進階語言程式設計中,在彙編中,就沒有指針這個概念,有的隻是位址。計算機的每一個記憶體單元都是編址的,對記憶體的通路都是通過位址進行的。進階語言把這些低級的硬體細節隐藏起來,程式處理的資料都存在資料對象裡,資料對象在能用其間有特定的存儲位置,占據一定的記憶體單元。由于通路記憶體需要位址,在進階語言中,就引入指針變量來存儲資料對象的位址,進而通路記憶體。
簡單來說,指針就是一個整數。計算機中的記憶體都是編址的,每個位址都有一個符号,就像家庭位址或者ip位址一樣。指針,是一個無符号整數(unsigned int,因不緻歧義,下簡稱“整數”),它是一個以目前系統尋址範圍為取值範圍的整數。聲明指針和聲明一個無符号整數實質上并沒有差別。
以位址為值的變量就是指針變量,簡稱指針。
既然指針中存儲的是位址,就要能夠對所有的記憶體單元尋址,也就是說指針的大小是系統尋址範圍。
所有指針占的存儲一樣大,因為它們的值都是位址,通常用一個機器字表示,那麼機器字通常就和位址範圍大小一緻了,資料總線和位址總線寬度在程式設計上最好保持一緻。
我們知道32位位址總線能夠尋址4gb的位址空間(0~232-1),指針是變量 ,其本身也要占據記憶體單元,那麼在32位系統中,整形int也是32位的。
32位系統的尋址能力(位址空間)是4gb(0~232-1),二進制表示長度為32比特,也就是4b。不難驗證,在32位系統裡,int類型也正好是4b(32-bit)長度,可以取遍上述範圍。同理,64位系統取值範圍為0~264-1,int類型長度為8b。
例證就是程式1得到的答案和程式2的答案一緻。
程式1:
程式2:
指針和整數的差別
既然指針的實質是一個整數,為何不用unsigned int直接聲明,或者統一用int *聲明,而要用不同的類型後面加上一個“*”表示呢?char *聲明過的類型,一次通路1個sizeof(char)長度,double *聲明過的類型,一次通路1個sizeof(double)長度。也正是以,程式2第6行加上“(char *)”是因為畢竟unsigned int和char *不是一回事,需要強制轉換,否則會有個警告。
在彙編裡,沒有資料類型這一概念,整數類型和指針就是一回事了。不論是整數還是指針,執行自增的時候,都是将原值加一。如果上文聲明char *pt;,彙編語言中pt自增(inc)之後值為1245049,可是c語言中pt++之後pt值為1245049。如果32位系統中,s上文聲明int *pt;,彙編語言中pt自增之後值為1245049,可是c語言中pt++之後pt值為1245052。
為什麼dos下面的turbo c,和windows下的vc的int類型自增時的步進不一樣長?因為dos是16位的,windows x86是32位的,int類型長度取決于作業系統的位長。可以預見,在windows x64中編譯,上文聲明int *pt;,在執行pt++之後pt值為1245056。
指針的空間配置設定
1、基本資料類型
在程式編譯或者運作時,系統[3]開辟了一張表。每遇到一次聲明語句(包括變量的聲明、函數的聲明和傳入參數的聲明等等)都會開辟一個記憶體空間,并在表中增加一行紀錄,記載一些對應關系。以32位作業系統下為例:
編号
聲明
變量名
記憶體位址
通路長度
值
int np;
np
2000
4b
0xcccccccc
char mychar;
mychar
2002
0xcc
int * mypointer;
mypointer
2003
char * mypointer2;
mypointer2
2005
2、進階資料類型
那麼,複雜的結構怎麼配置設定空間呢?c語言的結構體(彙編語言對應為record類型)按順序配置設定空間。
這就說明了為什麼sizeof(pst)=16而不是8。編譯器把結構體的大小規定為結構體成員中大小最大的那個類型的整數倍。至于pt的存儲,可以推得總長為160。如果執行pt++,答案不是自增,也不是160。因為數組聲明時,pt是常量,不能加減。
3、指針連接配接的資料類型
是以,我們就可以聲明:
用一個整數,代表一棵樹的結點。把它賦給某個結點的leftchild/rightchild值,就形成了上下級關系。隻要無法找到一個路徑,使得 a->lc/rc->lc/rc...->lc/rc==a(a泛指某一結點),這就構成了一棵二叉樹。反之就成了圖。
使用指針的目的
1、簡化代碼
如果沒有指針,我們很難用一個統一的模式去a的定位并修改一棵樹的結點。例如:不用指針要修改a的左子樹的左子樹的右子結點,隻有“a.lc.lc.rc=…”一種表達方式,不能通過指派而簡化。
2、參數傳遞
c中函數調用是按值傳遞的,傳入參數在子函數中隻是一個初值相等的副本,無法對傳入參數作任何改動。但實際程式設計中,經常要改動傳入參數的值。這一點我們可以用一個小技巧,即傳入參數的位址而不是原參數本身,當對傳入參數(位址)取“*”運算時,就可以直接在記憶體中修改,進而改動原想作為傳入參數的參數值。
在執行inc(&a);時,系統在記憶體配置設定表裡增加了一行“val@inc”,其位址為新位址,值為&a。操作“*val”,即是在操作a了。
指針的運算和聲明
1、取位址和取值運算
“*p”操作是這樣一種運算,傳回p的值作為位址之記憶體空間的取值。“&p”則是這樣一種運算,傳回當時聲明p時開辟的位址。顯然可以用指派語句對記憶體位址指派。
我們假設有這麼一段記憶體位址空間,他們取值如下:(機關:h,16進制)
位址 0000 … 2000 2001 2002 2003 2004 … 3000 3001 3002 3003 …
取值 ???? … 01 30 00 00 30 … 00 03 20 9a …
然後,執行這麼一段代碼“int *p;”,假設開辟空間時p被配置設定3001h、3002h兩個位置。則p為2003h,*p為3001h。
**p的值為多少?
**p=*(*(p))=*(*(2003h))=*(3000h)=0300h。
那麼&&p、*(&p)和&(*p)又等于多少?
&&p=&(&(p))=&(3001h),此時出錯了,3001h是個常數怎麼可能有位址呢?
*&p=*(&(p))=*(3001h)=2003h,也就是*&p=p。
&*p=&(*(p))=&(3001h),讀者可能以為&*p=p此時出錯了,3001h是個常數怎麼可能有位址呢?
2、指針和引用的聲明注記
我們再看看另類的*和&。這裡有兩個地方要注意:
(1)在程式聲明變量的時候的*,隻是表明“它是一個整數,這個整數指向某個記憶體位址,一次通路sizeof(type)長度”。這點不要和(*)操作符混淆;
(2)在c++程式聲明變量的時候的&,隻是表明“它是一個引用,這個引用聲明時不開辟新空間,它在記憶體配置設定表加入新的一行,該行記憶體位址等于和調用時傳入的對應參數記憶體位址”。這一點不要與“*”聲明符和“&”操作符混淆。
指針的複雜形式
1、二級指針(指向指針的指針)
二級指針是指向指針的指針,它是一個整數,這個整數指向某個記憶體位址,該位址的值是一個整數,指向給另一個記憶體位址(通常異于前者,但不排除二者相等)。綜合前述的btree定義,對于一棵樹,我們通常用它的根結點位址來表示這棵樹。找到了樹的根,其每個結點都可以找到。
但是有時候我們需要對樹進行删除結點,增加節點操作,往往考慮到删除根結點,增加的結點取代原來的根結點作為新根結點的情況。為了修改根結點這個“整數”,我們需要退一步,使用這個“整數”的記憶體位址,也就是指向這個“整數”的指針。在聲明時,我們用2個*号,聲明指向指針的指針。它的意思是“它是一個整數,這個整數指向某個記憶體位址,一次通路sizeof(unsigned int)長度,其指向的記憶體位址所存儲的值是一個整數,那個整數值指向某個記憶體位址,一次通路sizeof(btree)長度。”詳見資料結構有關“樹”的程式代碼。
2、指針數組
指針數組:就是一個整數數組,那個數組的各個元素都是整數,指向某個記憶體位址。
3、數組指針
數組指針:數組名本身就是一個指針,指向數組的首位址。注意這是聲明定長數組時,其數組名指向的數組首位址是常量。而聲明數組并使某個指針指向其值指向某個數組的位址(不一定是首位址),指針取值可以改變。
4、指向函數的指針
指向函數的指針:從二進制角度考慮,數組名是該數組資料段首位址,函數名就是該代碼段的首位址,可以用“int *fun()”。在二進制層面,代碼段和資料段什麼差別?沒什麼差別。很多人都說c語言是一種面向過程的語言,因為它最多隻有struct的定義,而沒有class的概念。根據本段所述,我們可以認為c語言能成為面向對象的語言,隻是表述比較麻煩而已。事實上很多開源程式都使用這種方式組織他們的代碼。