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)是指针的指针,它存储的是另一个指针变量的地址。
c
int 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 多级指针
理论上可以有多级指针,但实际很少用到三级以上:
c
int 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 指针数组
指针数组:一个数组,其元素都是指针。
c
int *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;
}
内存布局:
text
arr[0]: [地址指向 a]
arr[1]: [地址指向 b]
arr[2]: [地址指向 c]
3.2 数组指针
数组指针:一个指针,指向整个数组。
c
int (*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;
}
内存布局:
text
p: [地址指向整个数组]
↓
arr: [1][2][3][4][5]
3.3 辨析对比
| 写法 | 名称 | 含义 | 大小示例(64位) |
|---|---|---|---|
int *p[5] |
指针数组 | 数组,有5个 int* 元素 |
数组大小:5×8=40 |
int (*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 就是一个经典的指针数组:
c
int main(int argc, char *argv[])
-
argc:命令行参数的个数 -
argv:一个指针数组,每个元素指向一个命令行参数字符串
4.2 内存布局
假设程序这样运行:
bash
./program hello world 123
内存中的结构:
text
argv 本身是一个指针数组(在栈上):
argv[0] → "./program\0" (程序名)
argv[1] → "hello\0"
argv[2] → "world\0"
argv[3] → "123\0"
argv[4] → NULL (结束标志)
4.3 等价的表示方式
argv 也可以写成 char **argv:
c
int 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 指向的字符串(但通常不推荐):
c
int 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 混淆指针数组和数组指针
c
int *p[5]; /* 指针数组 */
int (*p)[5]; /* 数组指针 */
/* 错误赋值 */
int arr[5];
p = &arr; /* 如果 p 是指针数组,错误 */
6.2 二级指针初始化错误
c
int a = 10;
int **pp;
*pp = &a; /* 错误!pp 未初始化,*pp 指向随机位置 */
6.3 忘记多级解引用
c
int a = 10;
int *p = &a;
int **pp = &p;
printf("%d\n", *pp); /* 输出 p 的值(地址),不是 a */
printf("%d\n", **pp); /* 正确,输出 10 */
6.4 内存管理错误
c
char **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 的错误假设
c
int 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 越界访问