是c語言為什麼如此流行的一個重要原因,正是有了指標的存在,才使得c/c++能夠可以比使用其他語言編寫出更為緊湊和有效的程式,可以說,沒有掌握指標,就沒有權利說自己會用c/c++。然而。然而對於大多數初學者,面對指標這個概念簡直是望而生畏,如果前期指標運用的不熟練,後期編寫的程式隨時都有可能成為一顆定時炸彈,因此今天我就花點時間給大家解釋一下我自己對c/c++中指標的理解。

一,記憶體和地址

我們知道,計算機記憶體的每個位元組都有一個唯一的地址,CPU每次定址就是透過固定的步長(這就解釋了為什麼需要記憶體對齊)來跳躍進行定址的。舉個例子,我們可以把記憶體看做是一條長街上的一排房屋,每個房屋都有自己固定的門牌號,每座房屋裡面都可以容納資料,為了讀取到某個房屋裡面的資料,我們必須知道這個房屋的門牌號,根據這個門牌號來開啟這個房間,取走資料。同樣,計算機也必須為每個記憶體位元組都編上號碼,就像門牌號一樣,每個位元組的編號是唯一的,根據編號可以準確地找到某個位元組。

二,指標的本質就是地址

當我們在程式中宣告一個變數並給這個變數賦值的時候,編譯器做了什麼呢?實際上,變數名代表記憶體中的一個儲存單元,在編譯器對程式編譯連線的時候由系統給變數分配一個地址:

int

a

=

10

上面這行程式碼我們定義並初始化了這個變數a,系統會為a分配一塊記憶體單元,a只是這塊記憶體單元的別名,在程式中從變數中取值,實際上是透過變數名找到相應的記憶體單元,從其中讀取資料。

手把手教你深入理解cc++中的指標

假如系統為變數 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。總之一定要記住,符號&代表取值,符號*代表解引用:

符號

意義

&

取地址

*

解引用

這三行程式碼的記憶體模型如下:

手把手教你深入理解cc++中的指標

我們假設系統給變數 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,它的記憶體模型如下圖所示:

手把手教你深入理解cc++中的指標

二級指標q儲存的內容為一級指標p的地址而非內容,注意p地址是2008,p的內容為2000。 因此對q進行解引用也即*q得出的是p,也就是2008,再對(*q)進行解引用也即*(*q)得出的才是變數a的值,由於運算子的結合性自右向左,因此括號可以省略,也即**q才是a的值。我們可以編寫程式碼試一下:

cout

<<

“a的值為:”

<<

**

q

<<

endl

我們觀察一下輸出結果:

手把手教你深入理解cc++中的指標

沒錯,輸出的結果完全正確。

由此再擴充到多級指標,二級指標是指向一級指標的指標,那麼n級指標便是指向n-1級指標的指標,以此類推。

最新C/C++後臺開發/架構師面試題、學習資料、教學影片和學習路線腦圖(資料包括C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK,ffmpeg等),免費分享有需要的可以自行新增>>> 學習交流群

手把手教你深入理解cc++中的指標

三,常量指標與指標常量

請看下面兩行程式碼:

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 型別資料的陣列,即陣列指標。

手把手教你深入理解cc++中的指標

p1為陣列名,每個元素都是int型指標

手把手教你深入理解cc++中的指標

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++後臺開發/架構師:https://ke.qq.com/course/417774?flowToken=1031343

文章來源:https://cloud.tencent.com/developer/article/1776303