
1 引言考虑这样一个问题我们需要编写一个函数修改传入的指针本身而不仅仅是指针指向的内容。比如让指针指向一个新分配的内存块c#include stdio.h #include stdlib.h /* 错误试图修改指针本身但只是修改了副本 */ void allocate_bad(int *p, int size) { p (int*)malloc(size * sizeof(int)); /* 只修改了局部指针 */ } /* 正确使用二级指针可以修改调用者的指针 */ void allocate_good(int **p, int size) { *p (int*)malloc(size * sizeof(int)); /* 修改调用者的指针 */ } int main(void) { int *arr NULL; allocate_bad(arr, 10); if (arr NULL) { printf(arr 仍然是 NULL\n); } allocate_good(arr, 10); if (arr ! NULL) { printf(arr 现在指向分配的内存\n); free(arr); } return 0; }这就是二级指针的典型应用场景。本章我们将深入探讨这类问题。2 二级指针指向指针的指针2.1 基本概念二级指针Pointer to Pointer是指针的指针它存储的是另一个指针变量的地址。cint a 10; /* 普通变量 */ int *p a; /* 一级指针指向 int */ int **pp p; /* 二级指针指向 int* */内存示意图text变量 a: [10] 地址 0x1000 指针 p: [0x1000] 地址 0x2000 (存储 a 的地址) 指针 pp: [0x2000] 地址 0x3000 (存储 p 的地址)2.2 定义与使用c#include stdio.h int main(void) { int a 10; int *p a; /* 一级指针 */ int **pp p; /* 二级指针 */ printf(a %d\n, a); printf(*p %d\n, *p); /* 通过一级指针访问 a */ printf(**pp %d\n, **pp); /* 通过二级指针访问 a */ /* 修改值 */ **pp 20; /* 修改 a 的值 */ printf(a %d\n, a); return 0; }2.3 多级指针理论上可以有多级指针但实际很少用到三级以上cint a 10; int *p a; /* 一级 */ int **pp p; /* 二级 */ int ***ppp pp; /* 三级 */ printf(%d\n, ***ppp); /* 输出 10 */2.4 二级指针的典型应用2.4.1 修改调用者的指针c#include stdio.h #include stdlib.h #include string.h /* 为字符串分配内存并拷贝 */ void create_string(char **str, const char *content) { *str (char*)malloc(strlen(content) 1); if (*str ! NULL) { strcpy(*str, content); } } /* 释放字符串并置空 */ void destroy_string(char **str) { if (*str ! NULL) { free(*str); *str NULL; /* 置空调用者的指针 */ } } int main(void) { char *name NULL; create_string(name, Alice); if (name ! NULL) { printf(name %s\n, name); destroy_string(name); } /* name 现在是 NULL安全 */ return 0; }2.4.2 二维数组的动态分配c#include stdio.h #include stdlib.h /* 动态分配二维数组每行长度可以不同 */ int** create_matrix(int rows, int cols) { int **matrix (int**)malloc(rows * sizeof(int*)); if (matrix NULL) return NULL; for (int i 0; i rows; i) { matrix[i] (int*)malloc(cols * sizeof(int)); if (matrix[i] NULL) { /* 分配失败释放已分配的行 */ for (int j 0; j i; j) { free(matrix[j]); } free(matrix); return NULL; } } return matrix; } /* 释放矩阵 */ void free_matrix(int **matrix, int rows) { if (matrix NULL) return; for (int i 0; i rows; i) { free(matrix[i]); } free(matrix); } int main(void) { int rows 3, cols 4; int **mat create_matrix(rows, cols); if (mat ! NULL) { /* 使用矩阵 */ for (int i 0; i rows; i) { for (int j 0; j cols; j) { mat[i][j] i * cols j; } } /* 打印 */ for (int i 0; i rows; i) { for (int j 0; j cols; j) { printf(%2d , mat[i][j]); } printf(\n); } free_matrix(mat, rows); } return 0; }3 指针数组与数组指针这两个概念名称相似但含义完全不同非常容易混淆。3.1 指针数组指针数组一个数组其元素都是指针。cint *arr[5]; /* arr 是一个数组包含5个 int* 元素 */c#include stdio.h int main(void) { int a 10, b 20, c 30; int *arr[3]; /* 指针数组可以存放3个 int* */ arr[0] a; arr[1] b; arr[2] c; for (int i 0; i 3; i) { printf(%d , *arr[i]); /* 输出 10 20 30 */ } printf(\n); return 0; }内存布局textarr[0]: [地址指向 a] arr[1]: [地址指向 b] arr[2]: [地址指向 c]3.2 数组指针数组指针一个指针指向整个数组。cint (*p)[5]; /* p 是一个指针指向包含5个 int 的数组 */c#include stdio.h int main(void) { int arr[5] {1, 2, 3, 4, 5}; int (*p)[5] arr; /* p 指向整个数组 */ for (int i 0; i 5; i) { printf(%d , (*p)[i]); /* 通过指针访问数组元素 */ } printf(\n); printf(arr 大小: %zu\n, sizeof(arr)); /* 20 */ printf(p 大小: %zu\n, sizeof(p)); /* 8指针大小 */ printf(*p 大小: %zu\n, sizeof(*p)); /* 20指向的数组大小 */ return 0; }内存布局textp: [地址指向整个数组] ↓ arr: [1][2][3][4][5]3.3 辨析对比写法名称含义大小示例64位int *p[5]指针数组数组有5个int*元素数组大小5×840int (*p)[5]数组指针指针指向有5个int的数组指针大小8记忆技巧[]优先级高于*所以int *p[5]先形成数组括号改变优先级(*p)表示 p 是指针然后指向数组c/* 复杂声明解析 */ int *arr[5]; /* arr 是数组元素是 int* */ int (*ptr)[5]; /* ptr 是指针指向 int[5] 数组 */ int *func(); /* func 是函数返回 int* */ int (*fp)(); /* fp 是函数指针 */4 命令行参数 argv 的实质4.1 argv 的类型main函数的第二个参数argv就是一个经典的指针数组cint main(int argc, char *argv[])argc命令行参数的个数argv一个指针数组每个元素指向一个命令行参数字符串4.2 内存布局假设程序这样运行bash./program hello world 123内存中的结构textargv 本身是一个指针数组在栈上 argv[0] → ./program\0 (程序名) argv[1] → hello\0 argv[2] → world\0 argv[3] → 123\0 argv[4] → NULL (结束标志)4.3 等价的表示方式argv也可以写成char **argvcint main(int argc, char **argv) /* 完全等价 */这清楚地表明argv是一个二级指针它指向指针数组的第一个元素。4.4 遍历命令行参数c#include stdio.h int main(int argc, char *argv[]) { printf(参数个数%d\n, argc); /* 方式1用下标访问 */ for (int i 0; i argc; i) { printf(argv[%d] %s\n, i, argv[i]); } /* 方式2用指针遍历 */ char **p argv; while (*p ! NULL) { printf(%s , *p); p; } printf(\n); /* 方式3分析选项 */ for (int i 1; i argc; i) { /* 从1开始跳过程序名 */ if (argv[i][0] -) { printf(发现选项%s\n, argv[i]); } } return 0; }4.5 修改命令行参数理论上可以修改argv指向的字符串但通常不推荐cint main(int argc, char *argv[]) { /* 可以修改 argv 指向的字符串内容但要注意长度 */ if (argc 1) { argv[1][0] H; /* 修改第一个参数字符串 */ } /* 也可以修改 argv 指针数组本身 */ argv[1] modified; /* 指向新的字符串常量 */ return 0; }但这样做容易引起混乱一般只在特定场景下使用。5 综合示例5.1 字符串排序使用指针数组c#include stdio.h #include string.h #include stdlib.h /* 对指针数组中的字符串进行排序 */ void sort_strings(char *strings[], int n) { for (int i 0; i n - 1; i) { for (int j i 1; j n; j) { if (strcmp(strings[i], strings[j]) 0) { /* 交换指针而不是交换字符串内容 */ char *temp strings[i]; strings[i] strings[j]; strings[j] temp; } } } } int main(void) { /* 指针数组指向字符串常量 */ char *fruits[] { banana, apple, orange, grape, pear }; int n sizeof(fruits) / sizeof(fruits[0]); printf(排序前); for (int i 0; i n; i) { printf(%s , fruits[i]); } printf(\n); sort_strings(fruits, n); printf(排序后); for (int i 0; i n; i) { printf(%s , fruits[i]); } printf(\n); return 0; }5.2 动态字符串数组c#include stdio.h #include stdlib.h #include string.h typedef struct { char **data; /* 二级指针指向字符串数组 */ int size; int capacity; } StringArray; /* 初始化 */ void str_array_init(StringArray *arr, int capacity) { arr-data (char**)malloc(capacity * sizeof(char*)); arr-size 0; arr-capacity arr-data ? capacity : 0; } /* 添加字符串 */ int str_array_add(StringArray *arr, const char *str) { if (arr-size arr-capacity) { int new_cap arr-capacity 0 ? 4 : arr-capacity * 2; char **new_data (char**)realloc(arr-data, new_cap * sizeof(char*)); if (new_data NULL) return -1; arr-data new_data; arr-capacity new_cap; } /* 为字符串分配内存并拷贝 */ arr-data[arr-size] (char*)malloc(strlen(str) 1); if (arr-data[arr-size] NULL) return -1; strcpy(arr-data[arr-size], str); arr-size; return 0; } /* 释放所有内存 */ void str_array_destroy(StringArray *arr) { for (int i 0; i arr-size; i) { free(arr-data[i]); /* 释放每个字符串 */ } free(arr-data); /* 释放指针数组 */ arr-data NULL; arr-size arr-capacity 0; } int main(void) { StringArray arr; str_array_init(arr, 2); str_array_add(arr, Hello); str_array_add(arr, World); str_array_add(arr, C Language); for (int i 0; i arr.size; i) { printf(%s\n, arr.data[i]); } str_array_destroy(arr); return 0; }5.3 简单命令行解析器c#include stdio.h #include string.h typedef struct { const char *name; const char *value; } Option; int parse_options(int argc, char *argv[], Option opts[], int max_opts) { int opt_count 0; for (int i 1; i argc; i) { if (argv[i][0] -) { /* 发现选项 */ if (opt_count max_opts) { opts[opt_count].name argv[i] 1; /* 跳过 - */ /* 检查是否有参数值下一个参数不以 - 开头 */ if (i 1 argc argv[i 1][0] ! -) { opts[opt_count].value argv[i 1]; i; /* 跳过参数值 */ } else { opts[opt_count].value NULL; } opt_count; } } } return opt_count; } int main(int argc, char *argv[]) { Option options[10]; int n parse_options(argc, argv, options, 10); printf(发现 %d 个选项\n, n); for (int i 0; i n; i) { if (options[i].value) { printf( -%s %s\n, options[i].name, options[i].value); } else { printf( -%s (无参数)\n, options[i].name); } } return 0; }6 常见错误与注意事项6.1 混淆指针数组和数组指针cint *p[5]; /* 指针数组 */ int (*p)[5]; /* 数组指针 */ /* 错误赋值 */ int arr[5]; p arr; /* 如果 p 是指针数组错误 */6.2 二级指针初始化错误cint a 10; int **pp; *pp a; /* 错误pp 未初始化*pp 指向随机位置 */6.3 忘记多级解引用cint a 10; int *p a; int **pp p; printf(%d\n, *pp); /* 输出 p 的值地址不是 a */ printf(%d\n, **pp); /* 正确输出 10 */6.4 内存管理错误cchar **arr malloc(5 * sizeof(char*)); for (int i 0; i 5; i) { arr[i] malloc(10); /* 为每个字符串分配内存 */ } /* 使用... */ free(arr); /* 错误只释放了指针数组字符串内存泄漏 */ /* 正确释放 */ for (int i 0; i 5; i) { free(arr[i]); /* 先释放每个字符串 */ } free(arr); /* 再释放指针数组 */6.5 对 argv 的错误假设cint main(int argc, char *argv[]) { /* 假设 argv[1] 一定存在 */ printf(%s\n, argv[1]); /* 如果没有参数越界 */ /* 应该先检查 argc */ if (argc 1) { printf(%s\n, argv[1]); } }7 本章小结本章系统介绍了二级指针和指针数组1. 二级指针指向指针的指针定义int **pp;用于修改调用者的指针动态二维数组的实现多级指针理论上存在但很少用2. 指针数组 vs 数组指针写法名称含义int *p[5]指针数组数组元素是int*int (*p)[5]数组指针指针指向int[5]数组3. 命令行参数 argv 的实质char *argv[]等价于char **argv是指针数组每个元素指向一个参数字符串以NULL结尾argc表示参数个数4. 典型应用修改函数外部的指针分配内存动态二维数组字符串数组处理命令行参数解析5. 常见错误混淆指针数组和数组指针二级指针未初始化多级解引用错误内存释放不完全对 argv 越界访问