🔥 星光编译者 · 个人主页
📚 学习专栏: 《C/C++ 成长笔记》 · 《Linux 实践手册》 · 《数据结构与算法》
🌄 向云端飞扬,编译属于自己的代码星河。
☕ 写在开篇
你好,这里是 星光编译者。
这里记录我在 C/C++、Linux、数据结构与算法 学习中遇到的真实问题、亲手验证过的代码,以及那些容易被忽略的实现细节。
比起简单罗列结论,我更愿意从问题出发,把一个知识点的来由讲清楚,把"为什么会这样"和"应该怎样解决"说明白,让每一次踩坑都沉淀成可以复用的经验。
如果这篇记录能帮你少绕一点路,或让某个模糊的地方忽然变得清晰,那么这次分享便有了意义。愿我们在一次次阅读、编译与调试中稳步向前,慢慢搭起属于自己的技术世界。

🔥 本文定位:从"指针保存地址"继续向前,系统梳理字符串字面量、数组指针、二维数组传参、函数指针、函数指针数组和转移表之间的联系。
💡 学习目标 :能从变量名出发读懂复杂声明;区分指针数组与数组指针;解释二维数组为什么必须保留列数;能用
typedef简化函数指针,并用转移表替代重复的分支派发。📌 阅读说明:本文根据课程 PDF《第13讲:深入理解指针(3)》整理,并补充 ISO C 语义下的边界说明。示例按 C11 编写;涉及输入、除零和整数溢出的地方均给出必要保护。
文章目录
- 一、这一次,指针从"数据地址"走向"类型系统"
- 二、字符指针:字符串字面量里究竟存了什么
- 三、先读懂声明:从变量名向外看
- 四、数组指针:一步跨过一整行
- 五、二维数组传参的本质
- 六、函数指针:把"行为"也交给地址
- [七、用 typedef 驯服复杂函数指针](#七、用 typedef 驯服复杂函数指针)
- 八、函数指针数组:把多个行为排成表
- 九、转移表实战:重构一个安全的计算器
- 十、常见错误、安全边界与调试方法
- 十一、高频问答与动手练习
- 总结
一、这一次,指针从"数据地址"走向"类型系统"
前面学习指针时,我们常把它概括成一句话:指针变量保存地址。这句话没有错,却还不完整。真正决定一段指针表达式怎样解释的,是地址背后的类型。
c
int *pi; /* 指向 int */
char *pc; /* 指向 char */
int (*pa)[5]; /* 指向"含 5 个 int 的数组" */
int (*pf)(int, int); /* 指向"接收两个 int、返回 int 的函数" */
它们在某个平台上可能占用相同字节数,但编译器看到的含义完全不同:
pi + 1跨过一个int;pc + 1跨过一个char;pa + 1跨过一个完整的int[5];pf不是拿来做数据指针算术的,而是用来调用符合签名的函数。

因此,本讲真正的主线不是"又多记几种指针",而是建立两个能力:
- 看声明就能还原类型结构;
- 知道这个类型允许怎样访问、移动和调用。
后面的字符指针、数组指针与函数指针看似分散,其实都是同一条规则:先确定"指向什么",再判断能做什么。
二、字符指针:字符串字面量里究竟存了什么
2.1 指向单个字符
字符指针最直接的用法,是保存一个字符对象的地址:
c
#include <stdio.h>
int main(void)
{
char ch = 'w';
char *pc = &ch;
*pc = 'W';
printf("%c\n", ch);
return 0;
}
这里 pc 指向可修改的对象 ch,所以通过 *pc 写入是合法的。pc 保存的是地址,*pc 才是该地址处的字符对象。
2.2 指向字符串字面量
再看另一种写法:
c
const char *message = "hello bit.";
printf("%s\n", message);
右侧不是把整串字符"塞进"指针变量。字符串字面量会形成一个字符数组,表达式在通常场景中转换为指向其首元素的指针,message 保存的是第一个字符 'h' 的地址。
字符串末尾还隐含一个空字符 \0:
text
'h' 'e' 'l' 'l' 'o' ' ' 'b' 'i' 't' '.' '\0'
^
message
应把指向字符串字面量的指针写成 const char *。C 语言为了兼容历史代码,字符串字面量的类型不是 const 数组,但试图修改字符串字面量的行为未定义:
c
const char *message = "hello";
/* message[0] = 'H'; */ /* 错误思路:不要修改字符串字面量 */
如果确实需要修改内容,就用字符数组创建自己的可写副本:
c
char message[] = "hello";
message[0] = 'H';

2.3 数组与指针不是同一个东西
下面两行看起来都能配合 %s 输出,但对象模型不同:
c
char text[] = "hello";
const char *pointer = "hello";
| 比较项 | char text[] = "hello" |
const char *pointer = "hello" |
|---|---|---|
| 创建的对象 | 含 6 个字符的数组 | 一个指针变量,指向字符串字面量 |
| 字符内容能否修改 | 能,在数组范围内修改 | 不能修改所指字面量 |
sizeof 结果 |
数组总字节数,这里为 6 | 指针本身的大小 |
| 能否让名字改指向别处 | 数组名不能被赋值 | 指针变量可以重新赋值 |
数组表达式经常会转换成首元素地址,但这种转换不能把数组和指针变成同一种对象。sizeof、一元 & 和字符串初始化等场景会让差别显现出来。
2.4 比较字符串要比较内容,而不是猜地址
课程中有一个经典问题:
c
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char *str3 = "hello bit.";
const char *str4 = "hello bit.";
str1 与 str2 是两个独立数组,转换成首元素指针后通常不同。对于 str3 与 str4,编译器可以 合并相同字符串字面量,也可以不合并;ISO C 不保证两者地址必定相同。因此,无论数组还是指针,只要业务问题是"文本内容是否相同",都应使用 strcmp:
c
#include <stdio.h>
#include <string.h>
int main(void)
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char *str3 = "hello bit.";
const char *str4 = "hello bit.";
printf("str1 与 str2 内容:%s\n",
strcmp(str1, str2) == 0 ? "相同" : "不同");
printf("str3 与 str4 内容:%s\n",
strcmp(str3, str4) == 0 ? "相同" : "不同");
return 0;
}
== 比较的是两个指针值是否表示同一地址;strcmp 比较的是以 \0 结束的两个字符串内容。二者回答的是不同问题。
三、先读懂声明:从变量名向外看
很多人第一次看到 int (*p)[10] 或 int (*pf)(int, int) 会本能地从左向右念,结果越念越乱。更稳定的方法是:
先找到变量名,再按括号和操作符优先级向外扩展。

