前言其實(shí)很早就想要寫一篇關(guān)于指針和數(shù)組的文章,,畢竟可以認(rèn)為這是C語言的根本所在,。相信,任意一家公司如果想要考察一個人對C語言的理解,,指針和數(shù)組絕對是必考的一部分,。 但是之前一方面之前一直在忙各種事情,一直沒有時間靜下心來寫這些東西,,畢竟這確實(shí)是一件非常耗費(fèi)時間和精力的事情,;一方面,個人對C語言的掌握和理解也還有限,,怕寫出來的東西會對大家造成誤導(dǎo),。當(dāng)然,,今天寫的這些東西也肯定存在各種問題,不嚴(yán)謹(jǐn)甚至錯誤的地方肯定有,,也希望大家來共同探討,,相互改進(jìn)。 我會慢慢的寫完這幾章,,有想法的童鞋可以和我探討,。 指針預(yù)備知識在深入理解指針之前,我認(rèn)為有必要先復(fù)習(xí)或者學(xué)習(xí)一下計算機(jī)原理的基礎(chǔ)知識,。 計算機(jī)是如何從內(nèi)存中進(jìn)行取指的,? 計算機(jī)的總線可以分為3種:數(shù)據(jù)總線,地址總線和控制總線,。這里不對控制總線進(jìn)行描述,。數(shù)據(jù)總線用于進(jìn)行數(shù)據(jù)信息傳送。數(shù)據(jù)總線的位數(shù)一般與CPU的字長一致,。一般而言,,數(shù)據(jù)總線的位數(shù)跟當(dāng)前機(jī)器int值的長度相等。例如在16位機(jī)器上,,int的長度是16bit,,32位機(jī)器則是32bit。這個計算機(jī)一條指令最多能夠讀取或者存取的數(shù)據(jù)長度,。大于這個值,,計算機(jī)將進(jìn)行多次訪問。這也就是我們說的64位機(jī)器進(jìn)行64位數(shù)據(jù)運(yùn)算的效率比32位要高的原因,,因為32位機(jī)要進(jìn)行兩次取指和運(yùn)行,,而64位機(jī)卻只需要一次! 地址總線專門用于尋址,,CPU通過該地址進(jìn)行數(shù)據(jù)的訪問,,然后把處于該地址處的數(shù)據(jù)通過數(shù)據(jù)總線進(jìn)行傳送,傳送的長度就是數(shù)據(jù)總線的位數(shù),。地址總線的位數(shù)決定了CPU可直接尋址的內(nèi)存空間大小,,比如CPU總線長32位,其最大的直接尋址空間長232KB,,也就是4G,。這也就是我們常說的32位CPU最大支持的內(nèi)存上限為4G(當(dāng)然,實(shí)際上支持不到這個值,,因為一部分尋址空間會被映射到外部的一些IO設(shè)備和虛擬內(nèi)存上?,F(xiàn)在通過一些新的技術(shù),可以使32位機(jī)支持4G以上內(nèi)存,但這個不在這里的討論范圍內(nèi)),。 一般而言,,計算機(jī)的地址總線和數(shù)據(jù)總線的寬度是一樣的,我們說32位的CPU,,數(shù)據(jù)總線和地址總線的寬度都是32位。 計算機(jī)訪問某個數(shù)據(jù)的時候,,首先要通過地址總線傳送數(shù)據(jù)存儲或者讀取的位置,,然后在通過數(shù)據(jù)總線傳送需要存儲或者讀取的數(shù)據(jù)。一般地,,int整型的位數(shù)等于數(shù)據(jù)總線的寬度,,指針的位數(shù)等于地址總線的寬度。
計算機(jī)的基本訪問單元 學(xué)過C語言的人都知道,,C語言的基本數(shù)據(jù)類型中,,就屬char的位數(shù)最小,是8位,。我們可以認(rèn)為計算機(jī)以8位,,即1個字節(jié)為基本訪問單元。小于一個字節(jié)的數(shù)據(jù),,必須通過位操作來進(jìn)行訪問,。
內(nèi)存訪問方式 如圖1所示,計算機(jī)在進(jìn)行數(shù)據(jù)訪問的時候,,是以字節(jié)為基本單元進(jìn)行訪問的,,所以可以認(rèn)為,計算每次都是從第p個字節(jié)開始訪問的,。訪問的長度將由編譯器根據(jù)實(shí)際類型進(jìn)行計算,,這在后面將會進(jìn)行講述。 圖1 內(nèi)存訪問方式
想要了解更多,,就去翻閱計算機(jī)組成原理和編譯原理吧,。 sizeof關(guān)鍵字sizeof關(guān)鍵字是編譯器用來計算某些類型的數(shù)據(jù)的長度的,以字節(jié)為基本單位,。例如: sizeof(char)=1; sizeof(int)=4; sizeof(Type)的值是在編譯的時候就計算出來了的,,可以認(rèn)為這是一個常量! 什么是指針指針其實(shí)就是數(shù)據(jù)存放的地址,,圖1中的p就是一個指針,。在圖1中,n一般是CPU的位數(shù),,32位機(jī)上,,n=32。因為指針需要能夠指向內(nèi)存中的任意一個位置,因此,,指針的長度應(yīng)該是n位的,,32位機(jī)器上指針長度就是32位。這和整型的長度是相等的,! 在我個人的理解中,,可以將指針理解成int整型,只不過它存放的數(shù)據(jù)是內(nèi)存地址,,而不是普通數(shù)據(jù),我們通過這個地址值進(jìn)行數(shù)據(jù)的訪問,,假設(shè)它的是p,,意思就是該數(shù)據(jù)存放位置為內(nèi)存的第p個字節(jié)。 當(dāng)然,,我們不能像對int類型的數(shù)據(jù)那樣進(jìn)行各種加減乘除操作,,這是編譯器不允許的,因為這樣錯是非常危險的,! 圖2就是對指針的描述,,指針的值是數(shù)據(jù)存放地址,因此,,我們說,,指針指向數(shù)據(jù)的存放位置。 圖2 指針 指針的長度我們使用這樣的方式來定義一個指針: Type *p; 我們說p是指向type類型的指針,,type可以是任意類型,,除了可以是char,short, int, long等基本類型外,還可以是指針類型,,例如int *, int **, 或者更多級的指針,,也可是是結(jié)構(gòu)體,類或者函數(shù)等,。于是,,我們說: int * 是指向int類型的指針,; int **,也即(int *) *,是指向int *類型的指針,,也就是指向指針的指針,; int ***,,也即(int **) *,,是指向int**類型的指針,也就是指向指針的指針的指針,; …我想你應(yīng)該懂了 struct xxx *,,是指向struct xxx類型的指針,; 其實(shí),說這么多,,只是希望大家在看到指針的時候,,不要被int ***這樣的東西嚇到,就像前面說的,,指針就是指向某種類型的指針,,我們只看最后一個*號,前面的只不過是type類型罷了,。 細(xì)心一點(diǎn)的人應(yīng)該發(fā)現(xiàn)了,,在“什么是指針”這一小節(jié)當(dāng)中,已經(jīng)表明了:指針的長度跟CPU的位數(shù)相等,,大部分的CPU是32位的,因此我們說,,指針的長度是32bit,,也就是4個字節(jié)!注意:任意指針的長度都是4個字節(jié),,不管是什么指針?。ó?dāng)然64位機(jī)自己去測一下,應(yīng)該是8個字節(jié)吧,。,。。) 于是: Type *p; sizeof(p)的值是4,,Type可以是任意類型,,char,int, long, struct, class, int **… 以后大家看到什么sizeof(char*), sizeof(int *),sizeof(xxx *),,不要理會,,統(tǒng)統(tǒng)寫4,只要是指針,,長度就是4個字節(jié),,絕對不要被type類型迷惑!至于type是干什么用的,,這個是給編譯器用的,,用于指針運(yùn)算,這個在下面的章節(jié)中會有詳細(xì)介紹,。 取地址我們說指針指向的是數(shù)據(jù)的存放地址,,因此指針的值等于數(shù)據(jù)的存放地址。那么給指針賦值的時候就需要進(jìn)行數(shù)據(jù)的取地址操作,,這個我想不用我多說,,各位也知道是&符號,,沒錯,是&符號,。 我們可以這樣取地址: Type v,*p=&v; 當(dāng)然也可以: Type v, *p;(或者Type v; Type *p) p=&v; 這里的Type依然是任意的類型,,可以是N級指針、結(jié)構(gòu)體,、類或者函數(shù)什么的,。 指針運(yùn)算N多的面試會考這種東西了: Type *p; p++; 然后問你p的值變化了多少。 其實(shí),,也可以認(rèn)為這是在考編譯器的基本知識,。因此p的值并不像表面看到的+1那么簡單,編譯器實(shí)際上對p進(jìn)行的是加sizeof(Type)的操作,。
看一個一段代碼的測試結(jié)果: [cpp] view plaincopy
(這里注釋掉char一行的原因是因為cout<<(char*)會被當(dāng)成字符串輸出,,而不是char的地址) 執(zhí)行結(jié)果: 觀察結(jié)果,可以看出,,他們的增長結(jié)果分別是: 2(sizeof(short)) 4(sizeof(int)) 4(sizeof(long)) 8(sizeof(long long)) 4(sizeof(float)) 8(sizeof(double)) 12(sizeof(long double)) 喏,,增加的值是不是sizeof(Type)呢?別的什么struct,,class之類的,,就不驗證你,有興趣的自己去驗證,。
我們再對這樣的一段代碼進(jìn)行匯編,,查看編譯器是如何進(jìn)行指針的加法操作的: [cpp] view plaincopy
匯編結(jié)果: [cpp] view plaincopy
注意看注釋部分的結(jié)果,我們看到,,piv的值顯示加了4(sizeof(int)),,然后又加了16(4*sizeof(int))。 總結(jié)一點(diǎn): 指針的實(shí)際運(yùn)算,,將會由編譯器在編譯的時候,,根據(jù)指針指向數(shù)據(jù)類型的大小進(jìn)行實(shí)際的翻譯轉(zhuǎn)換。指針類型的作用就在于此,,讓編譯器能夠正確的翻譯這些指令的操作,,另一方面,也讓編譯器檢查程序員對指針的操作是否合法,,保證程序的正確性和健壯性,。 Type *p; p=p+i; 最終p的值實(shí)際上是(value of p) + i*sizeof(Type); Type *p; p=p-i; 最終p的值實(shí)際上是(value of p) - i*sizeof(Type),; 注意:指針只能進(jìn)行加法和減法操作,,不能進(jìn)行乘除法!(指針畢竟不是普通的整數(shù),,乘除法的跨度太大了,,出發(fā)還會搞出小數(shù)點(diǎn)神馬的,,這是我個人的理解。但是編譯器不允許進(jìn)行指針的乘除法,。) NULL指針NULL是C語言標(biāo)準(zhǔn)定義的一個值,,這個值其實(shí)就是0,只不過為了使得看起來更加具有意義,,才定義了這樣的一個宏,,中文的意思是空,表明不指向任何東西,。你懂得,。不過這里不討論空和零的區(qū)別,呵呵,。 在C語言中,,NULL其實(shí)就是0,就像前面說的指針可以理解成特殊的int,,它總是有值的,,p=NULL,其實(shí)就是p的值等于0,。對于不多數(shù)機(jī)器而言,0地址是不能直接訪問的,,設(shè)置為0,,就表示該指針哪里都沒指向。 當(dāng)然,,就機(jī)器內(nèi)部而言,,NULL指針的實(shí)際值可能與此不同,這種情況下,,編譯器將負(fù)責(zé)零值和內(nèi)部值之間的翻譯轉(zhuǎn)換,。 NULL指針的概念非常有用,它給了你一種方法,,表示某個特定的指針目前并未指向任何東西,。例如,一個用于在某個數(shù)組中查找某個特定值的函數(shù)可能返回一個指向查找到的數(shù)組元素的指針,。如果沒找到,,則返回一個NULL指針。 在內(nèi)存的動態(tài)分配上,,NULL的意義非同凡響,,我們使用它來避免內(nèi)存被多次釋放,造成經(jīng)常性的段錯誤(segmentation fault),。一般,,在free或者delete掉動態(tài)分配的內(nèi)存后,,都應(yīng)該立即把指針置空,避免出現(xiàn)所以的懸掛指針,,致使出現(xiàn)各種內(nèi)存錯誤,!例如: [cpp] view plaincopy
free函數(shù)是不會也不可能把p置空的。像下面這樣的代碼就會出現(xiàn)內(nèi)存段錯誤: [cpp] view plaincopy
因為,,第一次free操作之后,,p指向的內(nèi)存已經(jīng)釋放了,但是p的值還沒有變化,,free函數(shù)改不了這個值,,再free一次的時候,p指向的內(nèi)存區(qū)域已經(jīng)被釋放了,,這個地址已經(jīng)變成了非法地址,,這個操作將導(dǎo)致段錯誤的發(fā)生(此時,p指向的區(qū)域剛好又被分配出去了,,但是這種概率非常低,,而且對這樣一塊內(nèi)存區(qū)域進(jìn)行操作是非常危險的!) 但是下面這段代碼就不會出現(xiàn)這樣的問題: [cpp] view plaincopy
因為p的值編程了NULL,,free函數(shù)檢測到p為NULL,,會直接返回,而不會發(fā)生錯誤,。 這里順便告訴大家一個內(nèi)存釋放的小竅門,,可以有效的避免因為忘記對指針進(jìn)行置空而出現(xiàn)各種內(nèi)存問題。這個方法就是自定義一個內(nèi)存釋放函數(shù),,但是傳入的參數(shù)不知指針,,而是指針的地址,在這個函數(shù)里面置空,,如下: [cpp] view plaincopy
結(jié)果: my_free調(diào)用了之后,,p的值就變成了0(NULL),調(diào)用多少次free都不會報錯了,! 另外一個方式也非常有效,,那就是定義FREE宏,在宏里面對他進(jìn)行置空,。例如 [cpp] view plaincopy
執(zhí)行結(jié)果同上面一樣,,不會報段錯誤:
(關(guān)于內(nèi)存的動態(tài)分配,這是個比較復(fù)雜的話題,,有機(jī)會再專門開辟一章給各位講述一下吧,,寫個帖子還是很花費(fèi)時間和精力的,呵呵,,寫過的童鞋應(yīng)該都很清楚,,所以順便插一句,,轉(zhuǎn)帖可以,請注明出處,,畢竟,,大家都是本著共享的精神來討論問題的,寫的好壞都沒有向你所要什么,,請尊重每個人的勞動成果,。) void指針雖然從字面上看,void的意思是空,,但是void指針的意思,,可不是空指針的意思,空指針指的是上面所說的NULL指針,。 void指針實(shí)際上的意思是指向任意類型的指針,。任意類型的指針都可以直接賦給void指針,而不需要進(jìn)行強(qiáng)制轉(zhuǎn)換,。 例如: Type a, *p=&a;(Type等于char, int, struct, int *…) void *pv; pv=p; 就像前面說的,,void指針的好處,就在于,,任意的指針都可以直接賦值給它,,這在某些場合非常有用,因此有些操作對于任意指針都是相同的,。void指針最常用于內(nèi)存管理,。最典型的,也是大家最熟知的,,就是標(biāo)準(zhǔn)庫的free函數(shù)。它的原型如下: void free(void*ptr); free函數(shù)的參數(shù)可以是任意指針,,沒有誰見過free參數(shù)里面的指針需要強(qiáng)壯為void*的吧,?
malloc, calloc,realloc這些函數(shù)的返回值也是void指針,因為內(nèi)存分配,,實(shí)際上只需要知道分配的大小,,然后返回新分配內(nèi)存的地址就可以了,指針的值就是地址,,返回的不管是何種指針,,其實(shí)結(jié)果都是一樣的,因為所有的指針長度其實(shí)都是32位的(32位機(jī)器),,它的值就是內(nèi)存的地址,,指針類型只是給編譯器看的,目的是讓編譯器在編譯的時候能夠正確的設(shè)置指針的值(參見指針運(yùn)算章節(jié)),。如果malloc函數(shù)設(shè)置成下面這樣的原型,,完全沒有問題,。 char*malloc(size_t sz); 實(shí)際上設(shè)置成 Type*malloc(size_t sz); 也是完全正確的,使用void指針的原因,,實(shí)際上就像前面說的,,void指針意思是任意指針,這樣設(shè)計更加嚴(yán)謹(jǐn)一些,,也更符合我們的直觀理解,。如果對前面我說的指針概念理解的童鞋,肯定明白這一點(diǎn),。 未初始化和非法指針經(jīng)常有面試,,會考這樣的代碼校錯: int *a; … *a=12; 這段代碼,在*a=12這里出了問題,。這里的問題就在于,,a究竟指向哪里?我們聲明了這個變量,,但是從未對它進(jìn)行初始化,,一般而言,沒有初始化,,a的值是任意的,,隨機(jī)的。如果a是全局變量或者static類型,,它會被初始化為0(前面說過,,其實(shí)指針可以理解成值是內(nèi)存地址的int),但是不管哪種方式,,這種方式的賦值都是非常危險的,,如果你有著中體彩頭號彩票的運(yùn)氣,a的值剛好等于某個變量或者分配內(nèi)存的地址,,那么這里的運(yùn)行不會報錯,,但這時候的運(yùn)氣卻不是什么好運(yùn),相反,,是非常倒霉,!因為這是對一塊不屬于你的內(nèi)存進(jìn)行操作,這實(shí)在是太危險了,!如果a的初始值是個非法地址,,這個賦值語句在執(zhí)行的時候?qū)箦e,從而終止程序嗎,,這個錯誤同樣是段錯誤(segmentation fault),,如果是這樣,你是幸運(yùn)的,因為你發(fā)現(xiàn)了它,,這樣就可以修正它,。 關(guān)于這種問題,編譯器可能會,,也可能不會對它進(jìn)行檢測,。GNU的編譯器是會進(jìn)行檢測的,會對未初始化的指針或變量輸出警告信息,。 多級指針(也叫指針的指針)其實(shí)如果對前面的指針概念完全理解了,,這里都可以略過。指針的指針,,無非就是指針指向的數(shù)據(jù)類型是指針罷了,。 Type *p; 其中Type類型是指針,比如可以是int*,,也可以是int **,這樣p對應(yīng)的就是二級指針和三級指針,。一級指針的值存放的是數(shù)據(jù)的地址,二級指針的值存放的一級指針的地址,,三級指針的值存放的是二級指針的地址,,依此類推… 函數(shù)指針跟普通的變量一樣,每一個函數(shù)都是有其地址的,,我們通過跳轉(zhuǎn)到這個地址執(zhí)行代碼來進(jìn)行函數(shù)調(diào)用,,只是,跟取普通數(shù)據(jù)不同的在于,,函數(shù)有參數(shù)和返回值,,在進(jìn)行函數(shù)調(diào)用的時候,首先需要將參數(shù)壓入棧中,,調(diào)用完成后又需要將參數(shù)壓入棧中,。既然函數(shù)也是通過地址來進(jìn)行訪問的,那它也可以使用指針來指向,,事實(shí)上,,每一個函數(shù)名都是一個指針,不過它是指針常量和指針常量,,它的值是不能改的,指向的值也不能改,。 (關(guān)于常量指針和指針常量什么的,,有時間在專門開辟一章來說明const這個東東吧,也是很有講頭的一個東東,。,。。)
函數(shù)指針一般用來干什么呢,?函數(shù)指針最常用的場合就是回調(diào)函數(shù),。回調(diào)函數(shù),,顧名思義,就是某個函數(shù)會在適當(dāng)?shù)臅r候被別人調(diào)用,。當(dāng)期望你調(diào)用的函數(shù)能夠使用你的某些方式去操作的時候,,回調(diào)函數(shù)就很有用,比如,,你期望某個排序函數(shù)在比較的時候,,能夠使用你定義的比較方法去比較。 有過較深入的C編程經(jīng)驗的人應(yīng)該都接觸過,。C的標(biāo)準(zhǔn)庫中就有使用,,例如在strlib.h頭文件的qsort函數(shù),它的原型為: void qsort(void*__base, size_t __nmemb, size_t __size, int(*_compar)(const void *, const void*)); 其中int(*_compar)(const void *, const void *)就是回調(diào)函數(shù),,這個函數(shù)用于qsort函數(shù)用于數(shù)據(jù)的比較,。下面,我會舉一個例子,,來描述qsort函數(shù)的工作原理,。 一般,我們使用下面這樣的方式來定義函數(shù)指針: typedef int(*compare)(const void *x, const void *y); 這個時候,,compare就是參數(shù)為const void *, const void *類型,,返回值是int類型的函數(shù)。例如: [cpp] view plaincopy
用typedef來定義的好處,,就是可以使用一個簡短的名稱來表示一種類型,,而不需要總是使用很長的代碼來,這樣不僅使得代碼更加簡潔易讀,,更是避免了代碼敲寫容易出錯的問題,。強(qiáng)烈推薦各位在定義結(jié)構(gòu)體,指針(尤其是函數(shù)指針)等比較復(fù)雜的結(jié)構(gòu)時,,使用typedef來定義,。 后序關(guān)于指針與數(shù)組的部分將在后續(xù)介紹,多級指針在數(shù)組中的應(yīng)用也會更加深入的介紹,。 |
|