天天看點

由淺至深->C語言中指針及數組的經典問題分析(二)

文章向導

深入探索指針與數組

數組指針與指針數組

一、深入探索指針與數組

1.指針的運算

     ~~~~          ~~~    指針是一種特殊的變量,在程式設計工作中往往會用到:指針與整數進行運算,以及指針間的運算和比較。接下來逐個分析這幾項問題。

1) 指針與整數進行運算

     ~~~~          ~~~    上式為指針與整數的運算規則,其中(unsigned int)p代表指針在系統内部的位址,而n*sizeof(*p)則代表增加或減少的位元組數。

2) 指針與指針之間的運算

     ~~~~          ~~~    首先必須明确的是指針之間隻支援減法運算,且參與運算的指針類型必須相同。另外,隻有當兩個指針都指向同一數組中的元素時,指針相減才有意義,其意義為指針所指元素的下标內插補點。具體的運算公式如下: ( P 1 , P 2 為 兩 個 給 定 指 向 的 指 針 ) 。 (P_1,P_2為兩個給定指向的指針)。 (P1​,P2​為兩個給定指向的指針)。

P 1    −    P 2    ⇔    ( ( u n s i g n e d    i n t ) P 1    −    ( u n s i g n e d    i n t ) P 2 ) / s i z e o f ( p o i n t e r _ t y p e ) P_1\,\,-\,\,P_2\,\,\Leftrightarrow \,\,\left( \left( unsigned\,\,int \right) P_1\,\,-\,\,\left( unsigned\,\,int \right) P_2 \right) /sizeof\left( pointer\_type \right) P1​−P2​⇔((unsignedint)P1​−(unsignedint)P2​)/sizeof(pointer_type)

  看到這兒,某些讀者可能回想如果我非要進行乘除運算或者用不同類型間的指針參與運算,那會發生什麼?動手試驗一下便知,下面已經列出了一部分可能會操作到的指針運算。

#include <stdio.h>
int main()
{
	char s1[] = {'H', 'e', 'l', 'l', 'o'};
	int i = 0;
	char s2[] = {'W', 'o', 'r', 'l', 'd'};
	char* p0 = s1;
	char* p1 = &s1[3];
	char* p2 = s2;
	int* p = &i;
	printf("%d\n", p0 - p1); //元素下标差,故結果為-3
	printf("%d\n", p0 + p2); //error,指針之間隻支援減法運算
	printf("%d\n", p0 - p2); //無意義
	printf("%d\n", p0 - p); //error,指針類型不同
	printf("%d\n", p0 * p2); //error
	printf("%d\n", p0 / p2); //error
	
	return 0;
}
           

3) 指針的比較

     ~~~~          ~~~    指針的比較,即對指針使用關系運算符(<, > ,== 等)。此時一般是将指針應用于某種特定的場合中,比如指針周遊數組。下面已經給出了關系運算符在指針使用中的一個經典例子:

#include <stdio.h>
#define DIM(a) (sizeof(a) / sizeof(*a))
int main()
{
	char s[] = {'H', 'e', 'l', 'l', 'o'};
	char* pBegin = s;
	/* Key point:指向數組 s[4]的後一個位置,仍有意義,pEnd 相當于指向 &a+1;其中&a 為數組的位址,訓示長度為整個數組。*/
	char* pEnd = s + DIM(s); 
	char* p = NULL;
	
	printf("pBegin = %p\n", pBegin);
	printf("pEnd = %p\n", pEnd);
	printf("Size: %d\n", pEnd - pBegin); //5
	
	/*指針周遊數組*/
	for (p=pBegin; p<pEnd; p++) {
	    printf("%c", *p); //Hello
	}
	printf("\n");
	return 0;
}
           

//程式結果:

pBegin = 0x7ffc273ffc30

pEnd = 0x7ffc273ffc35

Size: 5

Hello

2. 數組的兩種通路方式

     ~~~~          ~~~    在使用數組時我們經常會碰到兩種通路數組的方式:1)數組下标形式;2)指針形式。兩種通路形式可以互相轉化,但效率上後者優于前者。

