【问题标题】:Declaring variables inside loops, good practice or bad practice?在循环内声明变量,好的做法还是坏的做法?
【发布时间】:2011-12-19 01:30:30
【问题描述】:

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

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

例子:

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

   cout << someString;
}

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

【问题讨论】:

  • 把它们放在接近它们的用途,除非分析另有说明。
  • @drnewman 我确实阅读了这些主题,但他们没有回答我的问题。我知道在循环内声明变量是有效的。我想知道这样做是否是一种好习惯,或者是否可以避免。

标签: c++ loops variable-declaration


【解决方案1】:

这是优秀的练习。

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

这边:

    1234563 -Wshadow GCC 上的警告指令)
  • 编译器知道变量作用域仅限于循环内部,因此如果变量在别处被错误地引用,编译器会发出适当的错误消息。

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

总之,你这样做是对的。

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

{
    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++ 类,我对构造函数的了解要少得多。我想分配可能不会成为问题,因为编译器应该足够聪明以重用相同的空间,但初始化可能会在每次循环迭代时进行。

【讨论】:

  • 很棒的答案。这正是我一直在寻找的东西,甚至给了我一些我没有意识到的东西。我没有意识到范围仅保留在循环内。感谢您的回复!
  • "但它永远不会比在函数开头分配要慢。"这并不总是正确的。该变量将被分配一次,但仍将根据需要多次构造和销毁。在示例代码的情况下,是 11 次。引用 Mooing 的评论“除非分析另有说明,否则请尽量使用它们。”
  • @JeramyRR :绝对不是——编译器无法知道对象在其构造函数或析构函数中是否具有有意义的副作用。
  • 不是这么简单的。这个答案适合 C 和特别简单的类型,编译器事先知道它们的大小(想想 int、char 等)。但是,对于更复杂的类型,特别是对于具有复杂构造函数的类(例如,需要文件或数据库输入、复杂计算或初始化大数据结构的构造函数),这可能会影响性能,原因很明显,无需注意分析。所以对于简单类型是的;对于复杂类型,请先考虑。良好做法应仅作为基本指南,众所周知,在现实世界中并不总是有效。
  • @BillyONeal:特别是对于stringvector,赋值运算符可以在每个循环中重用分配的缓冲区,这(取决于您的循环)可能会节省大量时间。
【解决方案2】:

一般来说,保持距离非常近是一种很好的做法。

在某些情况下,会考虑将变量拉出循环的合理性,例如性能。

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

这样做:

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

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

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

【讨论】:

  • 对于基本数据类型,如 float 或 int,在循环内声明变量会比在循环外声明该变量慢,因为每次迭代都必须为变量分配空间?
  • @Kasparov92 简短回答是 “不。忽略优化并尽可能将其放入循环中以提高可读性/局部性。编译器可以为您执行该微优化。” i> 更详细地说,这最终由编译器根据平台的最佳选择、优化级别等来决定。循环内的普通 int/float 通常会放在堆栈上。如果这样做有优化,编译器当然可以将其移出循环并重用存储。出于实际目的,这将是一个非常非常非常小的优化......
  • @Kasparov92 ...(续)您只会在每个周期都重要的环境/应用程序中考虑。在这种情况下,您可能只想考虑使用程序集。
【解决方案3】:

我不是为了回答 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 编译的,所以我知道它可以工作;另外,我试图在定义它的括号之外使用变量,但我收到了一个错误,所以我知道该变量被“破坏”了。

我不知道使用这种方法是否是不好的做法,因为很多未标记的括号会很快使代码不可读,但也许一些 cmets 可以解决问题。

【讨论】:

  • 对我来说,这是一个非常合理的答案,它带来了与问题直接相关的建议。你有我的投票!
【解决方案4】:

对于 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 秒内获得输出,因为您还没有破坏对象.....
【解决方案5】:

由于您的第二个问题更具体,我将先解决它,然后结合第二个给出的上下文处理您的第一个问题。我想给出一个比这里已经存在的更基于证据的答案。

问题 #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.01 秒。毕竟,这只是循环中的一个额外操作。 (对于一个字符串来说,循环内操作的差异显然要大一些,但不是很明显。)

更重要的是,这个问题主要是学术问题,因为优化级别为 -O1 或更高的 gcc 为两个源文件输出相同的 asm,clang 也是如此。因此,至少对于像这样的简单情况,无论哪种方式都不太可能对性能产生任何影响。当然,在现实世界的程序中,您应该始终剖析而不是做出假设。

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

与几乎每个这样的问题一样,这取决于。如果声明在一个非常紧凑的循环内,并且您在没有优化的情况下进行编译,例如出于调试目的,那么理论上将其移出循环可能会提高性能,以便在您的调试工作中方便使用。如果是这样,它可能是明智的,至少在您进行调试时是这样。虽然我不认为它可能对优化的构建产生任何影响,但如果您确实观察到一个,您/您的配对/您的团队可以判断它是否值得。

同时,您不仅要考虑编译器如何读取您的代码,还要考虑它如何传递给人类,包括您自己。我想你会同意在尽可能小的范围内声明的变量更容易跟踪。如果它在循环之外,则意味着它需要在循环之外,如果事实并非如此,这会令人困惑。在大型代码库中,随着时间的推移,像这样的小混乱会随着时间的推移而增加,并且在工作数小时后变得令人疲倦,并可能导致愚蠢的错误。这可能比从轻微的性能改进中获得的成本要高得多,具体取决于用例。

【讨论】:

    【解决方案6】:

    从前(C++98 之前);以下内容会中断:

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

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

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

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

    【讨论】:

      【解决方案7】:

      这是一个非常好的做法,因为以上所有答案都提供了问题的非常好的理论方面让我看一下代码,我试图通过 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。试一试....你会得到它。希望这能有所帮助。

      【讨论】:

      • 我认为这不适用于这个问题。显然,在您上面的情况下,这很重要。问题是处理变量定义可以在其他地方定义而不改变代码行为的情况。
      • 在您发布的代码中,问题不在于定义,而在于初始化部分。 flag 应在每次 while 迭代时重新初始化为 0。这是一个逻辑问题,而不是定义问题。
      【解决方案8】:

      K&R The C Programming Language 2.Ed.中的第 4.8 章 块结构

      一个自动变量声明和初始化 每次进入块时都会初始化块。

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

      一个自动变量声明和初始化 块在进入块之前只分配一次。

      但一个简单的测试可以证明所持有的假设:

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

      【讨论】:

        【解决方案9】:

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

        如果我在 a 内部或外部声明变量是否会有所不同,如果我在 Java 中的循环内部或外部声明变量是否会有所不同?这是 for(int i = 0; i

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

        【讨论】:

          猜你喜欢
          • 2020-01-18
          • 1970-01-01
          • 2021-07-06
          • 2016-11-10
          • 1970-01-01
          • 1970-01-01
          • 2020-05-19
          • 1970-01-01
          • 2013-01-24
          相关资源
          最近更新 更多