3.1 [] 与 * 谁先结合
c
int *p1[10];
int (*p2)[10];
读取 p1:
- 先找到名字
p1; []比*结合得更紧,p1[10]表明它是数组;- 数组的每个元素是
int *。
所以 p1 是指针数组 :数组里放 10 个 int *。
读取 p2:
- 括号让
p2先与*结合,因此p2是指针; - 指针右侧是
[10],说明它指向含 10 个元素的数组; - 数组元素类型是
int。
所以 p2 是数组指针 :它指向一个 int[10]。
3.2 一条实用口诀
可以把常见声明按这个顺序读:
text
名字 → 括号内最近的符号 → 下标/函数参数 → 星号 → 最左侧基础类型
但口诀只是辅助。真正可靠的是识别声明符结构:
name[...]:name是数组;name(...):name是函数;*name:name是指针;- 括号改变默认结合方式。
3.3 不要把"指针数组"和"数组指针"只当名词题
二者不仅考试名称不同,访问方式与步长也不同:
c
int a = 1, b = 2, c = 3;
int *pointer_array[3] = {&a, &b, &c};
int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}};
int (*array_pointer)[3] = matrix;
pointer_array + 1 跨过一个指针元素;array_pointer + 1 跨过一整行 int[3]。类型直接决定了指针算术的单位。
四、数组指针:一步跨过一整行
4.1 创建与初始化
一维数组本身也有地址:
c
int arr[10] = {0};
int (*p)[10] = &arr;
&arr 的类型是"指向 int[10] 的指针",即 int (*)[10],恰好与 p 匹配。
这里最容易混淆的是 arr 和 &arr:
- 在大多数表达式里,
arr转换为指向首元素的int *; &arr是整个数组的地址,类型为int (*)[10];- 两者数值表示常常看起来相同,但类型与
+1的步长不同。
c
printf("%zu\n", sizeof *arr); /* 一个 int 的大小 */
printf("%zu\n", sizeof *p); /* 整个 int[10] 的大小 */
不要用 %p 直接打印任意指针而忽略格式要求。打印对象指针时应转换为 void *:
c
printf("arr = %p\n", (void *)arr);
printf("&arr = %p\n", (void *)&arr);
4.2 数组指针的步长
假设 p 指向 int[10],那么 p + 1 指向"下一个 int[10]"。这正是数组指针处理二维数组时最有价值的能力:它天然以行为单位移动。