a [ n ] ⇔ ∗ ( a + n ) ⇔ ∗ ( n + a ) ⇔ n [ a ] a\left[ n \right] \Leftrightarrow *\left( a+n \right) \Leftrightarrow *\left( n+a \right) \Leftrightarrow n\left[ a \right] a[n]⇔∗(a+n)⇔∗(n+a)⇔n[a]

  上式即為兩種方式的等價形式,了解上可能會有些抽象,是以還是根據一個實際的例子來繼續加深了解吧。

#include <stdio.h>
int main()
{
	int a[5] = {0};
	int* p = a; 
	int i = 0;

	for (i=0; i<5; i++) {
		p[i] = i + 1; //
	}
	for (i=0; i<5; i++) {
		printf("a[%d] = %d\n", i, *(a + i)); //輸出數組a首次改變後的值
	}
	printf("\n");

	for (i=0; i<5; i++) {
		i[a] = i + 10; // 《==》 a[i] = i +10
	}
	for (i=0; i<5; i++) {
		printf("p[%d] = %d\n", i, p[i]); //輸出數組a第二次改變後的值
	}
	return 0;
}
           

3.深入探索數組名與指針的關系

 

  數組名可視為常量指針(即指針的值(存放在指針中的那個位址)不可修改,但位址處所存儲的内容可以修改)。

  容易與其相混淆的一個概念則為指針常量(指向常量的指針)(即位址處所存儲的内容為常量不可修改,但指針的指向卻可以修改)。

  雖然數組名可視為常量指針,可本質上數組與指針是兩個不同的事物。現通過下面一個例子,來細化兩者的差别:

/*main.c*/
#include <stdio.h>

extern int print();
int main()
{
	extern int *a; /*數組名, 指針?*/
	
	printf("In main: &a = %p\n", &a); //0x601040
	print(); //0x601040
	
	printf("a = %p\n", a); /* 取出的内容與32機或64位機有關 */
	printf("*a = %d\n", *a); /*segment fault, 通路低位址空間*/
	
	return 0;
}

/*extern.c*/
#include <stdio.h>

int a[] = {1, 2, 3, 4, 5};

int print()
{
	printf("In extern: &a = %p\n", &a);

	return 0;
}
           

     ~~~~          ~~~    從上面的例子中可看出,在main.c中外部聲明了一指針變量a,而a為在extern.c檔案中定義的數組名,可視為常量指針。聽起來有點繞,好像邏輯上也存在一點問題,但我們可以先來觀察下程式運作的具體表現。

  

測試結果:

由淺至深->C語言中指針及數組的經典問題分析(二)

  首先,前兩條列印語句的結果是在意料之中的,但後兩條的列印結果卻讓人匪夷所思,不僅結果奇奇怪怪而且程式竟然還出現了段錯誤。現在我們把視角集中于printf(“a = %p\n”, a); 這條語句,該語句表示按照指針的方式來解析a,而指針的位址在32位機上占用4個位元組,在64位機上則占用8個位元組(筆者測試環境為64為機)。

  

  于是,編譯器按照轉換說明符%p的意義,取出a存儲的數值(位址為8個位元組,就去數組a中取出8個位元組的内容作為位址)作為列印結果,具體可以按照下圖了解:

由淺至深->C語言中指針及數組的經典問題分析(二)

  由上圖可知,取出來的前8個位元組排列後正好就與第三條列印語句的結果相吻合。另外,在分析取出的位元組資料時應注意系統的大小端問題不清楚的讀者點此。

  顯然,圖中呈現的是小端系統,而筆者測試的linux系統也為小端系統。為了說明,數組中的資料在記憶體中真的就是如此排列的,讀者準備了如下的小case來說服哪些依然保持懷疑态度的讀者。

由淺至深->C語言中指針及數組的經典問題分析(二)

  最後,來談談printf("*a = %d\n", a); 語句為何産生段錯誤。既然數組a存放的是低位址,那麼a則表示試圖通路低位址空間的内容,一般在作業系統中這是不允許的,自然也就是産生了段錯誤。

