)
前言本系列篇幅有點(diǎn)長但若能完整的看下來相信你對指針能有一個全新的認(rèn)識。1. 內(nèi)存和地址內(nèi)存的物理意義就是插在電腦主板上的內(nèi)存條經(jīng)過cpu的處理映射后程序所能訪問到的其實(shí)就是一堆的01比特。計(jì)算機(jī)為了更好的管理內(nèi)存將8個連續(xù)的比特稱之為字節(jié)(1 Byte 8 bit)。每個字節(jié)在內(nèi)存中都有獨(dú)立的地址內(nèi)存空間單元每個內(nèi)存單元的大小取1個字節(jié)。圖1.1 常見單位補(bǔ)充其中每個內(nèi)存單元相當(dāng)于一個學(xué)生宿舍1個字節(jié)空間內(nèi)能放8個比特位每個內(nèi)存單元都有一個編號好比宿舍門牌號有了這個內(nèi)存單元的編號CPU就可以快速找到一個內(nèi)存空間。圖1.2 內(nèi)存單元編號在C中定義變量就是在某個地址開始的連續(xù)內(nèi)存分配給變量。生活中我們把門牌號稱為地址計(jì)算機(jī)中把內(nèi)存單元的編號也稱為地址C語言給地址起了新名字指針。內(nèi)存單元的編號好比門牌號、宿舍號 地址 指針那么如何理解編址?CPU訪問內(nèi)存中的某個字節(jié)空間必須知道字節(jié)空間在內(nèi)存的什么位置而內(nèi)存中字節(jié)很多所以需要給內(nèi)存進(jìn)行編址好比給房間設(shè)立門牌號。計(jì)算機(jī)中的編址不是把每個字節(jié)的地址記錄下來而是通過硬件設(shè)計(jì)完成。鋼琴、吉他上沒有寫“朵、來、咪、發(fā)”這樣的信息但演奏者照樣能找到琴弦精確的位置。這是因?yàn)橹圃焐桃呀?jīng)在樂器硬件層面設(shè)計(jì)好了并且所有演奏者都知道。本質(zhì)是一種約定好的共識硬件編址也是如此。計(jì)算機(jī)中有一組線地址總線可以進(jìn)行硬件單元的協(xié)同工作數(shù)據(jù)傳遞。簡單理解為32位機(jī)器有32根地址總線一根線有0、1倆種狀態(tài)電脈沖有無那么一根線代表倆種含義32根線代表2^32次方種含義每種含義代表一個地址。地址信息被下達(dá)給內(nèi)存在內(nèi)存上就可以找到該地址對應(yīng)的數(shù)據(jù)將數(shù)據(jù)通過數(shù)據(jù)總線傳入CPU內(nèi)寄存器。2. 指針變量和地址2.1 取地址操作符()C語言創(chuàng)建變量其實(shí)就是向內(nèi)存申請空間。#includestdio.h int main() { int a 10; return 0; }圖2.1 a變量地址空間及其值上述代碼創(chuàng)建了整型變量a內(nèi)存中申請4個字節(jié)的內(nèi)存空間用于存放整數(shù)100a為十六進(jìn)制每個字節(jié)都有其地址空間。上圖0a 00 00 00四個字節(jié)地址分別為0x00DAFC4C0x00DAFC4D0X00DAFC4E0X00DAFC4F那么如何得到a變量的地址?此時就要使用取地址操作符。int main() { int a 10; printf(%p\n,a); return 0; }圖2.2打印a變量地址圖2.2a變量地址如下0x00DCFDA80X00DCFDA90X00DCFDAA0X00DCFDAB //地址是依次遞增的由圖可見會打印出0x00DCFDA8,a取出的是a變量空間所占4字節(jié)地址中較小字節(jié)的地址千萬不要認(rèn)為取出的是整個a變量空間的地址雖然整型變量a占用4個字節(jié)的地址空間我們只需要知道第一個字節(jié)地址順藤摸瓜訪問到4個字節(jié)的數(shù)據(jù)也是可以的。2.2 指針變量和解引用操作符(*)2.2.1 指針變量我們通過取地址操作符()拿到的地址是一個數(shù)值比如0x00DCFDA8這個數(shù)值有時候也需要存儲起來方便后期再使用那我們把這樣的地址值存放在哪里呢?答案是指針變量中。指針變量也是一種變量那么也符合變量的定義創(chuàng)建指針變量時也會向內(nèi)存申請地址空間這很重要后續(xù)的二級、三級指針會用到指針變量就是專門存放地址的存放在指針變量中的值都會理解為地址。int main() { int a 10; int* pa a;//取出a變量的空間地址4個字節(jié)地址中較小的字節(jié)地址并存儲到指針變量pa中 printf(%p\n,pa); printf(%p\n,a); return 0; }一定要牢記我2.1節(jié)最后一段的話有助于你更好的理解指針圖2.3普通變量a和指針變量pa圖2.3普通變量a所占地址如下0x0116FE700x0116FE710x0116FE720x0116FE732.2.2 如何拆解指針類型在2.2.1代碼中我們看到pa變量的類型是int*,那么該如何理解指針類型呢?pa變量的類型int*, *是在說明pa是指針變量而前面的int是在說明pa指向的是整型int類型的對象。那如果有一個char類型的變量chch的地址要放在什么類型的指針變量中呢?讀者可以思考一下2.2.3 解引用操作符(*)那么我們?nèi)绾问褂帽4嫫饋淼牡刂纺?在現(xiàn)實(shí)生活中我們是根據(jù)門牌號來尋找房間的找到房間后可以在房間中拿取或存放物品。C語言也是一樣的我們只要拿到了地址指針變量所申請內(nèi)存空間中存儲的值就可以通過地址找到地址指向的對象此時就用到了解引用操作符*又稱間接訪問操作符。int main() { int a 100; int* pa a; /* pa指針變量所申請的空間中存放的是a變量的地址*pa就是把a(bǔ)變量地址解引用后找到了 a變量所申請空間,也就是說*pa就是a變量也可以對a變量地址空間中的值進(jìn)行修改 */ *pa 0;//將100修改為0 return 0; }有些讀者可能想哎呀不就是改a變量空間中存放的值嗎直接a 0就行了為啥使用指針阿?其實(shí)這里是把a(bǔ)變量的修改交給pa來操作這樣對a變量的修改就多了一種途徑(相當(dāng)于不用自己動手交給手下員工就行)多了一種方法。寫代碼就會更加靈活。2.3 指針變量的大小前面我們提到32位機(jī)器假設(shè)有32根地址總線每根地址線出來的電信號轉(zhuǎn)換成數(shù)字信號后為0或1那把32根地址線產(chǎn)生的2進(jìn)制序列當(dāng)作一個地址那么一個地址就是32個bit位需要4字節(jié)1字節(jié) 8比特才能存儲。如果指針變量是用來存放地址的那么指針變量的大小就得是4個字節(jié)的空間才可以。同理64位機(jī)器假設(shè)有64根地址線一個地址就是64個二進(jìn)制位組成的二進(jìn)制序列存儲起來就需要8個字節(jié)的空間指著變量的大小就是8字節(jié)。/* sizeof()函數(shù)是用來計(jì)算參數(shù)所占字節(jié)個數(shù)的函數(shù) */ //指針變量的??取決于地址的?? //32位平臺下地址是32個bit位即4個字節(jié) //64位平臺下地址是64個bit位即8個字節(jié) int main() { printf(char*:%zd\n, sizeof(char*)); printf(short*:%zd\n, sizeof(short*)); printf(int*:%zd\n, sizeof(int*)); printf(double*:%zd\n, sizeof(double*)); return 0; }圖2.4 32位平臺下所占字節(jié)圖2.5 64位平臺下所占字節(jié)總結(jié)32位平臺下地址是32個bit位指針變量大小是32位4字節(jié)64位平臺下地址是64個bit位指針變量大小是64位8字節(jié)指針變量的大小所占字節(jié)個數(shù)和類型是無關(guān)的只要指針類型的變量在相同平臺下大小都是相同的指針變量所占字節(jié)空間必須要放下地址32位平臺下是4字節(jié)64位平臺是8字節(jié)3. 指針變量類型的意義指針變量的大小所占字節(jié)個數(shù)和類型int 、char無關(guān)只要是指針變量在同一平臺下大小都是一樣的為什么還要有各種各樣的指針類型(int* 、char*)呢?其實(shí)指針類型也是有意義的。3.1 指針的解引用//代碼1 int main() { int n 0x11223344;//0x代表該數(shù)字是一個16進(jìn)制數(shù)字 int* pi n;//整型指針變量pi *pi 0; return 0; }圖3.1 n變量未修改前在內(nèi)存中的情況圖3.2 n變量和pi指針變量未修改前的情況圖3.3 n變量被*pi修改后在內(nèi)存中的情況圖3.4 n變量和pi指針變量修改后的情況//代碼2 int main() { int n 0x11223344;//0x代表該數(shù)字是一個16進(jìn)制數(shù)字 char *pc (char *)n;//字符類型的指針變量pc *pc 0; return 0; }圖3.5 n變量未修改前在內(nèi)存中的情況圖3.6 n變量和pc指針變量未修改前情況圖3.7 n變量被*pc修改后在內(nèi)存中情況圖3.8 n變量和pc指針變量修改后情況通過調(diào)試我們看到代碼1會將n變量申請的4個字節(jié)空間的值全部修改為0但是代碼2只是將n變量申請的4個字節(jié)空間中較小的字節(jié)空間中的值修改為0。結(jié)論指針的類型int*、char*決定了對指針解引用的時候有多大權(quán)限一次能操作幾個字節(jié)。比如char*類型的指針解引用只能訪問1個字節(jié)而int*的指針解引用就能訪問4個字節(jié)。3.2 指針-整數(shù)int main() { int n 10; char* pc (char*)n; int* pi n; printf(n :%p\n, n); printf(pc :%p\n, pc); printf(pc1 :%p\n, pc 1); printf(pi :%p\n, pi); printf(pi1 :%p\n, pi 1); return 0; }圖3.9 pc、pi、pc1、pi1地址我們可以看到char*類型的指針變量pc 1跳過1個字節(jié)int*類型的指針變量pi 1跳過4個字節(jié)。這就是指針變量的數(shù)據(jù)類型差異所帶來的變化。指針 1就是跳過1個指針指向元素所占的字節(jié)個數(shù)int類型元素就跳過4字節(jié)char類型元素就跳過1字節(jié)指針-1和1一個道理。總結(jié)指針變量的數(shù)據(jù)類型決定了指針變量向前或向后走一步有多大跳過幾個字節(jié)3.3 void*指針在指針類型中有一種特殊的類型是void*類型的可以理解為無具體類型的指針或者泛型指針這種類型的指針可以用來接受任意類型地址但也有局限性。void*類型的指針不能直接進(jìn)行指針的-整數(shù)和解引用運(yùn)算因?yàn)轭愋筒淮_定嘛。那么void*類型的指針有什么用呢?一般void*類型的指針是使用在函數(shù)參數(shù)的部分用來接受不同類型數(shù)據(jù)的地址這樣的設(shè)計(jì)可以實(shí)現(xiàn)泛型編程的效果使得函數(shù)來處理多種類型的數(shù)據(jù)。4. const修飾指針4.1 const修飾普通變量變量是可以修改的如果把變量的地址交給一個指針變量通過指針變量也可以修改這個變量。但如果我們希望一個變量加上一些限制不能被修改。這就要用到const了。int main() { int m 0; m 20;//m是可以修改的 const int n 0; n 20;//n是不能被修改的 return 0; }上述代碼中n是不能被修改的其實(shí)n本質(zhì)是變量只不過被const修飾后在語法上加了限制。當(dāng)我們對const修飾的n變量進(jìn)行修改就會報錯沒法直接修改n。/* 可以使用指針間接進(jìn)行修改n變量的值 */ int main() { const int n 0; printf(n %d\n, n); int*p n; *p 20; printf(n %d\n, n); return 0; }雖然通過指針確實(shí)修改了n的值但我們還是要思考一下const修飾n就是為了n不能被修改如果p拿到n的地址就能修改n這樣就打破了const的限制。是不合理的所以應(yīng)該讓p拿到n的地址也不能修改n那么要怎么做呢?4.2 const修飾指針變量一般來講const修飾指針變量可以放在*的左邊也可以放在*的右邊。都有不同的含義。int * p;//沒有const修飾 int const * p;//const 放在*的左邊做修飾 int * const p;//const 放在*的右邊做修飾#include stdio.h //代碼1 - 測試?const修飾的情況 void test1() { int n 10; int m 20; int *p n; *p 20;//ok? p m; //ok? } //代碼2 - 測試const放在*的左邊情況 void test2() { int n 10; int m 20; const int* p n; //p指針變量所指向字節(jié)空間的內(nèi)容不能變但p指針變量內(nèi)容可以變(字節(jié)空間地址) *p 20;//ok? p m; //ok? } //代碼3 - 測試const放在*的右邊情況 void test3() { int n 10; int m 20; int * const p n; //指針變量p的內(nèi)容(字節(jié)空間地址)不能變也就是p指針變量空間中的地址容不能變 *p 20; //ok? p m; //ok? } //代碼4 - 測試*的左右兩邊都有const void test4() { int n 10; int m 20; int const * const p n;//指針變量本身和指針變量所指向的內(nèi)容都不能修改 *p 20; //ok? p m; //ok? } int main() { //測試?const修飾的情況 test1(); //測試const放在*的左邊情況 test2(); //測試const放在*的右邊情況 test3(); //測試*的左右兩邊都有const test4(); return 0; }結(jié)論const修飾指針變量的時候1const如果放在*左邊修飾的是指針指向的內(nèi)容保證指針指向的內(nèi)容不能通過指針來改變也就是不能再用指針間接修改變量的值但是指針變量的內(nèi)容字節(jié)空間地址可以變。2const如果放在*右邊修飾的是指針變量本身保證了指針變量的內(nèi)容字節(jié)空間地址不能修改但是指針?biāo)赶虻膬?nèi)容可以通過指針改變。也就是說還可以通過指針間接修改被const修飾的變量5. 指針運(yùn)算指針的基本運(yùn)算有三種指針-整數(shù)指針-指針指針的關(guān)系運(yùn)算5.1 指針-整數(shù)因?yàn)閿?shù)組在內(nèi)存中是連續(xù)存放的只要知道第一個元素的地址順藤摸瓜就能找到后面的所有元素。#include stdio.h //指針- 整數(shù) int main() { int arr[10] {1,2,3,4,5,6,7,8,9,10}; int *p arr[0];//p存放數(shù)組首元素地址 int i 0; int sz sizeof(arr)/sizeof(arr[0]); for(i0; isz; i) { printf(%d , *(pi));//pi 這?就是指針整數(shù) } return 0; }5.2 指針-指針//指針-指針 #include stdio.h int my_strlen(char *s) { char *p s; while(*p ! \0 ) p; return p-s; } int main() { printf(%d\n, my_strlen(abc)); //指針-指針其實(shí)計(jì)算的是倆個指針之間的元素個數(shù) return 0; }6. 野指針野指針就是指針指向的位置是不可知的隨機(jī)的、不正確的、沒有明確限制的6.1 野指針原因1. 指針未初始化#include stdio.h int main() { int *p;//局部變量指針未初始化默認(rèn)為隨機(jī)值 *p 20; return 0; }2. 指針越界訪問#include stdio.h int main() { int arr[10] {0}; int *p arr[0]; int i 0; for(i0; i11; i) { //當(dāng)指針指向的范圍超出數(shù)組arr的范圍時p就是野指針 *(p) i; } return 0; }3. 指針指向空間被釋放#include stdio.h int* test() { int n 100; return n; } int main() { int*p test();//test函數(shù)執(zhí)行完后函數(shù)棧幀被釋放p指針無法指向n變量 printf(%d\n, *p); return 0; }6.2 如何避免野指針1指針初始化如果明確知道指針指向哪里就直接賦值地址如果不知道指針應(yīng)該指向哪里可以給指針賦NULLNULL是C語言中定義的一個標(biāo)識符常量值是00也是地址這個地址是無法使用的讀寫該地址會報錯。int main() { int num 10; int*p1 num; int*p2 NULL; return 0; }2避免指針越界一個程序向內(nèi)存申請了哪些空間通過指針也就只能訪問哪些空間不能超出范圍訪問。3指針變量不再使用時及時置NULL指針使用之前檢查有效性當(dāng)指針變量指向一塊區(qū)域的時候我們可以通過指針訪問該區(qū)域。后面不再使用這個指針訪問空間的時候我們可以把該指針置為NULL。這里有一個規(guī)則只要是NULL指針就不去訪問同時使用指針之前可以判斷指針是否為NULL。我們可以把野指針想象成流浪狗放任不管是非常危險的。所以我們可以把流浪狗拴在樹上把狗固定起來就相對安全了給指針變量及時賦值為NULL就好比把流浪狗固定住。把野指針暫時管理起來。不過流浪狗即使被固定住了我們也不要輕易招惹它對于指針也是在使用之前我們也要判斷是否為NULL??词欠袷恰氨还潭ㄗ〉牧骼斯贰比绻悄敲床荒苤苯邮褂梅粗畡t可以。int main() { int arr[10] {1,2,3,4,5,6,7,8,9,10}; int *p arr[0]; int i 0; for(i0; i10; i) { *(p) i; } //此時p已經(jīng)越界了可以把p置為NULL p NULL; //下次使?的時候判斷p不為NULL的時候再使? //... p arr[0];//重新讓p獲得地址 if(p ! NULL) //判斷 { //... } return 0; }4避免返回局部變量的地址比如6.1節(jié)中的第三種情況。7. assert斷言assert.h頭文件定義了宏assert(),用于在運(yùn)行時確保程序符合指定條件如果不符合就報錯終止運(yùn)行。這個宏常常被稱為斷言。assert(p ! NULL);程序運(yùn)行到上面這一行代碼時驗(yàn)證變量p是否等于NULL。如果確實(shí)不等于NULL程序繼續(xù)運(yùn)行否則終止運(yùn)行并且給出報錯信息提示。assert宏接受一個表達(dá)式作為參數(shù)。如果該表達(dá)式為真返回值非0assert不會產(chǎn)生任何作用程序繼續(xù)運(yùn)行。如果該表達(dá)式為假返回值為0assert就會報錯。在標(biāo)準(zhǔn)錯誤流stderr中寫入一條錯誤信息顯示沒有通過的表達(dá)式以及包含這惡表達(dá)式的文件名和行號。assert的使用對程序員是非常友好的使用assert有幾個好處1它不僅能自動標(biāo)識文件和出問題的行號還有一種無需更改代碼就能開啟或關(guān)閉assert的機(jī)制。如果已經(jīng)確認(rèn)程序沒有問題不需要再做斷言就在#includeassert.h前定義一個宏NDEBUG。#define NDEBUG #include assert.h然后重新編譯程序編譯器就會禁用文件中所有的assert語句。如果程序又出現(xiàn)問題可以移除這條#define NDEBUG再次編譯就又重新啟用assert語句。assert缺點(diǎn)是因?yàn)橐昧祟~外的檢查增加了程序的運(yùn)行時間。