C语言数组越界访问已知内存是否合法?解析与规避方案
关于C语言数组越界访问未定义行为的问题解答
先看这段代码,它在GCC中能运行并输出预期结果,但属于C标准定义的未定义行为:
#include <stdio.h> #include <assert.h> struct Int2 { int i[2]; }; struct Int4 { struct Int2 i2[2]; }; int main() { struct Int4 i4; i4.i2[1].i[-1] = 10; printf("%d\n", i4.i2[0].i[1]); // 输出10 static_assert(sizeof(struct Int4)==sizeof(int[4]), "sizeof(struct Int4)!=sizeof(int[4])"); return 0; }
根据C 2018 6.5.6 8规则:指针算术仅允许在同一数组对象的范围内进行(包括数组末尾之后的第一个位置,但不能越界到其他数组对象)。这段代码里i4.i2[1].i是一个独立的数组对象,对它做[-1]的指针算术,本质是越界访问到了i4.i2[0].i的内存区域,违反了标准规则,编译器优化可能导致这段代码失效。
问题1:既然数组基于指针算术实现,越界访问本应指向已知内存,为何实际可能无法正常工作?
核心原因是编译器会基于C标准的规则做激进优化:
- 编译器默认代码严格遵守标准,会假定所有指针算术操作都在合法数组范围内进行。一旦写出越界的指针操作,编译器可以直接忽略这段代码、重排执行顺序,甚至生成完全不符合预期的机器码——因为标准定义这种行为是“未定义”,编译器不需要为这种情况保证任何结果。
- 虽然当前环境下
struct Int4的内存布局是连续的(static_assert也验证了这一点),但C标准并没有强制要求结构体成员之间不能有填充(只是这里成员都是int类型,对齐要求一致才没有填充)。更关键的是,标准不允许跨数组对象的指针算术,哪怕物理内存是连续的,编译器也可以基于这个假设做优化,比如把i4.i2[1].i[-1] = 10直接当成无效操作优化掉,导致后续输出不是10。
问题2:如何规避该未定义行为?例如仅持有i2第二个元素的指针时,如何合法访问第一个i2成员及其内部int值?
可以通过标准允许的指针转换和偏移计算来实现,这里提供两种合法的方式:
方式1:通过结构体指针反向定位
利用offsetof宏(来自<stddef.h>)计算成员在结构体中的偏移,把指向i2[1]的指针转换为指向整个Int4结构体的指针,再通过结构体成员访问:
#include <stdio.h> #include <stddef.h> struct Int2 { int i[2]; }; struct Int4 { struct Int2 i2[2]; }; int main() { struct Int4 i4; struct Int2 *p = &i4.i2[1]; // 持有第二个i2的指针 // 反向计算得到整个Int4结构体的指针 struct Int4 *parent = (struct Int4 *)((char *)p - offsetof(struct Int4, i2[1])); // 合法访问第一个i2的内部int值 parent->i2[0].i[1] = 10; printf("%d\n", parent->i2[0].i[1]); // 输出10 static_assert(sizeof(struct Int4)==sizeof(int[4]), "sizeof(struct Int4)!=sizeof(int[4])"); return 0; }
offsetof是C标准定义的宏,能准确计算结构体成员相对于结构体起始地址的偏移量,完全符合标准规则。
方式2:转换为基础类型指针做合法算术
如果确定结构体内存没有填充(比如通过static_assert验证),可以把指向i2[1]的指针转换为int*,然后做合法的指针算术(因为整个Int4的内存等价于一个int[4]数组,此时指针操作在同一个数组范围内):
#include <stdio.h> struct Int2 { int i[2]; }; struct Int4 { struct Int2 i2[2]; }; int main() { struct Int4 i4; struct Int2 *p = &i4.i2[1]; // 持有第二个i2的指针 int *ip = (int *)p; ip[-2] = 10; // 此时指针操作在int[4]的合法范围内 printf("%d\n", i4.i2[0].i[1]); // 输出10 static_assert(sizeof(struct Int4)==sizeof(int[4]), "sizeof(struct Int4)!=sizeof(int[4])"); return 0; }
这种方式的前提是必须确保struct Int4的内存布局和int[4]完全一致,否则可能出现问题,所以第一种方式更通用、更安全。
内容的提问来源于stack exchange,提问作者CPlus
相关产品推荐
相关产品推荐

