【问题标题】:static struct initialization in c99c99中的静态结构初始化
【发布时间】:2015-10-30 01:23:27
【问题描述】:

GCCc99/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编译。

他们为什么要这样对我?

【问题讨论】:

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

标签: c gcc struct compound-literals


【解决方案1】:

这个is/was a gcc bugHT to cremno),错误报告说:

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

我们从gcc document on compound literals可以看出,作为扩展应该支持对具有静态存储时长的对象的初始化:

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

这已在gcc 5.2 中修复。因此,在gcc 5.2 中,您只会在使用-pedantic 标志see it live 时收到此警告,如果没有-pedantic,它不会抱怨。

使用-pedantic 意味着gcc should 提供标准要求的诊断:

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

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

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

gcc 允许接受其他形式的常量表达式作为扩展,来自6.6 部分:

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

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

【讨论】:

  • 非常感谢,我知道有些不对劲。现在我只需要等待 GCC 5.2 移植到我的嵌入式平台上:D
【解决方案2】:

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

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

因此,复合文字被视为未命名对象,不被视为编译时间常数。

就像你不能使用另一个 变量 来初始化一个静态变量一样,从 C99 开始,你也不能再使用这个复合字面量来初始化一个静态变量了。

【讨论】:

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

ISO C99确实支持复合文字 (according to this)。然而,目前 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 (Wikipedia)ANSI (Wikipedia)。)

有几个例子表明,更小更灵活的开源 C 工作组或委员会通过添加扩展来回应用户群表达的兴趣。例如,the switch case range extension

【讨论】:

  • 很多很好的信息,但是太多了 bolditalicitalic bold 易于阅读。我认为你应该缩小重点。
  • @JonathanLeffler - 是的,我确实被带走了。感谢您的编辑。
  • switch-case 扩展有时很有用。我会对标准化 'designated initializer ranges' 扩展更感兴趣(要将一系列元素初始化为相同的值,请编写 [first ... last] = value。这是一个 GNU 扩展。例如,int widths[] = { [0 ... 9] = 1, [10 ... 99] = 2, [100] = 3 };)
【解决方案4】:

有趣的是,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)。

同样,没有提及复合文字。

无论如何,隧道里有一盏灯。另一种完全净化的方法是将诸如 address constant 之类的未命名对象传递。该标准在 §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;
}

经检查,gcc 5.2.0 和 clang 3.6 上的 -std=c99 -pedantic-errors 标志都可以正常编译。

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

【讨论】:

    【解决方案5】:

    C 语言依赖于对常量表达式 的精确定义。仅仅因为某些东西看起来“在编译时已知”并不意味着它满足常量表达式的正式定义

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

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

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

    因此,从最严格的意义上说,使用复合文字进行初始化是非法的,而使用普通 { ... } 初始化程序进行初始化是可以的。一些编译器可能接受复合文字初始化作为扩展。 (通过扩展 constant expression 的概念或采用其他一些扩展路径。查阅编译器文档以找出它编译的原因。)

    【讨论】:

    • 谢谢,我现在清楚多了。可惜 GCC 直到 5.2 才放宽这个限制。
    猜你喜欢
    • 1970-01-01
    • 2023-04-05
    • 1970-01-01
    • 1970-01-01
    • 2017-05-13
    • 2021-01-31
    • 2011-01-07
    • 2013-09-22
    • 2014-10-07
    相关资源
    最近更新 更多