通過前面一段時間C語言的學習,我們了解了數組,函數,操作符等的相關知識,今天我們將要開始進行指針的學習,這是C語言中較難掌握的一個部分,一定要認真學習!!!
1.內存與地址
在正式學習指針前,我們首先要了解兩個概念,內存與地址。該如何理解它們呢?
舉個例子,一棟宿舍樓有不同房間,每間房門前都有屬于他自己的門牌號,讓我們可以正確的尋找到對應的宿舍。一棟宿舍樓對應一塊內存,每間宿舍對應一小塊內存,地址就是門牌號,方便我們快速找到。相對應的我們可以把內存劃分為一個個的內存單元,每個內存單元的大小是一個字節。字節是是計算機中最基本的存儲單位。以下是一些常見的計算機內存單位:
bit? ? --比特位
Byte? --字節? ? ? ? ? ? 1Byte = 8bit
KB? ? ? ? ? ? ? ? ? ? ? ? ? 1KB = 1024 Byte
MB? ? ? ? ? ? ? ? ? ? ? ? ??1MB = 1024 KB
GB? ? ? ? ? ? ? ? ? ? ? ? ? ?1GB = 1024 MB
TB? ? ? ? ? ? ? ? ? ? ? ? ? ?1TB = 1024 GB
其中,每個內存單元,相當于一個學生宿舍,一個字節空間能放8個比特位,就好比一個八人寢。每個內存單元也都有一個編號(相當于門牌號),有了這個編號(就是地址),CPU就能快速找到一個內存空間。C語言中給地址起了新的名字:指針。所以:
內存單元的編號==地址==指針
2.指針變量和地址
2.1取地址操作符(&)
理解了內存和地址的關系,我們再回到C語言,在C語言中創建變量其實就是向內存申請空間,比如:
上述代碼創建了整型變量a,向內存申請了四個字節的空間,用于存放整數10,每個字節都有自己的地址,整數10會優先存放在四個字節中地址較小的字節。
那么我們要如何得到a的地址呢?這就需要用到一個取地址操作符&,還是上面這個代碼,我們用&a來得到了a的地址,存放到p中,打印出來:
?雖然整型變量占?4個字節,我們只要知道了第?個字節地址,順藤摸瓜訪問到4個字節的數據也是可行的。
2.2指針變量和解引用操作符
上面代碼我們打印了&a的地址004FF818,這是一個十六進制的數值,這個數值是可以存儲起來的,如整數存放在整型變量中,指針當然也要存放在指針變量中,為了存放&a的地址我們用了p來充當指針變量,它的類型是int*,int*中的*是在說明p是指針變量,與整形變量做區分,而為什么是int呢?因為p地址對應的內存空間存放的是整型變量。
那如果是char類型的變量,要存放它的地址,指針變量該是什么呢?當然是char*,*做區分,char表明存放的是字符類型,如:
char ch = 'a';
char* pc = &ch;
我們知道變量利用取地址操作符得到了地址,那么知道地址有沒有對應的操作符得到變量呢?答案是肯定的,這時候就要拿出解引用操作符*了,沒錯,他跟上文的*一模一樣,但意義截然不同,我們看代碼:
?我們先創建了一個整型變量num,然后把該變量地址命名為pz,將pz解引用,賦值32,num里的值也產生了相應變化,說明我們通過*pz找到了對應23的內存空間,改變了它。
2.3指針變量的大小
指針變量的大小取決于所處環境(x86環境和x64環境)
int main()
{//在x86環境下,指針變量的大小是四個字節printf("%zd\n", sizeof(char*));//4printf("%zd\n", sizeof(short*));//4printf("%zd\n", sizeof(int*));//4printf("%zd\n", sizeof(long*));//4//在x64環境下,指針變量的大小是四個字節printf("%zd\n", sizeof(char*));//8printf("%zd\n", sizeof(short*));//8printf("%zd\n", sizeof(int*));//8printf("%zd\n", sizeof(long*));//8return 0;
}
? 32位平臺下地址是32個bit位,指針變量大小是4個字節? 64位平臺下地址是64個bit位,指針變量大小是8個字節?從代碼里,我們可以明白指針變量的大小和類型是無關的,只要在相同的環境下,大小都是相同的。
指針變量的大小既然和類型無關,為什么會有各種各樣的指針類型呢?我們繼續接下來的學習。
我們來看一個代碼:
void test1()
{int n = 0x11223344;int* pi = &n;*pi = 0;printf("%0x\n", n);//0
}void test2()
{int n = 0x11223344;char* pc = (char*)&n;*pc = 0;printf("%0x\n", n);//11223300
}int main()
{test1();test2();return 0;
}
?觀察結果,我們會發現test1會將n的四個字節對應的內存空間全部改為0,但是test2只將n的第一個字節對應的內存空間改為0。
指針的類型決定了,對指針解引用的時候有多大的權限(?次能操作幾個字節)。比如: char* 的指針解引用就只能訪問?個字節,而?int* 的指針的解引用就能訪問四個字節。
接下來我們再來看一個代碼:
指針的類型決定了指針向前或者向后走一步有多?(距離)。
?2.4 void*指針

