前言
這不是我第一次寫關於C指針的文章了,只是因為指針對於C來說太重要,而且隨着自己編程經歷越多,對指針的理解越多,因此有了本文。然而,想要全面理解指針,除了要對C語言有熟練的掌握外,還要有計算機硬件以及操作系統等方方面面的基本知識。所以我想通過一篇文章來盡可能的講解指針,以對得起這個文章的標題吧。
本文會持續更新。
為什么需要指針?
指針解決了一些編程中基本的問題。
指針是什么?
為什么程序中的數據會有自己的地址?

而作為一個程序員,我們不需要了解內存的物理結構,操作系統將DRAM等硬件和軟件結合起來,給程序員提供的一種對物理內存使用的抽象。這種抽象機制使得程序使用的是虛擬存儲器,而不是直接操作物理存儲器。所有的虛擬地址形成的集合就是虛擬地址空間。
在程序員眼中的內存應該是下面這樣的。(假設使用的是32位系統平台,虛擬存儲空間為4GB)
也就是說,虛擬存儲器是一個很大的,線性的字節數組(平坦尋址)。每一個字節都是固定的大小,由8個二進制位組成。最關鍵的是,每一個字節都有一個唯一的編號,編號從0開始,一直到最后一個字節。如上圖中,這是一個4GB的虛擬存儲器的模型,它一共有4x1024x1024x1024 個字節,那么它的虛擬地址范圍就是 0 ~ 4x1024x1024x1024-1 。
由於內存中的每一個字節都有一個唯一的編號,因此,在程序中使用的變量,常量,甚至數函數等數據,當他們被載入到內存中后,都有自己唯一的一個編號,這個編號就是這個數據的地址。指針就是這樣形成的。
下面用代碼說明
#include <stdio.h> int main(void) { char ch = 'a'; int num = 97; printf("ch 的地址:%p\n",&ch); //ch 的地址:0028FF47 printf("num的地址:%p\n",&num); //num的地址:0028FF40 return 0; }

操作系統為什么提供虛擬地址空間給程序員用而不是讓程序員直接使用物理地址空間?
本節內容屬於編程思想上的內容,可以先不看,或僅作了解。
1、提高物理內存的利用效率。
你可能會困惑,這怎么就提高物理內存使用效率了呢?我這里舉個共享單車的例子:假如一個國家有10個人,而只生產了2輛自行車(國家小,資源有限嘛~),這2輛車被2個人買了。把你自己想象為那8個沒有自行車的人之一,你的思維是什么——"我沒有自行車,我只能步行外出"。倘若是那2個有車的人呢——"我每次外出都可以騎車去,但是大部分時間,我的車是空閑的,沒其他人用"。后來,這個國家回收了這僅有的2輛車,把車刷成了統一的顏色,貼上二維碼,引入了共享單車系統,然后發出公告:只要車停在路邊沒人用,任何人都可以刷開騎走。那現在這10個人怎么想呢——“只要我看到有空閑的單車,我就可以使用它”。
2、抽象的東西更加簡單穩定。
從古至今,我們對於“去飯館吃飯”這個抽象社會行為沒有太大的變化——進入飯館,點菜,付錢,享用,走人。但是人們烹飪的方法卻發生了具大的改變,烹飪的器材、食材、食譜一直都在更新改進,如果你經常烹飪,你就需要不斷的學習,因為你需要掌握做一道菜的每個細節。
回到內存相關的話題來:無論機器的內存用的是ddr3還是ddr4,是4G物理內存還是8G物理內存,程序員都似乎無需太過關心,因為他們在編程時面向的是虛擬內存,而虛擬內存的模型到目前為止都是固定的。這給程序員帶來非常大的便利,他們無需為快速更新的計算機設備而改變自己的編程思維。
這並不意味着抽象的東西就一定不會改變。例如從“到飯館吃飯”到“點外賣”;從32位操作系統到64位操作系統。都屬於抽象的更新換代。
總結(我個人認為):
- 資產有限的情況下,使用合理的資產使用管理機制,可以使有限的資產服務於更多的人。
- 抽象的事物更加簡單穩定,特定的事物更加復雜易變。
- 底層通過給上層提供抽象服務來獲得利益,上層通過使用底層的抽象來獲得便利。
變量和內存
為了簡單起見,這里就用上面例子中的 int num = 97 這個局部變量來分析變量在內存中的存儲模型。

