Объявление переменных внутри циклов, хорошая практика или плохая практика?

266

Вопрос № 1: является ли объявление переменной внутри цикла хорошей или плохой практикой?

Я читал другие темы о том, есть ли проблемы с производительностью (большинство сказали нет), и что вы всегда должны объявлять переменные как можно ближе к месту их использования. Что мне интересно, так это то, следует ли этого избегать или это действительно предпочтительнее.

Пример:

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

   cout << someString;
}

Вопрос № 2: понимают ли большинство компиляторов, что переменная уже была объявлена, и просто пропускают эту часть, или она фактически создает место для нее в памяти каждый раз?

JeramyRR
источник
29
Поместите их близко к их использованию, если профилирование не говорит иначе.
Mooing Duck
1
Вот несколько похожих вопросов: stackoverflow.com/questions/982963/… stackoverflow.com/questions/407255/…
drnewman
3
@drnewman Я прочитал эти темы, но они не ответили на мой вопрос. Я понимаю, что объявление переменных внутри циклов работает. Мне интересно, будет ли это хорошей практикой или этого следует избегать.
JeramyRR

Ответы:

348

Это отличная практика.

Создавая переменные внутри циклов, вы гарантируете, что их область действия ограничена внутри цикла. На него нельзя ссылаться и вызывать вне цикла.

Сюда:

  • Если имя переменной немного «универсально» (например, «i»), нет риска смешать ее с другой переменной с тем же именем где-нибудь позже в вашем коде (это также можно уменьшить с помощью -Wshadowинструкции предупреждения в GCC)

  • Компилятор знает, что область действия переменной ограничена внутри цикла, и, следовательно, выдаст правильное сообщение об ошибке, если на переменную по ошибке ссылаются в другом месте.

  • И последнее, но не менее важное: некоторая специальная оптимизация может выполняться компилятором более эффективно (наиболее важно распределение регистров), поскольку он знает, что переменная не может использоваться вне цикла. Например, нет необходимости сохранять результат для последующего повторного использования.

Короче говоря, вы правы сделать это.

Однако обратите внимание, что переменная не должна сохранять свое значение между каждым циклом. В этом случае вам может потребоваться инициализировать его каждый раз. Вы также можете создать больший блок, охватывающий цикл, единственной целью которого является объявление переменных, которые должны сохранять свое значение от одного цикла к другому. Обычно это включает сам счетчик циклов.