?我們看到,void*類型的指針并不能進行相應的計算,那么void*指針到底有什么用呢?它一般使用在函數參數的部分,用來接收不同類型數據的地址,可以達到范式編程的效果。我們在后面的學習中會慢慢接觸到。
3.const修飾


可以看到,錯誤變成了警告,代碼得以正常運行 ,打破了const的限制。但我們的目的是讓變量無法被修改,有沒有其他的限制辦法呢?當然有,我們讓const修飾指針就行了,看下面代碼:
代碼馬上又報錯了,指針指向的內存空間將無法被修改,但是我們馬上又有了另一個發現?:
?將const放置在指針變量類型之后,代碼又可以正常運行了,這是為什么呢?
int main()
{int m = 10;m = 12;const n = 12;const int* p1 = &n;*p1 = 10;//errorp1 = &m;//rightreturn 0;
}int main()
{int m = 10;m = 12;const n = 12;int* const p1 = &n;p1 = &m;//error*p1 = 13;//rightreturn 0;
}
?在上述代碼中,我們發現:
? const如果放在*的左邊,修飾的是指針指向的內容,保證指針指向的內容不能通過指針來改變。但是指針變量本身的內容可變。? const如果放在*的右邊,修飾的是指針變量本身,保證了指針變量的內容不能修改,但是指針指向的內容,可以通過指針改變。
?4.指針運算
指針的基本運算有三種:
? 指針+-整數? 指針-指針? 指針的關系運算
4.1指針+-整數?
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?數組元素和下標?
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int* p1 = arr;int sz = sizeof(arr) / sizeof(arr[0]);for (int i = 0; i < sz; i++){printf("%d ", *(p1 + i));}return 0;
}
?4.2指針減指針
int my_strlen(char* ch)
{char* p = ch;while (*p != '\0'){p++;}return p - ch;
}int main()
{printf("%d\n", my_strlen("abcdef"));return 0;
}
4.3指針的運算關系
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int sz = sizeof(arr) / sizeof(arr[0]);int* p = arr;while (p < arr + sz){printf("%d ", *p);p++;}return 0;
}
5.野指針
概念: 野指針就是指針指向的位置是不可知的(隨機的、不正確的、沒有明確限制的)
5.1野指針成因?
5.1.1指針未初始化
5.1.2指針越界訪問?
?5.1.3指針指向的空間釋放
int* test()
{int num = 100;return #
}int main()
{int* p = test();printf("%d\n", *p);return 0;
}
5.2如何規避野指針
5.2.1指針初始化
int main()
{int num1 = 12;int* p1 = &num1;int* p2 = NULL;return 0;
}
5.2.2小心指針越界訪問
?個程序向內存申請了哪些空間,通過指針也就只能訪問哪些空間,不能超出范圍訪問,超出了就是越界訪問。
5.2.3指針變量不再使用時,及時置NULL,指針使用之前檢查有效性?
當指針變量指向?塊區域的時候,我們可以通過指針訪問該區域,后期不再使?這個指針訪問空間的時候,我們可以把該指針置為NULL。因為約定俗成的?個規則就是:只要是NULL指針就不去訪問,同時使?指針之前可以判斷指針是否為NULL。
5.2.4避免返回局部變量的地址
6.assert斷言
assert.h 頭?件定義了宏 assert() ,?于在運?時確保程序符合指定條件,如果不符合,就報錯終?運?。這個宏常常被稱為“斷?”。
#include <assert.h>
assert(expression);
這里的?
expression
?是一個布爾表達式,assert
?會對其進行求值。如果?expression
?的值為真(非零),程序會繼續正常執行;如果?expression
?的值為假(零),assert
?會觸發一個斷言失敗,程序會調用?abort()
?函數終止執行,并輸出錯誤信息。?
它不僅能自動標識?件和出問題的?號,還有?種?需更改代碼就能開啟或關閉 assert() 的機制。如果已經確認程序沒有問題,不需要再做斷?,就在 #include <assert.h> 語句的前?,定義?個宏 NDEBUG 。
#define NDEBUG
#include<assert.h>
然后,重新編譯程序,編譯器就會禁用文件中所有的 assert() 語句。如果程序又出現問題,可以移 除這條 #define NDBUG 指令(或者把它注釋掉),再次編譯,這樣就重新啟用了 assert() 語句。