2014-02-12 31 views
4

我似乎無法將自己的頭圍繞在C標準的某些部分,所以我來這裏是爲了澄清當我必須考慮這些技巧是如何定義行爲的時候出現的那種模糊,焦慮的不確定性,以及未定義或違反標準。我不在乎它是否有效,我關心C標準是否認爲它是合法的,明確的行爲。標準定義了什麼類型的雙關/指針魔術?

像這樣的,對此我相當肯定是UB:

struct One 
{ 
     int Hurr; 
     char Durr[2]; 
     float Nrrr; 
} One; 

struct Two 
{ 
     int Hurr; 
     char Durr[2]; 
     float Nrrr; 
     double Wibble; 
} Two; 

One = *(struct One*)&Two; 

這還不是全部,我在說什麼。比如把指針指向One來int *,並將它解引用等。我想很好地理解這些東西是如何定義的,所以我可以在晚上睡覺。如果可以的話,請在標準中引用地點,但一定要指定它是C89還是C99。對於IMHO這樣的問題,C11太新了,無法信任。

+0

我認爲這是將兩個變量的前三個變成一個,但我沒有測試它。無論如何,如果這是你想要的,分別移動每個變量。這是「好方法」。 – Abend

+3

我不想完成一項任務,我想更好地理解有關該語言的晦澀難懂的細節。 – Subsentient

+2

我強烈建議把'language-lawyer'標籤放在這個標籤上。這是你想要達到的觀衆。 – keshlam

回答

0

C99 6.7.2.1說:

帕拉5

如在6.2.5所討論的,這樣的結構是一種類型的成員組成的序列 的,其存儲在一個有序序列被分配

第12段

Ë結構或聯合對象的非位域成員按照與其類型相適應的實現定義的方式對齊到 。

帕拉13

內的結構對象,非比特音響場成員和 其中比特連接的視場的單元存在具有在聲明它們在 的順序增加的地址。指向結構對象的指針(適當地爲 轉換)指向其初始成員(或者如果該成員是位域,則指向其所在的單位),反之亦然。有 可能是一個結構對象中的無名進行填充,而不是在其 開始

最後一段涵蓋了您的第二個問題(鑄造的指針指向一個到int *,並解引用它)。

第一點 - 是否有效將某個Two*「Downcast」爲One* - 我找不到具體的地址。歸結到其他規則是否確保One的字段和Two的初始字段的內存佈局在所有情況下都是相同的。

成員必須以有序順序打包,開始時不允許填充,並且必須根據類型進行對齊,但標準實際上並未聲明佈局需要相同(即使在大多數編譯器中,我確定它是)。

有,然而,一個更好的方式來定義這些結構,這樣就可以保證它:

struct One 
{ 
     int Hurr; 
     char Durr[2]; 
     float Nrrr; 
} One; 

struct Two 
{ 
     struct One one; 
     double Wibble; 
} Two; 

你可能會認爲你現在可以安全地投下Two*One* - 第13段是這麼說的。然而strict aliasing可能會讓你感到不愉快。但是對於上面的示例,您不需要:

One = Two.one; 
+0

錯字:「C98」..... –

+0

@Subsentient進一步反思,並重新閱讀標準,我不認爲該標準保證它會起作用。我已經改變了我的答案,展示了一種更好的方式來實現你正在努力達到的目標。 – harmic

+0

@PascalCuoq謝謝,很好。相應更新 – harmic

1

我認爲在技術上這個例子也是UB。但它幾乎肯定會奏效,gcc和clang都不會用-pedantic來抱怨。

首先,下面是在C99(§ 6.5.2.3/6)明確定義的:[1]

union OneTwo { 
    struct One one; 
    struct Two two; 
}; 

OneTwo tmp = {.two = {3, {'a', 'b'}, 3.14f, 3.14159} }; 
One one = tmp.one; 

