1. 程式人生 > >攻克指標(二)精華篇

攻克指標(二)精華篇

從5個維度來看C語言指標(指標就是個紙老虎)

之前我寫過一篇文章: 從四個屬性的角度來理解C語言的指標也許會更好理解。在那篇文章中,我給指標總結出了4個屬性,分別是“有用資料的地址、有用資料的名字、有用資料的值以及有用資料的型別”,應該幫助了不少人,但現在回頭看,我覺得總結的還不夠好,還不便於理解。後來也是一直在繼續想著怎麼把指標的屬性總結的再簡單點、通俗易懂一點。正好這幾天又想到一些東西,就寫下來吧。希望這次挖掘出的5個指標屬性,本文稱“維度”,希望能幫助到學習指標的朋友。 本文中,我對指標總結的維度,用四個字來概括,就是:“兩己三他”!是不是讀起來一點都不順口,一點都不押韻啊,什麼個玩意兒。這“兩己三他”

,展開來說,就是:己址、己值、他值、他址、他型。

我覺得可以從這5個維度再來聊聊指標。不過在聊之前,我寫了個程式,把指標的“兩己三他”維度都包含進來,然後再來一個一個解釋每個維度的意思,你看看是不是這回事兒。

在大部分的使用指標的場景下,這5個維度應該足夠幫你去理解了。不過在一些使用指標特殊的場景下,可能5維度法幫助不了你。

前方長文預警,若看的不耐煩了,可以收藏本文,有時間了接著看。

一、程式程式碼

1.1. 程式碼

#include <stdio.h>

int main(void)
{
	int *pInt = NULL;
	printf("指標變數pInt自己的地址是:  0X%x\n", &pInt);
	//printf("指標變數pInt自己的值是:  0X%x\n", pInt);
	
	int para = 1;
	printf("變數para自己的地址是:  0X%x\n", &para);
	printf("變數para自己的值是:  0X%x\n", para);
	
	pInt = &para;
	printf("指標變數pInt自己的值是:  0X%x\n", pInt);
	printf("指標變數pInt的他值是:  0X%x\n", *pInt);

	int arr_int[2] = {1, 2};	
	pInt = arr_int;
	printf("arr_int第一個元素arr_int[0]的地址是:  0X%x\n", pInt);
	printf("arr_int第二個元素arr_int[1]的地址是:  0X%x\n", pInt + 1);
	
	double *pDouble = NULL;
	double arr_double[2] = {1.1, 2.2};
	pDouble = arr_double;
	printf("arr_double第一個元素arr_double[0]的地址是:  0X%x\n", pDouble);
	printf("arr_double第二個元素arr_double[1]的地址是:  0X%x\n", pDouble + 1);
	
	return 0;
} 
 

執行的結果如下:

我在自己的文章裡講我自己理解的一些東西時,我很喜歡用非常簡單的程式來闡明。所以那些認為要用天書般的程式來闡明觀點的,或者鄙視低水平程式的大牛們,請忽略我^_^。

2.2. int型變數para

程式中:

int para = 1;
printf("變數para自己的地址是:  0X%x\n", &para);
printf("變數para自己的值是:  0X%x\n", para);

定義了變數para,它有自己的資料值,也有自己的儲存地址,這些個都很好理解。從執行結果來看,變數para自己的資料值是16進位制的“0X1”,地址是16進位制的“0X22feb4”。換句話說,在記憶體中,地址為“0X22feb4”開始的儲存空間,有4個位元組儲存了一個數據值“0X1”。在我的機器上,一個int型變數佔用4個位元組,在你的機器上可能與我不一樣。

關於int型變數para大家都好理解。下面,我來畫一個示意圖,來指明變數para在記憶體中儲存的情況,如下:

下面開始說說“兩己三他”的概念。

二、兩己三他

“兩己三他”,展開來說,就是:己址、己值、他值、他址、他型。

2.1. 己址

2.1.1 “己址”的概念

“己址”,就是“自己的地址”的簡稱。指標pInt作為一個變數,與int變數para一樣,也需要儲存在記憶體中的一段儲存空間,這段儲存空間也會有一個開始地址,也就是說,指標變數pInt也會有自己的地址。上面說了,變數para的地址是 “ 0X22feb4”,那麼,指標變數pInt的地址是啥呢?

2.1.2 “己址”的獲取

我們都學過,“&”是一個取地址的運算子,在程式中:

printf("指標變數pInt自己的地址是:  0X%x\n", &pInt);

就是通過“&”來獲取指標變數pInt的地址。從執行結果來看,指標變數pInt的地址是“0X22feb8”。在我的機器上,指標變數pInt也是佔用4個位元組,因此,指標變數pInt儲存在開始地址是“0X22feb8”開始的4個位元組空間。

