是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級指標的指標,以此類推。
最新C/C++後臺開發/架構師面試題、學習資料、教學影片和學習路線腦圖(資料包括C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK,ffmpeg等),免費分享有需要的可以自行新增>>> 學習交流群
三,常量指標與指標常量
請看下面兩行程式碼:
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++後臺開發/架構師:https://ke.qq.com/course/417774?flowToken=1031343
文章來源:https://cloud.tencent.com/developer/article/1776303