深入理解指针(四):字符指针、数组指针、函数指针与转移表
数组和指针的关系,本质上就是“地址 + 类型 + 步长”的关系。 前面我们已经讨论了数组名、指针访问、传参退化、二级指针、指针数组和二维模拟。这一篇继续沿着这条主线,把字符指针、数组指针、二维数组传参、函数指针、函数指针数组和转移表一次讲透。

目录
本节目标
一、字符指针变量
1.1 字符指针的两种常见用法
字符指针的类型是 char *。它既可以指向单个字符,也可以指向字符串。
#include <stdio.h>
int main(void)
{
char ch = 'w';
char *pc = &ch;
printf("ch = %c\\n", ch);
printf("*pc = %c\\n", *pc);
*pc = 'W';
printf("修改后 ch = %c\\n", ch);
return 0;
}

输出示例:
ch = w
*pc = w
修改后 ch = W
另一种常见写法:
#include <stdio.h>
int main(void)
{
const char *pstr = "hello bit.";
printf("%s\\n", pstr);
return 0;
}
这里并不是把整个字符串“放进”了 pstr,而是把字符串首字符 'h' 的地址存入了 pstr。pstr 指向常量字符串,通常位于只读区域,因此不建议通过 pstr 修改内容。
1.2 字符数组 vs 字符指针
#include <stdio.h>
int main(void)
{
char arr[] = "hello bit."; // 字符数组:内容可修改
const char *p = "hello bit."; // 指向字符串字面量:内容不可修改
arr[0] = 'H';
printf("arr = %s\\n", arr);
printf("p = %s\\n", p);
printf("sizeof(arr) = %zu\\n", sizeof(arr));
printf("sizeof(p) = %zu\\n", sizeof(p));
// p[0] = 'H'; // 错误:修改字符串字面量是未定义行为
return 0;
}

输出示例:
arr = Hello bit.
p = hello bit.
sizeof(arr) = 12
sizeof(p) = 8
结论:
- char arr[] = "hello bit."; 是字符数组,有独立存储空间,可以修改。
- const char *p = "hello bit."; 是指针,指向字符串字面量,不能通过 p 修改内容。
- sizeof(arr) 是整个数组大小,sizeof(p) 是指针大小。
| 本质 | 数组 | 指针变量 |
| 存储内容 | 字符本身 | 地址 |
| 可修改内容 | 可以 | 不可以(指向字面量时) |
| sizeof | 整个数组大小 | 指针大小 |
| strlen | 字符串长度 | 字符串长度 |
| 整体赋值 | 不可以 | 可以重新指向 |
| 自增 | 不可以 | 可以 |
| 传参 | 退化为 char * | 本身就是 char * |
| 典型用途 | 可修改字符串缓冲区 | 只读字符串 / 遍历 |
| 修改元素 | arr[0] = 'H'; 合法 | p[0] = 'H'; 未定义行为 |
| 整体赋值 | arr = "world"; 错误 | p = "world"; 合法 |
| 复制内容 | strcpy(dst, arr); | strcpy(dst, p); |
| 自增 | arr++; 错误 | p++; 合法 |
| 返回局部 | 不能返回局部数组地址 | 不能返回局部数组地址 |
二、数组指针变量
2.1 数组指针是什么?
数组指针:指向数组的指针。
int (*p)[10];
读法:p 是一个指针,指向一个“有 10 个 int 元素的数组”。
对比:
int *p1[10]; // 指针数组:p1 是数组,有 10 个元素,每个元素是 int *
int (*p2)[10]; // 数组指针:p2 是指针,指向 int[10]
因为 [] 的优先级高于 *,所以 int *p1[10] 先看 p1[10],说明 p1 是数组;而 int (*p2)[10] 中 p2 先和 * 结合,说明 p2 是指针。
2.2 数组指针的初始化
数组指针用来存放数组的地址。数组的地址可以通过 &数组名 获得。
#include <stdio.h>
int main(void)
{
int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int (*p)[10] = &arr;
printf("arr = %p\\n", (void *)arr);
printf("&arr = %p\\n", (void *)&arr);
printf("p = %p\\n", (void *)p);
printf("p + 1 = %p\\n", (void *)(p + 1));
printf("sizeof(arr) = %zu\\n", sizeof(arr));
printf("sizeof(p) = %zu\\n", sizeof(p));
printf("sizeof(*p) = %zu\\n", sizeof(*p));
return 0;
}

