题 c99中的静态struct初始化


在使用复合文字进行静态结构初始化时,我遇到了一个奇怪的行为 GCC 在 c99/gnu99 模式。

显然这很好:

struct Test
{
    int a;
};

static struct Test tt = {1}; /* 1 */

但是,这不是:

static struct Test tt = (struct Test) {1}; /* 2 */

这会触发以下错误:

初始化元素不是常量

这也无济于事:

static struct Test tt = (const struct Test) {1}; /* 3 */

我确实理解静态结构的初始化值应该是编译时常量。但我不明白为什么这个最简单的初始化表达式不再被认为是常量?这是由标准定义的吗?

我问的原因是我在gnu90模式下遇到了一些用GCC编写的遗留代码,它使用了这种复合文字结构进行静态结构初始化(2)。显然,这是当时的GNU扩展,后来被C99采用。

现在它导致成功编译的代码 GNU90 无法编译 C99,甚至 GNU99

他们为什么要这样对我?


15
2017-08-06 18:42


起源


顺便说一句, 已在GCC 5中修复 (因为Linux内核正在使用此扩展但它没有使用 -std=gnu89 和GCC 5将默认值更改为gnu11)。 - cremno
如果你想使用const结构 static const struct Test tt = {1}; - cup
@cup - OP提到了这一点,但这不是他们的问题。他们的问题是“为什么”,而不是“如何”。 - Mr. Llama
@ Mr.Llama都是一样的:在解决问题方面,最好的办法是不要使用冗余(最好)的文字,只需编写简单的代码。 - M.M
@cup它应该是 static struct Test tt = { 1 };  。原始代码显然是定义非const对象。 - M.M


答案:


C语言依赖于什么是确切的定义 不断表达。仅仅因为看起来“在编译时已知”并不意味着它满足正式的定义 不断表达

C语言没有定义 常数表达式 非标量类型。它允许实现引入自己的常量表达式,但标准定义的常量表达式仅限于标量类型。

换句话说,C语言没有定义的概念 不断表达  为你的类型 struct Test。任何价值 struct Test 不是一个常数。你的复合文字 (struct Test) {1} 不是常量(并且不是字符串文字),因此,它不能用作具有静态存储持续时间的对象的初始化程序。添加一个 const 因为在C中它的限定符不会改变任何东西 const 限定符没有关系 任何 的概念 不断表达。在这种情况下,它永远不会有任何区别。

请注意,您的第一个变体根本不涉及复合文字。它使用原始的 { ... } 内置常量表达式的初始化语法。具有静态存储持续时间的对象明确允许这样做。

所以,在最严格的意义上,用复合文字初始化是非法的,而用普通的初始化 { ... } 初始化器很好。某些编译器可能会接受复合文字初始化作为扩展。 (通过扩展的概念 不断表达 或采取其他一些延伸路径。请参阅编译器文档以找出它编译的原因。)


4
2017-08-06 21:50



谢谢,现在对我来说更清楚了。太糟糕了,GCC没有放松那个限制,直到5.2。 - Ganil


这个 是/是一个gcc bug (HT到cremno),错误报告说:

我相信我们应该只允许使用static初始化对象   即使在gnu99 / gnu11中,复合文字的存储持续时间。 [...]   (但警告 - 但是。)

我们可以从中看到 关于复合文字的gcc文档应支持具有静态存储持续时间的对象的初始化作为扩展:

作为GNU扩展,GCC允许使用static初始化对象   复合文字的存储持续时间(这在ISO中是不可能的)   C99,因为初始化器不是常数)。

这是固定的 gcc 5.2。所以,在 gcc 5.2 你只会在使用时收到此警告 -pedantic 旗 看到它,没有抱怨 -pedantic

运用 -pedantic 意思是 gcc应该 按标准要求提供诊断:

要获得标准所要求的所有诊断,您应该   如果你想要它们,也可以指定-pedantic(或-pedantic-errors)   错误而不是警告)

复合文字不是C99标准草案部分所涵盖的常量表达式 6.6 常量表达式,我们从节中看到 6.7.8 初始化:

具有静态存储持续时间的对象的初始值设定项中的所有表达式都应为   常量表达式或字符串文字。

允许gcc从section开始接受其他形式的常量表达式作为扩展 6.6

实现可以接受其他形式的常量表达式。

有趣的是,clang并没有抱怨这个使用 -pedantic


10
2017-08-06 18:56



非常感谢,我知道有些不对劲。现在我只需要等待GCC 5.2移植到我的嵌入式平台上:D - Ganil


有趣的是, clang 不会抱怨这段代码,即使是 -pedantic-errors 旗。

这肯定是关于C11§6.7.9/ p4 初始化 (强调我的前进)

初始化程序中具有静态或。的对象的所有表达式   线程存储持续时间应为 常数表达式 或字符串   文字。

另一个要研究的子条款是§6.5.2.5/ p5 复合文字

复合文字的值是一个的值 未命名的对象   由初始化列表初始化。如果出现复合文字   在函数体外,对象具有 静态存储   持续时间;否则,它具有与之关联的自动存储持续时间   封闭的块。

和(完整性)§6.5.2.5/ p4:

在任何一种情况下,结果都是 左值

但这并不意味着,这样的未命名对象可以被视为 不断表达。 §6.6 常量表达式 除其他外说:

2)在翻译期间可以评估常量表达式   比运行时,因此可以在任何恒定的地方使用   也许。

3)常量表达式不应包含赋值,增量,   递减,函数调用或逗号运算符,除非它们是   包含在未评估的子表达式中。

10)实现可以接受其他形式的常量表达式。

