在循环中声明变量,良好实践或不良实践?

Jer*_*yRR 229 c++ loops variable-declaration

问题#1:在循环中声明一个变量是一个好习惯还是坏习惯?

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

例:

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

   cout << someString;
}
Run Code Online (Sandbox Code Playgroud)

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

Cya*_*yan 300

这是一种很好的做法.

通过在循环内创建变量,可以确保它们的范围仅限于循环内部.它不能在循环外引用或调用.

这条路:

  • 如果变量的名称有点"通用"(如"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 */
}
Run Code Online (Sandbox Code Playgroud)

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

通过限制和更精确的范围,可以获得更准确的优化.但更重要的是,它使您的代码更安全,在阅读代码的其他部分时需要担心更少的状态(即变量).

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

    int result;
    (...)
    result = f1();
    if (result) then { (...) }
    (...)
    result = f2();
    if (result) then { (...) }
Run Code Online (Sandbox Code Playgroud)

写起来更安全:

    (...)
    {
        int const result = f1();
        if (result) then { (...) }
    }
    (...)
    {
        int const result = f2();
        if (result) then { (...) }
    }
Run Code Online (Sandbox Code Playgroud)

差异似乎很小,特别是在这么小的例子上.但是在更大的代码库上,它会有所帮助:现在没有风险将某些result值传输f1()f2()阻止.每个result都严格限制在自己的范围内,使其角色更加准确.从评论者的角度来看,它更好,因为他需要担心和跟踪的长程状态变量较少.

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

补充资料

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

回应对分配的评论:上述规则在C中是正确的,但可能不适用于某些C++类.

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

但是,对于C++类,我不太了解这个构造函数.我认为分配可能不会成为问题,因为编译器应该足够聪明以重用相同的空间,但初始化很可能发生在每次循环迭代.

  • "但它永远不会比在功能开始时分配慢." 这并非总是如此.变量将被分配一次,但仍然会根据需要多次构造和销毁.在示例代码的情况下,是11次.引用Mooing的评论"将它们贴近它们的用法,除非分析说不然." (18认同)
  • 事情远没有这么简单。这个答案适合 C 和特别简单的类型,其中编译器事先知道它们的大小(想想 int、char 等)。但是,对于更复杂的类型,特别是具有复杂构造函数(例如,需要文件或数据库输入、复杂计算或初始化大数据结构的构造函数)的类,由于显而易见的原因,这可能会影响性能,无需注意分析。所以对于简单类型是的;对于复杂类型,首先考虑。良好做法仅应作为基本指南,并且在现实世界中并不总是有效。 (5认同)
  • 很棒的答案.这正是我所寻找的,甚至给了我一些我没有意识到的东西.我没有意识到范围只在循环内部.感谢您的答复! (4认同)
  • @JeramyRR:绝对不是 - 编译器无法知道对象在构造函数或析构函数中是否有有意义的副作用. (4认同)
  • @BillyONeal:具体来说,对于`string`和`vector`,赋值运算符可以重用每个循环分配的缓冲区,这取决于你的循环,可以节省大量时间. (3认同)
  • @Iron:另一方面,当你首先声明项目时,你只需要调用赋值运算符; 这通常与构建和销毁对象的成本大致相同. (2认同)

jus*_*tin 18

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

在某些情况下,会有诸如性能之类的考虑因素证明将变量拉出循环是正确的.

在您的示例中,程序每次都会创建并销毁字符串.有些库使用小型字符串优化(SSO),因此在某些情况下可以避免动态分配.

假设您想要避免那些多余的创建/分配,您可以将其写为:

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

或者你可以拉出常数:

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

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

它可以重用变量消耗的空间,并且可以将不变量拉出循环.在const char数组的情况下(上图) - 该数组可以被拉出.但是,在对象(例如std::string)的情况下,必须在每次迭代时执行构造函数和析构函数.在该情况下std::string,"空间"包括指针,该指针包含表示字符的动态分配.所以这:

for (int counter = 0; counter <= 10; counter++) {
   string testing = "testing";
   cout << testing;
}
Run Code Online (Sandbox Code Playgroud)

在每种情况下都需要冗余复制,并且如果变量高于SSO字符计数的阈值(并且SSO由std库实现),则动态分配和释放.

这样做:

string testing;
for (int counter = 0; counter <= 10; counter++) {
   testing = "testing";
   cout << testing;
}
Run Code Online (Sandbox Code Playgroud)

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

那么你如何处理所有这些选择(以及更多)?默认情况下保持非常接近 - 直到您了解成本并知道何时应该偏离.

  • 关于像 float 或 int 这样的基本数据类型,在循环内声明变量是否比在循环外声明变量慢,因为每次迭代都必须为变量分配空间? (2认同)
  • @ Kasparov92简短的回答是*“否。请忽略该优化,并在可能的情况下将其放在循环中,以提高可读性/局部性。编译器可以为您执行该微优化。” *更详细地讲,最终使编译器可以根据对平台的最佳选择,优化级别等来决定。循环内的普通int / float通常会放在堆栈上。如果进行了优化,则编译器当然可以将其移出循环并重新使用存储。出于实际目的,这将是非常非常小的优化… (2认同)

小智 13

对于C++,它取决于你在做什么.好吧,这是愚蠢的代码,但想象一下

class myTimeEatingClass
{
 public:
 //constructor
      myTimeEatingClass()
      {
          sleep(2000);
          ms_usedTime+=2;
      }
      ~myTimeEatingClass()
      {
          sleep(3000);
          ms_usedTime+=3;
      }
      const unsigned int getTime() const
      {
          return  ms_usedTime;
      }
      static unsigned int ms_usedTime;
};
Run Code Online (Sandbox Code Playgroud)
myTimeEatingClass::ms_CreationTime=0; 
myFunc()
{
    for (int counter = 0; counter <= 10; counter++) {

        myTimeEatingClass timeEater();
        //do something
    }
    cout << "Creating class took "<< timeEater.getTime() <<"seconds at all<<endl;

}
myOtherFunc()
{
    myTimeEatingClass timeEater();
    for (int counter = 0; counter <= 10; counter++) {
        //do something
    }
    cout << "Creating class took "<< timeEater.getTime() <<"seconds at all<<endl;

}
Run Code Online (Sandbox Code Playgroud)

您将等待55秒,直到获得myFunc的输出.仅仅因为每个循环构造函数和析构函数一起需要5秒才能完成.

在获得myOtherFunc的输出之前,您将需要5秒钟.

当然,这是一个疯狂的例子.

但它说明当构造函数和/或析构函数需要一些时间时,每个循环完成相同的构造时,它可能会成为性能问题.

  • 好吧,从技术上来说,在第二个版本中,你只需2秒即可得到输出,因为你还没有破坏对象...... (2认同)

Fea*_*ter 9

我没有发帖回答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.
Run Code Online (Sandbox Code Playgroud)

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

我的代码是用Microsoft Visual C++ 2010 Express编译的,所以我知道它的工作原理; 另外,我试图在它所定义的括号之外使用变量,我收到一个错误,所以我知道变量被"销毁"了.

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

  • 对我来说,这是一个非常合理的答案,它提出了与问题直接相关的建议.你有我的投票! (4认同)