6. 指向指針的指針與指針數組

指針可以指向基本類型,也可以指向復合類型,因此也可以指向另外一個指針變數,稱為指向指針的指針。

int i;
int *pi = &i;
int **ppi = π

這樣定義之後,表達式*ppipi的值,表達式**ppii的值。請讀者自己畫圖理解ipippi這三個變數之間的關係。

很自然地,也可以定義指向“指向指針的指針”的指針,但是很少用到:

int ***p;

數組中的每個元素可以是基本類型,也可以復合類型,因此也可以是指針類型。例如定義一個數組a由10個元素組成,每個元素都是int *指針:

int *a[10];

這稱為指針數組。int *a[10];int **pa;之間的關係類似於int a[10];int *pa;之間的關係:a是由一種元素組成的數組,pa則是指向這種元素的指針。所以,如果pa指向a的首元素:

int *a[10];
int **pa = &a[0];

pa[0]a[0]取的是同一個元素,唯一比原來複雜的地方在於這個元素是一個int *指針,而不是基本類型。

我們知道main函數的標準原型應該是int main(int argc, char *argv[]);argc是命令行參數的個數。而argv是一個指向指針的指針,為什麼不是指針數組呢?因為前面講過,函數原型中的[]表示指針而不表示數組,等價于char **argv。那為什麼要寫成char *argv[]而不寫成char **argv呢?這樣寫給讀代碼的人提供了有用信息,argv不是指向單個指針,而是指向一個指針數組的首元素。數組中每個元素都是char *指針,指向一個命令行參數字元串。

例 23.2. 打印命令行參數

#include <stdio.h>

int main(int argc, char *argv[])
{
	int i;
	for(i = 0; i < argc; i++)
		printf("argv[%d]=%s\n", i, argv[i]);
	return 0;
}

編譯執行:

$ gcc main.c
$ ./a.out a b c
argv[0]=./a.out
argv[1]=a
argv[2]=b
argv[3]=c
$ ln -s a.out printargv
$ ./printargv d e 
argv[0]=./printargv
argv[1]=d
argv[2]=e

注意程序名也算一個命令行參數,所以執行./a.out a b c這個命令時,argc是4,argv如下圖所示:

圖 23.4. argv指針數組

argv指針數組

由於argv[4]NULL,我們也可以這樣循環遍歷argv

for(i=0; argv[i] != NULL; i++)

NULL標識着argv的結尾,這個循環碰到NULL就結束,因而不會訪問越界,這種用法很形象地稱為SentinelNULL就像一個哨兵守衛着數組的邊界。

在這個例子中我們還看到,如果給程序建立符號連結,然後通過符號連結運行這個程序,就可以得到不同的argv[0]。通常,程序會根據不同的命令行參數做不同的事情,例如ls -lls -R打印不同的檔案列表,而有些程序會根據不同的argv[0]做不同的事情,例如專門針對嵌入式系統的開源項目Busybox,將各種Linux命令裁剪後集於一身,編譯成一個執行檔busybox,安裝時將busybox程序拷到嵌入式系統的/bin目錄下,同時在/bin/sbin/usr/bin/usr/sbin等目錄下創建很多指向/bin/busybox的符號連結,命名為cplsmvifconfig等等,不管執行哪個命令其實最終都是在執行/bin/busybox,它會根據argv[0]來區分不同的命令。

習題

1、想想以下定義中的const分別起什麼作用?編寫程序驗證你的猜測。

const char **p;
char *const *p;
char **const p;