问题1:在循环中声明变量是好做法还是坏做法?

我已经阅读了其他关于是否存在性能问题的线程(大多数人说没有),并且您应该始终在接近它们将被使用的地方声明变量。我想知道的是,这种情况是否应该避免,还是更可取。

例子:

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

   cout << someString;
}

问题2:大多数编译器是否意识到该变量已经被声明,而只是跳过这一部分,还是每次都在内存中为它创建一个位置?


当前回答

第4.8章K&R的C程序设计语言中的块结构

类中声明并初始化的自动变量 每次输入块时,块都被初始化。

我可能没有在书中看到相关的描述,比如:

类中声明并初始化的自动变量 在输入块之前,只分配一次块。

但一个简单的测试可以证明这个假设:

 #include <stdio.h>                                                                                                    

 int main(int argc, char *argv[]) {                                                                                    
     for (int i = 0; i < 2; i++) {                                                                                     
         for (int j = 0; j < 2; j++) {                                                                                 
             int k;                                                                                                    
             printf("%p\n", &k);                                                                                       
         }                                                                                                             
     }                                                                                                                 
     return 0;                                                                                                         
 }                                                                                                                     

其他回答

从前(c++ 98之前);以下将中断:

{
    for (int i=0; i<.; ++i) {std::string foo;}
    for (int i=0; i<.; ++i) {std::string foo;}
}

警告我已经声明(foo是好的,因为它的范围在{})。这可能是人们首先认为它不好的原因。但很久以前就不是这样了。

如果你仍然要支持这样一个旧的编译器(有些人是Borland),那么答案是肯定的,一个情况下可以把i的循环,因为不这样做使得它使它“更难”的人把多个循环与相同的变量,虽然老实说编译器仍然会失败,这是所有你想要的,如果有一个问题。

如果你不再需要支持这样一个旧的编译器,变量应该保持在你能得到的最小范围内,这样你不仅可以最小化内存使用;但也使项目更容易理解。这有点像问为什么不让所有变量都是全局变量。同样的论点也适用,但作用域略有变化。

这是很好的练习。

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

这种方式:

If the name of the variable is a bit "generic" (like "i"), there is no risk to mix it with another variable of same name somewhere later in your code (can also be mitigated using the -Wshadow warning instruction on GCC) The compiler knows that the variable scope is limited to inside the loop, and therefore will issue a proper error message if the variable is by mistake referenced elsewhere. Last but not least, some dedicated optimization can be performed more efficiently by the compiler (most importantly register allocation), since it knows that the variable cannot be used outside of the loop. For example, no need to store the result for later re-use.

总之,你这样做是对的。

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

{
    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 { (...) }
    }

差异可能看起来很小,尤其是在这样一个小例子中。 但在更大的代码基础上,这将有所帮助:现在从f1()块传输一些结果值到f2()块没有风险。每个结果都严格限制在自己的范围内,使其作用更加准确。从审阅者的角度来看,这样更好,因为他不必担心和跟踪长范围的状态变量。

甚至编译器也会提供更好的帮助:假设在将来,在一些错误的代码更改之后,result没有正确地用f2()初始化。第二个版本将简单地拒绝工作,在编译时声明一个明确的错误消息(比运行时好得多)。第一个版本不会发现任何东西,f1()的结果将被简单地测试第二次,与f2()的结果混淆。

补充信息

开源工具CppCheck (C/ c++代码的静态分析工具)提供了一些关于变量最佳作用域的极好提示。

在回应有关编配的评论时: 上面的规则在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;
}

在每次迭代时仍然需要一个字符的物理副本,但这种形式可能会导致一次动态分配,因为您分配了字符串,实现应该看到没有必要调整字符串的支持分配。当然,在本例中您不会这样做(因为已经演示了多个更好的替代方法),但是当字符串或向量的内容发生变化时,您可以考虑这样做。

那么,你该如何处理这些选项(以及更多选项)呢?保持它非常接近默认值—直到您充分了解成本并知道何时应该偏离。

下面的两个代码段生成相同的程序集。

// snippet 1
void test() { 
   int var; 
   while(1) var = 4;
}


// snippet 2
void test() {
    while(1) int var = 4;
}

输出:

test():
        push    rbp
        mov     rbp, rsp
.L2:
        mov     DWORD PTR [rbp-4], 4
        jmp     .L2

链接:https://godbolt.org/z/36hsM6Pen

因此,除非涉及到分析或计算扩展构造函数,否则保持声明接近其用法应该是默认方法。

我没有发帖回答JeremyRR的问题(因为他们已经回答了);相反,我只是发表了一个建议。

对于JeremyRR,你可以这样做:

{
  string someString = "testing";   

  for(int counter = 0; counter <= 10; counter++)
  {
    cout << someString;
  }

  // The variable is in scope.
}

// The variable is no longer in scope.

我不知道你是否意识到(我第一次开始编程时没有意识到),括号(只要它们是成对的)可以放在代码中的任何地方,而不仅仅是在“if”、“for”、“while”等后面。

我的代码编译在微软Visual c++ 2010 Express,所以我知道它的工作;此外,我已经尝试使用它在括号外定义的变量,我收到了一个错误,所以我知道变量被“销毁”。

我不知道使用这种方法是否是不好的做法,因为许多未标记的括号会很快使代码无法阅读,但也许一些注释可以澄清这些问题。