文章更新,更加詳細的介紹請看這篇:https://www.cnblogs.com/lulipro/p/7460206.html
很多人不敢講C的指針,有些人講不清,有些人怕講錯。初生牛犢不怕虎,就讓我講講。
下面開始。
一、指針的定義
指針是內存單元的編號。內存單元是以字節為單位的。所以指針就是字節的編號。
比如我們的個人電腦,內存一般4GB吧,那么一共就有 : 4*1024*1024*1024 = 4294967296 字節,也就是4294967296個編號。一個字節擁有一個編號,
范圍從 0 ~ 4294967296-1 。
畫個圖表示:(注意字節由8位bit組成,為了直觀我沒畫出來)

但是呢,一般我們是用16進制來表示的這些編號,但效果都一樣。
二、變量與內存的關系
現在我們來看C 變量在內存中的樣子。我們使用自動變量(局部變量)來講解。
int a = 5; //假設a存儲在編號為0x02開始的位置
說明:
內存可以存儲數據,所以我們把每個字節當做是一個“箱子”。數據存入內存就好比在箱子里面放數據
但是C語言的不同數據類型占用的字節數是不都一樣的,所以,每種數據類型占的”箱子”的個數不都一樣。
比如char型,只要一個字節就夠了,所以一個字符只需一個“箱子”。
而int型需要4(一般是4個字節)個“箱子”才放得下。
double型則需要8個“箱子”。
來分析上圖中用橙色框起來的4個字節的內存塊,這里就存儲了a這個變量。
我們從4個方面去討論這個內存塊:
1、內存的數據
我們的變量賦值為5,所以內存的數據就是 0000 0000 0000 0000 0000 0000 0000 0101 (大端模式)
每個字節地址: 0x02 0x03 0x04 0x05
2、*內存的名字 (對於我們的程序使用的內存來說,並不是每一個內存塊都有名字)
名字就是變量名a
3、內存的地址
變量a占用了4個字節,那么,哪一個字節的地址,才是變量a的地址呢?答:第一個低地址字節的地址,也就是0x02
4、內存的寬度
這里的a變量占用了4個字節,這就是他的寬度。
這里提一下,為后面講指針的寬度做鋪墊 :)
三、定義和使用指針變量
什么是指針變量。我們知道int類型變量用來存儲整形值,12、1003、9823......
那么,同樣的道理,指針變量就是用來保存地址的變量。
定義指針變量:在指向的變量的類型上加個* ,如下:
int* p_int; //指向int類型變量的指針 double* p_double; //指向idouble類型變量的指針 int(*p_func)(int,int); //指向返回類型為int,有2個int形參的函數的指針 int(*p_arr)[3]; //指向含有3個int元素的數組的指針 struct Student *p_struct; //結構體類型的指針 int** p_pointer; //指向 一個整形變量指針的指針
既然我們已經定義指針變量了,那么接下來就用指針變量來存儲其它變量的地址。
取地址符號 &
#include<stdio.h> int Add(int a,int b); struct Student { int age; double score; char name[31]; }; int main(void) { int val_int =100 ; double val_double = 12.00; int arr[3]={1,2,3}; struct Student stu={ 19, 98.00, "Jack" }; int*p = NULL; /**********************************************************/ int* p_int = &val_int; double* p_double = &val_double; int(*p_func)(int,int) = &Add; //or =Add int(*p_arr)[3] = &arr; struct Student *p_struct = &stu; int** p_pointer = &p; return 0; } int Add(int a,int b) { return a+b; }
四、指針的屬性和使用
我認為指針有2個屬性: ①指針的值 ②指針的寬度
指針的值很好理解,比如第一個例子 int a = 5; int*p = &a; 那么p的值就是a的地址0x02
指針的寬度:由指針的類型決定。
如果說指針的值標記了某個數據在內存的起始位置,那么,指針的寬度就決定了從起始地址
對應的字節開始,往后還有多少個字節,也是屬於這個內存數據的。
生活情景:“老王,去幫我去倉庫拿個貨,我的貨從78號箱開始,並且有2個。” 於是老王取出 78和79號箱子的貨物遞給你
假如你對老王說:“老王,幫我去倉庫取我的貨,我的從78號箱開始,一共
有幾個箱子,我也記不住了” “那我怎么取,取少了你就有損失了,取多了別人也不干了,你不要難為我啊”
這點我的另一篇隨筆也講到了 .
#include<stdio.h> int main(void) { int a =256 ; int* p=&a; printf("%d\n", *((unsigned char*)p) ); //讀取內存數據時,取的寬度的比存時的寬度小,數據缺失 return 0; }
#include<stdio.h> int main(void) { int arr[2] = {1,2}; arr[2] = 100; //ops! 訪問越界 return 0; }
前面我們講了內存塊的4點要素,第4點就是內存塊的寬度,他是和指針的寬度是一一對應的。一個指針變量指向了這個內存塊,
那么指針的寬度就是這個內存塊的寬度。這也表明,我們在使用指針的時候,不要越界,也不要取少,取少了取出的數據會不完整,
越界就更嚴重了,程序會掛掉的,因為你訪問了不屬於你的內存。
擴展:有木有 沒有寬度的指針呢? 答:這個可以有 void* p;
void* 類型的指針對應與C# or java中的Object類型變量,它可以指向任何類型變量。 不過他只保存了內存的起點,沒有保存寬度信息,如果你想
取出原來的數據,你必須做出正確的強制轉換。
五:一對相反操作的運算符 * 和 &
& 取地址符 &a 就獲取了a的指針,然后我們就可以用int*p變量出承接這個地址 p = &a; 我們就說p指向了a
* 解地址符 *操作符有一個很形象的動詞 :解 。p保存了a的起始地址和延續寬度,那么,*p則是確定起始地址,量出寬度,
獲取這個內存塊。 因此我們可以通過a 的地址p去操作(讀/寫)這個內存塊了。