输出示例:
arr = 000000000061FE00
&arr = 000000000061FE00
p = 000000000061FE00
p + 1 = 000000000061FE28
sizeof(arr) = 40
sizeof(p) = 8
sizeof(*p) = 40
结论:
- p + 1 跳过整个数组,也就是 10 * sizeof(int) = 40 字节。
- *p 就是整个数组,sizeof(*p) 等于数组大小。
- 数组指针的步长由它指向的数组类型决定。
| 本质 | 数组指针是指针,不是数组;指向一个数组 |
| 类型写法 | int (*p)[N]; 表示 p 指向 int[N] |
| 与指针数组区别 | int *p[N]; 是指针数组;int (*p)[N]; 是数组指针 |
| 优先级 | [] 高于 *,必须加括号 (*p) 保证 p 先与 * 结合 |
| 步长 | p + 1 跳过整个数组,步长为 N * sizeof(元素类型) |
| 解引用 | *p 得到整个数组;p[i] 得到第 i 个数组 |
| 初始化 | 用 &arr 或二维数组名 arr 初始化 |
| 传参 | 二维数组传参本质是传递首行地址,形参写 int (*p)[N] |
| 用途 | 二维数组传参、行指针、动态二维数组、模拟二维数组 |
2.3 数组指针类型解析
int (*p)[10];
拆开看:
- p:变量名。
- *p:说明 p 是指针。
- (*p)[10]:说明 p 指向一个含有 10 个元素的数组。
- int (*p)[10]:说明数组的每个元素是 int。
2.3.1 从内到外读类型
读复杂指针类型时,从变量名开始,先看它和谁结合:
所以 p 的类型是:指向 int[10] 的指针,即 int (*)[10]。
2.3.2 对比指针数组
| int *p[10]; | 指针数组 | p 是数组,有 10 个元素,每个元素是 int * |
| int (*p)[10]; | 数组指针 | p 是指针,指向一个 int[10] 数组 |
区别在于括号:int *p[10] 中 [] 优先级高,p 先和 [] 结合;int (*p)[10] 中 p 先和 * 结合。
2.3.3 优先级与括号
[] 的优先级高于 *。 如果写成 int *p[10];,编译器会理解为 int *(p[10]);,即 p 是数组。 要得到“指针”,必须加括号:int (*p)[10];。
2.3.4 步长与解引用
int arr[5] = {0};
int (*p)[5] = &arr;
printf("p = %p\\n", (void *)p);
printf("p + 1 = %p\\n", (void *)(p + 1));
printf("sizeof(*p) = %zu\\n", sizeof(*p));
- p + 1 跳过整个数组,步长是 5 * sizeof(int)。
- *p 得到整个数组,sizeof(*p) 等于 sizeof(arr)。
- (*p)[i] 访问数组第 i 个元素,等价于 arr[i]。
2.3.5 用 typedef 简化
typedef int (*parr_t)[10]; // parr_t 是“指向 int[10] 的指针”类型
parr_t p = &arr;
新的类型名 parr_t 必须写在 * 的右边。
2.3.6 常见错误
int *p1[10]; // 指针数组:数组,元素是 int *
int (*p2)[10]; // 数组指针:指针,指向 int[10]
// int (*p3)[10] = arr; // 如果 arr 是 int[10],应该写 &arr
// int (*p4)[10] = &arr; // 正确
- 忘记括号:int *p[10]; 变成指针数组。
- 初始化时漏掉 &:数组名 arr 退化为 int *,不能直接赋给 int (*)[10]。
- 把二维数组传参写成 int **:类型不匹配。
三、二维数组传参的本质
3.1 二维数组可以看作“数组的数组”
对于 int arr[3][5];
- arr 是二维数组,有 3 行,每行 5 个 int。
- arr 的首元素是第一行,类型是 int[5]。
- 数组名 arr 在大多数表达式中退化为首元素地址,也就是第一行的地址,类型是 int (*)[5]。
- arr + 1 跳过一整行。
3.2 形参写成数组形式
#include <stdio.h>
void print_matrix(int a[3][5], int rows, int cols)
{
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < cols; ++j) {
printf("%d ", a[i][j]);
}
printf("\\n");
}
}
int main(void)
{
int arr[3][5] = {
{1, 2, 3, 4, 5},
{2, 3, 4, 5, 6},
{3, 4, 5, 6, 7}
};
print_matrix(arr, 3, 5);
return 0;
}