指針變量 和 指向關系
用來保存 指針(地址) 的變量,就是指針變量。如果指針變量p1保存了變量 num的地址,則就說:p1指向了變量num,也可以說p1指向了num所在的內存塊 ,這種指向關系,在圖中一般用 箭頭表示。
上圖中,指針變量p1指向了num所在的內存塊 ,即從地址0028FF40開始的4個byte 的內存塊。
這里學2個名詞,讀英文資料的時候可能會用到
- pointer:指針,例如上面例子中的p1
- pointee:被指向的數據對象,例如上面例子中的num
- 所以我們可以說:a pointer stores the address of a pointee
int a ; //int類型變量 a int* p ; //int* 變量p int arr[3]; //arr是包含3個int元素的數組 int (* parr )[3]; //parr是一個指向【包含3個int元素的數組】的指針變量 //-----------------各種類型的指針------------------------------ int* p_int; //指向int類型變量的指針 double* p_double; //指向double類型變量的指針 struct Student *p_struct; //結構體類型的指針 int(*p_func)(int,int); //指向返回類型為int,有2個int形參的函數的指針 int(*p_arr)[3]; //指向含有3個int元素的數組的指針 int** p_pointer; //指向 一個整形變量指針的指針
int add(int a , int b) { return a + b; } int main(void) { int num = 97; float score = 10.00F; int arr[3] = {1,2,3}; //----------------------- int* p_num = # float* p_score = &score; int (*p_arr)[3] = &arr; int (*fp_add)(int ,int ) = &add; //p_add是指向函數add的函數指針 return 0; }
- 數組名的值就是這個數組的第一個元素的地址。
- 函數名的值就是這個函數的地址。
- 字符串字面值常量作為右值時,就是這個字符串對應的字符數組的名稱,也就是這個字符串在內存中的地址。
int add(int a , int b){ return a + b; } int main(void) { int arr[3] = {1,2,3}; //----------------------- int* p_first = arr; int (*fp_add)(int ,int ) = add; const char* msg = "Hello world"; return 0; }
int main(void) { int age = 19; int*p_age = &age; *p_age = 20; //通過指針修改指向的內存數據 printf("age = %d\n",*p_age); //通過指針讀取指向的內存數據 printf("age = %d\n",age); return 0; }
int* p1 = & num; int* p3 = p1; //通過指針 p1 、 p3 都可以對內存數據 num 進行讀寫,如果2個函數分別使用了p1 和p3,那么這2個函數就共享了數據num。
#ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif
void opp() { int*p = NULL; *p = 10; //Oops! 不能對NULL解地址 } void foo() { int*p; *p = 10; //Oops! 不能對一個未知的地址解地址 } void bar() { int*p = (int*)1000; *p =10; //Oops! 不能對一個可能不屬於本程序的內存的地址的指針解地址 }
指針的2個重要屬性
指針也是一種數據,指針變量也是一種變量,因此指針 這種數據也符合前面 變量和內存 主題中的特性。 這里我只想強調2個屬性: 指針的類型,指針的值。
int main(void) { int num = 97; int *p1 = # char* p2 = (char*)(&num); printf("%d\n",*p1); //輸出 97 putchar(*p2); //輸出 a return 0; }
結構體和指針
typedef struct { char name[31]; int age; float score; }Student; int main(void) { Student stu = {"Bob" , 19, 98.0}; Student*ps = &stu; ps->age = 20; ps->score = 99.0; printf("name:%s age:%d\n",ps->name,ps->age); return 0; }
數組和指針
int main(void) { int arr[3] = {1,2,3}; int*p_first = arr; printf("%d\n",*p_first); //1 return 0; }
int main(void) { int arr[3] = {1,2,3}; int*p = arr; for(;p!=arr+3;p++){ printf("%d\n",*p); } return 0; }
int main(void) { int arr[3] = {1,2,3}; int*p = arr; printf("sizeof(arr)=%d\n",sizeof(arr)); //sizeof(arr)=12 printf("sizeof(p)=%d\n",sizeof(p)); //sizeof(p)=4 return 0; }
函數和指針
void change(int a) { a++; //在函數中改變的只是這個函數的局部變量a,而隨着函數執行結束,a被銷毀。age還是原來的age,紋絲不動。 } int main(void) { int age = 19; change(age); printf("age = %d\n",age); // age = 19 return 0; }
void change(int* pa) { (*pa)++; //因為傳遞的是age的地址,因此pa指向內存數據age。當在函數中對指針pa解地址時, //會直接去內存中找到age這個數據,然后把它增1。 } int main(void) { int age = 19; change(&age); printf("age = %d\n",age); // age = 20 return 0; }
再來一個老生常談的,用函數交換2個變量的值的例子:
#include<stdio.h> void swap_bad(int a,int b); void swap_ok(int*pa,int*pb); int main() { int a = 5; int b = 3; swap_bad(a,b); //Can`t swap; swap_ok(&a,&b); //OK return 0; } //錯誤的寫法 void swap_bad(int a,int b) { int t; t=a; a=b; b=t; } //正確的寫法:通過指針 void swap_ok(int*pa,int*pb) { int t; t=*pa; *pa=*pb; *pb=t; }
typedef struct { char name[31]; int age; float score; }Student; //打印Student變量信息 void show(const Student * ps) { printf("name:%s , age:%d , score:%.2f\n",ps->name,ps->age,ps->score); }
void echo(const char *msg) { printf("%s",msg); } int main(void) { void(*p)(const char*) = echo; //函數指針變量指向echo這個函數 p("Hello "); //通過函數的指針p調用函數,等價於echo("Hello ") echo("World\n"); return 0; }
const 和 指針
如果const 后面是一個類型,則跳過最近的原子類型,修飾后面的數據。(原子類型是不可再分割的類型,如int, short , char,以及typedef包裝后的類型)
int main() { int a = 1; int const *p1 = &a; //const后面是*p1,實質是數據a,則修飾*p1,通過p1不能修改a的值 const int*p2 = &a; //const后面是int類型,則跳過int ,修飾*p2, 效果同上 int* const p3 = NULL; //const后面是數據p3。也就是指針p3本身是const . const int* const p4 = &a; // 通過p4不能改變a 的值,同時p4本身也是 const int const* const p5 = &a; //效果同上 return 0; }
typedef int* pint_t; //將 int* 類型 包裝為 pint_t,則pint_t 現在是一個完整的原子類型 int main() { int a = 1; const pint_t p1 = &a; //同樣,const跳過類型pint_t,修飾p1,指針p1本身是const pint_t const p2 = &a; //const 直接修飾p,同上 return 0; }
深拷貝和淺拷貝
如果2個程序單元(例如2個函數)是通過拷貝 他們所共享的數據的 指針來工作的,這就是淺拷貝,因為真正要訪問的數據並沒有被拷貝。如果被訪問的數據被拷貝了,在每個單元中都有自己的一份,對目標數據的操作相互 不受影響,則叫做深拷貝。
附加知識
指針和引用這個2個名詞的區別。他們本質上來說是同樣的東西。指針常用在C語言中,而引用,則用於諸如Java,C#等 在語言層面封裝了對指針的直接操作的編程語言中。引用是編程語言提供給程序員的抽象機制,而指針是操作系統提供給軟件開發模型的抽象機制。

#include<stdio.h> //測試機器使用的是否為小端模式。是,則返回true,否則返回false //這個方法判別的依據就是:C語言中一個對象的地址就是這個對象占用的字節中,地址值最小的那個字節的地址。 int isSmallIndain(void) { unsigned short val = 0x0001; unsigned char* p = (unsigned char*)&val; //C/C++:對於多字節數據,取地址是取的數據對象的第一個字節的地址,也就是數據的低地址 return (*p == 0x01); } int main(void) { if(isSmallIndain()) { puts("小端"); } else{ puts("大端"); } return 0; }
第二種方法,使用union類型
#include<stdio.h> typedef union { unsigned short us; unsigned char uc; }Test_t; int main(void) { Test_t val; val.us = 0x0001; if(val.uc==0x01) { puts("小端"); } else{ puts("大端"); } return 0; }
#include<stdio.h> //打印出一個unsigned short int 類型的原始字節流 //這個例子中很明顯看到,取到a的首地址后,我們循環遞增了p,而非遞減p,也從來不會看到有從首地址遞減輸出數據的字節的寫法。 //這也就佐證了:在C語言中,對於一個多字節數據,它的地址就是它占用的所有字節中的地址值最小的那個字節的虛擬空間地址 //這也又說明了一個事實:C語言中,一個多字節數據類型的實例,占用的虛擬內存空間是連續的。 int main(void) { size_t i; unsigned short int a = 0xA1FF; unsigned char*p = (unsigned char*)&a; for( i=0;i<sizeof(a);++i) { printf("%#x ",*p); //小端平台輸出:0xFF 0xA1 p++; //大端平台輸出:0xA1 0xFF } printf("\n\n"); return 0; }