手拉手教你如何理解c/c++中的指針
前言
指針是c語言為什麼如此流行的一個重要原因,正是有瞭指針的存在,才使得c/c++能夠可以比使用其他語言編寫出更為緊湊和有效的程序,可以說,沒有掌握指針,就沒有權利說自己會用c/c++.然而。然而對於大多數初學者,面對指針這個概念簡直是望而生畏,如果前期指針運用的不熟練,後期編寫的程序隨時都有可能成為一顆定時炸彈,因此今天我就花點時間給大傢解釋一下我自己對c/c++中指針的理解。
一,內存和地址
我們知道,計算機內存的每個字節都有一個唯一的地址,CPU每次尋址就是通過固定的步長(這就解釋瞭為什麼需要內存對齊)來跳躍進行尋址的。舉個例子,我們可以把內存看做是一條長街上的一排房屋,每個房屋都有自己固定的門牌號,每座房屋裡面都可以容納數據,為瞭讀取到某個房屋裡面的數據,我們必須知道這個房屋的門牌號,根據這個門牌號來打開這個房間,取走數據。同樣,計算機也必須為每個內存字節都編上號碼,就像門牌號一樣,每個字節的編號是唯一的,根據編號可以準確地找到某個字節。
二,指針的本質就是地址
當我們在程序中聲明一個變量並給這個變量賦值的時候,編譯器做瞭什麼呢?實際上,變量名代表內存中的一個存儲單元,在編譯器對程序編譯連接的時候由系統給變量分配一個地址:
int a = 10;
上面這行代碼我們定義並初始化瞭這個變量a,系統會為a分配一塊內存單元,a隻是這塊內存單元的別名,在程序中從變量中取值,實際上是通過變量名找到相應的內存單元,從其中讀取數據。
假如系統為變量 a 分配的內存地址為0xFF00, 那麼我們可以說這個地址就是變量 a 的門牌號。一個變量的地址稱為該變量的指針。所以說,指針的本質就是地址,指針變量是一種特殊的變量,它專門保存指針(也即地址),當我們說這個地址對應的內存單元的時候,我們可以說這個指針指向這塊內存單元。
例如:
int a = 10; int* p = &a; //定義指針變量 p *p = 20; //將指針p指向的值修改為 20
上面兩行代碼中,我們首先定義瞭一個整型變量 a ,然後又定義瞭一個指針變量 p 指向 a .第二行代碼中,符號&代表取地址,相當於把變量a的地址賦值給瞭指針變量p(p指向a),*加在指針變量前面代表解引用,意思找到指針p指向的值,因此,第三行代碼的意思就是講p指向的值也就是a修改為20.總之一定要記住,符號&代表取值,符號*代表解引用:
符號 | 意義 |
& | 取地址 |
* | 解引用 |
這三行代碼的內存模型如下:
我們假設系統給變量 a 分配的內存首地址為2000,我們又聲明瞭一個指針變量p,這個p也是要占用內存空間的(32位系統占用4個字節,64位系統占用8個字節,請思考為什麼),隻不過這個變量p保存的內容是變量a的地址,也就是2000,當我們想通過p來操縱a的話,首先要根據p保存的地址找到它指向的內容,也就是解引用*p,當*p的內容放生改變的時候,首地址為2000的內存單元存儲的值也會做出改變,因此變量當*p被重新賦值為20的時候,變量a的值也會做出改變,變為20.
由此擴展到二級指針,如果我們再定義一個指針變量q來指向p,那麼q就是一個二級指針,因為它指向的對象還是一個指針,隻不過比他自己低一級,是一級指針,那麼二級指針如何定義呢,請看下面的代碼:
int a = 10; int* p = &a; int** q = &p;
上面第三行代碼就是定義瞭一個二級指針q,它指向的是一級指針p,而一級指針p又指向瞭變量a,它的內存模型如下圖所示:
二級指針q保存的內容為一級指針p的地址而非內容,註意p地址是2008,p的內容為2000. 因此對q進行解引用也即*q得出的是p,也就是2008,再對(*q)進行解引用也即*(*q)得出的才是變量a的值,由於運算符的結合性自右向左,因此括號可以省略,也即**q才是a的值。我們可以編寫代碼試一下:
cout <<"a的值為:"<< **q << endl;
我們觀察一下輸出結果:
沒錯,輸出的結果完全正確。
由此再擴充到多級指針,二級指針是指向一級指針的指針,那麼n級指針便是指向n-1級指針的指針,以此類推。
三,常量指針與指針常量
請看下面兩行代碼:
int a = 10; const int * p1 = &a; //常量指針 int * const p2 = &a; //指針常量
上面第二行代碼中的p1是一個常量指針,就是指向常量的指針變量。意味著它指向的值不可以修改,但是指針的指向可以修改:
int a = 10; int b = 20; const int * p1 = &a; //常量指針 *p1 = 100; //錯誤,常量指針指向的值不可以修改 p1 = &b; //正確
而對於指針常量,它本質是一個常量,但是由指針修飾。意味著它指向的值可以修改,但是指針的指向不可修改,與常量指針剛剛相反:
int a = 10; int b = 20; int * const p1 = &a; //指針常量 *p1 = 100; //正確 p1 = &b; //錯誤,指針的指向不可以修改
因此,我們總結下:
名稱 | 意義 | 特點 |
const int * p | 常量指針 | 指向可修改,指向的值不可修改 |
int * const p | 指針常量 | 指向不可修改,指向的值可修改 |
四,指針與數組
我們知道,一維數組名本身就是一個指針,但是在使用的過程中要小心,因為這個指針分為指向數組首元素的指針與指向整個數組的指針,那麼如何區分它們呢?我們來看下面幾行代碼:
int arr[] = {1, 2, 3, 4, 5}; int* p1 = arr; int* p2 = &arr[0]; int* p3 = &arr; //報錯
上面三行代碼中,其中p1與p2是等價的,因為數組名arr本身就是一個指針,但是這個指針不是指向整個數組,而是指向數組的首元素的地址。第四行直接報錯,因為&arr指的是整個數組的指針,不能把數組指針賦值給整形指針。雖然arr與&arr在數值上是相同的,但是兩者意義不同。意味著&arr它的步長為整個數組,而對於arr,步長為單個元素。
所以,我們得出結論,對於一維數組arr:
名稱 | 意義 | 步長 |
arr | 指向數組首元素 | 單個元素 |
&arr[0] | 指向數組首元素 | 單個元素 |
&arr | 指向整個數組 | 整個數組 |
在定義瞭指向數組首元素的指針變量後,我們可以通過這個指針變量來訪問數組元素:
int arr[] = { 1,2,3,4,5 }; int* p1 = arr; int length = sizeof(arr) / sizeof(int); for (int i = 0; i < length; i++) { cout << p1[i] << endl; cout << *(p1 + i) << endl; }
上面幾行代碼中,p1[i]與*(p1+i)兩者是等價的,所以輸出的結果一樣。但是要註意,當用sizeof操作符操作arr的時候,這個時候不能把arr當做一個指針來對待,因為sizeof操作數組的時候它返回的是數組的字節長度,而單個指針變量隻占用四個字節。上面循環體中,我們也可以通過下面方式訪問:
cout << *p1++ << endl; cout << *(p1++) << endl;
*p1++與*(p1++)是等價的,這是因為++的運算符優先級比*要高,因此不管你加不加括號,都會優先執行p++,然而p++是先返回p的值,再與*結合,最後p再向後移動一位。
不過在這裡要特別註意,有一種情況下我們是不能通過sizeof操作符來計算數組的長度的,就是當數組名作為函數參數傳遞的時候:
void test(int arr[]) { int lenth = sizeof(arr) / sizeof(int); }
上面這行代碼語法上沒有問題,但是得出的結果卻不是我們想要的結果,為什麼呢,這是因為數組名作為函數傳遞的時候,會退化成一個指針,如果是二維數組的話,會退化成指向一維數組的指針,所以sizeof(arr)計算出來的結果就不是數組的字節長度瞭。所以說,在c/c++中傳遞數組的時候,一般我們也會把數組的長度作為形參傳遞過去。
但是我們不能通過下面方式去訪問數組元素:
cout << *arr++ << endl; //報錯
這是因為arr本身是一個指針常量,指針的指向不可更改,因此編譯器直接報錯。
五,數組指針與指針數組
數組指針顧名思義,本質就是一個指針,這個指針指向整個數組;指針數組本質上是一個數組,但是數組的每個元素都是指針。請看下面兩行代碼:
int *p1[10]; //指針數組 int (*p2)[10]; //數組指針
上面兩行代碼,p1是一個數組,而p2卻是一個指針,它指向一個匿名數組。為什麼是這樣呢?這是因為[]的優先級比*要高。p1 先與[]結合,構成一個數組的定義,數組名為p1,int *修飾的是數組的內容,即數組的每個元素。那現在我們清楚,這是一個數組,其包含10 個指向int 類型數據的指針,即指針數組。至於p2 就更好理解瞭,在這裡括號的優先級比[]高,*號和p2 構成一個指針的定義,指針變量名為p2,int 修飾的是數組的內容,即數組的每個元素。數組在這裡並沒有名字,是個匿名數組。那現在我們清楚p2 是一個指針,它指向一個包含10 個int 類型數據的數組,即數組指針。
p1為數組名,每個元素都是int型指針
p2為指針變量,指向一個匿名數組
如果我們定義:
int(*p)[10] = &arr;
那麼如何訪問數組的元素呢?且看,由於上行代碼中,p=&arr,那麼對其解引用,*p就是arr,因此我們可以通過(*p)[]來進行訪問數組的元素:
for(int i = 0; i < 10; i++) { cout<< (*p)[i] << endl; }
六,指針函數與函數指針
指針函數顧名思義,他是一個函數,但返回值是一個指針,例如下面這幾行代碼:
int* test() { int a = 10; int* p = &a; return p; }
這個test就是一個指針函數,它返回的是一個int型的指針。
函數指針本質是一個指針,這個指針指向一個函數,那麼我們如何定義函數指針呢?請看下面代碼:
int myAdd(int a, int b) { return a + b; } void test() { int(*pFun)(int, int) = myAdd; //定義一個函數指針 cout << (*pFun)(2, 5) << endl; //用函數指針調用函數 cout << pFun(2, 5) << endl; //用函數指針調用函數 }
上面test函數代碼中,我們定義瞭一個函數指針,在最後進行調用函數的時候,有兩種方法,一種是用*pFun來調用,一種是直接用pFun來調用,可見兩種方法結果都一樣。
最後,我們來看個比較混合指針復雜的案例:
char *(* c[10])(int **p);
乍一看,讓人眼花繚亂,不知道是什麼東西,在這裡請大傢記住一個規則:C語言標準規定,對於一個符號的定義,編譯器總是從它的名字開始讀取,然後按照優先級順序依次解析。註意是從名字開始,不是從開頭也不是從末尾,這是理解復雜指針的關鍵。
有瞭上面的規則,我們來逐步剖析上面哪行代碼的意義:
首先從*c[10]開始,由於[]的優先級比*高,因此,*c[10]代表一個指針數組,每個元素都是指針,但類型還不知道。再看右邊的(int** p),它是一個函數,參數為一個二級指針。最左邊char* 代表這個函數的返回類型。因此,整行代碼的含義就是:c 是一個擁有 10 個元素的指針數組,數組每個元素指向一個原型為char *(int **p)的函數。
總結
到此這篇關於c/c++中指針的文章就介紹到這瞭,更多相關c/c++中的指針內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- C語言實現高精度加法
- C++虛函數表與類的內存分佈深入分析理解
- 聊聊c++數組名稱和sizeof的問題
- C++拷貝構造函數中的陷阱
- C++中獲取字符串長度的函數sizeof()、strlen()、length()、size()詳解和區別(推薦)