猿问
下载APP

在循环中声明变量,良好实践或不良实践?

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


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


例:


for(int counter = 0; counter <= 10; counter++)

{

   string someString = "testing";


   cout << someString;

}

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


一只甜甜圈
浏览 71回答 3
3回答

幕布斯5086720

这是一种很好的做法。通过在循环内创建变量,可以确保它们的范围仅限于循环内部。它不能在循环外引用或调用。这条路:如果变量的名称有点“通用”(如“i”),那么在代码的后面某处将它与同名的另一个变量混合就没有风险(也可以使用-WshadowGCC上的警告指令进行缓解)编译器知道变量作用域仅限于循环内部,因此如果变量在其他地方被错误引用,则会发出正确的错误消息。最后但并非最不重要的是,编译器可以更有效地执行一些专用优化(最重要的是寄存器分配),因为它知道变量不能在循环之外使用。例如,无需存储结果以供以后重复使用。简而言之,你是对的。但请注意,变量不应该在每个循环之间保留其值。在这种情况下,您可能需要每次都初始化它。您还可以创建一个更大的块,包含循环,其唯一目的是声明必须将其值从一个循环保留到另一个循环的变量。这通常包括循环计数器本身。{&nbsp; &nbsp; int i, retainValue;&nbsp; &nbsp; for (i=0; i<N; i++)&nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp;int tmpValue;&nbsp; &nbsp; &nbsp; &nbsp;/* tmpValue is uninitialized */&nbsp; &nbsp; &nbsp; &nbsp;/* retainValue still has its previous value from previous loop */&nbsp; &nbsp; &nbsp; &nbsp;/* Do some stuff here */&nbsp; &nbsp; }&nbsp; &nbsp; /* Here, retainValue is still valid; tmpValue no longer */}对于问题#2:当调用函数时,变量被分配一次。实际上,从分配的角度来看,它几乎与在函数开头声明变量相同。唯一的区别是范围:变量不能在循环外使用。甚至可能没有分配变量,只是重新使用一些空闲槽(来自其范围已经结束的其他变量)。通过限制和更精确的范围,可以获得更准确的优化。但更重要的是,它使您的代码更安全,在阅读代码的其他部分时需要担心更少的状态(即变量)。即使在if(){...}街区之外也是如此。通常,而不是:&nbsp; &nbsp; int result;&nbsp; &nbsp; (...)&nbsp; &nbsp; result = f1();&nbsp; &nbsp; if (result) then { (...) }&nbsp; &nbsp; (...)&nbsp; &nbsp; result = f2();&nbsp; &nbsp; if (result) then { (...) }写起来更安全:&nbsp; &nbsp; (...)&nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; int const result = f1();&nbsp; &nbsp; &nbsp; &nbsp; if (result) then { (...) }&nbsp; &nbsp; }&nbsp; &nbsp; (...)&nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; int const result = f2();&nbsp; &nbsp; &nbsp; &nbsp; if (result) then { (...) }&nbsp; &nbsp; }差异似乎很小,特别是在这么小的例子上。但是在更大的代码库中,它会有所帮助:现在没有风险将某些result值传输f1()到f2()阻塞。每个result都严格限制在自己的范围内,使其角色更加准确。从评论者的角度来看,它更好,因为他需要担心和跟踪的长程状态变量较少。即使是编译器也会有所帮助:假设在将来,在一些错误的代码更改后,result未正确初始化f2()。第二个版本将简单地拒绝工作,在编译时声明一个明确的错误消息(比运行时更好)。第一个版本不会发现任何东西,结果f1()将只是第二次测试,被混淆的结果f2()。补充资料开源工具CppCheck(一种用于C / C ++代码的静态分析工具)提供了一些关于变量最佳范围的优秀提示。回应对分配的评论:上述规则在C中是正确的,但可能不适用于某些C ++类。对于标准类型和结构,变量的大小在编译时是已知的。在C中没有“构造”这样的东西,所以当调用函数时,变量的空间将被简单地分配到堆栈中(没有任何初始化)。这就是在循环中声明变量时出现“零”成本的原因。但是,对于C ++类,我不太了解这个构造函数。我认为分配可能不会成为问题,因为编译器应该足够聪明以重用相同的空间,但初始化很可能发生在每次循环迭代。