等价写法
下面三种形参完全等价:
void f(int a[3][5], int rows, int cols);
void f(int a[][5], int rows, int cols);
void f(int (*a)[5], int rows, int cols);
- 第一维可省,第二维不能省。
- 编译器按 int (*a)[5] 处理。
用宏或 typedef 简化
#define COLS 5
void f(int a[][COLS], int rows);
typedef int Row[COLS];
void f(Row a[], int rows);
常见错误
void f(int a[][], int rows, int cols); // 错误:第二维不能省
void f(int **a, int rows, int cols); // 类型不匹配
int rows = sizeof(a) / sizeof(a[0]); // 错误:得到指针除法
总结
| int a[3][5] | 合法 | int (*a)[5] |
| int a[][5] | 合法 | int (*a)[5] |
| int a[5][] | 非法 | 第二维不能省 |
| int **a | 类型不匹配 | 用于指针数组或动态二维数组 |
| int (*a)[5] | 合法 | 本质形式 |
二维数组传参:第一维可省,第二维不能省;行数必须传,列数可推出。
3.3 形参写成指针形式
#include <stdio.h>
void print_matrix(int (*p)[5], int rows, int cols)
{
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < cols; ++j) {
printf("%d ", *(*(p + i) + j));
}
printf("\\n");
}
}
int main(void)
{
int arr[3][5] = {
{1, 2, 3, 4, 5},
{2, 3, 4, 5, 6},
{3, 4, 5, 6, 7}
};
print_matrix(arr, 3, 5);
return 0;
}

这两种写法本质等价,编译器都按 int (*)[5] 处理。
指针形式解析
- int (*p)[5]:p 是指针,指向 int[5]。
- p + i:跳过 i 行,指向第 i 行。
- *(p + i):得到第 i 行数组,类型为 int[5]。
- *(p + i) + j:第 i 行第 j 个元素的地址。
- *(*(p + i) + j):第 i 行第 j 个元素的值。
等价写法
p[i][j] // 下标形式
*(*(p + i) + j) // 指针形式
两者完全等价。
形参写成指针形式,更清楚地表达“二维数组传参传递的是行指针”。
3.4 为什么不能写成 int **?
真正二维数组 int arr[3][5] 的内存是连续的,arr 退化为 int (*)[5],不是 int **。
内存布局对比
int arr[3][5] int **p
+—-+—-+—-+—-+ +—-+ +—-+—-+—-+—-+
| 1 | 2 | 3 | 4 | | * | —> | 1 | 2 | 3 | 4 |
+—-+—-+—-+—-+ +—-+ +—-+—-+—-+—-+
| 5 | 6 | 7 | 8 | | * | —> | 5 | 6 | 7 | 8 |
+—-+—-+—-+—-+ +—-+ +—-+—-+—-+—-+
| 9 | 10 | 11 | 12 | | * | —> | 9 | 10 | 11 | 12 |
+—-+—-+—-+—-+ +—-+ +—-+—-+—-+—-+
连续一整块 指针数组 + 每行独立分配
- 真正二维数组:一整块连续内存,arr + 1 跳过一行。
- int **:先有一个指针数组,每个指针再指向一块独立内存,行与行不一定连续。
类型对比
| int arr[3][5] | int (*)[5] | 指向一行(int[5])的指针 |
| int **p | int ** | 指向 int * 的指针 |
| int *p[3] | int *[3] | 指针数组,元素是 int * |
典型误区
void f(int **p, int rows, int cols); // 错误匹配真正二维数组
int arr[3][5];
f(arr, 3, 5); // 类型不匹配:int (*)[5] 不能传给 int **
int ** 的正确用途
// 用途一:指针数组
int a = 1, b = 2, c = 3;
int *p[3] = {&a, &b, &c}; // p 是 int *[3]
// 用途二:动态二维数组
int **m = malloc(rows * sizeof(int *));
for (int i = 0; i < rows; ++i)
m[i] = malloc(cols * sizeof(int));
四、函数指针变量
4.1 函数也有地址
函数名在大多数表达式中会转换为函数地址。
#include <stdio.h>
void test(void)
{
printf("hehe\\n");
}
int main(void)
{
printf("test = %p\\n", (void *)test);
printf("&test = %p\\n", (void *)&test);
return 0;
}

