Skip to content

多级指针与指针数组:int**、指针数组 vs 数组指针、argv

引言:指针之上再叠指针

第 1 章结尾我们埋了一句「指针自己也是变量、也有地址,所以可以有指向指针的指针」,这一章就来兑现它,并顺带啃下 C 里最容易让人看花眼的两类声明——「指针数组」和「数组指针」(这俩长得很像、含义相反)。多级指针听起来吓人,但它本质还是第 1 章那条「指针装地址」的规则在套娃:一个 int** 装的是「一个 int* 的地址」。这一章我们把它落到三个实处:指针数组(一堆字符串常用它)、数组指针(遍历二维数组的行用第 10 章提过的 int(*)[5])、以及每个 C 程序的入口 main(int argc, char** argv)

指针的指针 int**

第 1 章我们真跑过:int x = 42; int* p = &x; int** pp = &p;——p 指向 xpp 指向 ppp 是「指向 int* 的指针」)。顺着它走:*pp 得到 p(一个 int*),**pp 再解一层得到 xint)。理解它的钥匙就一句:int** 装的是「一个 int* 的地址」,解一层得到 int*、再解一层得到 int——没有新规则,只是 * 多叠了一层。int***int**** 同理(虽然超过两层在工程里几乎一定是设计错误、该用结构体了)。int** 真正常见的实际用途有两个:一个是下面的「指针数组传给函数时退化成的类型」,另一个是「函数里 malloc 一块内存、通过 int** 参数把新指针写回调用者」(第 3 章那种「输出参数」,只不过这次输出的是个指针)。

指针数组:每个元素都是一个指针

「指针数组」是「一个数组,它的每个元素都是指针」。最经典的场景是「一堆字符串」——每个字符串是一个 char*,把它们放进一个数组:

c
#include <stdio.h>

int main(void) {
    /* 指针数组:每个元素是一个 const char*(各指向一个字符串字面量) */
    const char* colors[] = {"red", "green", "blue"};
    for (int i = 0; i < 3; i++) {
        printf("colors[%d] = %s\n", i, colors[i]);
    }
    return 0;
}
text
$ gcc -std=c11 -Wall ptr_array.c -o pa && ./pa
colors[0] = red
colors[1] = green
colors[2] = blue

const char* colors[] 声明了一个数组,里面每个元素是 const char*(指向只读 char 的指针)。三个字符串字面量 "red"/"green"/"blue" 各自落在只读存储区(第 11 章),数组 colors 存的是「指向它们首字符的三个指针」。colors[i] 拿出第 i 个指针、%s 打印它指向的字符串,得 red/green/blue。注意这和「二维 char 数组」char colors[][6] = {"red", ...} 不一样——二维 char 数组是「一整块连续内存、每行固定 6 字节」;指针数组则是「散落在只读区的字符串 + 一个装它们地址的小数组」,省空间(短串不占固定行长)、但多一次跳转(先取指针、再去只读区读字符)。处理「一组长度不等的字符串」(命令行参数、菜单选项、错误信息表),指针数组是标准做法。

数组指针:指向「一个数组」的指针

*[] 的位置换一下、加个括号,就变成完全不同的东西——「数组指针」是「一个指针,它指向一个数组」。它正是第 10 章二维数组那里提过的「行指针」:

c
#include <stdio.h>

int main(void) {
    int m[3][5] = {{1, 2, 3, 4, 5}, {6, 7, 8, 9, 10}, {11, 12, 13, 14, 15}};
    /* row 是「指向 5 个 int 的数组」的指针(数组指针),指向 m 的第 0 行 */
    int (*row)[5] = m;
    printf("第 0 行 (*row)[0] = %d\n", (*row)[0]); /* 1 */
    row++; /* row+1 跨过一整行(5 个 int),到第 1 行 */
    printf("第 1 行 (*row)[0] = %d\n", (*row)[0]); /* 6 */
    return 0;
}
text
$ gcc -std=c11 -Wall array_ptr.c -o ap && ./ap
第 0 行 (*row)[0] = 1
第 1 行 (*row)[0] = 6

int (*row)[5](注意括号!)声明 row 是「指向『5 个 int 的数组』的指针」。mint m[3][5],按第 10 章「数组的数组」、m 退化成「指向第 0 行的指针」,类型正好是 int(*)[5],所以 row = m 合法。(*row)[0] 是「row 指向的那一行里的第 0 个」= 1;关键是 row++——指针加 1 的步长由「指向类型」决定(第 2 章),row 指向的是「5 个 int 的数组」(20 字节),所以 row++ 跨一整行到第 1 行,于是再 (*row)[0]6。这就是用「行指针」遍历二维数组的方式,第 10 章说的「二维数组传进函数时退化成行指针 int(*)[5]」就是它。