c
int matrix[3][5] = {
{1, 2, 3, 4, 5},
{6, 7, 8, 9, 10},
{11, 12, 13, 14, 15}
};
int (*row)[5] = matrix;
printf("%d\n", row[1][3]); /* 9 */
printf("%d\n", *(*(row + 1) + 3)); /* 同样是 9 */
因为:
text
row + 1 → 指向第 2 行
*(row + 1) → 第 2 行这个数组;表达式中再转换为首元素地址
*(row + 1) + 3 → 指向第 2 行第 4 个元素
*(*(row + 1)+3) → 取出该元素
实际代码中优先写 row[i][j],它更直接;展开形式适合用来理解下标运算的本质。
4.3 不要越过所属数组对象
指针算术只在同一个数组对象及其"尾后位置"范围内有定义。可以形成尾后指针,但不能解引用:
c
int (*end)[5] = matrix + 3; /* 合法:尾后位置 */
/* printf("%d", (*end)[0]); */ /* 未定义行为:不能访问尾后位置 */
地址在机器上看似连续,不代表任意算术都被 C 语言语义允许。
五、二维数组传参的本质
5.1 二维数组是"元素为一维数组的数组"
c
int matrix[3][5];
可以把它理解为:matrix 有 3 个元素,每个元素的类型都是 int[5]。因此:
matrix[0]是第 1 行,类型为int[5];matrix[1]是第 2 行,类型为int[5];matrix在通常表达式中转换为指向首行的指针,类型为int (*)[5]。

