【问题标题】:Is there a difference between initializing a variable and assigning it a value immediately after declaration?初始化变量和在声明后立即为其赋值有区别吗?
【发布时间】:2013-04-19 12:43:07
【问题描述】:

假设一个纯粹的非优化编译器,初始化变量和声明后赋值之间机器代码有什么区别吗?

初始化方法

int x = 2;

赋值方法

int x;
x = 2;

我使用 GCC 输出为这两种不同方法生成的程序集,并且都生成了一条机器指令:

movl    $2, 12(%esp)

这条指令只是将x变量持有的内存设置为2的值。 GCC 可能正在优化这一点,因为它可以识别操作的最终结果;但我认为这是解释这两个版本的唯一方法。我的理由是两个版本都做同样的事情:将一部分内存设置为特定值。

如果生成的机器代码相同,为什么经常在术语“initialization”和“assignment”之间进行区分?

术语“初始化”是否纯粹用于区分具有特定值的变量与那些在内存中留下垃圾值的(未初始化的)变量?

【问题讨论】:

  • 可能是因为初始化是第一个赋值。
  • x = 1 + 1 也会生成与x = 2 相同的代码。那是不是说赋值和加法也是一样的呢?
  • @BoPersson “巧合不是因果关系”的好例子。

标签: c assembly initialization variable-assignment c99


【解决方案1】:

假设一个纯粹的非优化编译器,有什么区别 初始化变量和为其赋值之间的机器代码 声明后?

当然。

  • char fubar[] = "hello world"; 有效。
  • char fubar[]; fubar = "hello world"; 不是。

更多?

  • int fubar[128] = { [60] = 42 }; 有效。
  • int fubar[128]; fubar = { [60] = 42 }; 不是。

更多?

  • struct foo bar = { .foo = 13, .bar = 42 }; 有效。
  • struct foo bar; bar = { .foo = 13, .bar = 42 }; 不是。

更多?

  • const int fubar = 0; 有效。
  • const int fubar; fubar = 0; 不是。

我可以继续下去......因此,机器代码可能存在于其中一个,而它很可能不会存在于另一个中。关于这一点,您是否听说过不是编译器的 C 实现?

那么为什么经常在初始化之间进行区分 如果生成的机器代码相同,则赋值?

C 编程语言中变量的概念对于低级机器代码表示来说太高级了。在机器代码中,寄存器没有作用域。 C 增加了作用域,更不用说类型融合和许多其他与变量相关的方面,以及初始化(您可以从前面的示例中看到完全一致,但 不幸 不一样)。

“初始化”这个词是不是纯粹用来区分变量的 在那些(未初始化的)上分配了一个特定的值 有什么垃圾值留在内存中的变量?

虽然“初始化”的变量不会包含任何“垃圾值”(或陷阱表示),但这并不是它的唯一影响。

在我的第一个示例中,初始化将提供否则不完整数组的大小。使用赋值运算符的等效项将需要显式提供数组的长度并使用strcpy,结果非常乏味。

在我的第二个示例中,索引 60 处的 int 将被初始化为 40,而其余未初始化的项将被初始化为 0。使用赋值运算符的等效项也将相当乏味。

在我的第三个示例中,成员 foobar 将被初始化为 13 和 42,而其余未初始化的成员将被初始化为 0。偶尔使用复合文字来达到类似的效果。

在我的第四个示例中,初始化设置了变量在其整个生命周期中将包含的值。不能给这个变量赋值。

【讨论】:

    【解决方案2】:

    当您添加 const 限定符时,一个重要的区别就会发挥作用:

    int const x = 2;
    

    是有效的C

    int const x;
    x = 2;
    

    不是。另一个重要的区别是static 变量:

    static int x = f();
    

    无效C

    static int x;
    x = f();
    

    有效。

    【讨论】:

    • (在 C++ 中它们的含义不同。static int x = f(); 只运行一次,第一次执行到达函数(因此它需要一个保护变量来检查每次,原子以防多个线程到达它)在第一个 f() 完成之前)。但是 x = f(); 在每次调用包含函数时都会运行,就像在 C 中一样。)
    【解决方案3】:

    行为必须相同,但生成代码中的任何差异实际上取决于编译器。

    例如,编译器可以为初始化变量生成这个:

    somefunction:
    pushl    %ebp
    movl     %esp, %ebp
    pushl    $2 ; allocate space for x and store 2 in it
    ...
    

    这用于未初始化但后来分配的变量:

    somefunction:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $4, %esp ; allocate space for x
    ...
    movl    $2, -4(%ebp) ; assign 2 to x
    ...
    

    在这些情况下,C 标准不要求生成的代码相同或不相同。它只要求在这两种情况下程序的行为相同。并且相同的行为并不一定意味着相同的机器代码。

    【讨论】:

    • 这真的很有趣,因为这意味着根据 C 标准,编译器理论上可以在最终执行赋值之前执行无意义的代码(例如循环 300 次交互)。尽管它在效率方面失败得很惨,但从技术上讲,它仍然符合 C 标准。
    • 没错。 C 标准中没有一个关于性能(如保证或要求)的词。但这里并不奇怪。 CPU 本身无法保证性能(因为缓存、操作系统中的调度、中断、磁盘/网络速度、缓冲等)。
    • @VilhelmGray:性能和健全性是实施质量问题。只有volatile 和(如果支持)_Atomic(特别是对于无锁原子对象)真正限制了代码生成的选择。制作符合标准但无用或几乎无法使用的 C 实现是很容易的。而不仅仅是性能:类型宽度、陷阱表示等的奇怪选择。
    • @PeterCordes:一个非常迟钝的实现可以决定给定int i;,像i = 5;这样的语句使用一个表达式(i = 5;)修改对象i的存储值,而不是兼容类型的左值(赋值表达式不是任何类型的左值,因为它根本不是左值)并因此调用 UB。赋值表达式当然会包含一个int 类型的左值作为其左操作数,但该左值本身不会修改i 的值。我认为可以创建一个类似...的功能。
    • struct wrappedInt {int x[1];} wrapInt(int x) { struct wrappedInt result; memcpy(&result, &x, sizeof x); return result;} 然后使用memcpy(&i, wrapInt(5).x, sizeof i);,因为没有任何时刻存在函数参数而不保存值,并且memcpy 可能对正常的 6.5p7 规则有一些神奇的豁免。当然,任何高质量的编译器都不应该需要这样的恶作剧。
    【解决方案4】:
    int x = 2;
    

    计算机将几乎同时创建变量 x 并为其赋值 2。


    int x;
    x = 2;
    

    计算机将创建变量 x。然后它将分配给它的值2。 好像没什么区别,但是...

    ...假设你的代码是这样的:

    int x; 
    {some operators};
    x = 2; 
    

    计算机可能必须访问变量 x 才能为其分配值 2。这意味着在运行程序时计算机将花费更多时间来访问 x 以赋予它一些值,这与创建变量并赋予该变量不同目前。

    无论如何,Deitel HM,Deitel PJ 在C How to Program 中描述了这一点。

    【讨论】:

    • int x;创建变量,它声明一个。计算机不创建变量:变量具有由其声明确定的范围。执行进入和退出变量的范围。即使对于非优化编译器,执行进入或退出变量范围通常也没有明显的个人成本(就时间而言)。初始化和赋值一样昂贵,但如果需要,编译器可以毫不费力地将初始化或赋值更接近首次使用。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-04-15
    • 2023-03-11
    • 1970-01-01
    相关资源
    最近更新 更多