{
    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 ++ есть конструктор, о котором я знаю гораздо меньше. Я предполагаю, что распределение, вероятно, не будет проблемой, так как компилятор должен быть достаточно умным, чтобы повторно использовать то же пространство, но инициализация, вероятно, будет происходить на каждой итерации цикла.

Cyan
источник
4
Потрясающий ответ. Это именно то, что я искал, и даже дал мне некоторое представление о том, чего я не осознавал. Я не осознавал, что область остается только внутри цикла. Спасибо за ответ!
JeramyRR
22
«Но это никогда не будет медленнее, чем выделение в начале функции». Это не всегда так. Переменная будет выделена один раз, но она будет создаваться и уничтожаться столько раз, сколько необходимо. Что в случае примера кода, в 11 раз. Цитирую комментарий Муинга: «Поместите их ближе к их использованию, если профилирование не говорит иначе».
IronMensan
4
@JeramyRR: Абсолютно нет - у компилятора нет возможности узнать, есть ли у объекта значимые побочные эффекты в его конструкторе или деструкторе.
ildjarn
2
@Iron: С другой стороны, когда вы сначала объявляете элемент, вы просто получаете много вызовов оператору присваивания; которая обычно стоит примерно столько же, сколько строительство и разрушение объекта.
Билли ONEAL
4
@BillyONeal: Для stringи в vectorчастности, оператор присваивания может повторно использовать выделенный буфер каждого цикла, который ( в зависимости от вашего цикла) может быть огромная экономия времени.
Mooing Duck
22

Как правило, это очень хорошая практика, чтобы держать это очень близко.

В некоторых случаях, например, производительность, которая оправдывает вытягивание переменной из цикла.

В вашем примере программа создает и уничтожает строку каждый раз. В некоторых библиотеках используется небольшая оптимизация строк (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;
}

потребует избыточного копирования в каждом случае, а также динамического размещения и освобождения, если переменная находится выше порогового значения для количества символов единого входа (а единый вход реализован вашей стандартной библиотекой).

Делая это:

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

по-прежнему потребуется физическая копия символов на каждой итерации, но форма может привести к одному динамическому выделению, потому что вы присваиваете строку, и реализация должна видеть, что нет необходимости изменять размер резервного размещения строки. Конечно, вы не сделаете этого в этом примере (потому что несколько улучшенных альтернатив уже были продемонстрированы), но вы можете рассмотреть это, когда содержимое строки или вектора меняется.

Так что вы делаете со всеми этими вариантами (и больше)? Держите его очень близко по умолчанию - пока вы не будете хорошо понимать затраты и не будете знать, когда вам следует отклоняться.

джастин
источник
1
Что касается базовых типов данных, таких как float или int, будет ли объявление переменной внутри цикла медленнее, чем объявление этой переменной вне цикла, так как для каждой итерации будет выделяться место для переменной?
Kasparov92
2
@ Kasparov92 Короткий ответ: «Нет. Игнорируйте эту оптимизацию и по возможности помещайте ее в цикл для улучшения читабельности / локальности. Компилятор может выполнить эту микрооптимизацию за вас». Более подробно, это в конечном итоге решать компилятору, исходя из того, что лучше для платформы, уровней оптимизации и т. Д. Обычный int / float внутри цикла обычно помещается в стек. Компилятор, безусловно, может переместить это за пределы цикла и повторно использовать хранилище, если в этом есть оптимизация. Для практических целей это была бы очень-очень маленькая оптимизация…
Джастин
1
@ Kasparov92… (продолжение), который вы рассматриваете только в средах / приложениях, где учитывается каждый цикл. В этом случае вы можете просто рассмотреть возможность использования сборки.
Джастин
14

Для 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.

Конечно, это безумный пример.

Но это показывает, что это может стать проблемой производительности, когда каждый цикл выполняется одинаковой конструкцией, когда конструктору и / или деструктору требуется некоторое время.

кричащий
источник
2
Ну, технически во второй версии вы получите результат всего за 2 секунды, потому что вы еще не уничтожили объект .....
Chrys
12

Я не отвечал на вопросы 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.

Я не знаю, понимаете ли вы (я не знал, когда впервые начал программировать), что квадратные скобки (если они попарно) могут быть размещены в любом месте кода, а не только после «если», «для», « время "и т. д.

Мой код скомпилирован в Microsoft Visual C ++ 2010 Express, поэтому я знаю, что он работает; Кроме того, я попытался использовать переменную вне скобок, в которой она была определена, и я получил ошибку, поэтому я знаю, что переменная была «уничтожена».

Я не знаю, является ли плохой практикой использование этого метода, так как многие непомеченные скобки могут быстро сделать код нечитаемым, но, возможно, некоторые комментарии могут прояснить ситуацию.

Fearnbuster
источник
4
Для меня это очень законный ответ, который приносит предложение, напрямую связанное с вопросом. У тебя есть мой голос!
Алексис Леклерк
0

Это очень хорошая практика, так как все вышеприведенные ответы дают очень хороший теоретический аспект вопроса, позвольте мне взглянуть на код, я пытался решить DFS через GEEKSFORGEEKS, я столкнулся с проблемой оптимизации ...... Если вы попытаетесь решить код, объявляющий целое число вне цикла, даст вам сообщение об ошибке оптимизации.

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 . просто дай ему шанс ... ты получишь это. Надеюсь, это поможет.

KhanJr
источник
Я не думаю, что это относится к вопросу. Очевидно, в вашем случае выше это имеет значение. Вопрос касался случая, когда определение переменной могло быть определено в другом месте без изменения поведения кода.
pcarter
В опубликованном вами коде проблема не в определении, а в части инициализации. flagследует повторно инициализировать в 0 на каждой whileитерации. Это логическая проблема, а не проблема определения.
Мартин Веронно
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;                                                                                                         
 }                                                                                                                     
софы
источник