Декларування змінних всередині циклів, належна практика чи погана практика?


265

Питання №1: Чи є оголошення змінної всередині циклу доброю практикою чи поганою практикою?

Я читав інші теми про те, чи існує проблема з продуктивністю (більшість сказаних "ні"), і що ви завжди повинні оголошувати змінні якомога ближче до того, де вони будуть використовуватися. Мені цікаво, чи слід цього уникати чи ні, чи це насправді перевагу.

Приклад:

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

   cout << someString;
}

Питання №2: Чи розуміє більшість компіляторів, що змінна вже була оголошена і просто пропускає цю частину, чи вона насправді створює місце для неї в пам'яті кожного разу?


29
Розмістіть їх близько до їх використання, якщо тільки в профілюванні не вказано інше.
Mooing Duck


3
@drnewman Я читав ці теми, але вони не відповіли на моє запитання. Я розумію, що оголошення змінних всередині циклів працює. Мені цікаво, чи добре це робити, чи потрібно чогось уникати.
JeramyRR

Відповіді:


348

Це відмінна практика.

Створюючи змінні всередині циклів, ви гарантуєте, що їх сфера дії обмежена всередині циклу. На нього не можна посилатись і не викликати поза циклу.

Сюди:

  • Якщо назва змінної трохи "generic" (наприклад, "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 ++ є ця конструкторська річ, про яку я знаю набагато менше. Я думаю, що виділення, ймовірно, не буде проблемою, оскільки компілятор повинен бути досить розумним, щоб повторно використовувати той самий простір, але ініціалізація, ймовірно, відбуватиметься при кожній ітерації циклу.


4
Дивовижна відповідь. Це саме те, що я шукав, і навіть дав мені уявлення про те, чого я не усвідомлював. Я не розумів, що область залишається тільки в циклі. Дякую за відповідь!
JeramyRR

22
"Але це ніколи не буде повільніше, ніж виділення на початку функції." Це не завжди так. Змінна буде виділена один раз, але вона все одно буде побудована та знищена стільки разів, скільки потрібно. Що у випадку з прикладом коду, це 11 разів. Цитуючи коментар Mooing "Поставте їх поруч із їх використанням, якщо тільки в профілюванні не сказано іншого".
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;
}

вимагатиме надмірного копіювання у кожному випадку, а також динамічного розподілу та безкоштовного, якщо змінна лежить вище порогової для кількості символів SSO (а SSO реалізована вашою бібліотекою std).

Робити це:

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

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

Отже, що ви робите з усіма цими варіантами (і більше)? Тримайте це дуже близько як за замовчуванням - до тих пір, поки ви добре не зрозумієте витрати і не дізнаєтесь, коли слід відхилятися.


1
Що стосується базових типів даних, таких як float або int, чи буде оголошення змінної всередині циклу повільнішим, ніж оголошення цієї змінної за межами циклу, оскільки їй доведеться виділяти простір для змінної кожної ітерації?
Kasparov92

2
@ Kasparov92 Коротка відповідь - "Ні. Ігноруйте цю оптимізацію та розмістіть її у циклі, коли це можливо для покращення читабельності / локальності. Компілятор може виконати цю мікрооптимізацію для вас."Більш детально, що в кінцевому підсумку компілятор повинен вирішити, виходячи з того, що найкраще для платформи, рівнів оптимізації тощо. Зазвичай звичайний int / float всередині циклу розміщується на стеці. Компілятор, безумовно, може перемістити це за межі циклу і повторно використовувати сховище, якщо в цьому буде оптимізовано. Для практичних цілей це була б дуже-дуже маленька оптимізація…
Justin

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, тому я знаю, що він працює; Крім того, я намагався використовувати змінну поза дужками, для якої вона визначена, і я отримав помилку, тому знаю, що змінна була "знищена".

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


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 . просто дайте йому постріл .... ви отримаєте це. Надійтесь на цю допомогу.


Я не думаю, що це стосується питання. Очевидно, у вашому випадку вище це має значення. Питання стосувалося випадку, коли визначення змінної можна було б визначити в іншому місці, не змінюючи поведінку коду.
pcarter

У опублікованому вами коді проблема полягає не в визначенні, а в частині ініціалізації. flagслід повторно ініціалізувати при 0 кожній whileітерації. Це логічна проблема, а не проблема визначення.
Мартін Веронно

0

Розділ 4.8 Структура блоків у мові програмування на C & R 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;                                                                                                         
 }                                                                                                                     
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.