在循环内声明变量好的做法还是不好的做法?

内容来源于 Stack Overflow,并遵循CC BY-SA 3.0许可协议进行翻译与使用

  • 回答 (2)
  • 关注 (0)
  • 查看 (22)

问题1:在循环中声明一个变量是一种好的做法还是不好的做法?

我已经阅读了关于是否存在性能问题的其他线索(大多数是否定的),并且你应该始终将变量声明为接近它们将要使用的位置。我想知道的是这是否应该避免,或者如果它真的是首选。

for(int counter = 0; counter <= 10; counter++)
{
   string someString = "testing";

   cout << testing;
}

问题2:大多数编译器是否意识到变量已经被声明并且只是跳过那部分,或者每次都在内存中创建一个内存点?

提问于
用户回答回答于

这是非常好的做法。

通过在循环内创建变量,确保它们的作用域被限制在循环内部。它不能被引用,也不能在循环之外调用。

这条路:

  • 如果变量的名称有点“通用”(比如“i”),那么不需要在代码中稍后的某个地方将其与另一个同名变量混合使用(也可以使用-WshadowGCC上的警告指令进行缓解)
  • 编译器知道变量作用域被限制在循环内部,因此如果该变量在其他地方被错误调用,将会发出适当的错误消息
  • 最后但并非最不重要的一点是,编译器可以更高效地执行一些专用优化(最重要的是寄存器分配),因为它知道变量不能在循环之外使用。例如,不需要存储结果以供以后重新使用。

总之,你是对的。

但请注意,该变量不应保留每个循环之间的值。在这种情况下,你可能需要每次初始化它。你也可以创建一个更大的块,包含循环,唯一的目的是声明必须从一个循环到另一个循环保持其值的变量。这通常包括循环计数器本身。

{
    int i, retainValue;
    for (i=0; i<N; i++)
    {
       int tmpValue;
       /* tmpValue is uninitialized */
       /* retainValue still has its previous value from previous loop */

       /* Do some stuff here */
    }
    /* Here, retainValue is still valid; tmpValue no longer */
}

对于问题#2:调用该函数时,该变量被分配一次。实际上,从分配的角度来看,它几乎与在函数开头声明变量相同。唯一的区别是范围:变量不能在循环外部使用。甚至有可能变量没有被分配,只是重新使用一些空闲插槽(来自其他变量的范围已经结束)。

有限制和更精确的范围可以实现更精确的优化。但更重要的是,它使您的代码更安全,并且读取代码的其他部分时需要考虑更少的状态(即变量)。

即使在if(){...}循环之外也是如此。通常,而不是:

    int result;
    (...)
    result = f1();
    if (result) then { (...) }
    (...)
    result = f2();
    if (result) then { (...) }

写下更安全:

    (...)
    {
        int const result = f1();
        if (result) then { (...) }
    }
    (...)
    {
        int const result = f2();
        if (result) then { (...) }
    }

这种差异似乎很小,特别是在这样一个小例子中。但是在更大的代码基础上,这将有助于:现在没有风险将某个result值传输f1()f2()块。每一个result都严格限制在自己的范围内,使其角色更加准确。从审稿人的角度来看,它更好,因为他有较少的远程状态变量需要担心和跟踪。

即使编译器会更好地帮助:假设在未来,在代码的某些错误更改之后,result未正确初始化f2()。第二个版本会拒绝工作,在编译时指出一个明确的错误消息(比运行时更好)。第一个版本不会发现任何东西,结果f1()将被简单地再次测试,对于结果感到困惑f2()

补充信息

开源工具CppCheck(针对C / C ++代码的静态分析工具)提供了有关最佳变量范围的一些优秀提示。

对于标准类型和结构,变量的大小在编译时已知。C中没有“构造”这样的东西,所以当函数被调用时,变量的空间将被简单地分配到堆栈中(不需要任何初始化)。这就是为什么在循环中声明变量时有“零”成本。

然而,对于C ++类来说,这个构造函数是我知道的更少。我想分配可能不会是问题,因为编译器应该足够聪明以重用相同的空间,但初始化可能发生在每次循环迭代时。

用户回答回答于

一般来说,保持它非常接近是一种非常好的做法。

在某些情况下,会考虑性能等因素,将变量拉出循环。

在你的例子中,程序每次创建并销毁字符串。一些库使用小字符串优化(SSO),因此在某些情况下可以避免动态分配。

假设你想避免那些多余的创作/分配,你可以把它写成:

for (int counter = 0; counter <= 10; counter++) {
   // compiler can pull this out
   const char testing[] = "testing";
   cout << testing;
}

或者你可以拉出常数:

const std::string testing = "testing";
for (int counter = 0; counter <= 10; counter++) {
   cout << testing;
}

大多数编译器是否意识到这个变量已经被声明并且只是跳过那个部分,或者它实际上每次都在内存中创建一个位置?

它可以重用变量消耗的空间,并且可以将不变量从循环中拉出。在const char数组的情况下(上面) - 该数组可以被拉出。然而,构造函数和析构函数必须在对象(如std::string)的每次迭代中执行。在这种情况下std::string,'空间'包含一个指针,其中包含表示字符的动态分配。所以这:

for (int counter = 0; counter <= 10; counter++) {
   string testing = "testing";
   cout << testing;
}

将需要在每种情况下进行冗余复制,如果变量位于SSO字符数阈值以上(SSO由std库实现),则动态分配和空闲。

这样做:

string testing;
for (int counter = 0; counter <= 10; counter++) {
   testing = "testing";
   cout << testing;
}

在每次迭代时仍然需要字符的物理副本,但是表单可能导致一次动态分配,因为您分配了字符串,并且实现应该看到不需要调整字符串的后备分配的大小。当然,在这个例子中你不会这么做(因为已经证明了多个优秀的替代方案),但是当字符串或向量的内容变化时你可能会考虑它。

扫码关注云+社区