关于c ++:在循环中声明变量,良好实践还是不良实践?

Declaring variables inside loops, good practice or bad practice?

问题1:在循环中声明变量是一种好的实践还是坏的实践?

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

例子:

1
2
3
4
5
6
for(int counter = 0; counter <= 10; counter++)
{
   string someString ="testing";

   cout << someString;
}

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


这是很好的练习。

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

这种方式:

    百万千克1

    如果变量的名称有点"通用"(如"i"),那么在代码的后面某个地方,将它与另一个同名变量混合是没有风险的(也可以使用gcc上的-Wshadow警告指令来减轻风险)。

    百万千克1百万千克1

    编译器知道变量作用域被限制在循环内部,因此如果变量在其他地方被错误引用,编译器将发出一条正确的错误消息。

    百万千克1百万千克1

    最后,编译器可以更有效地执行一些专用的优化(最重要的是寄存器分配),因为它知道变量不能在循环之外使用。例如,不需要存储结果以供以后重用。

    百万千克1

简而言之,你这样做是正确的。

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

1
2
3
4
5
6
7
8
9
10
11
12
{
    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(){...}块之外,这也是正确的。通常,而不是:

1
2
3
4
5
6
7
    int result;
    (...)
    result = f1();
    if (result) then { (...) }
    (...)
    result = f2();
    if (result) then { (...) }

写作更安全:

1
2
3
4
5
6
7
8
9
10
    (...)
    {
        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++类可能不是这样。

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

但是,对于C++类,有一个构造器的东西,我知道得少多了。我想分配可能不会成为问题,因为编译器应该足够聪明,可以重用相同的空间,但是初始化很可能在每个循环迭代中进行。


一般来说,保持密切联系是一个很好的做法。

在某些情况下,需要考虑性能等因素,以便将变量从循环中拉出。

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

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

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

或者你可以把常数拉出来:

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

Do most compilers realize that the variable has already been declared and just skip that portion, or does it actually create a spot for it in memory each time?

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

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

在每种情况下都需要冗余复制,如果变量位于SSO字符计数的阈值之上(并且SSO由您的std库实现),则需要动态分配和空闲。

这样做:

1
2
3
4
5
string testing;
for (int counter = 0; counter <= 10; counter++) {
   testing ="testing";
   cout << testing;
}

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

那么,你如何处理所有这些选项(以及更多)?把它作为违约保持在非常接近的位置——直到你很好地理解了成本并且知道什么时候应该偏离。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;

}

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

你将需要5秒钟,直到你得到肌热功能的输出。

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

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


我没有发帖回答杰里迈尔的问题(因为他们已经被回答了);相反,我发帖只是为了提出建议。

对杰里迈尔来说,你可以这样做:

1
2
3
4
5
6
7
8
9
10
11
12
{
  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中编译,所以我知道它是有效的,而且,我试着使用它定义的括号之外的变量,并且我收到了一个错误,所以我知道变量被"销毁"了。

我不知道使用这个方法是否是一个坏的实践,因为许多未标记的括号可能会很快使代码不可读,但也许一些注释可以清除这些内容。