C++指針與數組:指針詳解

本文主要介紹C語言中指針的基本概念與用法,本博文從零開始講解指針,隻要你瞭解基本的C語法,相信你看完本文後一定會有所收獲。

一. What(什麼是指針)

1. 地址初瞭解

要搞明白什麼是指針,首先我們得先瞭解一個概念:地址。

什麼是地址呢?

我們知道,計算機的內存是一塊連續的大塊存儲空間,為瞭提高 CPU 查找數據的效率,我們將這塊連續的存儲空間以字節為單位進行編號,每個字節都一一對應瞭一個編號,這個編號就叫做地址。

舉個形象的例子:

如果我們把一座宿舍樓看做是內存,那麼宿舍樓裡的房間我們就可以看成是內存裡的每一個字節,那麼每個房間的門牌號就可以看做是地址。

為什麼要有地址?

通過上面的例子,我們很容易瞭解到地址存在的意義。試想,如果宿舍樓裡的房間都沒有門牌號,我們就很難描述一指定房間的具體位置。CPU 對內存的處理也是如此,如果沒有地址,CPU 就沒有辦法確定指定內存的具體位置,從而正確且快速的訪問到該內存空間。

2. 指針概念

  • 指針就是地址
  • 指針就是地址
  • 指針就是地址

重要的事情說三遍,我們必須明確的知道一件事,指針就是地址,可以理解為,指針是地址在C語言裡的一種特殊叫法。

#include <stdio.h>
int main()
{
	int a = 10;
	int *p = &a;		// p是不是指針?
	printf("%p\n", p);  // %p 可以格式化輸出地址。
	return 0;
}

既然如此,上述代碼中 p 是不是指針呢?

我們說過指針就是地址,而 p 是不是地址?當然不是!地址是一個單獨的數據,就像 int a = 10; 我們能說 a 就是 10 嗎?當然不能,10是一個整形常量,而 a 是一個整形變量,我們隻能說 a 裡面存放的值是 10,而不能簡單的說 a 就是 10。

同理,上述代碼中的 p 是一個變量,裡面存放的是 a 的地址,我們稱它為指針變量。所以嚴格意義上來說,p 不是指針,而是指針變量。就像 a 不是整數10,而是整形變量一樣

那麼什麼是指針呢,我們把這段代碼運行起來:

結果

沒錯,嚴格意義上來說,這段數字才真正的叫做指針 (地址值在計算機中一般以16進制顯示)。

3. 指針與指針變量

通過上面的講解,相信大傢可以理解,指針是地址,是一個常量,而指針變量是一個變量,裡面存放的是指針,這兩者之間有著本質區別。

看到這,相信有些同學就有疑問瞭:明明上課的時候,或者某些書上都將指針變量統稱為指針,就像 上例中的 p 平時都叫做指針,為什麼現在又說 p 不是指針呢?

請大傢註意,我說 p 不是指針是在嚴格意義上來說。

在日常使用中,我們經常把指針和指針變量混為一談。這似乎成為瞭一種習慣,甚至我在下面的表述中都可能將指針變量表述為指針。

結論是:我們在日常使用中,可以模糊指針與指針變量的概念,把指針變量統稱為指針,但是,我們心裡必須清楚的意識到,指針和指針變量的本質區別,以及你叫的這個東西到底是指針還是指針變量。

二. Why(為什麼要有指針)

經過上面的講解,相信這個問題就非常簡單瞭。指針就是地址,那麼為什麼要有地址?究其原因就是,為瞭提高查找效率

試想如果沒有地址的存在,我們寫下 int a = 10; 的時候,系統會自動在內存中開辟一4字節的空間存放10這個數值。而我們 CPU 在訪問的時候,由於沒有地址的存在隻能在內存中一塊一塊的遍歷對比查找,而有瞭地址,內存就可以把地址值告訴 CPU,CPU 就可以根據地址值直接定位到該內存,大大提高瞭 CPU 訪問內存的效率與準確性。

三. How(如何使用指針)

1. 基本定義

指針變量的定義格式較簡單,隻要在類型和變量名之間加上 * 操作符,就可以定義該類型的指針變量。

