說到指針,估計仍是有不少小夥伴都仍是雲裏霧裏的,有點「知其然,而不知其因此然」。可是,不得不說,學了指針,C語言才能算是入門了。指針是C語言的「精華」,能夠說,對對指針的掌握程度,「直接決定」了你C語言的編程能力。程序員
在講指針以前,咱們先來了解下變量在「內存」中是如何存放的。編程
在程序中定義一個變量,那麼在程序編譯的過程當中,系統會根據你定義變量的類型來分配「相應尺寸」的內存空間。那麼若是要使用這個變量,只須要用變量名去訪問便可。數組
經過變量名來訪問變量,是一種「相對安全」的方式。由於只有你定義了它,你纔可以訪問相應的變量。這就是對內存的基本認知。可是,若是光知道這一點的話,其實你仍是不知道內存是如何存放變量的,由於底層是如何工做的,你依舊不清楚。安全
那麼若是要繼續深究的話,你就須要把變量在內存中真正的樣子是什麼搞清楚。內存的最小索引單元是1字節,那麼你其實能夠把內存比做一個超級大的「字符型數組」。在上一節咱們講過,數組是有下標的,咱們是經過數組名和下標來訪問數組中的元素。那麼內存也是同樣,只不過咱們給它起了個新名字:地址。每一個地址能夠存放「1字節」的數據,因此若是咱們須要定義一個整型變量,就須要佔據4個內存單元。網絡
那麼,看到這裏你可能就明白了:其實在程序運行的過程當中,徹底不須要變量名的參與。變量名只是方便咱們進行代碼的編寫和閱讀,只有程序員和編譯器知道這個東西的存在。而編譯器還知道具體的變量名對應的「內存地址」,這個是咱們不知道的,所以編譯器就像一個橋樑。當讀取某一個變量的時候,編譯器就會找到變量名所對應的地址,讀取對應的值。ide
那麼咱們如今就來切入正題,指針是個什麼東西呢?函數
所謂指針,就是內存地址(下文簡稱地址)。C語言中設立了專門的「指針變量」來存儲指針,和「普通變量」不同的是,指針變量存儲的是「地址」。測試
指針變量也有類型,實際上取決於地址指向的值的類型。那麼如何定義指針變量呢:url
很簡單:類型名* 指針變量名操作系統
char* pa;//定義一個字符變量的指針,名稱爲pa注意,指針變量必定要和指向的變量的類型同樣,否則類型不一樣可能在內存中所佔的位置不一樣,若是定義錯了就可能致使出錯。
獲取某個變量的地址,使用取地址運算符&,如:
char* pa = &a;若是反過來,你要訪問指針變量指向的數據,那麼你就要使用取值運算符*,如:
printf("%c, %d\n", *pa, *pb);這裏你可能發現,定義指針的時候也使用了*,這裏屬於符號的「重用」,也就是說這種符號在不一樣的地方就有不一樣的用意:在定義的時候表示「定義一個指針變量」,在其餘的時候則用來「獲取指針變量指向的變量的值」。
直接經過變量名來訪問變量的值稱之爲直接訪問,經過指針這樣的形式訪問稱之爲間接訪問,所以取值運算符有時候也成爲「間接運算符」。
好比:
//Example 01程序實現以下:
//Consequence 01像這樣的代碼是十分危險的。由於指針a到底指向哪裏,咱們不知道。就和訪問未初始化的普通變量同樣,會返回一個「隨機值」。可是若是是在指針裏面,那麼就有可能覆蓋到「其餘的內存區域」,甚至多是系統正在使用的「關鍵區域」,十分危險。不過這種狀況,系統通常會駁回程序的運行,此時程序會被「停止」並「報錯」。要是萬一中獎的話,覆蓋到一個合法的地址,那麼接下來的賦值就會致使一些有用的數據被「莫名其妙地修改」,這樣的bug是十分很差排查的,所以使用指針的時候必定要注意初始化。
有些讀者可能會有些奇怪,指針和數組又有什麼關係?這倆貨明明八竿子打不着井水不犯河水。彆着急,接着往下看,你的觀點有可能會改變。
咱們剛剛說了,指針實際上就是變量在「內存中的地址」,那麼若是有細心的小夥伴就可能會想到,像數組這樣的一大摞變量的集合,它的地址是啥呢?
咱們知道,從標準輸入流中讀取一個值到變量中,用的是scanf函數,通常貌似在後面都要加上&,這個其實就是咱們剛剛說的「取地址運算符」。若是你存儲的位置是指針變量的話,那就不須要。
//Example 02程序運行以下:
//Consequence 02在普通變量讀取的時候,程序須要知道這個變量在內存中的地址,所以須要&來取地址完成這個任務。而對於指針變量來講,自己就是「另一個」普通變量的「地址信息」,所以直接給出指針的值就能夠了。
試想一下,咱們在使用scanf函數的時候,是否是也有不須要使用&的時候?就是在讀取「字符串」的時候:
//Example 03程序執行以下:
//Consequence 03所以很好推理:數組名其實就是一個「地址信息」,實際上就是數組「第一個元素的地址」。我們試試把第一個元素的地址和數組的地址作個對比就知道了:
//Example 03 V2程序運行結果爲:
//Comsequense 03 V2這麼看,應該是實錘了。那麼數組後面的元素也就是依次日後放置,有興趣的也能夠本身寫代碼嘗試把它們輸出看看。
剛剛咱們驗證了數組的地址就是數組第一個元素的地址。那麼指向數組的指針天然也就有兩種定義的方法:
...當指針指向數組元素的時候,能夠對指針變量進行「加減」運算,+n表示指向p指針所指向的元素的「下n個元素」,-n表示指向p指針所指向的元素的「上n個元素」。並非將地址加1。
如:
//Example 04執行結果以下:
//Consequence 04有的小夥伴可能會想,編譯器是怎麼知道訪問下一個元素而不是地址直接加1呢?
其實就在咱們定義指針變量的時候,就已經告訴編譯器了。若是咱們定義的是整型數組的指針,那麼指針加1,實際上就是加上一個sizeof(int)的距離。相對於標準的下標訪問,使用指針來間接訪問數組元素的方法叫作指針法。
其實使用指針法來訪問數組的元素,不必定須要定義一個指向數組的單獨的指針變量,由於數組名自身就是指向數組「第一個元素」的指針,所以指針法能夠直接做用於數組名:
...執行結果以下:
p -> 00AFF838, p+1 -> 00AFF83C, p+2 -> 00AFF840如今你是否是感受,數組和指針有點像了呢?不過筆者先提醒,數組和指針雖然很是像,可是絕對「不是」一種東西。
甚至你還能夠直接用指針來定義字符串,而後用下標法來讀取每個元素:
//Example 05程序運行以下:
//Consequence 05在剛剛的代碼裏面,咱們定義了一個「字符指針」變量,而且初始化成指向一個字符串。後來的操做,不只在它身上可使用「字符串處理函數」,還能夠用「下標法」訪問字符串中的每個字符。
固然,循環部分這樣寫也是沒毛病的:
...這就至關於利用了指針法來讀取。
剛剛說了許多指針和數組相互替換的例子,可能有的小夥伴又開始說:「這倆貨不就是一個東西嗎?」
隨着你對指針和數組愈來愈瞭解,你會發現,C語言的創始人不會這麼無聊去建立兩種同樣的東西,還叫上不一樣的名字。指針和數組終究是「不同」的。
好比筆者以前看過的一個例子:
//Example 06當編譯器報錯的時候,你可能會開始懷疑你學了假的C語言語法:
//Error in Example 06咱們知道,*str++ != ‘\0’是一個複合表達式,那麼就要遵循「運算符優先級」來看。具體能夠回顧《C語言運算符優先級及ASCII對照表》。
str++比*str的優先級「更高」,可是自增運算符要在「下一條語句」的時候才能生效。因此這個語句的理解就是,先取出str所指向的值,判斷是否爲\0,如果,則跳出循環,而後str指向下一個字符的位置。
看上去貌似沒啥毛病,可是,看看編譯器告訴咱們的東西:表達式必須是可修改的左值
++的操做對象是str,那麼str究竟是不是「左值」呢?
若是是左值的話,那麼就必須知足左值的條件。
❝❞
- 擁有用於識別和定位一個存儲位置的標識符
- 存儲值可修改
第一點,數組名str是能夠知足的,由於數組名實際上就是定位數組第一個元素的位置。可是第二點就不知足了,數組名其實是一個地址,地址是「不能夠」修改的,它是一個常量。若是非要利用上面的思路來實現的話,能夠將代碼改爲這樣:
//Example 06 V2這樣就能夠正常執行了:
//Consequence 06 V2這樣咱們就能夠得出:數組名只是一個「地址」,而指針是一個「左值」。
看下面的例子,你能分辨出哪一個是指針數組,哪一個是數組指針嗎?
int* p1[5];單個的咱們均可以判斷,可是組合起來就有些難度了。
答案:
int* p1[5];//指針數組咱們挨個來分析。
數組下標[]的優先級是最高的,所以p1是一個有5個元素的「數組」。那麼這個數組的類型是什麼呢?答案就是int*,是「指向整型變量的指針」。所以這是一個「指針數組」。
那麼這樣的數組應該怎麼樣去初始化呢?
你能夠定義5個變量,而後挨個取地址來初始化。
不過這樣太繁瑣了,可是,並非說指針數組就沒什麼用。
好比:
//Example 07結果以下:
//Consequence 07這樣是否是比二維數組來的更加直接更加通俗呢?
()和[]在優先級裏面屬於「同級」,那麼就按照「前後順序」進行。
int(*p2)將p2定義爲「指針」, 後面跟隨着一個5個元素的「數組」,p2就指向這個數組。所以,數組指針是一個「指針」,它指向的是一個數組。
可是,若是想對數組指針初始化的時候,千萬要當心,好比:
//Example 08Visual Studio 2019報出如下的錯誤:
//Error and Warning in Example 08這實際上是一個很是典型的錯誤使用指針的案例,編譯器提示說這裏有一個「整數」賦值給「指針變量」的問題,由於p2歸根結底仍是指針,因此應該給它傳遞一個「地址」才行,更改一下:
//Example 08 V2但是怎麼仍是有問題呢?
咱們回顧一下,指針是如何指向數組的。
int temp[5] = {1, 2, 3, 4, 5};咱們本來覺得,指針p是指向數組的指針,可是實際上「並非」。仔細想一想就會發現,這個指針其實是指向的數組的「第一個元素」,而不是指向數組。由於數組裏面的元素在內存中都是挨着個兒存放的,所以只須要知道第一個元素的地址,就能夠訪問到後面的全部元素。
可是,這麼來看的話,指針p指向的就是一個「整型變量」的指針,並非指向「數組」的指針。而剛剛咱們用的數組指針,纔是指向數組的指針。所以,應該將「數組的地址」傳遞給數組指針,而不是將第一個元素的地址傳入,儘管它們值相同,可是「含義」確實不同:
//Example 08 V3程序運行以下:
//Consequence 08在上一節《C語言之數組》咱們講過「二維數組」的概念,而且咱們也知道,C語言的二維數組其實在內存中也是「線性存放」的。
假設咱們定義了:int array[4][5]
array做爲數組的名稱,顯然應該表示的是數組的「首地址」。因爲二維數組實際上就是一維數組的「線性拓展」,所以array應該就是指的指向包含5個元素的數組的指針。
若是你用sizeof()去測試array和array+1的話,就能夠測試出來這樣的結論。
首先從剛剛的問題咱們能夠得出,array+1一樣也是指的指向包含5個元素的數組的指針,所以*(array+1)就是至關於array[1],而這恰好至關於array[1][0]的數組名。所以*(array+1)就是指第二行子數組的第一個元素的地址。
有了剛剛的結論,咱們就不難推理出,這個實際上就是array[1][2]。是否是感受很是簡單呢?
總結一下,就是下面的這些結論,記住就好,理解那固然更好:
*(array + i) == array[i]咱們在上一節裏面講過,在初始化二維數組的時候是能夠偷懶的:
int array[][3] = {剛剛咱們又說過,定義一個數組指針是這樣的:
int(*p)[3];那麼組合起來是什麼意思呢?
int(*p)[3] = array;經過剛剛的說明,咱們能夠知道,array是指向一個3個元素的數組的「指針」,因此這裏徹底能夠將array的值賦值給p。
其實C語言的指針很是靈活,一樣的代碼用不一樣的角度去解讀,就能夠有不一樣的應用。
那麼如何使用指針來訪問二維數組呢?沒錯,就是使用「數組指針」:
//Example 09運行結果:
//Consequence 09void其實是無類型的意思。若是你嘗試用它來定義一個變量,編譯器確定會「報錯」,由於不一樣類型所佔用的內存有可能「不同」。可是若是定義的是一個指針,那就沒問題。void類型中指針能夠指向「任何一個類型」的數據,也就是說,任何類型的指針均可以賦值給void指針。
將任何類型的指針轉換爲void是沒有問題的。可是若是你要反過來,那就須要「強制類型轉換」。此外,不要對void指針「直接解引用」,由於編譯器其實並不知道void指針會存放什麼樣的類型。
//Example 10這樣會報錯:
//Error in Example 10若是必定要這麼作,那麼能夠用「強制類型轉換」:
//Example 10 V2固然,使用void指針必定要當心,因爲void指針幾乎能夠「通吃」全部類型,因此間接使得不一樣類型的指針轉換變得合法,若是代碼中存在不合理的轉換,編譯器也不會報錯。
所以,void指針能不用則不用,後面講函數的時候,還能夠解鎖更多新的玩法。
在C語言中,若是一個指針不指向任何數據,那麼就稱之爲「空指針」,用「NULL」來表示。NULL實際上是一個宏定義:
#define NULL ((void *)0)在大部分的操做系統中,地址0一般是一個「不被使用」的地址,因此若是一個指針指向NULL,就意味着不指向任何東西。爲何一個指針要指向NULL呢?
其實這反而是一種比較指的推薦的「編程風格」——當你暫時還不知道該指向哪兒的時候,就讓它指向NULL,之後不會有太多的麻煩,好比:
//Example 11第一個指針未被初始化。在有的編譯器裏面,這樣未初始化的變量就會被賦予「隨機值」。這樣指針被稱爲「迷途指針」,「野指針」或者「懸空指針」。若是後面的代碼對這類指針解引用,而這個地址又恰好是合法的話,那麼就會產生莫名其妙的結果,甚至致使程序的崩潰。所以養成良好的習慣,在暫時不清楚的狀況下使用NULL,能夠節省大量的後期調試的時間。
開始套娃了。其實只要你理解了指針的概念,也就沒什麼大不了的。
//Example 12程序結果以下:
//Consequence 12固然你也能夠無限地套娃,一直指下去。不過這樣會讓代碼可讀性變得「不好」,過段時間可能你本身都看不懂你寫的代碼了。
那麼,指向指針的指針有什麼用呢?
它可不是爲了去創造混亂代碼,在一個經典的實例裏面,就能夠體會到它的用處:
char* Books[] = {而後咱們須要將這些書進行分類。咱們發現,其中有一本是寫Python的,其餘都是C語言的。這時候指向指針的指針就派上用場了。首先,咱們剛剛定義了一個指針數組,也就是說,裏面的全部元素的類型「都是指針」,而數組名卻又能夠用指針的形式來「訪問」,所以就可使用「指向指針的指針」來指向指針數組:
...由於字符串的取地址值實際上就是其「首地址」,也就是一個「指向字符指針的指針」,因此能夠這樣賦值。
這樣,咱們就利用指向指針的指針完成了對書籍的分類,這樣既避免了浪費多餘的內存,並且當其中的書名要修改,只須要改一次便可,代碼的靈活性和安全性都獲得了提高。
常量,在咱們目前的認知裏面,應該是這樣的:
520, 'a'或者是這樣的:
#define MAX 1000常量和變量最大的區別,就是前者「不可以被修改」,後者能夠。那麼在C語言中,能夠將變量變成像具備常量同樣的特性,利用const便可。
const int max = 1000;在const關鍵字的做用下,變量就會「失去」原本具備的可修改的特性,變成「只讀」的屬性。
強大的指針固然也是能夠指向被const修飾過的變量,但這就意味着「不能經過」指針來修改它所引用的值。總結一下,就是如下4點:
❝❞
- 指針能夠修改成指向不一樣的變量
- 指針能夠修改成指向不一樣的常量
- 能夠經過解引用來讀取指針指向的數據
- 不能夠經過解引用來修改指針指向的數據
指針自己做爲一種「變量」,也是能夠修改的。所以,指針也是能夠被const修飾的,只不過位置稍稍「發生了點變化」:
...這樣的指針有以下的特性:
❝❞
- 指針自身不可以被修改
- 指針指向的值能夠被修改
在定義普通變量的時候也用const修飾,就獲得了這樣的指針。不過因爲限制太多,通常不多用到:
...
免責聲明:本文系網絡轉載,版權歸原做者全部。若有問題,請聯繫咱們,謝謝!
推薦閱讀