跳到主要内容

指针、指针运算与回调

本节目标

查询 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 == nullptrpointer != 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 *显式取得首元素地址
&valuesint (*)[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 完成固定类型的变换回调:

pointer_callback_report.c
#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、函数定义和调用参数保持兼容

排查指针问题时,按“指针对象是否已初始化 → 值是否为空 → 所指实体是否仍在生存期内 → 类型、限定与对齐是否匹配 → 算术和比较是否留在同一合法范围 → 回调签名是否兼容”逐项核对。后续“动态内存、生命周期与所有权”章节将继续讨论分配、释放和所有权协议;本章只提示该后续主题,不展开其内容。