从 C 规范的角度来看,从一个结构成员(故意)溢出到下一个是非法或未定义的行为吗?
这是未定义的行为。这arr[i]
运算符是语法糖*(arr + i)
。所以数组访问归结为二进制+
指针算术运算符,C17 6.5.6 加法运算符,来自 §7 和 §8:
就这些运算符而言,指向不是元素元素的对象的指针
数组的行为与指向长度为 1 的数组的第一个元素的指针相同
对象的类型作为其元素类型。
当一个整数类型的表达式与指针相加或相减时,
结果具有指针操作数的类型。 /--/
如果两个指针
操作数和结果指向同一个数组对象的元素,或者指向最后一个元素
数组对象的元素,求值不得产生溢出;否则,
行为未定义。
如果结果指向数组对象的最后一个元素,则它
不得用作所求值的一元 * 运算符的操作数。
正如您所注意到的,优化编译器可能会利用这些规则来生成更快的代码。
在这种情况下有没有办法阻止 gcc 展开循环?
有一个可以使用的特殊例外规则,C17 6.3.2.3/7:
当指向对象的指针转换为指向字符类型的指针时,
结果指向对象的最低寻址字节。连续递增
结果,直到对象的大小,产生指向对象的剩余字节的指针。
此外,严格别名不适用于字符类型,因为 C17 6.5 §7 中的另一个特殊规则
对象的存储值只能由具有以下之一的左值表达式访问
以下类型: ... 字符类型。
这两种特殊规则和谐共存。因此,假设我们在指针转换期间不会弄乱对齐等,这意味着我们可以这样做:
unsigned char* i;
for(i = (unsigned char*)&mystruct; i < (unsigned char*)(&mystruct + 1); i++)
{
do_something(*i);
}
然而,这可能会读取填充字节等,因此它是“实现定义的”。但理论上,您可以按字节访问结构体字节,并且只要按字节计算结构体偏移量,您就可以以这种方式迭代结构体(或任何其他对象)的多个成员。
据我所知,这个看起来非常有问题的代码应该是明确定义的:
#include <stdint.h>
#include <string.h>
#include <stdio.h>
struct __attribute__ ((__packed__)) {
int code[1];
int place_holder[100];
} s;
void test(int val, int n)
{
for (unsigned char* i = (unsigned char*)&s;
i < (unsigned char*)&s + n*sizeof(int);
i += _Alignof(int))
{
if((uintptr_t)i % _Alignof(int) == 0) // not really necessary, just defensive prog.
{
memcpy(i, &val, sizeof(int));
printf("Writing %d to address %p\n", val, (void*)i);
}
}
}
int main (void)
{
test(42, 3);
printf("%d %d %d\n", s.code[0], s.place_holder[0], s.place_holder[1]);
}
这在 gcc 和 clang (x86) 上运行良好。至于效率如何,那就是另一个故事了。但请不要编写这样的代码。