int *a = NULL;				//定義整形指針變量
double *d = NULL;			//定義雙精度浮點型指針變量
struct stdnt *ps = NULL;	//定義結構體指針變量

註: NULL 是指針變量初始化的時候常用的宏定義,稱其為空指針,本質為 0 值。

如果定義時不初始化,如:

int *a;

我們知道,變量如果不初始化,其中的值就是隨機值,也就是此時的指針變量 a 裡面存放的是隨機值,如果此時訪問 a 變量就會以這個隨機值作為地址訪問對應的內存,這種操作是非法的。這種不初始化的指針我們稱之為野指針。

所以,為瞭避免野指針的出現,我們在定義指針變量時盡量對其進行初始化。

2. 取地址操作符 &

我們使用指針就是使用地址,那麼地址從何而來?我們可不可以這麼寫代碼:

int *a = 0x11223344;

我們把 0x11223344 看做是地址值交給整形指針變量 a 來存儲。語法上沒有問題,但是這樣寫毫無意義!因為 0x11223344 這個地址對於我們來說是未知的,我們並沒有讀取和訪問的權限,這時候的 a 變量就相當於一個野指針。

事實上,我們的地址是由 & 取地址符取出來的。

#include <stdio.h>
int main()
{
	int a = 10;
	int *p = &a;
	return 0;
}

這裡我們定義瞭整形變量 a,取地址符取出瞭 a 的地址並把它賦給整形指針變量 p。

這裡就有一個疑問,a 是一個整形變量,占4字節的空間,而通過上面的介紹,我們知道內存的地址編號是以字節為單位的,這就意味著變量 a 其實存在4個地址,那麼 &a 究竟取出的是哪一個地址呢?

上結論:

在C語言中,對任意類型的變量或者數組取地址,永遠取得是數值上最小的那個地址。

我們畫一下上面代碼的內存分佈圖:

內存分佈圖

假設從左向右地址依次升高,那麼 p 裡面存放的就是 a 變量4字節中地址最低的那個字節的地址。

3. 解引用操作符 *

我們上面說到,對變量取地址取得是地址最低的字節即首字節地址,那麼我們如何通過這一個字節的地址訪問到原變量裡的數據呢,這裡就需要使用 * 操作符:

#include <stdio.h>
int main()
{
	int a = 10;
	int *p = &a;
	*p = 20;     // 等價於 a = 20
	printf("%d\n", *p);
	printf("%d\n", a);
	return 0;
}

看上面的代碼,我們把 a 變量的地址賦值給指針變量 p,通過 *p 即可訪問到該變量的內容。

那麼 * 操作符到底是如何使用的呢,下面給出結論:

對指針解引用,就是指針指向的目標。

就像上例中 *p 是什麼?*p 就是 a,*p 就是 a,*p就是a,重要的事情說三遍。所以,如果我改變 *p 的值,完全等價於直接改變 a 變量的值。

上面的代碼運行結果:

運行結果

那麼,我們看下面這段代碼:

int main()
{
	int a = 0x11223344;
	int *p = &a;
	printf("0x%x\n", *(char*)p);
	return 0;
}

註意,我們說 *p 就是 a,但是,這裡的 *p 被強制類型轉化為 char* 類型之後再進行的解引用操作,此時的 p 就是一個字符類型的指針,因為字符 char 類型在C中隻占一個字節,對其進行解引用隻能訪問一個字節的內容。而我們說過 p 中存放的是 a 變量的首字節的地址,即 44 的地址 (至於為什麼,請讀者自己瞭解大小端的內容),解引用就隻訪問到瞭 44:

運行結果

4. 結構體指針

下面定義一個結構體:

typedef struct Student
{
	int age;
	char name[20];
	char sex[2];
}STD;

C語言中任何數據類型都有其對於的指針類型,結構體也不例外,如下:

int main()
{
	STD s = { 18, "張三", "男" };
	STD *ps = &s;		// 定義結構體指針
	printf("%s\n%d\n%s\n", s.name, s.age, s.sex);
}

我們定義一個結構體變量並初始化,通過 . 操作符可以很容易訪問到結構體的內容。那麼我們如何通過結構體指針變量 ps 來訪問結構體的內容呢?

