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

时间:2011-10-31 20:50:42

标签: c++ loops variable-declaration

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

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

示例:

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

   cout << someString;
}

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

9 个答案:

答案 0 :(得分:269)

这是优秀的练习。

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

这样:

  • 如果变量的名称有点“通用”(如“i”),则没有风险将其与代码中稍后某处的同名变量混合使用(也可以使用关于海湾合作委员会的-Wshadow警告指示

  • 编译器知道变量范围仅限于循环内部,因此如果变量在其他地方被错误引用,则会发出正确的错误消息。

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

简而言之,你是对的。

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

{
    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 ++类,有一些我不太了解的构造函数。我认为分配可能不会成为问题,因为编译器应该足够聪明以重用相同的空间,但初始化很可能在每次循环迭代时发生。

答案 1 :(得分:17)

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

在某些情况下,会有一些考虑因素,例如将变量拉出循环的理由。

在您的示例中,程序每次都会创建并销毁字符串。有些库使用小型字符串优化(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的情况下,'space'包括一个指针,该指针包含表示字符的动态分配。所以这个:

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

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

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

答案 2 :(得分:12)

对于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秒才能完成。

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

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

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

答案 3 :(得分:8)

我没有发帖回答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。试一试...。您将得到它。希望获得帮助。

答案 5 :(得分: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;                                                                                                         
 }                                                                                                                     

答案 6 :(得分:0)

由于您的第二个问题更具体,因此我将首先解决它,然后根据第二个问题给出的上下文来讨论您的第一个问题。我想给出一个比现在更多的基于证据的答案。

问题2 :大多数编译器是否意识到该变量已经 被声明,然后跳过该部分,或者实际上创建了一个 每次都在内存中找到它吗?

您可以通过在运行汇编器之前停止编译器并查看asm来自己回答此问题。 (如果编译器具有gcc风格的界面,请使用-S标志;如果要使用我在此处使用的语法样式,请使用-masm=intel标志。)

在任何情况下,对于x86-64的现代编译器(gcc 10.2,clang 11.0),如果禁用优化,它们只会在每次循环传递时重新加载变量。考虑以下C ++程序-为了直观地映射到asm,我将大部分内容保留为C样式,并使用整数而不是字符串,尽管在字符串情况下适用相同的原理:

#include <iostream>

static constexpr std::size_t LEN = 10;

void fill_arr(int a[LEN])
{
    /* *** */
    for (std::size_t i = 0; i < LEN; ++i) {
        const int t = 8;

        a[i] = t;
    }
    /* *** */
}

int main(void)
{
    int a[LEN];

    fill_arr(a);

    for (std::size_t i = 0; i < LEN; ++i) {
        std::cout << a[i] << " ";
    }

    std::cout << "\n";

    return 0;
}

我们可以将其与具有以下区别的版本进行比较:

    /* *** */
    const int t = 8;

    for (std::size_t i = 0; i < LEN; ++i) {
        a[i] = t;
    }
    /* *** */

在禁用优化的情况下,对于循环内声明版本,gcc 10.2在循环的每一遍都将8放入堆栈中:

    mov QWORD PTR -8[rbp], 0
.L3:
    cmp QWORD PTR -8[rbp], 9
    ja  .L4
    mov DWORD PTR -12[rbp], 8 ;✷

而对于循环外版本它只执行一次:

    mov DWORD PTR -12[rbp], 8 ;✷
    mov QWORD PTR -8[rbp], 0
.L3:
    cmp QWORD PTR -8[rbp], 9
    ja  .L4

这会对性能产生影响吗?直到我将迭代次数推至数十亿次之后,我才发现它们与我的CPU(Intel i7-7700K)在运行时间上没有明显的差异,即使那时平均差异还不到0.01s。毕竟,这只是循环中的一个额外操作。 (对于一个字符串,循环内操作的差异显然要大一些,但并不是那么明显。)

此外,这个问题在很大程度上是学术上的,因为优化级别为-O1或更高的gcc对于两个源文件都输出相同的asm,与clang一样。因此,至少对于像这样的简单情况,不可能以任何一种方式对性能产生任何影响。当然,在现实世界中的程序中,您应该始终概述而不是进行假设。

问题#1 :在循环中声明变量是一种好的做法,还是 不好的做法?

与几乎每个这样的问题一样,它取决于。如果声明在一个非常紧密的循环中,并且您没有进行优化就进行编译(例如出于调试目的),则从理论上讲,将其移出循环将有可能提高性能,从而在调试工作中非常方便。如果是这样,至少在调试时,这可能是明智的。而且,尽管我认为优化的构建可能不会有任何改变,但是如果您确实观察到了,则您/您的团队/您的团队可以就是否值得进行判断。

同时,您不仅必须考虑编译器如何读取您的代码,还必须考虑它如何对人类(包括您自己)产生影响。我认为您会同意,在最小范围内声明的变量更易于跟踪。如果它在循环之外,则意味着在循环外需要它,如果实际并非如此,这会造成混淆。在大型代码库中,随着时间的流逝,诸如此类的小混乱逐渐加重,并在工作数小时后变得疲劳,并可能导致愚蠢的错误。根据使用情况,这可能会比通过稍微提高性能而获得的成本高得多。

答案 7 :(得分:0)

在循环内或循环外声明变量,这是JVM规范的结果,但以最佳编码实践的名义,建议在尽可能小的范围内声明变量(在本例中,它在循环内,因为这是唯一使用变量的地方)。在最小范围内声明对象可以提高可读性。局部变量的范围应该总是尽可能的小。在你的例子中,我假设 str 没有在 while 循环之外使用,否则你不会问这个问题,因为在 while 循环中声明它不会是一个选项,因为它不会编译。

如果我在 a 内部或外部声明变量会有所不同,如果我在 Java 中在循环内部或外部声明变量有什么不同吗?这是 for(int i = 0; i < 1000; i++) { int 在单个变量的层面上,效率没有显着差异,但如果你有一个有 1000 个循环和 1000 个变量的函数(别介意坏风格暗示)可能存在系统差异,因为所有变量的所有生命周期都相同而不是重叠。

在 for 循环中声明循环控制变量,当你在 for 循环中声明一个变量时,有一点需要记住:当 for 语句执行时,该变量的作用域结束。 (也就是说,变量的范围仅限于 for 循环。)这个 Java 示例展示了如何使用声明块在 Java For 循环中声明多个变量。

答案 8 :(得分:0)

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

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

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

如果您仍然必须支持这样一个旧的编译器(有些人在 Borland 上),那么答案是肯定的,可以将 i 排除在循环之外,因为不这样做会使它变得“更难”让人们将多个循环放入同一个变量中,但老实说,编译器仍然会失败,如果出现问题,这就是你想要的。

如果你不再需要支持这样一个旧的编译器,变量应该保持在你可以获得的最小范围内,这样你不仅可以最大限度地减少内存使用;但也使理解项目更容易。这有点像问为什么不将所有变量设为全局变量。相同的论点适用,但范围略有变化。