2.1.3 “己址”的程式碼寫法

在程式碼中,表示指標變數pInt的“己址”的程式碼寫法,常見的是:

&pInt;

2.1.4 示意圖

現在,我們來完善那個示意圖,圖中加入指標變數pInt的“己址”,指出指標變數pInt在記憶體中是怎麼個儲存形式。

“己址”,就是這個意思。你看,沒什麼特別難的吧!

2.2 己值

2.2.1 “己值”的概念

“己值”,就是“自己的資料值”的簡稱。指標pInt作為一個變數,跟變數para一樣,也有著自己的資料值。

2.2.2 “己值”的獲取

上面提到,變數para自己的資料值是“1”,那麼指標變數pInt自己的資料值是多少。在程式中:

pInt = &para;
printf("指標變數pInt自己的值是:  0X%x\n", pInt);

我通過“&”運算子,將變數para的地址值賦給了指標變數pInt,通過printf來輸出指標變數pInt的資料值。從執行結果中來看,指標變數pInt自己的資料值是“0X22feb4”。我們再看,變數para的地址也是“0X22feb4”,所以,

pInt = &para;

這個語句的本質,就是將變數para的地址,給了指標變數pInt的己值,這樣就將指標變數pInt與變數para繫結在一起了。

在“己址”中提到了,指標pInt的資料值儲存在地址為“0X22feb8”開始的4個位元組的記憶體上,那麼也就是說,地址為“0X22feb8”開始的記憶體,後面的4個位元組都用來儲存著一個數據值“0X22feb4”。

2.2.3 “己值”的程式碼寫法

在程式碼中,表示指標變數pInt的“己值”的程式碼寫法,常見的有

pInt;

也有的程式碼寫法是:

pInt + N;
pInt - N;

這種寫法的意思是用pInt的“己值”加上一個數字N或者減去一個數字N,這個等講到“他型”這個屬性時會提到。也有的寫法是:

pIntA - pIntB;

這種寫法表示的是兩個指標變數用“己值”做減法。

2.2.4 示意圖

現在,繼續來完善上面的示意圖,加入指標變數pInt的己值。

所以,一般而言,“己值”對於指標變數pInt來講,是自己的資料值;對其它的int型別的變數來講,就是它們的地址。

2.3 他址

2.3.1 “他址”的概念

“他址”的概念就是“他人的地址”的意思。其實在上面提到己值時,就已經不那麼明顯地提到了“他址”的概念。

2.3.2 “他址”的獲取

整型變數para儲存在記憶體地址為"0X22feb4"開始的4個位元組。在程式中,我通過

pInt = &para;

將變數para的地址給了指標變數pInt,這樣就將指標變數pInt與變數para繫結在一起了。更為本質的說,是把“他人的地址”賦值給了指標變數pInt的“己值”,這裡,“他人的地址”的“他”,指的就是變數para,“他人地址的址”的“址”,指的就是變數para的地址。注意,你看,”他址“和”己值“在資料值上是一樣的,所以,你領悟出了什麼東西來了沒?

很多教材所謂的“指標是一個地址變數,儲存的是其它變數的地址”,說白了,就是在說“他址”這個維度的資料值等於“己值”這個維度的資料值,只是教材沒說的那麼明白。

2.3.3 示意圖

再來完善那個示意圖,這次加入“他址”的概念。

2.4 他值

2.4.1 “他值”的概念

“他值”,就是“他人的資料值”的意思。

2.4.2 “他值”的獲取

在程式中,我通過

pInt = &para;

將變數para的地址給了指標變數pInt的“己值”,這樣就將指標變數pInt與變數para繫結在一起了。這個時候,“他人的資料值”的“他”,指的就是變數para,“他人的資料值”的“資料值”,指的就是變數para的資料值“1”。在程式中,我通過

printf("指標變數pInt的他值是:  0X%x\n", *pInt);

也就是指標變數pInt前面加上“ * ”,來輸出指標變數的”他值“,從執行結果來看,是“0X1”。 注意,你看,指標變數pInt的“他值”,與變數para的資料值是一樣的,你又領悟到了什麼?想不出來嗎?繼續看!

2.4.3 “他值”的程式碼寫法

你經常在程式碼中看到的那些個程式碼寫法,比如什麼*pInt寫法,是在表達什麼意思啊,其實就是在計算指標變數pInt的“他值”啊!

這些個寫法呢:*(pInt + 1)、*pInt + 1、pInt[1]?

*(pInt + 1):如果把pInt + 1 看成是另外一個指標,比如

