359

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

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

例子:

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

   cout << someString;
}

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

4

9 回答 9

448

这是极好的做法。

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

这边走:

  • 如果变量的名称有点“通用”(如“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++ 类,有一个构造函数,我对此知之甚少。我想分配可能不会成为问题,因为编译器应该足够聪明以重用相同的空间,但初始化很可能发生在每次循环迭代中。

于 2011-10-31T20:57:37.890 回答
34

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

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

在您的示例中,程序每次都会创建和销毁字符串。一些库使用小字符串优化 (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;
}

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

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

于 2013-08-01T21:28:26.680 回答
18

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

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

于 2015-04-08T17:11:45.300 回答
16

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

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

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

于 2015-09-16T17:48:44.077 回答
1

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

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

您可以通过在汇编程序运行之前停止编译器并查看 asm.xml 来自己回答这个问题。(-S如果您的编译器具有 gcc 样式的接口,并且-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:在循环中声明变量是好习惯还是坏习惯?

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

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

于 2020-12-12T02:23:01.147 回答
1

从前(C++ 98 之前);以下将打破:

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

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

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

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

于 2021-02-19T22:12:13.157 回答
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。试一试......你会得到它。希望这有帮助。

于 2020-01-07T19:37:16.127 回答
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;                                                                                                         
 }                                                                                                                     
于 2020-04-09T11:37:11.727 回答
-1

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

如果我在 a 内部或外部声明变量是否会有所不同,如果我在 Java 中的循环内部或外部声明变量是否会有所不同?这是 for(int i = 0; i < 1000; i++) { int 在单个变量的级别上,效率没有显着差异,但是如果您有一个具有 1000 个循环和 1000 个变量的函数(别介意不好的样式暗示)可能存在系统性差异,因为所有变量的所有生命都是相同的,而不是重叠的。

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

于 2020-12-27T12:39:42.003 回答