通常 test 和 &test 输出相同。
4.2 定义函数指针
int (*pf)(int, int);
读法:pf 是一个指针,指向“参数为 (int, int),返回值为 int”的函数。
完整示例:
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x – y;
}
int main(void)
{
int (*pf)(int, int) = Add;
printf("Add(2, 3) = %d\\n", Add(2, 3));
printf("pf(2, 3) = %d\\n", pf(2, 3));
printf("(*pf)(2, 3) = %d\\n", (*pf)(2, 3));
pf = Sub;
printf("pf(5, 3) = %d\\n", pf(5, 3));
return 0;
}

输出示例:
Add(2, 3) = 5
pf(2, 3) = 5
(*pf)(2, 3) = 5
pf(5, 3) = 2
结论:
- pf(2, 3) 和 (*pf)(2, 3) 都合法。
- 函数指针可以像变量一样重新赋值,指向不同函数。
4.2.1 初始化方式
函数指针可以用函数名初始化,也可以用 &函数名 初始化:
int (*pf1)(int, int) = Add;
int (*pf2)(int, int) = &Add;
两种写法等价,因为函数名在表达式中会转换为函数地址。
4.2.2 调用方式
下面两种调用完全等价:
pf(2, 3);
(*pf)(2, 3);
- pf(2, 3):直接通过函数指针调用。
- (*pf)(2, 3):先解引用得到函数,再调用。
C 语言允许省略 *,所以两种写法都合法。
4.2.3 可以重新赋值
函数指针是变量,可以重新指向其他函数,只要签名一致:
pf = Sub;
printf("%d\\n", pf(5, 3)); // 输出 2
4.2.4 常见错误
| int *pf(int, int); | 声明了函数,不是函数指针 |
| int (*pf)(int, int) = Add(); | Add() 是调用结果,不是函数地址 |
| pf = Add(2, 3); | 把返回值赋给函数指针,类型不匹配 |
| 调用时写成 *pf(2, 3); | 优先级问题,等价于 *(pf(2, 3)),不是调用函数指针 |
4.3 函数指针作为参数
函数指针最常见的用途之一是“回调函数”。
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x – y;
}
int Calc(int (*op)(int, int), int x, int y)
{
return op(x, y);
}
int main(void)
{
printf("Calc(Add, 10, 3) = %d\\n", Calc(Add, 10, 3));
printf("Calc(Sub, 10, 3) = %d\\n", Calc(Sub, 10, 3));
return 0;
}

输出示例:
Calc(Add, 10, 3) = 13
Calc(Sub, 10, 3) = 7
4.4 两段有趣的代码
《C陷阱和缺陷》中有两段经典代码:
(*(void (*)())0)();
void (*signal(int, void (*)(int)))(int);
第一段代码:把 0 强制转换为“无参数、返回 void 的函数指针”,然后调用它。 第二段代码:signal 是一个函数,参数为 int 和 void (*)(int),返回类型是 void (*)(int)。
用 typedef 可以简化:
typedef void (*pfun_t)(int);
pfun_t signal(int, pfun_t);
typedef 用来给类型起别名。对于函数指针:
typedef void (*pfun_t)(int);
新的类型名 pfun_t 必须在 * 的右边。
五、函数指针数组
5.1 定义函数指针数组
函数指针数组:数组的每个元素都是函数指针。
int (*parr[3])(int, int);
从内到外:
- parr 先和 [] 结合 → parr 是数组。
- 数组有 3 个元素。
- 每个元素的类型是 int (*)(int, int),即函数指针。
所以 parr 是一个“存放函数指针的数组”。
类型拆解
int (*parr[3])(int, int);
| parr | 变量名 |
| parr[3] | 数组,有 3 个元素 |
| *parr[3] | 每个元素是指针 |
| (*parr[3])(int, int) | 每个指针指向参数为 (int, int) 的函数 |
| int (*parr[3])(int, int) | 每个函数返回 int |
常见错误写法
int *parr2[3](int, int); // 错误:parr2 是数组,元素是返回 int* 的函数?
int (*parr3[3])(int, int); // 正确
int *parr2[3](int, int); 会被解析为“数组,每个元素是函数声明”,语法不合法或语义错误。 函数指针数组必须把 * 和数组名用括号括起来:(*parr[3])。
5.2 函数指针数组示例
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x – y;
}
int Mul(int x, int y)
{
return x * y;
}
int main(void)
{
int (*ops[3])(int, int) = { Add, Sub, Mul };
int a = 10;
int b = 5;
for (int i = 0; i < 3; ++i) {
printf("ops[%d](%d, %d) = %d\\n", i, a, b, ops[i](a, b));
}
return 0;
}

