繁体   English   中英

这种用法中的指针算术是否会导致未定义的行为

[英]Does the pointer arithmetic in this usage cause undefined behavior

这是对以下问题的跟进。 我假设我最初使用的指针算法会导致未定义的行为。 但是,一位同事告诉我,该用法实际上已明确定义。 下面是一个简化的例子:

typedef struct StructA {
    int a;
} StructA ;

typedef struct StructB {
    StructA a;
    StructA* b;
} StructB;

int main() {
    StructB* original = (StructB*)malloc(sizeof(StructB));
    original->a.a = 5;
    original->b = &original->a;

    StructB* copy = (StructB*)malloc(sizeof(StructB));
    memcpy(copy, original, sizeof(StructB));
    free(original);
    ptrdiff_t offset = (char*)copy - (char*)original;
    StructA* a = (StructA*)((char*)(copy->b) + offset);
    printf("%i\n", a->a);
    free(copy)
}

根据 C++11 规范的§5.7 ¶5:

如果指针操作数和结果都指向同一数组 object 的元素,或数组 object 的最后一个元素,则评估不应产生溢出; 否则,行为未定义。

我假设,代码的以下部分:

ptrdiff_t offset = (char*)copy - (char*)original;
StructA* a = (StructA*)((char*)(copy->b) + offset);

导致未定义的行为,因为它:

  1. 减去两个指针,分别指向不同的 arrays
  2. 偏移量计算的结果指针不再指向同一个数组。

这会导致未定义的行为,还是我误解了 C++ 规范? 这同样适用于 C 吗?

编辑:

在评论之后,我假设以下修改仍然是未定义的行为,因为在生命周期结束后使用 object:

ptrdiff_t offset = (char*)(copy->b) - (char*)original;
StructA* a = (StructA*)((char*)copy + offset);

在使用索引时是否会定义它:

typedef struct StructB {
    StructA a;
    ptrdiff_t b_offset;
} StructB;

int main() {
    StructB* original = (StructB*)malloc(sizeof(StructB));
    original->a.a = 5;
    original->b_offset = (char*)&(original->a) -  (char*)original

    StructB* copy = (StructB*)malloc(sizeof(StructB));
    memcpy(copy, original, sizeof(StructB));
    free(original);
    StructA* a = (StructA*)((char*)copy + copy->b_offset);
    printf("%i\n", a->a);
    free(copy);
}

这是未定义的行为,因为对指针算术可以做什么有严格的限制。 您所做的编辑和建议的编辑对解决此问题没有任何作用。

此外未定义的行为

StructA* a = (StructA*)((char*)copy + offset);

首先,由于添加到copy上,这是未定义的行为:

当具有整数类型的表达式 J 被添加到指针类型的表达式 P 中或从其中减去时,结果具有 P 的类型。

  • (4.1) 如果 P 计算结果为 null 指针值且 J 计算结果为 0,则结果为 null 指针值。
  • (4.2) 否则,如果 P 指向具有 n 个元素 ( [dcl.array] ) 的数组 object x 的数组元素 i,则表达式 P + J 和 J + P(其中 J 的值为 j)指向 (如果 0 ≤ i + j ≤ n,则 x 的可能-假设的)数组元素 i+j 并且如果 0 ≤ i - j ≤ n,则表达式 P - J 指向 x 的(可能-假设的)数组元素 i - j。
  • (4.3)否则,行为未定义。

https://eel.is/c++draft/expr.add#4

简而言之,对非数组和非空指针执行指针运算始终是未定义的行为。 即使copy或其成员是 arrays,添加到指针上使其变为:

  • 超过数组末尾的两个或多个
  • 至少在第一个元素之前

也是未定义的行为。

减法中的未定义行为

ptrdiff_t offset = (char*)original - (char*)(copy->b);

你的两个指针的减法也是未定义的行为:

当两个指针表达式 P 和 Q 相减时,结果的类型是实现定义的有符号整数类型; [...]

  • (5.1) 如果 P 和 Q 都计算为 null 指针值,则结果为 0。
  • (5.2) 否则,如果 P 和 Q 分别指向同一数组 object x 的数组元素 i 和 j,则表达式 P - Q 的值为 i - j。
  • (5.3)否则,行为未定义。

https://eel.is/c++draft/expr.add#5

因此,当它们不是 null 或指向同一数组的元素的指针时,相互减去指针是未定义的行为。

C 中的未定义行为

C 标准有类似的限制:

(8) [...] 如果指针操作数指向数组 object 的元素,并且数组足够大,则结果指向与原始元素偏移的元素,使得结果和下标的差异原始数组元素等于 integer 表达式。

(标准没有提到非数组指针添加会发生什么)

(9) 当两个指针相减时,都指向同一个数组 object 的元素,或数组 object 的最后一个元素的后一个; [...]

请参阅C11 标准 (n1570)中的 §6.5.6 加法运算符。

改用数据成员指针

C++ 中一个干净且类型安全的解决方案是使用数据成员指针。

typedef struct StructB {
    StructA a;
    StructA StructB::*b_offset;
} StructB;

int main() {
    StructB* original = (StructB*) malloc(sizeof(StructB));
    original->a.a = 5;
    original->b_offset = &StructB::a;

    StructB* copy = (StructB*) malloc(sizeof(StructB));
    memcpy(copy, original, sizeof(StructB));
    free(original);
    printf("%i\n", (copy->*(copy->b_offset)).a);
    free(copy);
}

笔记

标准引用来自 C++ 草案。 您引用的 C++11 似乎对指针运算没有任何更宽松的限制,只是格式不同。 参见C++11 标准 (n3337)

该标准明确规定,在它表征为未定义行为的情况下,实现可以“以环境的文档化方式特征”运行。 根据基本原理,这种特征化的目的之一是确定“符合语言扩展”的途径; 何时实现支持这种“流行的扩展”的问题是最好留给市场的实现质量问题。

许多旨在和/或配置用于普通平台上的低级编程的实现通过指定以下等价性来扩展语言,对于T*类型的任何指针pq以及 integer 表达式i

  • p(uintptr_t)p(intptr_t)p的位模式是相同的。
  • p+i等价于(T*)((uintptr_t)p + (uintptr_t)i * sizeof (T))
  • pi等价于(T*)((uintptr_t)p - (uintptr_t)i * sizeof (T))
  • 在除法没有余数的所有情况下, pq等价于((uintptr_t)p - (uintptr_t)q) / sizeof (T)
  • p>q等价于(uintptr_t)p > (uintptr_t)q ,对于所有其他关系和比较运算符也是如此。

该标准不承认始终支持这些等价的任何类别的实现,与那些不支持的实现不同,部分原因是他们不希望将这种支持等价的不切实际的不寻常平台描述为“劣质”实现。 相反,它希望在有意义的实现上支持此类实现,并且程序员会知道他们何时瞄准此类实现。 为 68000 或小型 8086(自然会存在这种等价性)编写内存管理代码的人可以编写 memory 管理代码,这些代码可以在这些等价性成立的其他系统上互换运行,但有人为大型 8086 需要为该平台明确设计它,因为这些等价不成立(指针为 32 位,但单个对象限制为 65520 字节,大多数指针操作仅作用于指针的底部 16 位)。

不幸的是,即使在这种等价通常成立的平台上,某些类型的优化也可能产生与这些等价所暗示的不同的极端情况行为。 商业编译器通常秉承 C 原则“不妨碍程序员做该做的事”的精神,即使启用了大多数优化,也可以配置为支持等价。 然而,gcc 和 clang C 编译器不允许对语义进行这种控制。 当所有优化都被禁用时,它们将在普通平台上支持这些等价性,但没有其他优化设置可以阻止它们做出与它们不一致的推论。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM