在 c 中越界访问内部数组是否是未定义的行为?



我正在玩弄c中的一些数组和指针,并开始怀疑这样做是否是未定义的行为。

int (*arr)[5] = malloc(sizeof(int[5][5]));
// Is this undefined behavior?
int val0 = arr[0][5];
// Rephrased, is it guaranteed it'll always have the same effect as this line?
int val1 = arr[1][0];

感谢您的任何见解。

在 C 中,你正在做的是未定义的行为。

表达式arr[0]的类型为int [5]。 因此,表达式arr[0][5]取消引用数组末尾之后的一个元素arr[0],而取消引用超过数组末尾是未定义的行为。

C 标准中关于数组下标的第 6.5.2.1p2 节指出:

下标运算符[]的定义是E1[E2](*((E1)+(E2)))相同。

C 标准中关于添加剂操作员的第 6.5.6p8 节指出:

将整数类型的表达式添加到 或 时 从指针中减去,结果具有指针的类型 操作数。 如果指针操作数指向数组的元素 对象,并且数组足够大,结果指向一个元素 与原始元素的偏移使得 生成的数组元素和原始数组元素的下标等于 整数表达式。 换句话说,如果表达式P指向 数组对象的第 i个元素,表达式(P)+N(等效地,N+(P))和(P)-N(其中N的值为n) 分别指向 的i+n 和i−n第 数组对象(前提是它们存在)。 此外,如果 表达式P指向数组对象的最后一个元素,即 表达式(P)+1指向数组最后一个元素之后的 1 点 对象,如果表达式Q指向最后一个 元素,表达式(Q)-1指向 数组对象的最后一个元素。如果指针操作数 并且结果指向同一数组对象的元素, 或者一个超过数组对象的最后一个元素,求值 不得产生溢出;否则,行为是未定义的。 如果结果指向数组对象的最后一个元素,则 不得用作一元*运算符的操作数 评价。

粗体部分指定数组下标中隐含的加法可能不会导致指针超过数组末尾的一个元素,并且指向数组末尾之后的一个元素的指针可能不会被推迟。

有问题的数组本身是数组的成员,这意味着每个子数组的元素在内存中是连续的,这一事实并没有改变这一点。 编译器中的激进优化设置可能会注意到,访问数组末尾并基于这一事实进行优化是未定义的行为。

该标准显然旨在避免要求编译器给出如下内容:

int foo[5][10];
int test(int i)
{
foo[1][0] = 1;
foo[0][i] = 2;
return foo[1][0];
}

必须重新加载foo[1][0]的值,以适应对foo[0][i]的写入可能会影响foo[1][0]的可能性。 另一方面,在编写标准之前,写这样的东西是惯用的:

void dump_array(int *p, int rows, int cols)
{
int i,j;
for (i=0; i<rows; i++)
{
for (j=0; j<cols; j++)
printf("%6d", *p++);
printf("n");
}
}
int foo[5][10];
...
dump_array(foo[0], 5, 10);

并且已发表的Rationale中没有任何内容表明作者有意禁止此类结构或破坏使用它们的代码。 实际上,要求连续放置数组的行(即使添加填充会提高效率)的主要好处是允许此类代码运行。

在编写标准时,当为接收指针的函数生成代码时,编译器会将指针视为可以识别某个任意较大对象的某个任意部分,而无需努力了解或关心该封闭对象可能是什么。 因此,作为一种非常流行的"符合语言扩展"的形式,他们将支持像dump_array这样的结构,而不考虑标准是否要求他们这样做,因此,标准的作者认为没有理由担心标准何时强制要求这种支持。 相反,他们留下了《标准》可以放弃管辖权的执行质量问题。

不幸的是,由于该标准的作者期望编译器将传递指向函数的指针的行为视为隐式"清洗"它,因此该标准的作者认为没有必要定义任何显式方法来清洗有关指针封闭对象的信息,以防函数需要处理标识"原始"存储的指针。 考虑到 1980 年代编译器技术的状态,这种区别并不重要,但如果代码执行以下操作,则可能非常相关:

int matrix[10][10];
void test2(int c)
{
matrix[4][0] = 1;
dump_array(matrix[0], 1, c);
matrix[4][0] = 2;
}

void test3(int r)
{
matrix[4][0] = 1;
dump_array((int*)matrix, r, 10);
matrix[4][0] = 2;
}

根据函数的意图,让编译器优化出第一次写入以matrix[4][0]一个或两个可能会提高效率,或者可能导致生成的代码行为无用。 将显式指针转换视为擦除类型信息,但将数组到指针衰减视为保留类型信息,将允许程序员在编写代码时实现所需的语义,如第二个示例所示,同时允许编译器在编写源代码时执行相关优化,如第一个示例所示。 不幸的是,该标准没有区别,自由编译器的维护者不愿意放弃他们认为该标准给他们的任何"优化",使语言除了"希望最好"的语义之外什么都没有,除了那些要么避免跨过程优化或记录需要做什么来阻止它们的实现。

相关内容

最新更新