C語言之漫談指針(上)
C語言之漫談指針(上)
在C語言學習的途中,我們永遠有一個繞不了的坑,那就是——指針。
在這篇文章中我們就談一談指針的一些基礎知識。
綱要:
- 零.談指針之前的小知識
- 一.指針與指針變量
- 二.指針變量的類型
- 三.指針的解引用
- 四.野指針
- 五.指針運算
- 六.指針與數組
- 七.二級指針
- 八.指針數組
- 九.相關例題
零.談指針之前的小知識
在談指針之前我們先來說一說 計算機的儲存器.
我們在碼代碼時, 每當聲明一個變量,計算機都會在存儲器中某個地方為它創建空間。
如果在函數(例如main()函數)中聲明變量,計算機會把它保存在一個叫棧(Stack)的存儲器區段中;
如果你在函數以外的地方聲明變量,計算機則會把它保存在存儲器的全局量段(Globals)。
程序內存分配的幾個區域:
1. 棧區(stack):在執行函數時,函數內局部變量的存儲單元都可以在棧上創建,函數執行結束時這些
存儲單元自動被釋放。棧內存分配運算內置於處理器的指令集中,效率很高,但是分配的內存容量有
限。 棧區主要存放運行函數而分配的局部變量、函數參數、返回數據、返回地址等。
2. 堆區(heap):一般由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS回收 。分配方式類似
於鏈表。
3. 數據段(靜態區)(static)存放全局變量、靜態數據。程序結束後由系統釋放。
4. 代碼段:存放函數體(類成員函數和全局函數)的二進制代碼。
如下圖:
當然了,這張圖對於一些初學者並不是很友好。但是接下來一張圖就友好了很多。
那麼,我們如何知道我們的變量儲存在哪?
這時我們就需要用到 & 操作符了。
這時可能有人問了,我這兩次 x 的地址為什麼不相同,這是因為:
我們每一次程序開始運行的時候,系統都會給我們的變量重新分配地址,
程序結束的時候銷毀地址,再次從頭開始運行時再次重新分配,結束時再次銷毀。
有了一定的知識之後,我們便開始正文。
一.指針以及指針變量。
1.指針
在計算機科學中,指針(Pointer)是編程語言中的一個對象,利用地址,它的值直接指向
(points to)存在電腦存儲器中另一個地方的值。由於通過地址能找到所需的變量單元,可以
說,地址指向該變量單元。因此,將地址形象化的稱為「指針」。意思是通過它能找到以它為地址
的內存單元。
對於上面的概念我們再簡練一些便可以概括為:
指針 :內存單元的地址(編號)。如上例中 x 的地址 (指針) 為 0x00D3FD34,
2.指針變量
指針變量:存儲地址 (指針) 的變量。
3.指針和指針變量的關係
1、指針就是地址,地址就是指針。
2、地址就是內存單元的編號。
3、指針變量就是存放內存地址的變量。
指針變量的值就是變量的地址。指針與指針變量的區別,就是變量值與變量的區別。
4、為表示指針變量和它指向的變量之間的關係,用指針運算符”*”表示。如:
//分別定義了 int、float、char 類型的指針變量 int *x = 1; float *f = 1.3; char *ch = 'z';
要注意的一點就是 此時 x、y、z 就是指針變量的名字,而不是 *x、*y、*z。
如下:
#include <stdio.h> int main() { int a = 10; //在內存中開闢一塊空間來放制a int* p = &a;//這裡我們對變量a,取出它的地址,可以使用 & 操作符。 //將a的地址存放在p變量中,p就是一個之指針變量。 return 0; }
4.指針變量的大小
指針變量所佔空間的大小和該指針變量指向的數據類型沒有任何直接關係,而是跟其所在地址的所佔空間的大小有關。
同一編譯器下,同一編譯選項下所有類型的指針變量大小都是一樣的,
指針變量的大小是編譯器所對應的系統環境所決定的。
如:
指針變量的大小在16位平台是2個位元組,在32位平台是4個位元組,在64位平台是8個位元組。
二.指針變量的類型
提前聲明一下:為了方便講述後面的知識,下文將以指針代替指針變量!
我們先來看以下代碼:
int a = 1; p = &a;
現在我們都知道了 p 是一個存儲着a的地址的指針變量。
可是它的類型是什麼呢?
這時,我們就需要看它所存儲的地址中的變量是什麼類型了。
存儲類型 指針類型
int ---> int* short ---> short* float ---> float* char ---> char* double ---> double* long ---> long*
我們可以看到:
指針的定義方式是: type + * 。
其實: char* 類型的指針是為了存放 char 類型變量的地址。
short* 類型的指針是為了存放 short 類型變量的地址。
int* 類型的指針是為了存放int 類型變量的地址。
那麼指針類型又意味着什麼呢?
我們來看看下述代碼:
#include <stdio.h> int main() { int n = 10; char* pc = (char*)&n; int* pi = &n; printf("%p\n", &n); printf("%p\n", pc); printf("%p\n", pc + 1); printf("%p\n", pi); printf("%p\n", pi + 1); return 0; }
我們可以發現:
int* + 1 向後跳過了4個位元組—恰好為一個int 型的大小
char* + 1 向後跳過了1個位元組—恰好為一個char 型的大小
而這,是不是巧合呢——答案當然是否定的。
所以,我們可以得到一個結論:
針的類型決定了指針向前或者向後走一步有多大(距離)。
三.指針的解引用
1.指針的解引用
現在我們有了變量存儲的地址,可是我們要怎麼樣用到它呢?
這時就需要指針的解引用操作符了—” * “
int main() { int a = 1; int* b = &a; printf("%d", a); printf("%d", *b); //*p 就相當於把p所指向的空間的內容拿出來 return 0; }
我們會發現其結果都相同。
我們也可以利用解引用的方式來改變一個變量的值:
int main() { int a = 1; int* b = &a; printf("改變之前: %d\n", a); *b = 2; printf("改變之後: %d\n", a); return 0; }
如果上面圖中的語言有點抽象,那我們可以舉一個形象的例子:
有一個人叫張三,有一天他在XX賓館中定了一間房,且房子的門號為100,到這天晚上的時候,他覺得有點寂寞,
於是打電話喊了他好朋友小劉來找他玩,張三在描述地址時是這樣說的:我在XX賓館100號房間……但小劉那時有事,所以等到小劉來賓館的時候
已經是第二天中午了,可時張三在第二天早上就退了房,且李四又住了進來,所以當小劉打開賓館的100室見到的還會是張三嗎?肯定不會了
住在100室的人——–a
100室——&a、b
張三——-1
李四——–2
不知大家這回理解了沒有
2.指針的類型與解引用的關係
我們來看看這一個例子:
#include <stdio.h> //在此程序運行時,我們要重點在調試的過程中觀察內存的變化 int main() { int n = 0x11223344; char* pc = (char*)&n; int* pi = &n; *pc = 0; *pi = 0; return 0; }
所以,我們又可以推出:
指針的類型決定了,對指針解引用的時候有多大的權限(能操作幾個位元組)。
比如: char* 的指針解引用就只能訪問一個位元組,而 int* 的指針的解引用就能訪問四個位元組。
四.野指針
野指針:野指針就是指針指向的位置是不可知的(隨機的、不正確的、沒有明確限制的)
1.野指針的危害
1、指向不可訪問的地址
2、指向一個可用的,但是沒有明確意義的空間
3、指向一個可用的,而且正在被使用的空間,造成癱瘓
2.野指針成因:
1. 指針未初始化:
#include <stdio.h> int main() { int* p;//局部變量指針未初始化,默認為隨機值 *p = 20; return 0; }
2. 指針越界訪問:
#include <stdio.h> int main() { int arr[10] = { 0 }; int* p = arr; int i = 0; for (i = 0; i <= 11; i++) { //當指針指向的範圍超出數組arr的範圍時,p就是野指針 *(p++) = i; } return 0; }
3.指針指向的空間釋放:
int func() { int *p = malloc(sizeof(int)); free(p);//沒有將p值為NULL的操作 }
3.如何避免野指針
1. 指針初始化
2. 小心指針越界
3. 指針指向空間釋放及時置NULL
4. 指針使用之前檢查有效性
五.指針運算
1.指針+-整數
在我們指針變量大小那塊我們便展示了一個例子,接下來我們繼續看一個:
#define N_VALUES 5 int main() { float values[N_VALUES]; float* vp; //指針+-整數;指針的關係運算 for (vp = &values[0]; vp < &values[N_VALUES];) { *vp++ = 0; } for (int i = 0; i < N_VALUES; i++) { printf("%f ", values[i]); } return 0; }
此例是運用指針來放置數組變量
到這,我們便又掌握了一種操作數組的方法。(詳見六.指針與數組)
2.指針-指針
在之前我們模擬strcpy()的實現中,便用到此方法,下面我們繼續再來寫一下
//1. 計數器的方法 int my_strlen(char* str) { int count = 0; while (*str != '\0') { count++; str++; } return count; }
//2.遞歸實現 int my_strlen(const char* str) { if (*str == '\0') return 0; else return 1 + my_strlen(str + 1); }
//3.指針-指針 int my_strlen(char* s) { char* p = s; while (*p != '\0') p++; return p - s; }
詳情參見:C語言之庫函數的模擬與使用
在指針-指針中我們需要注意的一點就是:兩個指針一定要指向的是同一塊連續的空間
下面以一個實例來說明:
int main() { int arr[10] = { 0 }; printf("%d\n", &arr[0] - &arr[9]);//-9 char ch[5] = {0}; //指針-指針 絕對值的是指針和指針之間的元素個數 printf("%d\n", &arr[9] - &ch[3]);//err //指針-指針 計算的前提條件是:兩個指針指向的是同一塊連續的空間的 return 0; }
我們會看到編譯器報一個警告
3.指針的關係運算
#define N_VALUES 5 int main() { float values[N_VALUES]; float* vp; //指針的關係運算 for (vp = &values[N_VALUES - 1]; vp >= &values[0]; vp--) { *vp = 0; } for (int i = 0; i < N_VALUES; i++) { printf("%f ", values[i]); } return 0; }
我們來看看上面這個例子有沒有什麼問題
六.指針與數組
我們先在這看一個實例:
#include <stdio.h> int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,0 }; printf("%p\n", arr); printf("%p\n", &arr[0]); return 0; }
在運行結果中,我們居然發現,這兩個地址居然一樣!
由此我們可以得到:數組名表示的是數組首元素的地址。
那麼我們用指針來接收數組時便可寫成這個樣子
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int *p = arr;//p存放的是數組首元素的地址
int main() { int arr[5] = { 1, 2, 3, 4, 5 }; int* p = arr; int i = 0; for (i = 0; i < 5; i++) { printf("%d ", *(p + i));//通過指針來訪問數組元素 } printf("\n"); for (i = 0; i < 5; i++) { printf("&arr[%d] = %p < === > %p\n", i, &arr[i], p+i);//打印兩地址看是否相同 } return 0; }
所以 p+i 其實計算的是數組 arr 下標為i的地址。
那我們就可以直接通過指針來訪問數組。
如下:
int main() { int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }; int* p = arr; //指針存放數組首元素的地址 int sz = sizeof(arr) / sizeof(arr[0]); int i = 0; for (i = 0; i < sz; i++) { printf("%d ",*(p + i)); } return 0; }
七.二級指針
我們知道,存放地址變量的叫做指針變量,那存放指針變量的呢,自然就叫做二級指針變量了啊!
由此我們可以繼續推下去:
int main() { int a = 10; int * p = &a;//p是一級指針 int * * pp = &p;//pp是二級指針 **pp = 20; printf("%d\n", a);// //int*** ppp = &pp;//三級指針 //int**** pppp = &ppp;//四級指針
//……………………
//…………………… return 0; }
八.指針數組
在談這個主題之前,我們先來想一想 指針數組到底是指針還是數組?
數組
整形數組 – 存放整形的數組
字符數組 – 存放字符的數組
所以:
指針數組 – 存放的指針
如:
int main() { //int arr[10] = {0};//整形數組 //char ch[5] = { 'a', 'b' };//字符數組 //指針數組 int a = 10; int b = 20; int c = 30; //arr就是指針數組 //存放整形指針的數組 int* arr[3] = { &a, &b, &c };//int* char* ch[5];//存放字符指針的數組 return 0; }
九.相關例題
在我們解決問題時,第一個遇到用指針的問題應該是這個:
寫一個函數可以交換兩個整形變量的內容。
#include <stdio.h> void Swap1(int x, int y) { int tmp = 0; tmp = x; x = y; y = tmp; } int main() { int num1 = 1; int num2 = 2; Swap1(num1, num2); printf("Swap1::num1 = %d num2 = %d\n", num1, num2); return 0; }
當時,我們說,函數里的 x,y 只是對於數據的一份臨時拷貝,而與真實數據並沒有本質的聯繫。
所以,我們把函數改寫成了:
void Swap2(int* px, int* py) { int tmp = 0; tmp = *px; *px = *py; *py = tmp; }
這時,我們或許知道了這樣是怎麼把值給交換過來的
我們將地址先傳過去,然後再進行解引用賦值操作,整個函數過程都與我們傳過去的變量息息相關
例題1:
#include <stdio.h> int main() { int a = 0x11223344; char* pc = (char*)&a; *pc = 0; printf("%x\n", a); return 0; }
這一題要用到我們之前談到過的大小端儲存模式—–詳見: C語言之數據在內存中的存儲
首先 a在內存中按小端存儲的是 44 33 22 11
char* 只能解引用一個位元組,所以獲取的是 44 這個位元組
而現在 *pc=0 就是把這個位元組的值由 44 變為了 0;
所以該程序在Vs的編譯器結果是 0x 11 22 33 00
例題2:
#include <stdio.h> int main() { int arr[] = { 1,2,3,4,5 }; short* p = (short*)arr; int i = 0; for (i = 0; i < 4; i++) { *(p + i) = 0; } for (i = 0; i < 5; i++) { printf("%d ", arr[i]); } return 0; }
首先我們要知道 short* 解引用只能解引用 2 個位元組
而 int 類型為 4 個位元組
所以第一個for循環只改變了數組 arr 的前兩個變量(改為了0)
所以最後的結果為: 0 0 3 4 5
到此,我們便掌握了指針的一些基礎知識
下節,我們將談到一些指針的高級應用
詳見:C語言之漫談指針(下)
|——————————————————————|
|因筆者水平有限,若有錯誤之處,還望多多指正。|
|——————————————————————|