输出示例:
ops[0](10, 5) = 15
ops[1](10, 5) = 5
ops[2](10, 5) = 50
六、转移表
6.1 什么是转移表?
转移表本质就是“函数指针数组”。 它可以把冗长的 switch-case 改成按索引调用函数。
例如计算器:
- 1:加法
- 2:减法
- 3:乘法
- 4:除法
- 0:退出
用转移表写:
int (*table[5])(int, int) = { NULL, Add, Sub, Mul, Divide };
调用:
table[choice](a, b);
6.1 什么是转移表?
转移表(jump table)本质上就是一个函数指针数组。 它把“根据编号选择函数”的逻辑,从冗长的 switch-case 改成按索引查表调用。
例如计算器:
| 1 | 加法 |
| 2 | 减法 |
| 3 | 乘法 |
| 4 | 除法 |
| 0 | 退出 |
用转移表写:
int (*table[5])(int, int) = { NULL, Add, Sub, Mul, Divide };
调用:
table[choice](a, b);
为什么叫“转移表”?
程序原本要经过一连串 if 或 switch 判断,才能决定调用哪个函数。 使用转移表后,直接用下标从数组中取出函数地址并调用,控制流“转移”到目标函数,所以叫转移表。
6.2 普通 switch 版计算器
#include <stdio.h>
int Add(int a, int b)
{
return a + b;
}
int Sub(int a, int b)
{
return a – b;
}
int Mul(int a, int b)
{
return a * b;
}
int Divide(int a, int b)
{
if (b == 0) {
printf("除数不能为 0\\n");
return 0;
}
return a / b;
}
int main(void)
{
int x, y;
int input = 1;
int ret = 0;
do {
printf("************************\\n");
printf(" 1:add 2:sub\\n");
printf(" 3:mul 4:div\\n");
printf(" 0:exit\\n");
printf("************************\\n");
printf("请选择:");
scanf("%d", &input);
switch (input) {
case 1:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = Add(x, y);
printf("ret = %d\\n", ret);
break;
case 2:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = Sub(x, y);
printf("ret = %d\\n", ret);
break;
case 3:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = Mul(x, y);
printf("ret = %d\\n", ret);
break;
case 4:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = Divide(x, y);
printf("ret = %d\\n", ret);
break;
case 0:
printf("退出程序\\n");
break;
default:
printf("选择错误\\n");
break;
}
} while (input);
return 0;
}

