ChatGPT解决这个技术问题 Extra ChatGPT

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

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

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

例子:

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

   cout << someString;
}

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

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

K
Krishna Pradyumna Mokshagundam

这是极好的做法。

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

这边走:

如果变量的名称有点“通用”(如“i”),则不会有将其与代码中稍后某处的另一个同名变量混合的风险(也可以使用 GCC 上的 -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++ 类,有一个构造函数,我对此知之甚少。我想分配可能不会成为问题,因为编译器应该足够聪明以重用相同的空间,但初始化很可能发生在每次循环迭代中。


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

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

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

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

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

它可以重用 variable 占用的空间,并且可以将不变量拉出循环。在 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 简短的回答是“不。忽略该优化并将其放在循环中以提高可读性/局部性。编译器可以为您执行该微优化。”更详细地说,这最终由编译器根据平台的最佳选择、优化级别等来决定。循环内的普通 int/float 通常会放在堆栈上。如果这样做有优化,编译器当然可以将其移出循环并重用存储。出于实际目的,这将是一个非常非常非常小的优化......
@Kasparov92 …(续)您只会在每个周期都很重要的环境/应用程序中考虑。在这种情况下,您可能只想考虑使用汇编。
P
Peter Mortensen

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

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


对我来说,这是一个非常合理的答案,它带来了与问题直接相关的建议。你有我的一票!
g
gdelfino

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

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

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


好吧,从技术上讲,在第二个版本中,您将在 2 秒内获得输出,因为您还没有破坏对象......
Z
Zoë Sparks

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

问题#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:在循环中声明变量是好做法还是坏做法?

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

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


U
UKMonkey

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

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

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

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

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


M
Muhtasim Ulfat Tanmoy

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

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

因此,在涉及分析反对或计算广泛的构造函数之前,保持 declation 接近其使用应该是默认方法。


K
KhanJr

这是一个非常好的做法,因为以上所有答案都提供了问题的非常好的理论方面让我看一下代码,我试图通过 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。这是一个逻辑问题,而不是定义问题。
s
sof

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

m
madanswer

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

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

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