指针、指针运算与回调
本节目标
查询 C23 指针对象、空指针、类型转换、指针算术、const 组合、指针参数与类型明确的回调。
本章用于查询 C23 指针值的形成、转换和合法使用范围。读指针代码时,先确定它引用的对象或函数及其生存期,再核对类型和限定符,最后检查算术、比较或调用是否仍在语言保证的边界内。
指针对象、地址与解引用
指针对象存储一个指针值,不等于被指向的对象本身。下面的 pointer 是一个独立对象;它的值引用 value,而 *pointer 是用于访问 value 的左值:
int value = 7;
int *pointer = &value;
*pointer = 9;
一元 & 对可取地址的对象产生指向该对象的指针,也能取得函数地址;位域和声明为 register 的对象不能作为普通取址对象。一元 * 要求操作数具有指针类型,但仅满足类型还不够:指针值必须在当次访问时有效、正确对齐,并指向仍处于生存期内的相应对象或函数。
因此,声明 int *pointer; 只创建指针对象,不创建一个供它自动指向的 int。有关对象、名字和生存期的基础先看基本类型、对象、声明与初始化。
C23 空指针
nullptr 是 C23 的预定义空指针常量,其类型为 nullptr_t。nullptr 可转换为任意指针类型并产生该类型的空指针;nullptr_t 由 <stddef.h> 声明,用在确实需要按类型识别 nullptr 的场景。
#include <stddef.h>
int *result = nullptr;
nullptr_t absent = nullptr;
空指针保证不等于任何对象或函数的地址,同一类型或可按规则比较的空指针彼此相等。空指针可以比较,不可解引用;应先用 pointer == nullptr 或 pointer != nullptr 判断,再进入需要访问对象的分支。NULL 仍是实现定义形式的空指针常量,新写的 C23 代码可用 nullptr 更明确地表达缺失地址。
指针类型与转换
指针类型记录它引用的类型。指向兼容对象类型的指针可按赋值约束传递;从未限定的被指向类型增加 const 等限定符是允许的,反向移除限定符则不能依赖隐式转换。即使显式强制转换移除了 const,修改一个原本就以 const 限定类型定义的对象仍是未定义行为。
void * 与对象指针之间的转换不会把函数指针纳入同一规则。任意对象指针可转换为 void * 并转换回来,回转结果与原指针比较相等;这一保证不包括函数指针。不同对象指针类型之间的显式转换还要满足目标类型的对齐要求,而转换成功本身并不授权通过不相容类型访问对象。
int value = 42;
int *original = &value;
void *erased = original;
int *restored = erased;
需要判断显式转换和访问类型时,可同时查阅运算符、表达式、求值与类型转换。
数组与指针的关系
多数表达式上下文会把数组转换为指向首元素的指针,但数组对象与指针对象仍是不同类型的对象。以 int values[4] 为例:
| 表达式 | 此处的类型或结果 | 含义 |
|---|---|---|
values | 通常转换为 int * | 指向 values[0] |
&values[0] | int * | 显式取得首元素地址 |
&values | int (*)[4] | 指向整个四元素数组 |
sizeof values | 数组总字节数 | sizeof 抑制数组到指针转换 |
values 与 &values[0] 在这里指向首元素;&values 的地址数值可能看起来相同,但引用类型和步进单位不同。数组转换的例外、数组形参调整和多维布局见数组与字符串。
指针算术与单尾位置
对指向数组元素的指针加减整数,步长以元素大小为单位。指针算术必须留在同一数组对象与其单尾位置的范围内,非数组对象按长度 1 的数组处理。这里的“单尾位置”是最后一个元素之后的位置,适合构造半开区间 [begin, end):
int values[] = {1, 2, 3, 4};
int *begin = values;
int *end = values + 4;
for (int *current = begin; current != end; ++current) {
*current += 1;
}
单尾指针可用作区间边界,不可解引用。若指针算术的指针操作数或结果越出同一数组对象及其单尾位置,行为未定义;不能先越界再指望通过后续运算“走回来”。对一个独立的 int object,&object + 1 可按长度为 1 的数组规则形成单尾指针,但同样不能解引用。
指针相减与比较
指针相减的结果类型为 ptrdiff_t,它是 <stddef.h> 提供的有符号整数类型,用 printf 输出时对应 %td。相减的两个指针必须指向同一数组的元素或其单尾位置,结果才是两个下标之差;若该差不能由 ptrdiff_t 表示,行为未定义。
#include <stddef.h>
#include <stdio.h>
int values[4];
int *begin = values;
int *end = values + 4;
printf("%td\n", end - begin); // 输出:4
相等比较和有序比较的用途不同:==、!= 可用于检查空指针、同一对象身份或区间终点。在两个指针所指对象类型兼容(可有不同限定符)的前提下,<、>、<=、>= 只在标准规定的相对位置有定义,例如同一数组内较大下标的元素指针更大、单尾指针大于元素指针,结构体中后声明成员的指针更大。同一联合体的成员指针比较相等。对独立非数组对象,其对象指针与单尾指针的有序比较也遵循长度 1 的数组规则。不要用有序比较给互不相关对象的地址排序。
指针与 const 组合
const int * 与 int * const 约束的是不同对象。读声明时从标识符附近向外看,可以区分四种常用组合:
| 声明 | 指针能改指向 | 能通过该指针修改 int |
|---|---|---|
int *pointer | 是 | 是 |
const int *pointer(等价于 int const *pointer) | 是 | 否 |
int *const pointer = &value | 否 | 是 |
const int *const pointer = &value | 否 | 否 |
第一处 const 若位于 * 左侧,限定被指向的 int;位于 * 右侧则限定指针对象本身。const int * 表达通过此路径只读,不代表所指对象一定最初就定义为 const,也不阻止其他合法的非 const 路径修改原本可修改的对象。
指针参数与输出参数
C 仍然按值传递指针实参:形参得到的是指针值副本。函数重新给形参赋值不会改写调用方的指针对象;通过有效指针访问所指对象,则可能修改调用方与函数共同引用的那个对象。
static int write_total(int left, int right, int *output) {
if (output == nullptr) {
return 0;
}
*output = left + right;
return 1;
}
输出参数的接口必须说明前置条件:是否允许空指针、指针需要引用多少个对象、对象是否仍在生存期内、是否正确对齐且可写。上例明确把 nullptr 当作失败;若接口不允许空值,则调用方必须在调用前满足该约定。按值传递的完整规则见函数、参数、返回值与递归。
函数指针与类型明确的回调
函数名在多数表达式中转换为函数指针。用 typedef 给固定签名命名,可让回调接口直接表达输入和返回类型:
typedef int (*int_transform)(int value);
static int doubled(int value) {
return value * 2;
}
回调 typedef 必须保持返回类型和参数类型兼容。函数指针可以按标准规则转换成另一函数指针类型并转换回来,但若通过转换后的不兼容类型实际调用函数,行为未定义。类型明确的 int_transform 因而既便于编译器检查,也避免把对象指针的 void * 转换规则误套到函数指针。
下面的完整程序用 [begin, end) 遍历同一数组,以 nullptr 表示查找失败,并通过 int_transform 完成固定类型的变换回调:
#include <stddef.h>
#include <stdio.h>
typedef int (*int_transform)(int value);
static const int *find_value(const int *begin, const int *end, int target) {
for (const int *current = begin; current != end; ++current) {
if (*current == target) {
return current;
}
}
return nullptr;
}
static int doubled(int value) {
return value * 2;
}
static void transform_values(int *begin, int *end, int_transform transform) {
for (int *current = begin; current != end; ++current) {
*current = transform(*current);
}
}
static int sum_values(const int *begin, const int *end) {
int total = 0;
for (const int *current = begin; current != end; ++current) {
total += *current;
}
return total;
}
int main(void) {
int values[] = {1, 2, 3, 4};
int *begin = values;
int *end = values + (sizeof values / sizeof values[0]);
const int *found = find_value(begin, end, 3);
const int *missing = find_value(begin, end, 9);
printf("range-distance=%td\n", end - begin);
printf("found=%d\n", found != nullptr ? *found : -1);
printf("missing=%s\n", missing == nullptr ? "yes" : "no");
transform_values(begin, end, doubled);
printf("transformed=%d,%d,%d,%d\n", values[0], values[1], values[2], values[3]);
printf("sum=%d\n", sum_values(begin, end));
return 0;
}
输出:
range-distance=4
found=3
missing=yes
transformed=2,4,6,8
sum=20
常见陷阱
| 症状 | 为什么危险 | 排查与修正 |
|---|---|---|
| 未初始化指针 | 自动指针对象可能保存不确定表示,读取或解引用没有有效目标 | 声明时绑定有效对象,或初始化为 nullptr |
| 空指针解引用 | 空指针不指向任何对象或函数 | 在解引用前按接口约定检查 nullptr |
| 悬空指针 | 所指对象的生存期已结束,旧地址不再授权访问该对象 | 核对生存期;离开作用域后不继续使用其局部对象地址 |
| 越界或单尾解引用 | 单尾只表示边界,数组范围之外也不产生可访问元素 | 使用同一数组内的半开区间,并在 current != end 时才解引用 |
| 不相关指针有序比较 | 语言没有给任意独立对象建立通用地址顺序 | 先保证所指对象类型兼容,再只比较同一数组、按长度 1 处理的非数组对象或标准明确规定的聚合对象成员 |
| 不相关指针相减 | 语言没有给独立对象建立元素距离 | 指针相减只用于同一数组(含按长度 1 处理的非数组对象)及其单尾位置 |
| 不兼容函数指针调用 | 转换可往返不代表可以用错误签名调用 | 让 typedef、函数定义和调用参数保持兼容 |
排查指针问题时,按“指针对象是否已初始化 → 值是否为空 → 所指实体是否仍在生存期内 → 类型、限定与对齐是否匹配 → 算术和比较是否留在同一合法范围 → 回调签名是否兼容”逐项核对。后续“动态内存、生命周期与所有权”章节将继续讨论分配、释放和所有权协议;本章只提示该后续主题,不展开其内容。