6.3 转移表版计算器
#include <stdio.h>
#include <stddef.h>
int Add(int a, int b)
{
return a + b;
}
int Sub(int a, int b)
{
return a – b;
}
int Mul(int a, int b)
{
return a * b;
}
int Divide(int a, int b)
{
if (b == 0) {
printf("除数不能为 0\\n");
return 0;
}
return a / b;
}
int main(void)
{
int x, y;
int input = 1;
int ret = 0;
int (*table[5])(int, int) = { NULL, Add, Sub, Mul, Divide };
do {
printf("************************\\n");
printf(" 1:add 2:sub\\n");
printf(" 3:mul 4:div\\n");
printf(" 0:exit\\n");
printf("************************\\n");
printf("请选择:");
scanf("%d", &input);
if (input >= 1 && input <= 4) {
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = table[input](x, y);
printf("ret = %d\\n", ret);
} else if (input == 0) {
printf("退出计算器\\n");
} else {
printf("输入有误\\n");
}
} while (input);
return 0;
}
优点:
- 代码更简洁。
- 扩展功能时只需增加函数和表项。
- 很适合菜单、命令分发、状态机等场景。
注意:
- 使用转移表前必须检查索引范围,防止越界。
- 函数指针数组中的函数签名必须完全一致。
- 表中第 0 项可以放 NULL,因为 0 表示退出,不会调用。
与 switch-case 对比
| 代码长度 | 分支多时代码冗长 | 简洁,一个数组搞定 |
| 扩展性 | 增加分支要改多处 | 增加函数和表项即可 |
| 可读性 | 分支逻辑直观 | 需要理解函数指针数组 |
| 运行方式 | 逐个比较或跳转 | 直接按索引取函数地址 |
| 适用场景 | 分支少、逻辑差异大 | 分支集中、签名一致、索引连续 |
| 索引检查 | default 兜底 | 必须手动检查范围 |
七、常见陷阱与调试建议
7.1 易错点
char *s = "hello";
// s[0] = 'H'; // 未定义行为
应写成:
char s[] = "hello";
s[0] = 'H';
int *p1[5]; // 指针数组
int (*p2)[5]; // 数组指针
真正二维数组:
void f(int (*p)[5], int rows);
动态指针数组:
void f(int **p, int rows, int cols);
两者不是一回事。
int (*ops[3])(int, int); // 正确
// int *ops[3](int, int); // 错误
ret = table[input](x, y); // input 必须在合法范围内
7.2 调试建议
7.2.1 用调试宏打印地址、类型大小和步长
#include <stdio.h>
#define PRINT_PTR(p) printf(#p " = %p\\n", (void *)(p))
#define PRINT_SIZE(x) printf("sizeof(" #x ") = %zu\\n", sizeof(x))
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
int main(void)
{
int arr[5] = {1, 2, 3, 4, 5};
int (*p)[5] = &arr;
PRINT_PTR(arr);
PRINT_PTR(&arr);
PRINT_PTR(p);
PRINT_PTR(p + 1);
PRINT_SIZE(arr);
PRINT_SIZE(p);
PRINT_SIZE(*p);
printf("ARRAY_SIZE(arr) = %zu\\n", ARRAY_SIZE(arr));
return 0;
}

要点:
- 打印指针一定要转 (void *)。
- p + 1 跳过整个数组,所以地址差是 sizeof(int[5])。
- ARRAY_SIZE 只对真正的数组名有效。
7.2.2 用 assert 给函数加“合同”
#include <stdio.h>
#include <assert.h>
static void print_array(const int *arr, int n)
{
assert(arr != NULL);
assert(n > 0);
for (int i = 0; i < n; ++i) {
printf("%d ", arr[i]);
}
printf("\\n");
}
static int calc(int (*op)(int, int), int x, int y)
{
assert(op != NULL);
return op(x, y);
}
static int add(int a, int b) { return a + b; }
static int sub(int a, int b) { return a – b; }
int main(void)
{
int arr[] = {1, 2, 3, 4, 5};
int n = (int)(sizeof(arr) / sizeof(arr[0]));
print_array(arr, n);
printf("calc(add, 10, 3) = %d\\n", calc(add, 10, 3));
printf("calc(sub, 10, 3) = %d\\n", calc(sub, 10, 3));
return 0;
}

要点:
- assert 用于检查“这里必须成立”。
- 函数指针调用前先断言非空。
- 发布版可用 #define NDEBUG 关闭断言。
7.2.3 数组退化与长度传递调试
#include <stdio.h>
#include <assert.h>
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
static void print_array(const int *arr, int n)
{
assert(arr != NULL);
assert(n > 0);
for (int i = 0; i < n; ++i) {
printf("%d ", arr[i]);
}
printf("\\n");
}
int main(void)
{
int arr[] = {10, 20, 30, 40, 50};
int n = (int)ARRAY_SIZE(arr);
printf("main: sizeof(arr) = %zu, n = %d\\n", sizeof(arr), n);
print_array(arr, n);
return 0;
}

要点:
- 函数内 sizeof(arr) 得到的是指针大小,不是数组大小。
- 正确做法是“指针 + 长度”一起传。
- 调试时打印 sizeof(arr) 和 n,立刻能看出是否退化。
7.2.4 函数指针与转移表调试
#include <stdio.h>
#include <assert.h>
typedef int (*op_t)(int, int);
static int add(int a, int b) { return a + b; }
static int sub(int a, int b) { return a – b; }
static int mul(int a, int b) { return a * b; }
static int divide(int a, int b)
{
if (b == 0) {
printf("除数不能为 0\\n");
return 0;
}
return a / b;
}
int main(void)
{
op_t table[5] = { NULL, add, sub, mul, divide };
printf("add = %p\\n", (void *)add);
printf("table[1] = %p\\n", (void *)table[1]);
printf("table[2] = %p\\n", (void *)table[2]);
printf("table[3] = %p\\n", (void *)table[3]);
printf("table[4] = %p\\n", (void *)table[4]);
int choice = 1;
int x = 10;
int y = 5;
assert(choice >= 1 && choice <= 4);
assert(table[choice] != NULL);
int result = table[choice](x, y);
printf("result = %d\\n", result);
return 0;
}

