[英]Can a struct have a member which is a pointer to a struct of the same type?
[英]Can a pointer to a member of a struct be used to access another member of the same struct?
我試圖理解當將值存儲到結構或聯合的成員中時,類型懲罰是如何工作的。
標准N1570 6.2.6.1(p6)
規定了
當值存儲在結構或聯合類型的對象中(包括在成員對象中)時,對應於任何填充字節的對象表示的字節采用未指定的值。
所以我把它解釋為好像我們有一個對象存儲到一個成員中,使得對象的大小等於sizeof(declared_type_of_the_member) + padding
與padding相關的字節將具有未指定的值(即使我們已經擁有了定義原始對象中的字節)。 這是一個例子:
struct first_member_padded_t{
int a;
long b;
};
int a = 10;
struct first_member_padded_t s;
char repr[offsetof(struct first_member_padded_t, b)] = //some value
memcpy(repr, &a, sizeof(a));
memcpy(&(s.a), repr, sizeof(repr));
s.b = 100;
printf("%d%ld\n", s.a, s.b); //prints 10100
在我的機器sizeof(int) = 4, offsetof(struct first_member_padded_t, b) = 8
。
是否為這樣的程序定義了打印10100
的行為? 我覺得它是。
這個問題很糟糕。 我們先來看看代碼:
char repr[offsetof(struct first_member_padded_t, b)] = //some value
memcpy(repr, &a, sizeof(a));
memcpy(&(s.a), repr, sizeof(repr));
首先請注意, repr
已初始化,因此其中的所有元素都是給定的值。
第一個memcpy
很好 - 它將a
的字節復制到repr
。
如果第二個memcpy
是memcpy(&s, repr, sizeof repr);
,它會將repr
字節復制到s
。 這會將字節寫入sa
並且由於repr
的大小,將其寫入sa
和sb
之間的任何填充。 根據標准的其他標准,允許訪問對象的字節(“訪問”表示讀取和寫入,每3.1 1)。 因此,將此副本復制到s
就可以了,並且它會導致sa
與a
具有相同的值。
但是, memcpy
使用&(sa)
而不是&s
。 它使用sa
的地址而不是s
的地址。 我們知道將sa
轉換為指向字符類型的指針將允許我們訪問sa
(6.5 7及更多)的字節(並將其傳遞給memcpy
與此類轉換具有相同的效果,因為指定memcpy
具有效果復制字節),但不清楚它允許我們訪問s
其他字節。 換句話說,我們有一個問題是我們是否可以使用&s.a
來訪問sa
字節以外的字節。
6.7.2.1 15告訴我們,如果指向結構的第一個成員的指針被“適當轉換”,則結果指向結構。 所以,如果我們轉換&s.a
的指針struct first_member_padding_t
,它會指向s
,我們當然可以用一個指針s
訪問所有字節s
。 因此,這也將很好地定義:
memcpy((struct first_member_padding t *) &s.a, repr, sizeof repr);
但是, memcpy(&s.a, repr, sizeof repr);
只將&s.a
轉換為void *
(因為memcpy
聲明為void *
,因此&s.a
在函數調用期間自動轉換)而不是指向結構類型的指針。 這是一個合適的轉換嗎? 注意,如果我們做了memcpy(&s, repr, sizeof repr);
,它會將&s
轉換為void *
。 6.2.5 28告訴我們指向void
的指針與指向字符類型的指針具有相同的表示形式。 請考慮以下兩個陳述:
memcpy(&s.a, repr, sizeof repr);
memcpy(&s, repr, sizeof repr);
這兩個語句都將void *
傳遞給memcpy
,這兩個void *
具有相同的表示形式,並指向相同的字節。 現在,我們可以迂腐而嚴格地解釋標准,以便它們不同,因為后者可用於訪問s
所有字節而前者可能不會。 然后奇怪的是,我們有兩個必然相同的指針,表現不同。
理論上對C標准的這種嚴格解釋似乎是可能的 - 指針之間的差異可能在優化期間而不是在memcpy
的實際實現中出現 - 但我不知道任何編譯器會這樣做。 請注意,這種解釋與標准的第6.2節不一致,后者告訴我們類型和表示。 解釋標准使得(void *) &s.a
和(void *) &s
表現不同意味着具有相同值和類型的兩個事物可能表現不同,這意味着一個值包含的值超過其值和類型,似乎一般不是6.2或標准的意圖。
問題是:
我試圖理解當將值存儲到結構或聯合的成員中時,類型懲罰是如何工作的。
這不是類型懲罰,因為這個術語是常用的。 從技術上講,代碼使用與其定義不同類型的左值來訪問sa
(因為它使用memcpy
,它被定義為像字符類型一樣進行復制,而定義的類型是int
),但是字節來自int
並且是復制而不修改,這種復制對象的字節通常被視為機械過程; 它完成了一個副本而不是重新解釋一個新類型的字節。 “類型懲罰”通常是指使用不同的左值來重新解釋值,例如編寫unsigned int
和讀取float
。
無論如何,打字並不是問題的主題。
標題問:
我們可以在結構或聯盟成員中存儲什么值?
這個標題似乎與問題的內容有關。 標題問題很容易回答:我們可以存儲在成員中的值是成員類型可以表示的值。 但問題是繼續探討成員之間的填充。 填充不會影響成員中的值。
問題引用了標准:
當值存儲在結構或聯合類型的對象中(包括在成員對象中)時,對應於任何填充字節的對象表示的字節采用未指定的值。
並說:
所以我把它解釋為好像我們有一個對象存儲到一個成員中,使得對象的大小等於s
izeof(declared_type_of_the_member) + padding
與填充相關的字節將具有未指定的值...
標准中引用的文本意味着,如果s
的填充字節已設置為某些值,則與memcpy
,然后我們執行sa = something;
,然后不再需要填充字節來保存其先前的值。
問題中的代碼探討了不同的情況。 代碼memcpy(&(sa), repr, sizeof(repr));
在6.2.6.1中意義上的結構成員中不存儲值。它不存儲在成員sa
或sb
。 它正在復制字節,這與6.2.6.1中討論的不同。
6.2.6.1 6表示,例如,如果我們執行此代碼:
char repr[sizeof s] = { 0 };
memcpy(&s, repr, sizeof s); // Set all the bytes of s to known values.
s.a = 0; // Store a value in a member.
memcpy(repr, &s, sizeof s); // Get all the bytes of s to examine them.
for (size_t i = sizeof s.a; i < offsetof(struct first_member_padding_t, b); ++i)
printf("Byte %zu = %d.\n", i, repr[i]);
那么所有的零都不會被打印 - 填充中的字節可能已經改變了。
在編寫C標准的語言的許多實現中,嘗試在結構或聯合內編寫N字節對象將影響結構或聯合內最多N個字節的值。 另一方面,在支持8位和32位存儲但不支持16位存儲的平台上,如果有人聲明了類似的類型:
struct S { uint32_t x; uint16_t y;} *s;
然后執行s->y = 23;
如果不關心y
的兩個字節發生了什么,那么對y
執行32位存儲會更快,盲目地覆蓋它后面的兩個字節,而不是執行一對8位寫操作來更新上部和下部的一半y
。 該標准的作者不希望禁止這種治療。
如果標准包含了一種方法,通過該方法可以指示對結構或聯合成員的寫入是否會干擾超出它們的存儲,並且可能會被這種干擾打破的程序可能拒絕在可能發生的實現上運行,這將是有用的。 然而,該標准的作者可能期望那些對這些細節感興趣的程序員會知道他們的程序預期會運行什么類型的硬件,從而知道這種內存干擾是否會成為這類硬件的問題。
不幸的是,現代編譯器編寫者似乎解釋了旨在幫助實現異常硬件的自由作為一種開放的邀請來獲得“創造性”,即使是在沒有這種讓步的情況下有效處理代碼的平台。
正如@ user694733所說,如果在sa
和sb
之間存在填充, memcpy()
正在訪問一個無法通過&a
訪問的內存區域:
int a = 1;
int b;
b = *((char *)&a + sizeof(int));
這是未定義的行為,它基本上是在memcpy()
發生的事情。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.