在今天写C代码的过程中,发现了一个奇怪的现象:
- 假设我们有如下数据结构:
- struct AddressMapping
- {
- u64 lba;
- union{
- u64 addr;
- u64 addrPage;
- struct ScatterIter sgiter;
- };
- u64 size;
- }__attribute__ ((__packed__));
-
-
- struct ScatterIter{
- struct ScatterList *head;
- u16 flag;
- u16 idx;
- u32 offset;
- };
强调一下,AddressMapping数据结构包含了一个匿名联合体,在该匿名联合体中,又包含了一个结构体。
然后,我想要对这个结构体赋初值,因为这个联合体总长为2个64位的数据,且应该是长这个模样:
那么,如果我既想要把sgiter.flag赋值为1,又想要赋值addr字段,实际上从内存角度来说,他们是不是不应该冲突的呢,是的,原理上来说,完全不会存在冲突。
好的,现在我打算将这个union的前64位赋值为data,sgiter.flag赋值为1,请问,你认为以下的赋值方式,是否能达到想要的效果,或者它们各会达到什么样的效果?
case1:
- struct AddressMapping stripMapping = {
- .sgiter.flag = 1,
- .addr = (u64)data,
- .size = depth * BLK_SIZE
- };
case2:
- struct AddressMapping stripMapping = {
- .addr = (u64)data,
- .sgiter.flag = 1,
- .size = depth * BLK_SIZE
- };
case3:
- struct AddressMapping stripMapping = {
- .sgiter.flag = 1,
- .sgiter.head = (u64)data,
- .size = depth * BLK_SIZE
- };
结果可能超乎你想象,以上3个case,将会得到三个结果,它们完全不一样。
对于case1,运行代码后观察到,对于sgiter.flag的赋值失败了,且赋值0,对于case2,运行代码后观察到,对于addr的赋值失败了且赋值0,对于case3,运行代码后观察到,符合我们的需求。
于是我们询问一下高材生ChatGPT,看看它对自己的祖师爷C语言有没有这方面的了解:
问题:我想知道 对于 type a = {.xxx = xxx}这样的赋初值,编译器是如何处理的?
回答是:
对于类似.xxx = xxx
这样的赋初值语法,它是由C语言提供的一种结构体初始化语法,通常称为"设计器初始化器"(designated initializer)。
设计器初始化器允许您通过指定结构体成员的名称来选择性地初始化特定成员,而不需要按照结构体定义中成员的顺序进行初始化。这在初始化复杂的结构体时非常有用。
当编译器遇到设计器初始化器时,它会根据指定的成员名称来识别并初始化对应的成员。编译器会使用指定的成员名称来匹配结构体定义中的成员,并为其赋予指定的值。
以下是一个示例说明:
- struct ExampleStruct {
- int a;
- int b;
- int c;
- };
-
-
- int main() {
- struct ExampleStruct example = {
- .b = 2,
- .a = 1,
- .c = 3
- };
- return 0;
- }
在这个示例中,ExampleStruct
结构体具有三个整型成员a
、b
和c
。在main
函数中,通过使用设计器初始化器,我们可以选择性地为结构体的成员赋值。在这种情况下,我们为成员b
赋值2,a
赋值1,c
赋值3。请注意,我们可以以任意顺序指定成员的初始化值。
编译器会根据指定的成员名称和值,将正确的赋值操作应用于对应的结构体成员。这样,您可以通过设计器初始化器来初始化结构体的特定成员,而不需要关心成员在结构体定义中的顺序。
需要注意的是,设计器初始化器是C99标准引入的特性,因此旧版本的编译器可能不支持该语法。请确保您的编译器支持C99标准或更高版本。
追问:请告诉我更多关于设计器初始化器的信息。
回答:
当使用设计器初始化器时,您可以选择性地为结构体或联合体的成员指定初始化值。这种初始化语法更加灵活,可以按照成员的名称进行初始化,而不需要按照定义的顺序。
以下是一些关于设计器初始化器的更多信息:
语法格式:设计器初始化器使用大括号({}
)来表示初始化器,并在其中使用点操作符(.
)来指定成员名称和对应的初始化值。语法为:.member = value
。
初始化顺序:设计器初始化器允许您以任意顺序指定成员的初始化值。编译器会根据指定的成员名称来匹配并初始化对应的成员。
重复赋值:在设计器初始化器中,如果多次为同一个成员指定初始化值,最后一次赋值将起作用。这意味着后面的初始化值将覆盖前面的值。
未指定的成员:如果在设计器初始化器中没有为某个成员指定初始化值,那么该成员将使用其定义时指定的默认值(如果有)进行初始化。对于基本数据类型,未初始化的成员将具有未定义的值。
嵌套结构体和联合体:设计器初始化器可以嵌套使用,以初始化嵌套结构体或联合体的成员。您可以使用嵌套的设计器初始化器为内部结构体或联合体的成员指定初始化值。
兼容性:设计器初始化器是C99标准引入的特性,因此旧版本的C编译器可能不支持该语法。请确保使用支持C99或更高版本的编译器。
通过使用设计器初始化器,您可以以更直观和灵活的方式初始化结构体和联合体的成员。这对于具有许多成员或具有复杂嵌套结构的数据结构特别有用。
似乎让我们明白了我们的问题,当对于设计器初始化器(网上有翻译为定制初始化),对于相同成员,只有最后一个起效,addr与sgiter为相同成员,编译器可能会自动略过对前一个的处理。
为了进一步探究这个问题,我又去翻了翻C99标准,里面有这么一句话:
翻译过来就是除非另有明确说明,否则就本子句而言,结构和联合类型的对象的未命名成员不参与初始化。结构对象的未命名成员即使在初始化后也具有不确定的值。
也就是说对匿名的联合体赋值似乎是一个未定义行为,照我们的实验来看,更倾向于是略过了相同成员中的前者,直接处理后者。
ChatGPT也给出了解决方案:
在您的情况下,要为addr
字段赋值,您需要通过联合体本身进行操作。以下是一种可能的解决方案:
- struct AddressMapping mapping = {
- .lba = 0, // 为其他字段设置适当的初始值
- .size = BLK_SIZE
- };
-
-
- mapping.addr = (u64)data;
- mapping.sgiter.flag = 1;
也就是不要对它初始化,到后面再赋值就好了。