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

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

例子:

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

   cout << someString;
}

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


当前回答

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

// 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

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

其他回答

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

类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的输出。

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

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

这是一个很好的实践,因为所有以上的答案都提供了非常好的理论方面的问题,让我看了一下代码,我试图解决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。试一试....你会明白的。希望这对你有所帮助。

第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;                                                                                                         
 }                                                                                                                     

这是很好的练习。

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

这种方式:

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++类,有一个构造函数的东西,我知道得少得多。我想分配可能不是问题,因为编译器应该足够聪明,可以重用相同的空间,但初始化很可能发生在每次循环迭代中。

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

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

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

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

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