”指針是C語言的精髓!“
——出自學校教《C語言程序設計》的老師
1 內存和地址
1.1 內存
為了理解指針,首先要從內存和地址講起。
在講之前,先舉一個現實世界中的例子。大學宿舍都有門牌號,當需要找到某個學生時,我們只需要知道宿舍的門牌號就可以了。
在計算機中內存很重要,程序經常需要從內存中讀取和寫入數據。在購買電腦的時候,內存的大小常有8/16/32GB等,這些空間又是如何被管理的?
其實也是把內存劃分為一個個的內存單元,每個內存單元的大小是1字節(byte)。
其中,每個內存單元,相當於一個學生宿舍,一個字節空間裏面能放8個比特位,就好比同學們住的八人間,每個人是一個比特位。
每個內存單元也都有一個編號(這個編號就相當於宿舍房間的門牌號),有了這個內存單元的編號,CPU就可以快速找到一個內存空間。
生活中我們把門牌號也叫地址,在計算機中我們把內存單元的編號也稱為地址。C語言中給地址起了新的名字叫:指針。
所以我們可以理解為:內存單元的編號 = 地址 = 指針
1.2 如何理解編址
CPU與內存之間有大量的數據交互,這些交互通過地址總線、數據總線、控制總線等,我們這裏關注的是地址總線。這裏可以簡單理解,32位機器上有32根地址總線,每根線有0、1兩種狀態,能表示2^32種含義,每一種含義都代表了1個地址。CPU通過地址總線獲取到了內存地址後,就可以通過其他總線對內存進行操作。
2 指針變量和地址
2.1 取地址操作符(&)
理解了內存和地址的關係,回到C語言中,創建變量其實就是向內存申請空間。例如int a = 10,就是創建了整形變量a,內存中申請4個字節,用於存放整數10,其中每個字節都有地址。
那我們如何得到a的地址呢?這就需要用到取地址操作符(&)。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
//打印整形變量a的地址
//環境為x64
int main()
{
int a = 10;
printf("a的地址是:%p\n", &a);
return 0;
}
如圖,&a取出的是a所佔4個字節中地址較小的字節的地址。
雖然整型變量佔用4個字節,我們只要知道了第一個字節地址,順藤摸瓜訪問到4個字節的數據是完全可行的。
2.2 指針變量和解引用操作符(*)
2.2.1 指針變量
那我們通過取地址操作符(&)拿到的地址是一個數值,比如:0000009E504FFC84,這個數值有時候也是需要存儲起來,方便後期再使用的,那我們把這樣的地址值存放在哪裏呢?答案就是:指針變量中。
比如:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
//環境為x64
int main()
{
int a = 10;
int* pa = &a;//取出a的地址並存儲到指針變量pa中
printf("a的地址是:%p\n", pa);
return 0;
}
指針變量也是一種變量,這種變量就是用來存放地址的,存放在指針變量中的值都會理解為地址。
2.2.2 拆解指針類型
我們看到指針變量pa的類型是int* ,該如何理解指針的類型呢?
int a = 10;
int* pa = &a;
這裏pa左邊寫的是int*,*是在説明pa是指針變量。而前面的int是在説明pa指向的是整型(int)類型的對象。
那如果有一個char類型的變量ch,ch的地址,要放在什麼類型的指針變量中呢?自然是放在char*類型的指針變量中。
2.2.3 解引用操作符(*)
我們將地址保存起來,未來是要使用的,那怎麼使用呢?
在現實生活中,我們使用地址要找到一個房間,在房間裏可以拿去或者存放物品。
C語言中其實也是一樣的,我們只要拿到了地址(指針),就可以通過地址(指針)找到地址(指針)指向的對象,這裏必須學習一個操作符叫解引用操作符(*)。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
//環境為x64
int main()
{
int a = 10;
int* pa = &a;
printf("修改前:a = %d\n", a);
*pa = 5;
printf("修改後:a = %d\n", a);
return 0;
}
上面代碼中第7行就使用瞭解引用操作符,*pa的意思就是通過pa中存放的地址,找到指向的空間,*pa其實就是a變量了;所以*pa = 0,這個操作符是把a改成了0。
有同學肯定在想,這裏如果目的就是把a改成0的話,寫成a = 0;不就完了,為啥非要使用指針呢?
其實這裏是把a的修改交給了pa來操作,這樣對a的修改,就多了一種的途徑,寫代碼就會更加靈活,後期慢慢就能理解了。
2.3 指針變量的大小
1.2中提到:
在32位機器上有32根地址總線,每根線有0、1兩種狀態,能表示2^32種含義。
那我們把32根地址線產生的2進制序列當做一個地址,那麼一個地址就是32個bit位,需要4個字節才能存儲。如果指針變量是用來存放地址的,那麼指針變量的大小就得是4個字節的空間才可以。
同理64位機器,假設有64根地址線,一個地址就是64個二進制位組成的二進制序列,存儲起來就需要8個字節的空間,指針變量的大小就是8個字節。
通過原理可以得知,指針變量的大小與其類型無關,只要指針類型的變量在相同的平台下,大小都是相同的。
3 指針變量類型的意義
既然指針變量的大小和類型無關,只要是指針變量,在同一個平台下,大小都是一樣的,為什麼還要有各
種各樣的指針類型呢?其實指針類型是有特殊意義的。接下來我們一起探討。
3.1 指針的解引用
下面,我們來對比兩段代碼:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
//代碼1
int main()
{
int n = 0x11223344;
int* pi = &n;
*pi = 0;
return 0;
}
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
//代碼2
int main()
{
int n = 0x11223344;
char* pc = &n;
//char* pc = (char*)&n;(可寫成這種顯式類型轉換)
*pc = 0;
return 0;
}
淺看一下,兩段代碼都希望將整形變量n的值改為0,代碼1是正常的寫法肯定沒問題,但是代碼2很怪,用了一個char*類型的指針變量pc來存儲整形變量n的地址,這會導致什麼,打開調試在內存中看一看。
我們先看看代碼1:
創建整形變量n,內存中申請了4個字節的空間,存儲16進制數0x11223344,然後*pc = 0將n的4個字節全部改為0,任務完成。
接下來看代碼2:
我們發現,代碼2只是將n的第一個字節改為0,其他3個字節的值原封不動的保留了下來。
結論:指針的類型決定了,對指針解引用的時候有多大的權限(一次能操作幾個字節)。
比如:char*的指針解引用就只能訪問一個字節,而int*的指針的解引用就能訪問四個字節。
3.2 指針 + / - 整數
先看一段代碼,調試觀察地址的變化。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int n = 10;
char* pc = (char*)&n;
int* pi = &n;
printf("&n = %p\n", &n);
printf("pc = %p\n", pc);
printf("pc+1= %p\n", pc + 1);
printf("pi = %p\n", pi);
printf("pi+1= %p\n", pi + 1);
return 0;
}
代碼運行的結果如下:
我們可以看出,char*類型的指針變量+1跳過1個字節,int*類型的指針變量+1跳過了4個字節。這就是指針變量的類型差異帶來的變化。指針+1,其實就是跳過1個指針指向的元素。指針可以+1,那也可以-1。
結論:指針的類型決定了指針向前或者向後走一步有多大(距離)。
3.3 void*指針
在指針類型中有一種特殊的類型是void*,可以理解為無具體類型的指針(或者叫泛型指針),這種類型的指針可以用來接受任意類型地址。但是也有侷限性,void*類型的指針不能直接進行指針的+-整數和解引用的運算。
一般void*類型的指針是使用在函數參數的部分,用來接收不同類型數據的地址,這樣的設計可以實現泛型編程的效果。使得一個函數來處理多種類型的數據,在後面會經常遇到。
4 指針運算
指針的基本運算有三種,分別是:
- 指針+-整數
- 指針-指針
- 指針的關係運算
4.1 指針+-整數
因為數組在內存中是連續存放的,只要知道第一個元素的地址,順藤摸瓜就能找到後面的所有元素。
下面我們用指針來讀取一個數組:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
//指針+-整數
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
int sz = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < sz; i++)
{
printf("%d ", *(p + i));//p+i 這裏就是指針+整數
}
return 0;
}
4.2 指針-指針
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
//指針-指針
int my_strlen(char* s)
{
char* p = s;
while (*p != '\0')
p++;
return p - s;
}
int main()
{
printf("%d\n", my_strlen("abc"));
return 0;
}
4.3 指針的關係運算
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
//指針的關係運算
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
int sz = sizeof(arr) / sizeof(arr[0]);
while (p < arr + sz)//指針的大小比較
{
printf("%d ", *p);
p++;
}
return 0;
}