5.2 两种形参写法实际对应同一种类型
c
void print_matrix(int a[][5], size_t rows);
void print_matrix(int (*a)[5], size_t rows);
在函数形参列表中,数组形式会调整为对应的指针形式,因此这两种声明描述的是同一种参数类型。完整实现可以这样写:
c
#include <stdio.h>
#define COLS 5
void print_matrix(const int a[][COLS], size_t rows)
{
for (size_t i = 0; i < rows; ++i) {
for (size_t j = 0; j < COLS; ++j) {
printf("%d%c", a[i][j], j + 1 == COLS ? '\n' : ' ');
}
}
}
int main(void)
{
const int matrix[][COLS] = {
{1, 2, 3, 4, 5},
{2, 3, 4, 5, 6},
{3, 4, 5, 6, 7}
};
print_matrix(matrix, sizeof matrix / sizeof matrix[0]);
return 0;
}
5.3 为什么第一维可以省略,第二维不能随便省略
函数接收到的是首行地址,本身不知道总共有多少行,所以行数通常额外传入。编译器计算 a[i][j] 时,却必须知道每一行多宽,才能从 a + i 找到第 i 行:
text
第 i 行地址 = 首行地址 + i × sizeof(int[COLS])
如果固定列数丢失,编译器便不知道"跨一行"要跨多少字节。因此传统写法中,最左边的行数可以省略,后续维度必须完整。
5.4 列数运行时才知道怎么办
C99 引入了可变长度数组(VLA)形参写法,支持把列数作为参数:
c
void print_vla(size_t rows, size_t cols,
const int a[rows][cols])
{
for (size_t i = 0; i < rows; ++i) {
for (size_t j = 0; j < cols; ++j) {
printf("%d%c", a[i][j], j + 1 == cols ? '\n' : ' ');
}
}
}
参数顺序很重要:rows 与 cols 必须先出现,后面的数组声明才能使用它们。还要注意,VLA 支持在 C11 起成为可选特性,并非所有编译器都同样支持;跨平台项目可考虑传入一维连续缓冲区,并手动使用 i * cols + j 计算下标。
5.5 int ** 不能替代真正的二维数组
c
void wrong(int **p); /* 不能用来接收 int matrix[3][5] */
int ** 指向的是 int *;真正的二维数组转换后是 int (*)[5],它指向的是 int[5]。两者的目标类型、内存解释和步长都不同。
只有当数据结构本身是"指针数组",例如每一行分别动态分配,再把各行地址存在 int *rows[] 中时,才可能得到与 int ** 兼容的接口。连续二维数组与行指针表是两种布局,不要只因为都能写两个下标就混为一谈。
六、函数指针:把"行为"也交给地址
6.1 函数也可以被指向
定义一个普通函数:
c
int add(int left, int right)
{
return left + right;
}
与数组名类似,函数名在大多数表达式中会转换为指向该函数的指针。可以这样声明并初始化:
c
int (*operation)(int, int) = add;
从 operation 向外读:
(*operation):operation是指针;(int, int):它指向的函数接收两个int;- 最左侧
int:函数返回int。
括号不可省略。若写成下面这样:
c
int *operation(int, int);
operation 会先与 () 结合,含义变成"一个接收两个 int、返回 int * 的函数",不再是函数指针变量。
6.2 通过函数指针调用
两种常见调用形式都可以:
c
int result1 = operation(2, 3);
int result2 = (*operation)(2, 3);
第一种更简洁,第二种更直观地强调了"先解引用函数指针再调用"。在 C 语言语义中,两者效果相同。

一个完整例子:
c
#include <stdio.h>
int add(int left, int right)
{
return left + right;
}
int calculate(int left, int right,
int (*operation)(int, int))
{
return operation(left, right);
}
int main(void)
{
printf("%d\n", calculate(20, 22, add));
return 0;
}
calculate 不关心具体执行加法、减法还是乘法,只要求传入的函数符合约定签名。这就是回调思想的雏形:调用者提供行为,被调用者在合适时机执行它。
6.3 函数指针类型必须兼容
不要把参数或返回类型不同的函数强行转换后调用:
c
double average(double, double);
int (*operation)(int, int);
/* operation = (int (*)(int, int))average; */ /* 危险且错误的设计 */
即使转换能让编译器少报一条诊断,通过不兼容类型调用函数仍会产生未定义行为。参数传递方式、寄存器使用和返回值约定都可能不同。正确做法是统一接口,或写一个签名匹配的适配函数。
6.4 函数地址的打印并不完全可移植
printf("%p", (void *)add) 在许多常见平台上能工作,但 ISO C 只明确保证对象指针与 void * 之间的转换;函数指针未必能无损转换成 void *。学习函数指针时,无须依赖"把地址打印出来"来证明函数存在地址,直接进行类型正确的赋值与调用更可靠。
七、用 typedef 驯服复杂函数指针
7.1 普通类型重命名
typedef 为已有类型创建别名,不会创建一种语义全新的类型:
c
typedef unsigned int uint;
typedef int *int_ptr;
但指针别名有一个著名陷阱:
c
typedef int *int_ptr;
const int_ptr p = NULL;
这里 const 修饰的是别名代表的整个指针类型,因此 p 是"常量指针",不是"指向常量整数的指针"。若希望所指对象只读,应明确写 const int *p,避免别名隐藏星号后造成误判。
7.2 数组指针别名
c
typedef int (*row_ptr)[5];
int matrix[3][5] = {0};
row_ptr row = matrix;
新类型名 row_ptr 放在原来变量名所在的位置。它代表 int (*)[5],即指向一行 5 个 int 的指针。
7.3 函数指针别名
c
typedef int (*binary_operation)(int, int);
binary_operation operation = add;
复杂声明一下变得像普通变量声明。将它作为参数也更清楚:
c
int calculate(int left, int right, binary_operation operation)
{
return operation(left, right);
}

7.4 解读 signal 风格的复杂声明
课程中给出过这样一条声明:
c
void (*signal(int, void (*)(int)))(int);
从 signal 开始读:
signal(...):signal是函数;- 它接收一个
int和一个"接收int、返回void的函数指针"; - 它返回一个"接收
int、返回void的函数指针"。
用别名可明显降低认知负担:
c
typedef void (*signal_handler)(int);
signal_handler signal(int number, signal_handler handler);
工程代码的目标不是向读者展示能写多复杂的声明,而是让接口的角色一眼可见。别名应表达概念,如 signal_handler、compare_fn、event_callback,而不是只写含义模糊的 pf_t。
八、函数指针数组:把多个行为排成表
如果一个函数指针保存一种行为,那么函数指针数组就能保存一组签名一致的行为:
c
int (*operations[4])(int, int);
从 operations 开始读:
operations[4]:它先是一个含 4 个元素的数组;*operations[4]:每个元素都是指针;(int, int):每个指针指向接收两个int的函数;- 最左侧
int:这些函数返回int。
借助别名更自然:
c
typedef int (*binary_operation)(int, int);
binary_operation operations[4];
8.1 初始化与调用
c
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }
binary_operation operations[] = {
add,
subtract,
multiply
};
int result = operations[1](9, 4); /* subtract(9, 4),结果为 5 */

8.2 函数指针数组与"返回函数指针的函数"
这两种声明也容易混淆:
c
int (*table[4])(int, int); /* table 是数组 */
int (*select_op(int))(int, int); /* select_op 是函数 */
前者的名字先与 [] 结合,是函数指针数组;后者的名字先与 () 结合,是一个返回函数指针的函数。仍然从名字向外读,就不会只靠星号数量猜答案。
8.3 派发表适合什么场景
函数指针数组经常用于:
- 菜单命令与处理函数映射;
- 状态机中的状态处理器;
- 协议类型与解析函数映射;
- 驱动接口或平台适配层;
- 解析器中的操作码派发;
- 排序、遍历等算法接收回调。
它的价值不只是"少写几个 case",而是把编号到行为的映射变成数据结构,便于统一校验、扩展和测试。
九、转移表实战:重构一个安全的计算器
课程使用计算器演示转移表。先看传统思路:读取菜单编号,再通过 switch 调用不同函数。当分支逐渐增多时,每个 case 都会重复读取参数、调用函数和输出结果。
转移表把操作编号映射到函数指针:
text
编号 1 → add
编号 2 → subtract
编号 3 → multiply
编号 4 → divide
选择通过校验后,统一执行 table[choice],公共流程只保留一份。
9.1 先统一函数接口
为了让所有函数都能放进同一个数组,它们必须具有兼容签名。除法还需要报告除零和 INT_MIN / -1 的溢出风险,因此与其让四个操作都简单返回 int,不如统一成"成功或失败 + 输出参数"的接口:
c
typedef int (*operation_fn)(int left, int right, int *result);
返回 1 表示成功,返回 0 表示失败,真正的计算结果通过 result 输出。这样所有操作都能一致地表达错误。
9.2 完整实现
c
#include <limits.h>
#include <stdio.h>
typedef int (*operation_fn)(int left, int right, int *result);
static int add_op(int left, int right, int *result)
{
if ((right > 0 && left > INT_MAX - right) ||
(right < 0 && left < INT_MIN - right)) {
return 0;
}
*result = left + right;
return 1;
}
static int sub_op(int left, int right, int *result)
{
if ((right < 0 && left > INT_MAX + right) ||
(right > 0 && left < INT_MIN + right)) {
return 0;
}
*result = left - right;
return 1;
}
static int mul_op(int left, int right, int *result)
{
if (left > 0) {
if ((right > 0 && left > INT_MAX / right) ||
(right < 0 && right < INT_MIN / left)) {
return 0;
}
} else if (left < 0) {
if ((right > 0 && left < INT_MIN / right) ||
(right < 0 && left < INT_MAX / right)) {
return 0;
}
}
*result = left * right;
return 1;
}
static int div_op(int left, int right, int *result)
{
if (right == 0 || (left == INT_MIN && right == -1)) {
return 0;
}
*result = left / right;
return 1;
}
int main(void)
{
static const operation_fn table[] = {
NULL, add_op, sub_op, mul_op, div_op
};
static const char *const names[] = {
"exit", "add", "subtract", "multiply", "divide"
};
int choice = 0;
for (;;) {
int left = 0;
int right = 0;
int result = 0;
printf("1:add 2:sub 3:mul 4:div 0:exit\n");
printf("请选择:");
if (scanf("%d", &choice) != 1) {
puts("输入不是有效整数,程序结束。");
return 1;
}
if (choice == 0) {
puts("退出计算器。");
break;
}
if (choice < 1 ||
(size_t)choice >= sizeof table / sizeof table[0]) {
puts("菜单编号无效。");
continue;
}
printf("请输入两个整数:");
if (scanf("%d%d", &left, &right) != 2) {
puts("操作数输入无效,程序结束。");
return 1;
}
if (table[choice](left, right, &result)) {
printf("%s 结果:%d\n", names[choice], result);
} else {
puts("运算失败:请检查除零或 int 范围溢出。");
}
}
return 0;
}

