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

eblbsuwk  于 2023-11-19  发布在  其他
关注(0)|答案(9)|浏览(179)

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

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

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

   cout << someString;
}

字符串

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

kse8i1jr

kse8i1jr1#

这是一个很好的练习。
通过在循环内创建变量,你可以确保它们的作用域被限制在循环内。它不能在循环外被引用或调用。
这样:

  • 如果变量的名称有点“通用”(如“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:当函数被调用时,变量被分配一次。事实上,从分配的Angular 来看,它(几乎)与在函数开始时声明变量相同。唯一的区别是作用域:变量不能在循环之外使用。甚至可能变量没有被分配,只是重新使用一些空闲槽(从其他作用域已经结束的变量中)。
限制和更精确的作用域会带来更精确的优化。但更重要的是,它使代码更安全,当阅读代码的其他部分时,需要担心的状态(即变量)更少。
即使在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都严格限制在自己的范围内,使其角色更加准确。从审阅者的Angular 来看,它要好得多,因为他需要担心和跟踪的“长程状态变量”更少。
即使是编译器也会有更好的帮助:假设在将来,在一些错误的代码更改之后,result没有正确地用f2()初始化。第二个版本将简单地拒绝工作,在编译时声明一个明确的错误消息(比运行时好得多)。第一个版本不会发现任何东西,f1()的结果将简单地进行第二次测试,结果为f2()

补充信息

开源工具CppCheck(C/C代码的静态分析工具)提供了一些关于变量最佳作用域的极好提示。
对于分配的评论:上面的规则在C中是正确的,但对于某些C
类可能不是。
对于标准类型和结构,变量的大小在编译时就已经知道了。在C中没有“构造”这样的东西,所以当函数被调用时,变量的空间将被简单地分配到堆栈中(没有任何初始化)。这就是为什么在循环中声明变量时成本为“零”。
然而,对于C++类,有一个构造函数,我对它知之甚少。我想分配可能不会成为问题,因为编译器应该足够聪明,可以重用相同的空间,但初始化很可能发生在每次循环迭代中。

vc6uscn9

vc6uscn92#

一般来说,这是一个非常好的做法,保持它非常接近。
在某些情况下,会有一个考虑因素,如性能,证明拉变量的循环。
在你的例子中,程序每次都创建和销毁字符串。一些库使用小字符串优化(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;
}


仍然需要在每次迭代时都有一个字符的物理副本,但是表单可能会导致一个动态分配,因为您分配了字符串,并且实现应该看到没有必要调整字符串的支持分配。当然,在这个例子中您不会这样做(因为已经演示了多个上级替代方案),但是当字符串或向量的内容变化时,您可以考虑它。
那么,你如何处理所有这些选项(以及更多选项)呢?将其作为默认值保持在非常接近的位置--直到你很好地理解了成本,知道什么时候应该偏离。

3htmauhk

3htmauhk3#

我没有发帖回答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”,“while”等之后。
我的代码在Microsoft Visual C++ 2010 Express中编译,所以我知道它可以工作;此外,我试图在定义它的括号之外使用变量,我收到了一个错误,所以我知道变量被“销毁”了。
我不知道使用这种方法是否是一种不好的做法,因为大量未标记的括号可能会很快使代码变得不可读,但也许一些注解可以澄清这一点。

xwmevbvl

xwmevbvl4#

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

class myTimeEatingClass

个字符
你将等待55秒直到你得到myFunc的输出。只是因为每个循环构造函数和析构函数一起需要5秒才能完成。
你需要5秒钟,直到你得到mylog Func的输出。
当然,这是一个疯狂的例子。
但它说明了当构造函数和/或析构函数需要一些时间时,每次循环都完成相同的构造时,这可能会成为一个性能问题。

szqfcxe2

szqfcxe25#

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

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

默认的方法应该是使声明接近它们的用法,除非分析表明存在性能问题或者存在涉及大量计算的构造函数。

sg3maiej

sg3maiej6#

由于你的第二个问题更具体,我将首先解决它,然后在第二个问题给出的背景下回答你的第一个问题。我想给出一个比这里已经给出的更有证据基础的答案。

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

你可以通过在汇编器运行之前停止编译器并查看asm来自己回答这个问题(如果你的编译器有gcc风格的接口,请使用-S标志,如果你想要我在这里使用的语法风格,请使用-masm=intel标志)。
在任何情况下,对于x86-64的现代编译器(gcc 10.2,clang 11.0),如果您禁用优化,它们只会在每次循环时重新加载变量。考虑以下C++程序-为了直观地Map到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- 7700 K)直到我将迭代次数推到数十亿次,即使这样,平均差异也不到0.01s。毕竟,这只是循环中的一个额外操作。(对于字符串,循环内操作的差异显然要大一些,但并不显著。
更重要的是,这个问题在很大程度上是学术性的,因为在-O1或更高的优化级别下,gcc为两个源文件输出相同的asm,clang也是如此。所以,至少对于像这样的简单情况,这两种方式都不太可能对性能产生任何影响。当然,在实际的程序中,你应该总是分析而不是假设。

Question #1:在循环中声明一个变量是好的做法还是坏的做法?

对于几乎所有类似的问题,这取决于。如果声明在一个非常紧密的循环中,并且您在没有优化的情况下进行编译,例如出于调试目的,理论上可能将其移动到循环外会提高性能,以便在调试过程中使用。如果是这样,这可能是明智的,虽然我不认为这对优化的构建有什么影响,但如果你观察到了一个,你/你的搭档/你的团队可以判断它是否值得。
同时,你不仅要考虑编译器如何读取你的代码,还要考虑它对人类(包括你自己)的影响。我想你会同意,在尽可能小的范围内声明的变量更容易跟踪。如果它在循环之外,这意味着它需要在循环之外,如果事实并非如此,这会令人困惑。在一个大的代码库中,像这样的小混乱随着时间的推移而积累起来,在工作数小时后变得疲惫不堪,并可能导致愚蠢的错误。这可能比您从轻微的性能改进中获得的代价要高得多,具体取决于用例。

ubbxdtey

ubbxdtey7#

很久以前(C++98之前),下面的代码会被破坏:

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

字符串
并警告我已经声明了(foo很好,因为它的作用域在{}内)。这可能是人们首先认为它不好的原因。尽管很久以前它就不再是真的了。
如果你仍然必须支持这样一个旧的编译器(有些人在Borland上),那么答案是肯定的,可以把i放在循环之外,因为不这样做会让人们用同一个变量放入多个循环变得“更难”,尽管老实说编译器仍然会失败,如果有问题的话,这就是你想要的。
如果你不再需要支持这样一个旧的编译器,变量应该保持在你能得到的最小范围内,这样你不仅可以最小化内存的使用,而且可以更容易地理解这个项目。这有点像问为什么你不把所有的变量都设置为全局变量。同样的论点适用,但是范围只是改变了一点。

yv5phkfx

yv5phkfx8#

这是一个非常好的实践,因为上面所有的答案都提供了非常好的理论方面的问题让我给予了一下代码,我试图解决DFS over GEEKSFORGEKS,我遇到了优化问题.

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.只要给予一个机会.你会得到它的。希望这个帮助。

8fsztsew

8fsztsew9#

K&R's 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;                                                                                                         
 }

字符串

相关问题