在訪問 「punned」 struct One通過union必須工作意味着事實上struct Two前綴的佈局與struct One相同。這不能取決於是否存在union,因爲給定的複合類型只能有一個存儲佈局,其佈局不能取決於其在union中的使用情況,因爲union不需要對每個翻譯單元都可見使用struct。此外,在C中,所有類型不超過一個字節序列(例如,與C++不同)(§ 6.2.6.1/4)[2]。因此,下面的內容也是保證工作:

struct One one; 
struct Two two = ...; 
unsigned char tmp[sizeof one]; 
memcpy(tmp, two, sizeof one); 
memcpy(one, tmp, sizeof one); 

鑑於上述情況和任何指針類型的一個void*可兌換,我認爲這是合理的結論是上述臨時存儲是不必要的,它可能有直接寫爲:通過別名指針在OP

struct One one; 
struct Two two = ...; 
unsigned char tmp[sizeof one]; 
memcpy(one, two, sizeof one); 

從那裏直接分配至於是不是一個非常大的飛躍,但對於別名指向另外一個問題:它在理論上是可能的指針轉換來創建一個無效指針,因爲它可能是bi t格式的struct Two*不同於struct One*。儘管將一個指針類型轉換爲寬鬆對齊的另一個指針類型(§ 6.3.2.3/7)[3]然後再次轉換它是合法的,但不保證轉換的指針實際可用,除非轉換是到一個字符類型。具體而言,struct Two的比對有可能與struct One的比對不同(更嚴格),並且更強對齊的指針的比特格式不能直接用作指向不太強對齊的結構的指針。然而,很難看到與幾乎相同的論點:

one = *(struct One*)(void*)&two; 

雖然這可能沒有明確保證的標準。

在評論中,不同的人提出了別名優化的幽靈。上面的討論根本不涉及別名,因爲我認爲這與簡單的賦值無關。該任務必須在任何前面的表達之後並在任何後續表達之前排序;它清楚地修改了one並且幾乎如同清楚地引用two。高度懷疑優化使得two之前的法律變異對作業不可見。

但是通常情況下可以使用別名優化。因此,即使所有上述指針轉換在單個賦值表達式的上下文中都應該是可接受的,當然而不是是合法的行爲來保留類型爲struct One*的轉換的指針,該指針實際上指向struct Two類型的對象並期望它可用於改變其目標成員或訪問其目標的成員,否則該成員會發生變異。使用指向struct One的指針就好像它是指向struct Two前綴的指針的唯一上下文是當兩個對象重疊在union中時。

---標準的引用:

[1]「,如果一個聯合包含共享公共初始序列幾種結構(見下文),並且如果聯合對象當前包含這些結構中的一個,它被允許檢查他們任何一個共同的初始部分,任何地方都可以看到完整的工會類型的聲明。「

[2]「存儲在任何其他對象類型的非位字段對象中的值由n×CHAR_BIT 位組成,其中n是該類型對象的大小(以字節爲單位),可以複製該值到 類型爲unsigned char [n]的對象(例如,通過memcpy)…「 [3]「指向對象類型的指針可能會轉換爲指向不同對象類型的指針;當指向對象的指針轉換爲指向字符類型的指針時,結果指向最低尋址的字節連續遞增的結果,直到對象的大小,產生指向對象剩餘字節的指針。「

+1

「這就是爲什麼我相信你的例子幾乎肯定會工作」別名優化不會注意到或關心兩個指針指向具有相同內存佈局的事物,只是它們指向不同的,不兼容的類型。 – tab

-1

A1。未定義的行爲,因爲Wibble。 A2。定義。

S9.2 in N3337。

兩個標準佈局結構(第9節)類型是佈局兼容如果 它們具有相同數量的非靜態數據成員和相應的 非靜態數據成員(聲明順序)具有佈局兼容 類型

您的結構將佈局兼容,因此可以互換,但對於Wibble。還有一個很好的理由:Wibble可能在struct Two中導致不同的填充。

指向一個標準佈局結構對象,使用 一個的reinterpret_cast適當轉換,點到它的初始成員(或如果該構件是 一個位域,然後以在其駐留的單元)和反之亦然。

我認爲這可以保證你可以解引用初始int。

+1

問題被標記爲C,而不是C++。 – dyp