int *pTemp = pInt + 1;

那麼*(pInt + 1)計算的本質上就是指標變數pTemp的“他值”;

*pInt + 1:這個就是用pInt的“他值”加1;

pInt[1]:這個呢?其實就是*(pInt + 1)。

2.4.4 示意圖

繼續完善上面的示意圖,這次加入“他值”的概念:

2.5 他型

2.5.1 “他型”的概念

“他型”,就是“他人的型別”的簡稱。在程式中,我們看到,宣告指標變數pInt時這樣寫的:

int *pInt = NULL;

指標變數pInt前面的” int "並不是說指標變數pInt的“己值”是一個int型別的資料值;而是說,指標變數pInt的“他值”是一個“ int ”型別的資料值,此處指標變數pInt的“他值”是變數para的資料值“0X1”,因此,指標變數pInt前面的“int”指的就是資料值“0X1”是一個“int”型。

總之一句話,宣告指標時的型別是用來修飾“他值”的,而不是“己值”。

你再看,在宣告變數para的時候:

int para = 1;

變數para前面的“int"就是指變數para的型別是一個整型,此時的”int“對para來說是一個”自型“,也就是”自己的型別“的意思,只有指標在宣告時的型別是”他型“,是“他人的型別”

既然”他型“是來修飾“他值”,那麼在宣告指標時還要加上這個“他型”有什麼意義呢?繼續看!

在程式中,如下程式碼片段:

int arr_int[2] = {1, 2};	
pInt = arr_int;
printf("arr_int第一個元素arr_int[0]的地址是:  0X%x\n", pInt);
printf("arr_int第二個元素arr_int[1]的地址是:  0X%x\n", pInt + 1);

我把一個整型陣列arr_int的地址賦給了指標變數pInt,那麼pInt的“己址”沒有變化,還是 0X22feb8,但是“己值”卻變了。

剛才指標變數pInt的“己值”還是“ 0X22feb4”,也就是變數para的地址,現在變成了“0X22feac”,這個可是陣列arr_int第一個元素的地址。也就是說,指標變數pInt的“己址”不會改變,但是“己值”是可以被改變的。

現在我們來看看“pInt”與“pInt + 1”的區別,這是在用pInt的“己值”在做運算。從執行結果來看,pInt的”己值“此時是”0X22feac“,而pInt + 1的”己值“是” 0X22feb0“,你發現了嗎,兩者正好相差4個位元組,而一個“int”型別的資料也正好佔用了4個位元組。

你可能會認為,既然pInt + 1是用“己值”加1,那麼應該是”0X22feac + 1” = “0X22fead”才對啊,為什麼不是這樣呢?這就是指標變數pInt的“他型”搞的鬼。

“他型”的意思,用大白話來說,就是:“我說 pInt 大兄弟啊,你的他值是個int型的資料值,你今後要是用你的己值 +1,+2,或者 -1,-2,可千萬別傻乎乎的就真的加1個位元組,加2個位元組, 或者就真的減1個位元組、減2個位元組。人家int型別佔4個位元組,你就得按照4個位元組為一個單位,去加 1* 4個位元組 、2 * 4個位元組,或者減去1 * 4個位元組、2 * 4個位元組,知道不?哦,順便說下,pInt + N的N,可以為正數也可以為負數”

當然啦,如果你的機器上“int”型資料佔8個位元組,那麼pInt + 1就是在“己值”上加8個位元組,pInt + 2就是在“己值”上加 8 *2 = 16個位元組,就這麼個意思。

我在程式中又舉了個例子來說明這個“他型”。程式如下:

double *pDouble = NULL;
double arr_double[2] = {1.1, 2.2};
pDouble = arr_double;
printf("arr_double第一個元素arr_double[0]的地址是:  0X%x\n", pDouble);
printf("arr_double第二個元素arr_double[1]的地址是:  0X%x\n", pDouble + 1);

這次宣告一個指標變數pDouble,它的“他型”是個“double”型,它的“己值”是陣列arr_double的地址,它的“他值”是陣列arr_double[0]這個元素的資料值“ 1.1 ”。在我的機器上,一個double型佔用8個位元組,那麼pDouble + 1就是用pDouble 的“己值”加 1 * 8個位元組,pDouble + 2就是用pDouble 的“己值” 加 2 * 8= 16個位元組,pDouble - 1就是pDouble 的“己值”減去 1 * 8個位元組,pDouble - 2就是pDouble 的“己值”減去 2 * 8 = 16個位元組,我滴個乖乖!朋友們可以對著執行結果自己計算對不對!

3. 總結

是時候來總結下了。

我宣告一個指標變數:

type *pType = NULL;

pType有5個維度,分別是:

pType = (己址,己值,他址,他值,他型);

3.1 己址:即“自己的地址”

指標變數pType作為一個變數,也有自己的地址,常見的程式碼寫法是“&pType ”。

己址在一般的程式中不會被頻繁地用到,如果要用的話,就涉及到“指標的指標”,這又是另外一個話題了,本文不討論;

3.2 己值:即“自己的資料值”

指標變數pType 作為一個變數,也有自己的資料值,程式碼的寫法是“pType ”。

也可以在己值上做加減法運算,常見的程式碼寫法有“pType + N”、“pType - N”、“pType2 - pType1”等。

3.3 他址:即“他人的地址”

指標變數pType的己值,意義除了表示自己的資料值外,還表示了與pType繫結在一起的“type”型別的變數的地址。一般而言,指標變數pType的“己值”與“他址”在資料值上是一樣的。

將一個type型別的變數與pType繫結在一起的常見方式是:pType = &變數;

3.4 他值:即“他人的資料值”

一旦type型別的變數與pType繫結在一起,指標變數pType可以通過一些程式碼寫法,來獲取type型別變數的值,也就是“他值”。常見的程式碼寫法有:“ *pType ”、“ pType-> ”等。

而這些程式碼的寫法:“ *(pType + N) ”、“ *(pType - N) ”、“ pType[N]”也是獲取的“他值”,不過需要特別說明一下:

pType + N 你可以看成是:

type *pTemp = pType + N;

“ *(pType + N) ”其實計算的就是指標變數pTemp的“他值”。

“ *(pType - N) ” 你就好理解了吧;

“ pType[N]”其實就是“ *(pType + N) ”,你就死記硬背吧。

3.5 他型:即“他人的型別”

宣告指標變數pType時,前面的“type”不是用來修飾pType 的“己值”的,而是用來修飾“他值”的,也就是說,“type”不是說pType的“己值”是一個type型別的資料值,而是指pType 的“他值”是一個type型別的資料值。

“他型”在程式碼中的作用,主要是計算“pType + N”、“pType - N”時,pType要加上或者減去 ( N * sizeof(type) )個位元組。

指標總是讓人暈暈的,很可能就是讓你暈在這5個維度裡的一個或者幾個上。把這5個維度好好理解透,指標啊,只是個紙老虎。

4. 習題講解

講完了5個維度,不來點上手習題怎麼行。下面列舉幾個習題,都是跟指標有關的,都是讓初學者暈的歇菜的。我用這5個維度來解讀這些題,你看看是不是要輕鬆一點!

4.1 陣列元素求和

4.1.1 程式

第一個例題是很常見的程式,就是求一個數組元素的和,程式如下:

#include <stdio.h>

int main(void)
{
	int *pArr = NULL;
	int sum = 0;
	int arr[3]= {1, 2, 3};
	pArr = arr;
	
	printf("陣列元素是:  ");
	for(int index = 0; index < 3; index++)
	{
		printf("%d ", pArr[index]);
	}
	
	printf("\n");
	
	for(int index = 0; index < 3; index++)
	{
		sum = sum + *(pArr + index);
	}
	
	printf("陣列元素和是:  %d\n", sum);
	
	return 0;
}

程式很簡單,先是輸出陣列的所有元素,然後計算出陣列所有元素的和。執行結果如下:

4.1.2 “兩己三他”的解讀

4.1.2.1 輸出陣列元素

在輸出陣列元素時,程式碼如下:

pArr = arr;
printf("%d ", pArr[index]);

這句程式碼等同於

pArr = arr;
printf("%d ", *( pArr + index));

這裡面使用了”己值“、“他型”做了加法運算,使用了”他值“獲取陣列元素。

”己值“:程式碼先是將陣列名 arr的資料值賦值給了pArr的“己值”。而陣列名arr的資料值是啥啊,是arr[0]元素的地址,對吧!那麼pArr的己值,也是arr[0]的地址,對吧!這樣一來,pArr就和arr[0]繫結起來了。

至於( pArr + index)的意思呢,你就看成有一個間接的、臨時的指標變數pTemp:

int *pTemp = pArr + index;

也就是說,pArr + index其實也是一個指標pTemp,只不過這個pTemp的己值是pArr的己值加上 index * sizeof(int) 個位元組數。

“他型”:pArr的他型是“int”型,pArr + index,是在pArr己值的基礎上,加多少個位元組呢?pArr的他型是int型,那麼pArr + index,是不是意味著pArr的己值加上 index * sizeof(int) 個位元組啊!