慕瓜9086354

一般来说,保持非常接近是一种非常好的做法。在某些情况下,会有诸如性能之类的考虑因素证明将变量拉出循环是正确的。在您的示例中,程序每次都会创建并销毁字符串。有些库使用小型字符串优化(SSO),因此在某些情况下可以避免动态分配。假设你想避免那些冗余的创作/分配,你会把它写成:for (int counter = 0; counter <= 10; counter++) {&nbsp; &nbsp;// compiler can pull this out&nbsp; &nbsp;const char testing[] = "testing";&nbsp; &nbsp;cout << testing;}或者你可以拉出常数:const std::string testing = "testing";for (int counter = 0; counter <= 10; counter++) {&nbsp; &nbsp;cout << testing;}大多数编译器是否已经声明变量已经被声明并且只是跳过那个部分,或者它实际上每次都在内存中为它创建一个位置?它可以重用变量消耗的空间,并且可以将不变量拉出循环。在const char数组的情况下(上图) - 该数组可以被拉出。但是,在对象(例如std::string)的情况下,必须在每次迭代时执行构造函数和析构函数。在该情况下std::string,“空间”包括指针,该指针包含表示字符的动态分配。所以这:for (int counter = 0; counter <= 10; counter++) {&nbsp; &nbsp;string testing = "testing";&nbsp; &nbsp;cout << testing;}在每种情况下都需要冗余复制,并且如果变量高于SSO字符数的阈值(并且SSO由std库实现),则动态分配和释放。这样做:string testing;for (int counter = 0; counter <= 10; counter++) {&nbsp; &nbsp;testing = "testing";&nbsp; &nbsp;cout << testing;}在每次迭代时仍然需要字符的物理副本,但是表单可能会导致一次动态分配,因为您分配了字符串,并且实现应该看到不需要调整字符串的后备分配。当然,在这个例子中你不会这样做(因为已经演示了多个优秀的替代方案),但是当字符串或向量的内容变化时你可能会考虑它。那么你如何处理所有这些选择(以及更多)?默认情况下保持非常接近 - 直到您了解成本并知道何时应该偏离。

眼眸繁星

对于C ++,它取决于你在做什么。好吧,这是愚蠢的代码,但想象一下class myTimeEatingClass{&nbsp;public:&nbsp;//constructor&nbsp; &nbsp; &nbsp; myTimeEatingClass()&nbsp; &nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sleep(2000);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ms_usedTime+=2;&nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; ~myTimeEatingClass()&nbsp; &nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sleep(3000);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ms_usedTime+=3;&nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; const unsigned int getTime() const&nbsp; &nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return&nbsp; ms_usedTime;&nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; static unsigned int ms_usedTime;};myTimeEatingClass::ms_CreationTime=0;&nbsp;myFunc(){&nbsp; &nbsp; for (int counter = 0; counter <= 10; counter++) {&nbsp; &nbsp; &nbsp; &nbsp; myTimeEatingClass timeEater();&nbsp; &nbsp; &nbsp; &nbsp; //do something&nbsp; &nbsp; }&nbsp; &nbsp; cout << "Creating class took "<< timeEater.getTime() <<"seconds at all<<endl;}myOtherFunc(){&nbsp; &nbsp; myTimeEatingClass timeEater();&nbsp; &nbsp; for (int counter = 0; counter <= 10; counter++) {&nbsp; &nbsp; &nbsp; &nbsp; //do something&nbsp; &nbsp; }&nbsp; &nbsp; cout << "Creating class took "<< timeEater.getTime() <<"seconds at all<<endl;}您将等待55秒,直到获得myFunc的输出。仅仅因为每个循环构造函数和析构函数一起需要5秒才能完成。在获得myOtherFunc的输出之前,您将需要5秒钟。当然,这是一个疯狂的例子。但它说明当构造函数和/或析构函数需要一些时间时,每个循环完成相同的构造时,它可能会成为性能问题。
打开App,查看更多内容
随时随地看视频慕课网APP
我要回答