要点:
- 用 typedef 简化函数指针类型。
- 打印每个表项地址,确认初始化顺序正确。
- 使用前检查索引范围和表项非空。
7.2.5 字符指针与字符串调试
#include <stdio.h>
#include <string.h>
int main(void)
{
char arr[] = "hello";
const char *p = "hello";
printf("arr = %s\\n", arr);
printf("p = %s\\n", p);
printf("sizeof(arr) = %zu, strlen(arr) = %zu\\n",
sizeof(arr), strlen(arr));
printf("sizeof(p) = %zu, strlen(p) = %zu\\n",
sizeof(p), strlen(p));
printf("arr[0] = %c, p[0] = %c\\n", arr[0], p[0]);
arr[0] = 'H';
printf("修改后 arr = %s\\n", arr);
/* p[0] = 'H'; // 错误:字符串字面量不可修改 */
return 0;
}

要点:
- 只读字符串用 const char *。
- 需要修改就用 char arr[]。
- 打印 sizeof 和 strlen,立刻看清区别。
7.2.6 二维数组与数组指针调试
#include <stdio.h>
static void print_matrix(int (*p)[5], int rows, int cols)
{
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < cols; ++j) {
printf("%d ", p[i][j]);
}
printf("\\n");
}
}
int main(void)
{
int arr[3][5] = {
{1, 2, 3, 4, 5},
{2, 3, 4, 5, 6},
{3, 4, 5, 6, 7}
};
int (*p)[5] = arr;
for (int i = 0; i < 3; ++i) {
printf("p + %d = %p\\n", i, (void *)(p + i));
}
printf("p[1][2] = %d\\n", p[1][2]);
printf("*(*(p + 1) + 2) = %d\\n", *(*(p + 1) + 2));
print_matrix(arr, 3, 5);
return 0;
}

要点:
- 真正二维数组传参,形参写 int (*p)[N]。
- p + 1 跳过一整行。
- 不要写成 int **,两者内存布局不同。
7.2.7 最小复现与单元测试
#include <stdio.h>
#include <assert.h>
static int add(int a, int b) { return a + b; }
static int sub(int a, int b) { return a – b; }
static void test_add(void)
{
assert(add(2, 3) == 5);
assert(add(–1, 1) == 0);
}
static void test_sub(void)
{
assert(sub(5, 3) == 2);
assert(sub(3, 5) == –2);
}
int main(void)
{
test_add();
test_sub();
printf("all tests passed\\n");
return 0;
}

要点:
- 怀疑指针问题时,先写最小复现程序。
- 给关键函数写简单断言测试。
- 测试通过后再集成到完整代码中。
7.2.8 调试清单
| 打印地址 | printf("%p", (void *)p); |
| 查看类型大小 | printf("%zu", sizeof(x)); |
| 数组长度 | #define ARRAY_SIZE(a) (sizeof(a)/sizeof((a)[0])) |
| 断言非空 | assert(p != NULL); |
| 断言索引 | assert(i >= 0 && i < n); |
| 函数指针非空 | assert(op != NULL); |
| 转移表索引 | assert(choice >= 1 && choice <= 4); |
| 二维数组形参 | int (*p)[N] |
| 释放后置空 | free(p); p = NULL; |
| 最小复现 | 单独写一个小 main |
| 单元测试 | 用 assert 写测试函数 |
八、总结
| 字符指针变量 | char * 指向字符或字符串;字符串字面量不可修改,建议用 const char * |
| 数组指针变量 | int (*p)[N] 是指针,指向 int[N],p + 1 跳过整个数组 |
| 二维数组传参 | 形参写 int (*p)[N] 或 int a[][N],第二维不能省,不能写成 int ** |
| 函数指针变量 | int (*pf)(int, int) 指向函数,pf(…) 与 (*pf)(…) 等价 |
| 函数指针数组 | int (*p[N])(int, int) 是数组,元素为函数指针 |
| 转移表 | 用函数指针数组按索引调用函数,可替代长 switch,简洁且易扩展 |
留个关注支持一下吧
网硕互联帮助中心




评论前必须登录!
注册