对结构成员的不良访问

时间:2014-01-13 17:40:38

标签: c pointers struct undefined-behavior

由于structs及其成员的内存布局方式,我能够做到以下几点:

typedef struct
{
    int a;
    int b;
    int c;
} myStruct;

main()

myStruct aStruct;
aStruct.a = 1;
aStruct.b = 2;
aStruct.c = 3;

int *aBadBoyPointer = &aStruct.a;

printf("%i %i %i", *(aBadBoyPointer), *(++aBadBoyPointer), *(++aBadBoyPointer));

很容易。

  

以上行会发出警告:

     

无序修改和访问aBadBoyPointer

但它分别编译并运行精细打印1, 2, 3

我的问题在这里:

为了正确地做事,你能举出一个可能会破坏的场景,这个场景证实了编译器这是一种糟糕的做法/不好的做事方式吗?

或者:或许这实际上是在一些罕见情况下做事的“好方法”?

附录:

除此部分导致未定义的行为:

printf("%i %i %i", *(aBadBoyPointer), *(++aBadBoyPointer), *(++aBadBoyPointer));

我真正想知道的是:

  

使用指针指向struct的某个成员但是以struct方式访问其他成员(通过递增,使其成为可能)被认为是好的(一种好的做法)由于struct成员的内存布局,还是标准不赞成的做法?

其次如上所述,如果这是一种不好的做法,那么是否会出现在结构中以这种方式使用指针然后获得对某些情况下有益的另一个成员变量的访问的情况?

5 个答案:

答案 0 :(得分:5)

有一些问题,您看到的警告是由于unspecified评估您的函数参数的顺序。 C99 draft standard部分6.5.2.2 函数调用 10 表示:

  

函数指示符的评估顺序,实际参数和   实际参数中的子表达式未指定,但有一个序列点   在实际通话之前。

您还在sequence point undefined behavior内修改了一个变量,而6.5 表达式段落在see it live并且无法依赖它工作 2 说(强调我的前进):

  

在上一个和下一个序列点之间,对象应具有其存储值   通过表达式的评估最多修改一次。 72)此外,先前的值应该是只读的,以确定要存储的值。 73)

另外,请注意,该标准允许在 struct 的元素之间进行填充,但超出该标量被认为是一个元素的数组,因此递增超出数组然后执行间接也将是不确定的。这将在6.5.6 添加剂运算符 7 部分中介绍:

  

出于这些运算符的目的,指向不是元素的对象的指针   数组的行为与指向长度为1的数组的第一个元素的指针相同,其中对象的类型为其元素类型。

在p {1>} 添加运算符 8

段中,未定义

并超过一个超过数组边界访问数组范围的数组边界>

  

[...]如果指针操作数和结果都指向同一个数组对象的元素,或者指向数组对象的最后一个元素,则评估不应产生溢出;否则,行为未定义。 如果结果指向数组对象的最后一个元素之后,则不应将其用作评估的一元*运算符的操作数。

我们可以看到,根据优化级别6.5.6将输出( see it live ):

gcc

或( offsetof ):

3 3 2

这两者都不是理想的输出。

通过指针访问结构成员的符合标准的方法是使用here和{{3}},这需要包括3 3 3 。访问成员stddef.h将如下所示:

a

这里有三个要素:

  1. 使用*( (int*) ((char*)aBadBoyPointer+offsetof(myStruct, a)) ) ^ ^ ^ 3 2 1 确定成员
  2. 字节中的偏移量
  3. 转换为* char **,因为我们需要以字节为单位的指针算法
  4. 转回* int **,因为这是正确的类型

答案 1 :(得分:4)

我同意现有的答案(这种未经测序的访问调用未定义的行为并且很糟糕)。

但是,为了举一个具体的例子,我在MS Visual Studio中编译了你的代码。输出是(在调试和释放模式下):

3
3
3

答案 2 :(得分:2)

函数参数不按C标准确定的顺序进行评估(C99§6.5.2.2),因此您的代码会调用未定义的行为。不同平台上的不同编译器或相同编译器可能会给您不同的结果。在任何情况下都不会引用undefined behavior做好事情的好方法。

作为参考,标准的文字说:

  

10函数指示符的评估顺序,实际参数和    实际参数中的子表达式未指定,但有一个序列点    在实际通话之前。

<强>附录

要回答问题的第二部分,C编译器可以根据§6.7.2.1第12段在结构成员之间添加填充:

  

结构或联合对象的每个非位字段成员都以适合其类型的实现定义方式对齐。

在某些情况下,结构可以像数组一样运行,并且增加指向成员的指针可以为您解决问题(请参阅#pragma pack__attribute__((packed))),但您的代码将明确地(如果明确地)非-portable,你可能会遇到一些编译器错误。通常,使用数组和枚举来代替结构。

答案 3 :(得分:2)

除了在Shafik Yaghmournmichaels答案中已经说过的内容之外,您还必须注意到某些编译器会将alignment应用于结构中的变量,通常为4个字节。例如:

struct something {
    char a;
    char b;
};

这个结构似乎有2个字节,但它可能有8个,因为编译器可能会填充结构中的每个元素,使其覆盖一个可被4整除的内存空间。将有6个字节只是垃圾,但它们是仍然保留。在这样的例子中,将结构作为char的序列读取将失败。

答案 4 :(得分:1)

编译器可以在每个struture的成员之间添加填充。如果确实如此,OP的代码就会失败。


此外,这可能是未定义的行为,因为可能无法取消引用指向数组边界的指针。

然而,是否可以考虑

int a;

我不确定的1个元素的数组。