pArr:可以寫成pArr + 0,就是加上 0 * 4 = 0個位元組,此時pTemp的己值是arr[0]的地址;

pArr + 1:就是加上1 * 4 = 4 個位元組,此時pTemp的己值是arr[1]的地址;

pArr + 2:就是加上2 * 4 = 8 個位元組,此時pTemp的己值是arr[2]的地址;

這樣,pArr + index就遍歷到了陣列所有元素的地址了。

你會發現,pTemp的己值,一直在發生變化;pArr的己值和己址,一直未變。

“他值”:既然pArr + index能夠遍歷到陣列所有元素的地址,再使用 *(pArr + index) ,也就是*pTemp,是不是就能獲取到pTemp的他值了,這樣也就變數到陣列所有元素的值了!

4.1.2.2 陣列元素求和

求陣列元素的和時,使用的程式碼如下:

sum = sum + *(pArr + index);

根據剛才我對輸出陣列元素的分析,這句程式碼中pArr是怎麼玩的,大家也清楚了吧!

pArr + index依然是在用pArr的己值做加法運算,獲取到一個臨時指標pTemp的己值,這個pTemp的己值是每個陣列元素的地址;

再用 *(pArr + inedx),也就是*pTemp, 獲取臨時指標pTemp的他值,也就是每個元素的值。

最後將pTemp的每一個他值,疊加起來,算出陣列元素的和。

4.1.2.3 總結

大家試著用“兩己三他”的維度去理解pArr、pArr + index、*(pArr + index)、pArr[index]、*pArr + index等常見的程式碼寫法!

4.2 指標陣列

指標陣列是將指標與陣列結合起來的東東,對於初學者朋友而言,會比較難理解。指標陣列是一個比較大的話題,相關概念請參見一般的C語言教材,這裡只是用”兩己三他“的概念來解釋程式中的指標部分概念。

4.2.1 程式

指標陣列,我舉了一個例子如下:

#include <stdio.h>

int main(void)
{
	char *arr[3] = {"abc", "def", "ghi"};
	char *pArr = arr[0];
		
	printf("字串陣列arr的每個字串元素是:  ");
	for(int index = 0; index < 3; index++)
	{
		printf("%s ", arr[index]);
	}
	
	printf("\n");
	printf("字串陣列arr第一個字串的每個元素是:  ");
	for(int index = 0; index < 3; index++)
	{
		printf("%c ", *(pArr + index) );
	}
	
	printf("\n");
	
	return 0;
}

執行結果如下:

4.2.2 ”兩己三他“的解讀

4.2.2.1 輸出所有的字串

先看指標陣列的定義:

char *arr[3] = {"abc", "def", "ghi"};

你看到的這個陣列的每個元素好像是一個字串,其實本質上是這樣的:

char *pChar1 = "abc", *pChar2 = "def", *pChar3 = "ghi";
char *arr[3] = {pChar1, pChar2, pChar3};

陣列arr的每個元素其實是一個“他型”是“char”的指標。

arr[0]就是pChar1這個指標,那麼pChar1的己值或者他址是啥,當然是字串”abc“的字元‘a'的地址,那麼:

printf("%s ", arr[0]);

本質上是:

printf("%s ", pChar1);

使用pChar1的己值或者他址,從字元'a'的地址開始,一個一個地輸出後面的'b'和'c'。

對於pChar2和pChar3也是一樣地理解。

4.2.2.2 輸出第一個字串“abc”的每個字元

程式碼如下:

char *pArr = arr[0];
printf("%c ", *(pArr + index) );

將arr[0],也就是pChar1的己值給了pArr的己值,那麼pArr的己值和他址都是字元'a'的地址。

pArr + index 是在pArr的己值上,加上 index * sizeof(char) 個位元組,給了一個臨時指標變數pTemp:

char *pTemp = pArr + index;

這個指標pTemp的己值或者他址,會依次為字元'a', 'b', 'c'的地址,也就是pTemp的他值也會依次為字元'a', 'b','c',這樣指標pTemp就會依次遍歷到字串“abc”的每一個字元了。

4.3 連結串列

連結串列是使用指標最為頻繁的,什麼插入節點、刪除節點等,都會遇到如下的程式碼寫法:

p2 = p1->next;
p1->next = p3->next;
...... 

這TM什麼玩意兒,暈的一腿啊!這個next指標,那個next指標,跳來跳去的,我了個去,頭腦已經充滿漿糊了。呵呵,指標5維度分析法來了!不過,關於連結串列,我覺得還是另外開闢一個文章講吧。等把連結串列講完,回頭再講這些個next指標,我跟你講,連結串列的本質也就那樣,你懂了之後,連結串列比指標還要紙老虎。