我們說過對指針解引用,就是指針指向的目標,那麼請讀者思考,*ps 是什麼呢?沒錯 *ps 就是 s變量,既然如此,我們就可以這麼訪問:

	printf("%s\n%d\n%s\n", (*ps).name, (*ps).age, (*ps).sex);
	//註:因 . 操作符優先級高於 * 操作符,故 (*ps) 必須加括號

結果

C語言可能認為這樣寫太麻煩,於是對於結構體指針我們可以使用 -> 操作符直接訪問到結構體的內容:

	printf("%s\n%d\n%s\n", ps->name, ps->age, ps->sex);

ps->name 寫法完全等價於 (*ps).name 這樣的寫法。
註:-> 操作符隻在結構體指針變量訪問結構體成員時使用。

5. 多級指針

首先問大傢一個問題,指針變量是不是變量?是變量。既然是變量,那麼就有地址,我們依然可以把指針變量的地址存入一個新的變量,此變量我們可以稱為二級指針變量:

int a = 10;
int *pa = &a;
int **ppa = &pa;  // 定義二級指針變量
*ppa;  //等價於 pa
**ppa; //等價於 *pa,即等價於 a

二級指針大傢不要看的多麼神秘,所謂二級指針其實也就是一個指針變量,隻不過裡面存放的是另一個指針變量的地址而已。
既然如此,二級指針變量是不是變量呢?它能不能取地址存入另外一個三級指針變量呢?那麼,三級指針變量是不是變量呢?… 如果你願意,可以一直這麼套娃下去~

6.指針變量的命名規范

之所以把這塊單獨分出來講,是因為對指針變量進行一個好的命名規范,不僅有利於提高代碼的可讀性,更有助於我們理解一些復雜的數據結構的代碼。

指針的命名習慣上在原變量名前加字母 p。 如定義整形變量 a ,其指針變量就命名為 pa, 定義結構體變量 std 其對應指針變量命名為 pstd,如果對 pstd 再次取地址存入二級指針變量,那麼該二級指針變量就應該命名為 ppstd。

四. 我對指針的一些理解

都說指針是C語言的靈魂,那麼指針的魅力究竟在何處?

我們看這段交換兩個數的代碼:

#include <stdio.h>
void Swap(int a, int b)
{
	int t = a;
	a = b;
	b = t;
}
int main()
{
	int a = 10;
	int b = 20;
	Swap(a, b);
	printf("a = %d\n", a);
	printf("b = %d\n", b);
	return 0;
}

我們應該知道,這段代碼是無法完成 a, b 的交換的,因為 Swap() 裡的 a, b 隻不過是 main() 函數裡 a, b 的一份拷貝。即,Swap() 函數裡 a, b 的改變是不會影響 main() 函數的。這種傳參的方式我們稱之為傳值。

可是我們這麼寫:

#include <stdio.h>
void Swap(int *pa, int *pb)
{
	int t = *pa;
	*pa = *pb;
	*pb = t;
}
int main()
{
	int a = 10;
	int b = 20;
	Swap(&a, &b);
	printf("a = %d\n", a);
	printf("b = %d\n", b);
	return 0;
}

main() 傳參時傳 a, b 的地址,Swap() 函數使用指針進行接收,內部使用解引用方式進行交換,我們就可以完成真正的交換功能。這種傳參的方式我們稱之為傳址。

我在一篇文章上看到這樣一個說法,可謂是對指針理解到瞭本質:

  • 傳值就是傳值。
  • 傳址就是傳值本身。

值和值本身兩個概念希望大傢能夠深刻理解。

如同我們 Swap() 函數的第一種寫法,main() 函數僅僅是將 a, b 的值傳遞給瞭 Swap() 函數進行處理,可無論 Swap() 函數對其做什麼樣的處理,也依然無法改變原來 a, b 的值。

而 Swap() 的第二種寫法,main() 函數傳的是 a, b 的地址,相當於傳的是 a, b 本身,這樣 Swap() 在進行處理時便可直接改變原來的 a, b 的值。

總結

本篇文章就到這裡瞭,希望能夠給你帶來幫助,也希望您能夠多多關註WalkonNet的更多內容!

推薦閱讀: