在循环内部声明变量,是好的做法还是坏的做法?


264

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

我已经阅读了其他有关是否存在性能问题(大多数人说没有)的主题,并且应该始终将变量声明为接近将要使用的变量。我想知道的是,是否应该避免这种情况,或者实际上是否应该这样做。

例:

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

   cout << someString;
}

问题2:大多数编译器是否意识到该变量已经被声明并且仅跳过了该部分,还是实际上每次都在内存中为其创建了一个位置?


29
除非配置另有说明,否则请使其接近使用。
Mooing Duck 2011年


3
@drnewman我确实读过那些线程,但是它们没有回答我的问题。我知道在循环内声明变量是可行的。我想知道这样做是否是个好习惯,还是应该避免。
JeramyRR 2011年

Answers:


348

这是极好的实践。

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

这条路:

  • 如果变量的名称有点“泛型”(如“ 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 ++类。

对于标准类型和结构,在编译时已知变量的大小。C语言中没有“构造”之类的东西,因此在调用函数时,变量的空间将简单地分配到堆栈中(无需任何初始化)。这就是在循环内声明变量时成本为“零”的原因。

但是,对于C ++类,有一些我不太了解的构造函数。我猜分配可能不会成为问题,因为编译器应足够聪明以重用相同的空间,但是初始化很可能在每次循环迭代时进行。


4
很棒的答案。这正是我一直在寻找的东西,甚至使我对一些我没有意识到的东西有了一些了解。我没有意识到作用域仅保留在循环内。感谢您的答复!
JeramyRR

22
“但是,它永远不会比在函数开始处分配慢。” 并非总是如此。该变量将被分配一次,但仍将根据需要构造和破坏多次。在示例代码的情况下,是11倍。引用Mooing的评论“除非配置另有说明,否则请使其接近使用”。
IronMensan 2011年

4
@JeramyRR:绝对不是,编译器无法知道对象在其构造函数或析构函数中是否具有有意义的副作用。
ildjarn 2011年

2
@Iron:另一方面,当您首先声明该项目时,您只会收到许多对赋值运算符的调用;通常,其成本与构造和销毁对象的成本大致相同。
Billy ONeal

4
@BillyONeal:对于stringvector具体而言,赋值运算符可以重用分配的缓冲区每个循环,(取决于你的循环)可能是一个巨大的节省时间。
Mooing Duck,

22

通常,将其保持非常紧密是一种很好的做法。

在某些情况下,将考虑诸如性能之类的考虑因素,以证明将变量从循环中拉出是合理的。

在您的示例中,程序每次都会创建并销毁字符串。一些库使用小字符串优化(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由您的标准库实现),则动态分配和释放是免费的。

这样做:

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

每次迭代仍然需要字符的物理副本,但是这种形式可能会导致动态分配,因为您分配了字符串,并且实现应该看到不需要调整字符串的后备分配的大小。当然,在此示例中您不会这样做(因为已经演示了多个高级替代方法),但是当字符串或向量的内容变化时,您可能会考虑使用它。

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


1
对于像float或int这样的基本数据类型,在循环内声明变量要比在循环外声明变量要慢,因为在每次迭代时都必须为变量分配空间?
卡斯帕罗夫92年

2
@ Kasparov92简短的回答是“否。请忽略该优化,并在可能的情况下将其放在循环中,以提高可读性/局部性。编译器可以为您执行该微优化。” 更详细地说,最终由编译器根据对平台的最佳选择,优化级别等来决定。循环内的普通int / float通常将放置在堆栈上。如果进行了优化,则编译器当然可以将其移出循环并重新使用存储。出于实际目的,这将是一个非常非常小的优化……
贾斯汀·马丁(Justin)

1
@ Kasparov92…(续),您只会在每个周期都非常重要的环境/应用程序中考虑使用。在这种情况下,您可能只想考虑使用汇编。
贾斯汀'18

14

对于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;
};
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秒钟,直到获得myOtherFunc的输出。

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

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


2
好吧,从技术上讲,在第二个版本中,您将仅在2秒钟内获得输出,因为您尚未破坏对象……..
Chrys

12

我没有发布回答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”,“一会儿”,等等。

我的代码在Microsoft Visual C ++ 2010 Express中编译,因此我知道它可以工作;另外,我尝试在定义该变量的方括号之外使用该变量,并且收到错误消息,因此我知道该变量已“销毁”。

我不知道使用此方法是否不好,因为许多未标记的括号会很快使代码不可读,但也许有些注释可以使事情变得清晰。


4
对我来说,这是一个非常合理的答案,它带来与该问题直接相关的建议。你有我的票!
亚历克西斯·勒克莱尔

0

这是一个很好的实践,因为以上所有答案都提供了很好的理论方面的问题,让我瞥一眼代码,我正在尝试通过GEEKSFORGEEKS解决DFS,我遇到了优化问题……如果您尝试解决在循环外声明整数的代码将为您带来优化错误。

stack<int> st;
st.push(s);
cout<<s<<" ";
vis[s]=1;
int flag=0;
int top=0;
while(!st.empty()){
    top = st.top();
    for(int i=0;i<g[top].size();i++){
        if(vis[g[top][i]] != 1){
            st.push(g[top][i]);
            cout<<g[top][i]<<" ";
            vis[g[top][i]]=1;
            flag=1;
            break;
        }
    }
    if(!flag){
        st.pop();
    }
}

现在将整数放入循环中,这将为您提供正确的答案...

stack<int> st;
st.push(s);
cout<<s<<" ";
vis[s]=1;
// int flag=0;
// int top=0;
while(!st.empty()){
    int top = st.top();
    int flag = 0;
    for(int i=0;i<g[top].size();i++){
        if(vis[g[top][i]] != 1){
            st.push(g[top][i]);
            cout<<g[top][i]<<" ";
            vis[g[top][i]]=1;
            flag=1;
            break;
        }
    }
    if(!flag){
        st.pop();
    }
}

这完全反映了@justin先生在第二条评论中说的话....在这里尝试 https://practice.geeksforgeeks.org/problems/depth-first-traversal-for-a-graph/1。试一试...。您将得到它。希望获得帮助。


我认为这不适用于这个问题。显然,在您的情况下,它很重要。问题在于在不更改代码行为的情况下可以在其他位置定义变量定义的情况。
pcarter

在您发布的代码中,问题不在于定义,而在于初始化部分。flag每次while迭代应在0​​重新初始化。那是逻辑问题,而不是定义问题。
MartinVéronneau

0

第4.8章K&R的C编程语言中的块结构2.Ed。

每次进入块时,都会在块中声明和初始化的自动变量被初始化。

我可能会错过看书中的相关描述,例如:

在块中声明和初始化的自动变量仅在进入该块之前分配一次。

但是简单的测试可以证明假设成立:

 #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;                                                                                                         
 }                                                                                                                     
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.