9.3 为什么要先校验下标
table[choice] 本质上是数组下标访问。若 choice 越界,程序会读取数组之外的内存;若得到的位模式再被当成函数指针调用,后果不可预测。因此顺序必须是:
- 读取选择;
- 处理退出值;
- 验证下标范围;
- 确认表项不是空指针;
- 最后才调用。
若编号稀疏、来源不可信或需要权限控制,还可以用结构体把编号、名称和函数放在一起,再通过查表找到匹配项,而不是直接把外部整数当数组下标。
9.4 转移表并不总比 switch 好
当各分支的参数、返回类型和前置条件差异很大时,强行统一函数签名可能产生大量包装函数。分支很少且逻辑简单时,switch 更直观。选择标准应是:映射关系是否稳定、处理器签名是否一致、扩展与测试是否因此更清晰,而不是单纯追求"少写代码"。
十、常见错误、安全边界与调试方法
10.1 忘记 const,误以为字符串字面量可写
c
char *text = "hello"; /* C 中可能通过编译,但容易诱发错误写入 */
优先写:
c
const char *text = "hello";
编译器会在尝试通过 text 修改内容时及时诊断。
10.2 用 == 比较字符串内容
c
if (left == right) { /* 比的是地址 */ }
内容比较应使用 strcmp(left, right) == 0,并确保两个参数都指向有效、以 \0 结束的字符串。
10.3 把 int ** 当作二维数组指针
int ** 与 int (*)[COLS] 的内存模型不同。打开编译器警告后,这类不兼容参数通常会被指出。不要用强制转换压掉诊断;修正函数形参类型。
10.4 数组指针列数写错
c
int matrix[3][5];
/* int (*p)[4] = matrix; */ /* 类型不兼容 */
列数属于目标类型的一部分。写错以后不仅是"每行少看一个元素",而是整个步长都错了。
10.5 函数指针签名不兼容
回调的参数、返回类型必须与约定匹配。启用严格警告:
text
GCC / Clang: -std=c11 -Wall -Wextra -Wpedantic -Wconversion
MSVC: /W4
不要把警告当成"编译器太严格"。指针章节里,许多警告正是在阻止未定义行为。
10.6 调用空函数指针
c
operation_fn operation = NULL;
/* operation(1, 2, &result); */ /* 错误 */
对可选回调或稀疏表项,调用前检查:
c
if (operation != NULL) {
operation(1, 2, &result);
}
10.7 表下标来自外部输入却没有校验
菜单编号、网络操作码和文件中的类型码都不可信。验证范围后才能索引;对负数要特别小心,因为转成 size_t 后可能变成很大的无符号值。
10.8 调试时先看类型,再看地址
遇到指针问题,可以按这份清单逐项检查:
- 变量的完整声明是什么?
- 它指向单个元素、整行数组,还是函数?
+1的单位是什么?- 解引用后得到的类型是什么?
- 对象的生命周期是否仍然有效?
- 是否保留了
const限定? - 数组下标和函数指针是否经过校验?
- 编译器是否已经给出不兼容类型警告?
地址值相同并不能证明类型正确。类型、边界与生命周期才是解释指针行为的依据。
十一、高频问答与动手练习
高频问答
Q1:char *p = "abc" 为什么在 C 中常能编译?
C 为兼容历史代码,没有把字符串字面量的数组类型直接写成 const,但修改它仍是未定义行为。现代代码应主动使用 const char *p。
Q2:arr 与 &arr 打印出来可能一样,为什么类型还要区分?
它们可能表示同一起始位置,但 arr 通常转换为首元素指针,&arr 指向整个数组。arr + 1 跨一个元素,&arr + 1 跨整个数组。地址数值只是信息的一部分。
Q3:二维数组传参为什么不用传第一维?
形参接收首行指针,函数不会从指针类型中得知总行数,所以行数通常单独传入;列数决定一行的类型和步长,传统数组指针写法必须保留。
Q4:函数名和 &函数名 有什么区别?
在用于初始化或调用的常见场景中,两者都可得到指向该函数的指针,类型可兼容。写 operation = add; 通常更简洁。
Q5:pf(args) 为什么不写 (*pf)(args) 也能调用?
C 的函数调用运算符允许函数指针作为被调用表达式;两种写法语义相同。团队保持一种清晰风格即可。
Q6:函数指针数组是不是"数组指针"?
不是。int (*table[4])(int, int) 的 table 先与 [] 结合,所以它是数组,数组元素才是函数指针。数组指针的变量名会先与 * 结合,例如 int (*p)[4]。
Q7:用 typedef 会不会改变底层类型?
不会。它创建别名,让声明更可读;类型兼容性和底层调用规则没有因此改变。
动手练习
- 分别解释
int *a[5]、int (*b)[5]和int (*c[5])(int)。 - 写一个函数,接收
int matrix[][4]与行数,返回所有元素之和。 - 定义
compare_fn,表示"接收两个const void *、返回int的函数指针"。 - 为转移表增加取模操作,并处理除数为
0。 - 说明为什么
const char *x = "same"; const char *y = "same";中x == y的结果不能用来判断文本内容。 - 判断
int **p能否接收int a[2][3],并说明目标类型差异。
参考答案要点:
a是 5 个int *组成的数组;b是指向int[5]的指针;c是 5 个函数指针组成的数组,每个函数接收int并返回int。- 形参可写
const int matrix[][4]或const int (*matrix)[4],双层循环累加,并根据业务范围选择足够宽的结果类型。 typedef int (*compare_fn)(const void *, const void *);。- 新增
mod_op,先判断右操作数是否为0,再将函数加入表,并同步扩展名称与范围校验。 - 实现可以合并相同字面量,也可以分别保存;内容比较用
strcmp。 - 不能。前者指向
int *,后者转换后是指向int[3]的int (*)[3]。
总结
这一讲把指针从"保存某个数据的地址"推进到了更完整的类型系统:字符指针让我们区分字符串字面量与字符数组;数组指针让指针算术以整行为单位;二维数组传参揭示了列数其实属于元素类型;函数指针让程序可以保存、传递和选择行为;函数指针数组与转移表则把"编号到行为"的关系组织成可扩展的数据结构。
面对复杂声明时,不要数星号,也不要只盯着地址。先从变量名向外读出完整类型,再问三个问题:它指向什么、一步跨多远、解引用或调用后得到什么。当这套分析顺序形成习惯,指针就不再是一团符号,而会变成可以逐层验证的规则。