二、數組指針與指針數組

1.數組類型與數組指針

1)重命名數組類型

     ~~~~          ~~~    當使用一個數組比如int array[10]的時候,有時會考慮到數組的類型是什麼,數組的類型由元素類型和數組大小共同決定,即僅需去掉數組名則可到數組類型為int[10]。

  使用數組類型的目的是為了友善定義數組指針,首先我們可以通過typedef關鍵字來為數組類型重命名,如typedef int(AINT10)[10]; 這樣AINT10就是數組類型int[10]的别名。進而可通過别名AINT10定義任意符合該類型的數組。

2)數組指針(用于指向一個數組)

  

  使用數組類型定義數組指針的标準格式為:ArrayType* pointer,當然也可以采用直接定義的方式:type(*pointer)[n],其中pointer為指針變量,type為指向的數組元素的類型,n為指向的數組的大小。

  下面的執行個體完整地示範了數組類型的用法,以及數組指針使用時的一些特性:

#include<stdio.h>

/*數組類型*/
typedef int(AINT5)[5];
typedef float(AFLOAT10)[10];
typedef char(ACHAR9)[9];

int main()
{
	AINT5 a1; //等價于 int a1[5]
	float fArray[10];
	AFLOAT10 *pf = &fArray; //使用數組類型來定義數組指針,等價于 float(*pf)[10]
	ACHAR9 cArray; //等價于 char cArray[9]
	char(*pc)[9] = &cArray; //直接定義數組指針
	char(*pcw)[4] = cArray; //warning: 指針類型指派不比對
	int i = 0;

	printf("sizeof(AINT5) = %d, sizeof(a1) = %d\n", sizeof(AINT5), sizeof(a1)); //20 20

	for (i=0; i<10; i++) {
		(*pf)[i] = i; //等價于 fArray[i] = i;
	}

	for (i=0; i<10; i++) {
		printf("%f\n",fArray[i]);
	}

	//pc+1 = (unsigned int)pc + sizeof(*pc) = (unsigned int)pc + 9
	//pcw+1 = (unsigned int)pcw + sizeof(*pc) = (unsigned int)pc + 4
	printf("%p, %p, %p\n", &cArray, pc+1, pcw+1);
	
	return 0;
}
           

程式運作結果:

由淺至深->C語言中指針及數組的經典問題分析(二)

 

2.指針數組(本質為數組)  

  

  指針數組其實就隻是一個普通的數組,隻不過其中的每一個元素都為指針。形如type* pArray[n]則為指針數組的定義,其中type*為數組中每個元素的類型,pArray為數組名,n為數組大小。簡單來說,數組包含了n個指針變量p[0],p[1],…,p[n-1]。

  知道了指針數組的定義後,接下來則是考慮其實際的用途。

#include<stdio.h>
#include<string.h>

#define DIM(a) (sizeof(a)/sizeof(*a))

int lookup_keyword(const char* key, const char* table[], const int size)
{
	int ret = -1;
	int i = 0;
	for (i=0; i<size; i++) {
		if (strcmp(key, table[i]) == 0) {
			ret = i;
			break;
		}
	}
	return ret;
}

int main()
{
	const char* keyword[] = { //數組中每個元素的類型為 const char*
	"do",
	"for",
	"if",
	"register",
	"return",
	"switch",
	"while",
	"case",
	"static"
	};
	printf("%d\n", lookup_keyword("register", keyword, DIM(keyword))); //3
	printf("%d\n", lookup_keyword("main", keyword, DIM(keyword))); //-1

	return 0;
}
           

     ~~~~          ~~~    上述程式中定義的指針數組,其中每個元素都為字元串(char*指針)。然後利用這一特性,則可使用諸如strcmp之類的字元串系列函數。

參閱資料

狄泰軟體學院—C進階剖析教程

C primer plus

高品質嵌入式Linux C程式設計

你必須知道的495個C語言問題

繼續閱讀