虽然没有明确提及复合文字,因此我会解释这一点,它们作为常量表达式无效 严格遵守计划 (因此,我会说,那 clang有一个错误)。

第J.2节 未定义的行为 (资料性的)还澄清了:

初始值设定项中的常量表达式未评估或未评估   to,以下之一:算术常量表达式,null   指针常量,地址常量或地址常量   完整对象类型加上或减去整数常量表达式   (6.6)。

再一次,没有提到复合文字。

毫无疑问,隧道里有一盏灯。完全消毒的另一种方式是传达这样的未命名对象 地址常数。标准在§6.6/ p9中指出:

一个 地址常数 是一个空指针,指向一个指针 左值   指定静态存储持续时间的对象,或指向   功能指示器;它应该使用一元明确创建 &   运算符或整数常量强制转换为指针类型,或隐式地通过   使用数组或函数类型的表达式。该   阵列下标 [] 和成员访问 . 和 -> 运营商,地址 &   和间接 * 可以使用一元运算符和指针强制转换   创建一个地址常量,但对象的值应该是   不能通过使用这些运算符访问。

因此你可以 安然 用这种形式的常量表达式初始化它,因为这样的复合文字确实指定了一个对象的左值,它具有静态存储持续时间:

#include <stdio.h>

struct Test
{
    int a;
};

static struct Test *tt = &((struct Test) {1}); /* 2 */

int main(void)
{
    printf("%d\n", tt->a);

    return 0;
}

如果检查它编译好 -std=c99 -pedantic-errors 两者上的标志 gcc 5.2.0和 clang 3.6。

注意,与C ++相反,在C中 const 限定符对常量表达式没有影响。


4
2017-08-06 19:08





ISO C99  支持复合文字(根据这个)。但是,目前 只要 GNU扩展提供了对象的初始化 静态存储持续时间 通过复合文字,但仅适用于C90和C ++。

复合文字看起来像包含初始值设定项的强制转换。它的值是转换中指定类型的对象,包含初始值设定项中指定的元素;这是一个左值。作为延伸, GCC支持C90模式和C ++中的复合文字, 虽然C ++中的语义有些不同。

通常,指定的类型是结构。假使,假设 struct foo 和结构声明如图所示

 struct foo {int a; char b[2];} structure;

这是构建一个的例子 struct foo 复合文字:

 structure = ((struct foo) {x + y, 'a', 0});

这相当于编写以下内容:

 {
   struct foo temp = {x + y, 'a', 0};
   structure = temp;
 }

GCC扩展
作为GNU扩展,GCC允许通过复合文字初始化具有静态存储持续时间的对象(这在ISO C99中是不可能的,因为初始化器不是常量)。如果复合文字和对象的类型匹配,则处理就好像仅使用括号括起列表初始化对象一样。复合文字的初始化列表必须是常量。如果正在初始化的对象具有未知大小的数组类型,则大小由复合文字大小确定。

 static struct foo x = (struct foo) {1, 'a', 'b'};
 static int y[] = (int []) {1, 2, 3};
 static int z[] = (int [3]) {1};

注意:
您帖子上的编译器标签仅包含GCC;但是,您可以与C99(以及多个GCC版本)进行比较。值得注意的是,GCC比其他更大的C标准组更快地为其编译器添加扩展功能。这有时会导致错误的行为和版本之间的不一致。同样重要的是要注意,对于众所周知且受欢迎的编译器的扩展,但是不符合公认的C标准,会导致潜在的非可移植代码。在决定使用尚未被较大的C工作组/标准组织接受的扩展时,始终值得考虑目标客户。 (看到 ISO(维基百科) 和 ANSI(维基百科)。)

有几个例子,较小的更灵活的开源C工作组或委员会通过添加扩展来响应用户群表示的兴趣。例如, 开关盒范围扩展


3
2017-08-06 19:03



很多很好的信息,但是太多了 胆大 和 斜体 和 斜体大胆 易于阅读。我认为你应该重新调整重点。 - Jonathan Leffler
@JonathanLeffler - 是的,我确实被带走了。谢谢你的编辑。 - ryyker
开关盒扩展偶尔也很有用。我更感兴趣的是 '指定初始化程序范围' 扩展标准化(要将一系列元素初始化为相同的值,请写入 [first ... last] = value。这是一个GNU扩展。例如, int widths[] = { [0 ... 9] = 1, [10 ... 99] = 2, [100] = 3 };) - Jonathan Leffler


引用 C11 标准,章节§6.5.2.5, 复合文字,第3段,(强调我的

后缀表达式由带括号的类型名称后跟括号括起的初始值设定项列表组成 复合字面量它提供了一个未命名的对象,其值由初始化列表给出。

因此,复合文字作为未命名的对象,它不被视为编译时常量。

就像你不能使用另一个 变量 要初始化一个静态变量,向前C99,你不能再使用这个复合文字来初始化一个静态变量。


1
2017-08-06 19:03



@ user3386109:C99和C11说同样的话。该对象具有静态存储持续时间,但其值不是常量表达式,因此不能在静态对象的初始化程序中使用它。 (我试图弄清楚如何使用具有静态存储持续时间的复合文字。) - Keith Thompson
按照我之前的评论,我认为你不能有意义地使用 值 一个静态复合文字,但你可以使用它的地址(这是一个常量地址)。例如: struct foo *foo_ptr = &(struct foo){.x = 10, .y = 20}; int *int_ptr = (int[]){100, 200, 300};。两个指针都指向与复合文字相关联的静态对象(或者指向它的第一个元素) int_ptr)。 - Keith Thompson