现在把「指针数组」和「数组指针」并排看,这是 C 声明里最经典的易混淆点:

int*  a[5];     /* 指针数组:a 是「5 个 int*」的数组 */
int (*a)[5];    /* 数组指针:a 是「指向 5 个 int 的数组」的指针 */

差别全在那个括号。规则是「[] 的优先级高于 *」:int* a[5]a 先和 [5] 结合(a 是数组)、元素类型是 int*(指针数组);int (*a)[5] 里括号让 a 先和 * 结合(a 是指针)、指向的是 int[5](数组指针)。读这种声明的系统方法叫「右左法则」(螺旋法则),第 10 章会专门讲、这里先记住「*[] 谁先结合」这个关键。

命令行参数:argc 与 argv

每个 C 程序的入口 main 都可以接收命令行参数,签名是 int main(int argc, char** argv)(§5.1.2.2.1):

c
#include <stdio.h>

int main(int argc, char** argv) { /* argv 是「指向 char* 的指针」=字符串数组 */
    printf("argc = %d\n", argc);
    for (int i = 0; i < argc; i++) {
        printf("argv[%d] = %s\n", i, argv[i]);
    }
    return 0;
}
text
$ gcc -std=c11 -Wall argv.c -o av && ./av hello world
argc = 3
argv[0] = ./av
argv[1] = hello
argv[2] = world

argc(argument count)是「命令行参数的个数」、argv(argument vector)是「参数字符串数组」。运行 ./av hello world 时,argc = 3(程序名本身算一个 + 两个参数),argv[0] 是程序名 "./av"argv[1]"hello"argv[2]"world"argv 的类型 char** 就是上面「指针数组退化」的结果——它等价于 char* argv[](函数参数里的数组声明都退化成指针,第 10 章),即「指向 char* 的指针」;argv[i] 拿出第 ichar*(一个字符串),%s 打印。按约定 argv[argc]NULL(数组末尾的空指针哨兵),所以遍历参数除了 for (i=0; i<argc; i++) 还可以写 for (char** p = argv; *p != NULL; p++)(指针遍历指针数组,第 5 章那套)。命令行参数是 int**/指针数组最贴近日常的实战场景——解析选项、读文件名、过滤参数都从这里开始。

小结

指针自己也是变量、也有地址(第 1 章),于是可以叠出多级指针:int** 装的是「一个 int* 的地址」,解一层得 int*、再解一层得 int(第 1 章真跑 **ppx),超过两层几乎一定是设计错误。指针数组 int* a[5][] 优先级高于 *a 是「5 个 int*」的数组)的典型场景是一堆字符串:const char* colors[] = {"red","green","blue"}(真跑打印三个颜色),每个元素是指针、指向散落在只读区的字符串字面量,省空间、多一次跳转。数组指针 int (*a)[5](括号让 * 先结合,a 是「指向 5 个 int 的数组」的指针)是第 10 章二维数组的「行指针」:int (*row)[5] = m; row++; 真跑跨一整行(步长 = 20 字节 = 5 个 int),(*row)[0] 从第 0 行的 1 跳到第 1 行的 6。两者区别全在「*[] 谁先结合」,系统读法是右左法则(第 10 章详讲)。命令行参数 int main(int argc, char** argv)(§5.1.2.2.1):argc 是参数个数(含程序名)、argvchar**(指针数组退化,等价 char* argv[]),argv[0] 是程序名、argv[argc] 按约定是 NULL 哨兵,真跑 ./av hello worldargc=3argv[0..2] = ./av/hello/world。下一章我们看另一类「指向函数」的指针——函数指针。

参考资源

  • ISO/IEC 9899:2011 §6.2.5(派生类型:指针的指针)、§6.7.6.1/§6.7.6.2(声明符:指针数组 vs 数组指针的语法)、§5.1.2.2.1(mainargc/argv 约定、argv[argc] 为空指针)
  • K. N. King《C Programming: A Modern Approach》第 12 章(指针与多维数组、行指针 int(*)[N])、第 13 章(字符串数组、命令行参数 argv
  • Robert C. Seacord《Effective C》第 6 章(指针数组、argv 处理约定)
  • Brian W. Kernighan & Dennis M. Ritchie《The C Programming Language》第 5 章(指针数组、多维数组与指针、argc/argv
  • 阶段2·第1/2章(指针、算术)、第 10 章:数组(多维 m[3][5]、行指针退化)、第 11 章:C 字符串(字面量 char*
  • 阶段2·第9章:函数指针、第10章:复杂声明与 typedef(右左法则读懂 int (*a)[5] 这类声明)