在循环中声明变量,好习惯还是坏习惯?

在循环中声明变量是好做法还是坏做法?

我已经阅读了其他关于是否存在性能问题的线程(大多数人说没有),并且您应该始终在接近它们将被使用的地方声明变量。我想知道的是,这种情况是否应该避免,还是更可取。

例子:

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


cout << someString;
}

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

227270 次浏览

这是优秀的实践。

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

这种方式:

  • 如果变量的名称有点“通用”(如“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 { (...) }
}
差异可能看起来很小,尤其是在这样一个小例子中。 但在更大的代码基础上,这将有所帮助:现在从f1()块传输一些result值到f2()块没有风险。每个result都严格限制在自己的范围内,使其作用更加准确。从评论者的角度来看,这样更好,因为他不必担心和跟踪长范围状态变量

甚至编译器也会有更好的帮助:假设在将来,在一些错误的代码更改之后,result没有正确地用f2()初始化。第二个版本将简单地拒绝工作,在编译时声明一个明确的错误消息(比运行时好得多)。第一个版本不会发现任何东西,f1()的结果将被简单地测试第二次,与f2()的结果混淆。

补充信息

开源工具CppCheck (C/ c++代码的静态分析工具)提供了一些关于变量的最佳作用域的极好提示。

在回应关于分配的评论时: 以上规则在C中是正确的,但对于某些c++类可能不是

对于标准类型和结构,变量的大小在编译时已知。在C语言中没有“构造”这样的东西,所以当函数被调用时,变量的空间将被简单地分配到堆栈中(没有任何初始化)。这就是为什么在循环中声明变量时代价为“零”的原因。

然而,对于c++类,有一个构造函数的东西,我知道得少得多。我想分配可能不是问题,因为编译器应该足够聪明,可以重用相同的空间,但初始化很可能发生在每次循环迭代中。

一般来说,把它放在很近的地方是一个很好的做法。

在某些情况下,出于性能等考虑,需要将变量从循环中取出。

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

在每次迭代时仍然需要一个字符的物理副本,但这种形式可能会导致一次动态分配,因为您分配了字符串,实现应该看到没有必要调整字符串的支持分配。当然,在本例中您不会这样做(因为已经演示了多个更好的替代方法),但是当字符串或向量的内容发生变化时,您可以考虑这样做。

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

我没有发帖回答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”等后面。

我的代码编译在微软Visual c++ 2010 Express,所以我知道它的工作;此外,我已经尝试使用它在括号外定义的变量,我收到了一个错误,所以我知道变量被“销毁”。

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

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

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

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

这是一个非常好的实践,因为所有以上的答案都提供了非常好的理论方面的问题,让我看了一下代码,我试图解决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。试一试....你会明白的。

第4.8章K&R的C编程语言2.Ed。中的块结构:

类中声明并初始化的自动变量

. Block每次输入时都被初始化

我可能没有在书中看到相关的描述,比如:

类中声明并初始化的自动变量

. Block在输入前只分配一次

但一个简单的测试可以证明这个假设:

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

既然你的第二个问题比较具体,我就先回答你的第二个问题,然后结合第二个问题的背景回答你的第一个问题。我想给出一个比现有的更有根据的答案。

问题# 2:大多数编译器都意识到变量已经 被声明过,跳过这部分,或者它实际上创建了一个 每次都在记忆中找到它?< / p >

您可以通过在运行汇编程序之前停止编译器并查看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:在循环中声明变量是一种好做法还是 糟糕的实践?< / p >

就像几乎所有类似的问题一样,这要视情况而定。如果声明是在一个非常紧密的循环中,并且您在编译时没有进行优化,比如为了调试目的,理论上可以将其移到循环之外以提高性能,从而在调试过程中更加方便。如果是这样,那么至少在调试时这样做是合理的。尽管我不认为这对优化构建有什么影响,但如果你确实观察到了一个,你/你的搭档/你的团队可以判断它是否值得。

与此同时,你不仅要考虑编译器如何读取你的代码,还要考虑它如何呈现给人类,包括你自己。我想您会同意,在尽可能小的范围内声明的变量更容易跟踪。如果它在循环之外,这意味着它在循环之外被需要,如果实际情况不是这样的话,这是令人困惑的。在一个大的代码库中,像这样的小混乱会随着时间的推移而累积,并在数小时的工作后变得疲惫,并可能导致愚蠢的bug。根据用例的不同,这可能比从轻微的性能改进中获得的收益要昂贵得多。

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

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

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

如果你仍然要支持这样一个旧的编译器(有些人在Borland),那么答案是肯定的,可以把i从循环中放出来,因为不这样做会使它变得“更难”。对于使用同一个变量进行多次循环的人来说,尽管说实话,编译器仍然会失败,如果出现问题,这就是你想要的。

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

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

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

因此,除非涉及到分析或计算扩展构造函数,否则保持声明接近其用法应该是默认方法。