簡體   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