关于C#:结构成员之间的指针差异?

Pointer difference across members of a struct?

C99标准规定:

When two pointers are subtracted, both shall point to elements of the same array object, or one past the last element of the array object

考虑以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct test {
    int x[5];
    char something;
    short y[5];
};

...

struct test s = { ... };
char *p = (char *) s.x;
char *q = (char *) s.y;
printf("%td
"
, q - p);

这显然打破了上述规则,因为pq指针指向不同的"数组对象",并且根据规则,q - p的差异是未定义的。

但在实践中,为什么这样的事情会导致不明确的行为呢?毕竟,结构成员是按顺序排列的(就像数组元素一样),成员之间有任何潜在的填充。是的,填充的量会随着实现的不同而变化,这会影响计算的结果,但是为什么结果应该是"未定义的"?

我的问题是,我们是否可以假设标准只是"无知"这个问题,或者是否有充分的理由不扩大这一规则?不能将上述规则改为"两者都应指向同一数组对象的元素或同一结构的成员"?

我唯一的怀疑是分段的内存结构,其中的成员可能以不同的段结束。是这样吗?

我也怀疑这就是为什么GCC定义了自己的__builtin_offsetof,以便对offsetof宏有一个"符合标准"的定义。

编辑:

正如已经指出的,标准不允许对空指针进行算术运算。它是一个GNU扩展,只有当GCC通过-std=c99 -pedantic时才会发出警告。我用char *指针替换void *指针。


同一结构成员的地址之间的减法和关系运算符(类型char*上)定义得很好。

任何对象都可以被视为unsigned char的数组。

引用N1570 6.2.6.1第4段:

Values stored in non-bit-field objects of any other object type
consist of n × CHAR_BIT bits, where n is the size of an object of that
type, in bytes. The value may be copied into an object of type
unsigned char [ n ] (e.g., by memcpy); the resulting set of bytes is
called the object representation of the value.

My only suspicion are segmented memory architectures where the members
might end up in different segments. Is that the case?

不。对于具有分段内存结构的系统,通常编译器会施加一个限制,即每个对象必须适合于一个段。或者它可以允许占用多个段的对象,但是它仍然必须确保指针算术和比较工作正常。


指针算法要求添加或减去的两个指针是同一对象的一部分,因为否则它没有意义。本标准引用部分具体指两个不相关的对象,如int a[b];int b[5]。指针算法要求知道指针指向的对象的类型(我确信您已经知道了这一点)。

1
2
int a[5];
int *p = &a[1]+1;

这里,p是通过知道&a[1]是指int对象来计算的,因此增加到4个字节(假设sizeof(int)是4个字节)。

对于结构示例,我认为它不可能被定义为使结构成员之间的指针算术合法。

举个例子,

1
2
3
4
5
struct test {
    int x[5];
    char something;
    short y[5];
};

c标准不允许在void指针中使用指针算术(使用gcc -Wall -pedantic test.c编译将捕获这一点)。我认为你使用的GCC假设void*char*相似,并允许这样做。所以,

1
2
printf("%zu
"
, q - p);

等于

1
printf("%zu", (char*)q - (char*)p);

如果指针指向同一对象内并且是字符指针(char*unsigned char*,则指针算法定义得很好。

使用正确的类型,它将是:

1
2
3
4
5
struct test s = { ... };
int *p = s.x;
short *q = s.y;
printf("%td
"
, q - p);

现在,如何执行q-p?基于sizeof(int)sizeof(short)?如何计算位于这两个数组中间的char something;的大小?

这就解释了不可能对不同类型的对象执行指针运算。

即使所有成员都是同一类型(因此没有如上所述的类型问题),最好使用标准宏offsetof(来自)来获得与成员之间指针算术效果类似的结构成员之间的差异:

1
2
printf("%zu
"
, offsetof(struct test, y) - offsetof(struct test, x));

所以我认为没有必要用C标准定义结构成员之间的指针算术。


我相信这个问题的答案比它看起来要简单,操作人员问:

but why should that result be"undefined"?

好吧,让我们看一下未定义行为的定义在C99标准草案3.4.3中:

behavior, upon use of a nonportable or erroneous program construct or
of erroneous data, for which this International Standard imposes no
requirements

这仅仅是标准没有强制要求的行为,完全符合这种情况,结果将根据体系结构而变化,并且试图指定结果可能很困难,如果不是以可移植的方式不可能的话。这就留下了一个问题,为什么他们会选择未定义的行为,而不是说实现未定义的行为?

很可能是由于未定义的行为限制了创建无效指针的方式数量,这与我们获得了offsetof来删除不相关对象的指针减法的一个潜在需求是一致的。

虽然该标准并未真正定义"无效指针"一词,但我们在国际标准编程语言C的基本原理中得到了很好的描述,在6.3.2.3部分指针中指出(强调我的):

Implicit in the Standard is the notion of invalid pointers. In
discussing pointers, the Standard typically refers to"a pointer to an
object" or"a pointer to a function" or"a null pointer." A special
case in address arithmetic allows for a pointer to just past the end
of an array. Any other pointer is invalid.

C99的基本原理还补充说:

Regardless how an invalid pointer is created, any use of it yields
undefined behavior. Even assignment, comparison with a null pointer
constant, or comparison with itself, might on some systems result in
an exception.

这强烈地表明指向padding的指针是无效的指针,尽管很难证明padding不是对象,但object的定义是:

region of data storage in the execution environment, the contents of
which can represent values

以及注意事项:

When referenced, an object may be interpreted as having a particular
type; see 6.3.2.1.

我不知道我们如何解释结构元素之间填充的类型或值,因此它们不是对象,或者至少强烈地表示填充不应该被视为对象。


是的,允许您对结构字节执行指针算术:

N1570-6.3.2.3指针P7:

... When a pointer to an object is converted to a pointer to a character type,
the result points to the lowest addressed byte of the object. Successive increments of the
result, up to the size of the object, yield pointers to the remaining bytes of the object.

这意味着,对于程序员来说,无论结构在硬件中是如何实现的,它的字节都应被视为一个连续的区域。

但是,不使用void*指针,这是非标准编译器扩展。如标准段落所述,它仅适用于字符类型指针。

编辑:

正如Mafso在评论中指出的那样,只要减影结果类型ptrdiff_t对结果有足够的范围,以上才是正确的。由于size_t的范围可以大于ptrdiff_t,并且如果结构足够大,地址可能相距太远。

因此,最好对结构构件使用offsetof宏,并从中计算结果。


我应该指出以下几点:

根据C99标准第6.7.2.1节:

在结构对象中,非位字段成员和位字段所在的单位。居住地址的声明顺序会增加。指向结构对象,适当转换,指向其初始成员(或如果该成员是位字段,然后是它所在的单元,反之亦然。可能没有名字在结构对象中填充,但不是在其开始处填充。

成员之间的指针减法结果没有定义太多,因此不可靠(即,当应用相同的算术时,同一结构类型的不同实例之间不保证相同)。