p也是一個變量,他自己也有地址,這就長產生了指針的指針。
注意:我所說的指針的寬度並不是說指針變量占用的內存大小,而是通過這個指針指向的內存塊的寬度。指針的占用的字節數是一定的,一般情況下,指針變量都占用4個字節。上面圖中我也畫了4個字節。
六、為什么要使用指針
你可能會問:我有變量名,為什么還要繞一轉,用指針去使用內存數據?
但並不是這樣,下面是2個例子。
①通過動態申請的內存是匿名的,沒有名字,只能通過指向這塊內存的指針去使用。
malloc() realloc() calloc() 這里不擴展了。
②一個經典的題目:用函數交換2個變量的值。
#include<stdio.h> void swap_2b(int a,int b); void swap_hack(int *pa,int *pb); void swap_normal(int*pa,int*pb); int main() { int a = 5; int b = 3; swap_2b(a,b); //Can`t swap; swap_normal(&a,&b); //OK swap_hack(&a,&b); //OK return 0; } //2b青年寫的函數 void swap_2b(int a,int b) { int t; t=a; a=b; b=t; } //程序員寫的函數 void swap_normal(int*pa,int*pb) { int t; t=*pa; *pa=*pb; *pb=t; } //黑客寫的函數 void swap_hack(int *pa,int *pb) { *pa = *pa^*pb; *pb = *pa^*pb; *pa = *pa^*pb; }
我們發現只有2b青年沒用指針哈。
原因很簡單,C語言函數是按值傳遞的,2b青年寫的代碼之所以達不到效果是因為它操作的內存塊錯了。

無論他在函數里面怎么換a和b的值,main函數中的a和b都不會變。因為swap函數中的a和b是隨函數調用新生成的變量,是副本,而不是源對象。
但是傳遞指針就不一樣了,因為內存數據的指針是獨一無二的。一個人有唯一的身份證一樣。
七、野指針和NULL指針
指針變量在使用前或者使用完,好的習慣是賦值為NULL ,NULL 是編號為0的字節的地址。指向NULL表示不指向任何變量。
NULL就像劍鞘,野指針就像暗箭,如果你不像被暗箭所傷,就讓他歸鞘。
最后:由於內容比較多,可能有許多地方表達不當,或有疏漏,錯誤,希望大家及時指出,謝謝 : )
