Döngülerdeki değişkenleri bildirme, iyi uygulama veya kötü uygulama?


265

Soru 1: Döngü içindeki bir değişkeni bildirmek iyi bir uygulama mı yoksa kötü bir uygulama mı?

Bir performans sorunu (en çok hayır dedi) olup olmadığı hakkında diğer konuları okudum ve değişkenleri her zaman kullanılacakları yere yakın olarak beyan etmelisiniz. Merak ettiğim şey bundan kaçınılması gerekip gerekmediği veya gerçekten tercih edilip edilmediğidir.

Misal:

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

   cout << someString;
}

Soru # 2: Çoğu derleyici değişkenin zaten bildirildiğini fark eder mi ve sadece o kısmı atlar mı, yoksa her seferinde bellekte bir nokta oluşturur mu?


29
Profil oluşturma aksini söylemedikçe, kullanımlarına yakın koyun.
Mooing Duck


3
@drnewman Bu konuları okudum ama soruma cevap vermediler. Döngüler içindeki değişkenleri bildirmenin işe yaradığını anlıyorum. Bunu yapmak için iyi bir uygulama ya da kaçınılması gereken bir şey olup olmadığını merak ediyorum.
JeramyRR

Yanıtlar:


348

Bu mükemmel bir uygulamadır.

Döngüler içinde değişkenler oluşturarak, kapsamlarının döngü içinde sınırlı olmasını sağlarsınız. Döngü dışında referans gösterilemez veya çağrılamaz.

Bu yoldan:

  • Değişkenin adı biraz "genel" ise ("i" gibi), daha sonra kodunuzda bir yerde aynı ada sahip başka bir değişkenle karıştırılma riski yoktur ( -WshadowGCC üzerindeki uyarı talimatı kullanılarak da azaltılabilir )

  • Derleyici, değişken kapsamının döngü içinde sınırlı olduğunu bilir ve bu nedenle değişken yanlışlıkla başka bir yere başvurulursa uygun bir hata iletisi verir.

  • Son olarak, değişkenin döngünün dışında kullanılamayacağını bildiğinden, bazı özel optimizasyonlar derleyici tarafından daha verimli bir şekilde gerçekleştirilebilir (en önemlisi kayıt ayırma). Örneğin, sonucu daha sonra tekrar kullanmak üzere saklamanıza gerek yoktur.

Kısacası, bunu yapma hakkınız var.

Ancak değişkenin her döngü arasındaki değerini korumasının gerekmediğini unutmayın . Bu durumda, her seferinde başlatmanız gerekebilir. Ayrıca, tek amacı değerini bir döngüden diğerine tutması gereken değişkenleri bildirmek olan döngüyü kapsayan daha büyük bir blok da oluşturabilirsiniz. Bu genellikle döngü sayacının kendisini içerir.

{
    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. soru için: İşlev çağrıldığında değişken bir kez tahsis edilir. Aslında, bir tahsis perspektifinden bakıldığında, (neredeyse) fonksiyonun başlangıcında değişkeni bildirmekle aynıdır. Tek fark kapsamdır: değişken, döngünün dışında kullanılamaz. Değişkenin tahsis edilmemesi bile mümkündür, sadece bazı boş yuvalar (kapsamı sona eren diğer değişkenlerden) yeniden kullanılır.

Kısıtlı ve daha hassas kapsam ile daha doğru optimizasyonlar gelir. Ancak daha da önemlisi, kodunuzun diğer bölümlerini okurken endişelenmeniz gereken daha az durum (yani değişkenler) ile kodunuzu daha güvenli hale getirir.

Bu, bir if(){...}bloğun dışında bile geçerlidir . Genellikle, yerine:

    int result;
    (...)
    result = f1();
    if (result) then { (...) }
    (...)
    result = f2();
    if (result) then { (...) }

yazmak daha güvenli:

    (...)
    {
        int const result = f1();
        if (result) then { (...) }
    }
    (...)
    {
        int const result = f2();
        if (result) then { (...) }
    }

Fark, özellikle böyle küçük bir örnekte küçük görünebilir. Ancak daha büyük bir kod tabanına yardımcı olacaktır: şimdi bir resultdeğeri bloke f1()etmek için bir risk yoktur f2(). Her resultbiri kesinlikle kendi kapsamıyla sınırlıdır ve rolünü daha doğru hale getirir. Gözden geçiren bir bakış açısından, çok daha hoş, çünkü endişelenecek ve izleyecek daha az uzun menzilli durum değişkenleri var .

Derleyici bile daha iyi yardımcı olacaktır: gelecekte, bazı hatalı kod değişikliklerinden sonra resultdüzgün bir şekilde başlatılmadığını varsayarsak f2(). İkinci sürüm, derleme zamanında net bir hata mesajı belirterek çalışmayı reddeder (çalışma süresinden çok daha iyi). İlk sürüm hiçbir şey tespit f1()etmeyecek , sonucu sadece ikinci kez test edilecek ve sonucu için karıştırılacaktır f2().

Tamamlayıcı bilgiler

Açık kaynaklı araç CppCheck (C / C ++ kodu için statik bir analiz aracı), değişkenlerin optimum kapsamı hakkında bazı mükemmel ipuçları sağlar.

Tahsis hakkındaki yoruma yanıt olarak: Yukarıdaki kural C için geçerlidir, ancak bazı C ++ sınıfları için olmayabilir.

Standart tipler ve yapılar için, değişkenin boyutu derleme zamanında bilinir. C'de "yapı" diye bir şey yoktur, bu nedenle işlev çağrıldığında değişken için alan sadece yığına (herhangi bir başlatma olmadan) tahsis edilecektir. Bu yüzden değişkeni bir döngü içinde bildirirken "sıfır" maliyet vardır.

Ancak, C ++ sınıfları için, daha az bildiğim bu yapıcı şey var. Derleyici aynı alanı yeniden kullanmak için yeterince akıllı olacağından, tahsis muhtemelen sorun olmayacaktır, ancak başlatma her döngü yinelemesinde gerçekleşecektir.


4
Müthiş cevap. Bu tam da aradığım şeydi ve farkına varmadığım bir şey hakkında bana biraz fikir verdi. Kapsamın sadece döngü içinde kaldığının farkında değildim. Cevap için teşekkürler!
JeramyRR

22
"Ama asla fonksiyonun başında tahsis etmekten daha yavaş olmayacaktır." Bu her zaman doğru değildir. Değişken bir kez tahsis edilecektir, ancak yine de gerektiği kadar inşa edilecek ve tahrip edilecektir. Örnek kod durumunda, 11 kez. Mooing'in "Profilleme aksini belirtmedikçe kullanımlarına yakın koyun."
IronMensan

4
@JeramyRR: Kesinlikle hayır - derleyicinin nesnenin yapıcısında veya yıkıcısında anlamlı yan etkileri olup olmadığını bilmesinin bir yolu yoktur.
ildjarn

2
@ Demir: Öte yandan, önce öğeyi beyan ettiğinizde, atama operatörüne birçok çağrı alırsınız; ki bu genellikle bir nesneyi inşa etmek ve imha etmekle aynı maliyete sahiptir.
Billy ONeal

4
@BillyONeal: için stringve vector, atama operatörü (Döngünüzden bağlı olarak), her döngü, tampon tahsis yeniden kullanabilir özellikle büyük bir zaman tasarrufu olabilir.
Mooing Duck

22

Genel olarak, çok yakın tutmak çok iyi bir uygulamadır.

Bazı durumlarda, değişkenin döngüden çekilmesini haklı kılan performans gibi bir husus olacaktır.

Örneğinizde, program her seferinde dizgiyi oluşturur ve yok eder. Bazı kütüphaneler küçük bir dize optimizasyonu (SSO) kullanır, bu nedenle bazı durumlarda dinamik ayırma önlenebilir.

Bu gereksiz kreasyonlardan / tahsislerden kaçınmak istediğinizi varsayalım, şöyle yazarsınız:

for (int counter = 0; counter <= 10; counter++) {
   // compiler can pull this out
   const char testing[] = "testing";
   cout << testing;
}

veya sabiti dışarı çekebilirsiniz:

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

Çoğu derleyici değişkenin zaten bildirildiğini fark eder mi ve sadece o bölümü atlar mı, yoksa her seferinde bellekte bir nokta oluşturur mu?

Değişkenin tükettiği alanı yeniden kullanabilir ve değişmezleri döngünüzden çıkarabilir. Const char dizisi (yukarıda) durumunda - bu dizi çıkarılabilir. Ancak, bir nesne (örneğin std::string) söz konusu olduğunda, yapıcı ve yıkıcı her yinelemede yürütülmelidir . Durumunda std::string, bu 'boşluk' karakterleri temsil eden dinamik ayırmayı içeren bir işaretçi içerir. Yani bu:

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

her durumda yedekli kopyalama ve değişken SSO karakter sayısı eşiğinin üstünde (ve SSO std kitaplığınız tarafından uygulanır) dinamik ayırma ve ücretsiz gerektirir.

Bunu yapmak:

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

yine de her yinelemede karakterlerin fiziksel bir kopyasını gerektirir, ancak dizeyi atadığınız ve uygulamanın dizenin destek ayırmasını yeniden boyutlandırmaya gerek olmadığını görmesi nedeniyle form tek bir dinamik ayırmaya neden olabilir. Tabii ki, bu örnekte bunu yapmayacaksınız (çünkü zaten çok sayıda üstün alternatif gösterilmişti), ancak dize veya vektörün içeriği değiştiğinde bunu düşünebilirsiniz.

Peki tüm bu seçeneklerle (ve daha fazlasıyla) ne yaparsınız? Maliyetleri iyi anlayana ve ne zaman sapmanız gerektiğini bilinceye kadar varsayılan olarak çok yakın tutun.


1
Float veya int gibi temel veri türleri ile ilgili olarak, döngü içindeki değişkeni bildirmek, her yinelemede değişken için bir alan ayırması gerektiği için döngü dışındaki bu değişkeni bildirmekten daha yavaş olur mu?
Kasparov92

2
@ Kasparov92 Kısa cevap: "Hayır. Bu optimizasyonu yok sayın ve daha iyi okunabilirlik / konum için mümkün olduğunda döngüye yerleştirin. Derleyici bu mikro optimizasyonu sizin için yapabilir." Daha ayrıntılı olarak, bu derleyicinin platform için en iyi olana, optimizasyon seviyelerine, vb. Dayanarak karar vermesidir. Genellikle bir döngü içindeki sıradan bir int / float genellikle yığına yerleştirilir. Bir derleyici bunu kesinlikle döngü dışına taşıyabilir ve bunu yaparken bir optimizasyon varsa depolamayı yeniden kullanabilir. Pratik amaçlar için, bu çok çok çok küçük bir optimizasyon olacaktır ...
justin

1
@ Kasparov92… (devam) yalnızca her döngünün sayıldığı ortamlarda / uygulamalarda dikkate alırsınız. Bu durumda, sadece montaj kullanmayı düşünebilirsiniz.
justin

14

C ++ için ne yaptığınıza bağlıdır. Tamam, bu aptalca bir kod ama hayal et

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;

}

MyFunc çıktısını elde edene kadar 55 saniye bekleyeceksiniz. Çünkü her döngü yapıcısı ve yıkıcı birlikte bitirmek için 5 saniyeye ihtiyaç duyar.

MyOtherFunc çıktısını elde edene kadar 5 saniye gerekir.

Tabii ki, bu çılgın bir örnek.

Ancak, yapıcı ve / veya yıkıcı bir süre gerektiğinde aynı yapının her döngü yapıldığında bir performans sorunu haline gelebileceğini göstermektedir.


2
Teknik olarak ikinci versiyonda çıktıyı sadece 2 saniyede alacaksınız, çünkü nesneyi henüz tahrip etmediniz .....
Chrys

12

JeremyRR'nin sorularını cevaplamak için mesaj atmadım (zaten cevaplandıkları için); bunun yerine, sadece bir öneri vermek için gönderdim.

JeremyRR için şunları yapabilirsiniz:

{
  string someString = "testing";   

  for(int counter = 0; counter <= 10; counter++)
  {
    cout << someString;
  }

  // The variable is in scope.
}

// The variable is no longer in scope.

(Programlamaya ilk başladığımda bilmiyordum), köşeli parantezlerin (çiftler halinde oldukları sürece) kodun herhangi bir yerine yerleştirilebileceğini, "" iken ", vb.

Kodumu Microsoft Visual C ++ 2010 Express derlenmiş, bu yüzden çalışır biliyorum; Ayrıca, ben tanımlanmış parantez dışında değişken kullanmaya çalıştım ve bir hata aldı, bu yüzden değişken "yok" olduğunu biliyorum.

Etiketlenmemiş köşeli parantezlerin çoğu kodu hızlı bir şekilde okunamaz hale getirebileceğinden, bu yöntemi kullanmanın kötü bir uygulama olup olmadığını bilmiyorum, ancak belki de bazı yorumlar işleri temizleyebilir.


4
Bana göre bu, doğrudan soruyla bağlantılı bir öneri getiren çok meşru bir cevap. Benim oyum sende!
Alexis Leclerc

0

Bu çok iyi bir uygulama, yukarıdaki tüm cevap çok iyi teorik yönü sağlamak bana kod bir bakış vereyim, GEEKSFORGEEKS üzerinde DFS çözmeye çalışıyordu, ben optimizasyon sorunu ile karşılaşırsanız ...... Eğer döngü dışında tamsayı bildiren kodu çözmek Optimizasyon Hatası verecektir ..

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();
    }
}

Şimdi döngü içine tamsayılar koymak bu size doğru cevap verecektir ...

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();
    }
}

bu efendim @ justin'in 2. yorumda söylediklerini tamamen yansıtıyor .... bunu burada deneyin https://practice.geeksforgeeks.org/problems/depth-first-traversal-for-a-graph/1 . sadece denemek .... sen-ecek almak bu yardım umuyoruz


Bunun soru için geçerli olduğunu sanmıyorum. Açıkçası, yukarıdaki durumunuzda önemlidir. Soru, değişken tanımının kodun davranışını değiştirmeden başka bir yerde tanımlanabileceği durumla ilgiliydi.
pcarter

Gönderdiğiniz kodda sorun tanım değil, başlatma bölümüdür. flagher whileyinelemede 0 olarak yeniden başlatılmalıdır. Bu bir mantık problemi, tanım problemi değil.
Martin Véronneau

0

Bölüm 4.8 K & R'lerde Blok Yapısı C Programlama Dili 2.Ed. :

Bir blokta bildirilen ve başlatılan otomatik bir değişken, blok her girildiğinde başlatılır.

Kitapta ilgili açıklamayı görmeyi kaçırmış olabilirim:

Bir blokta bildirilen ve başlatılan otomatik bir değişken, blok girilmeden sadece bir kez ayrılır.

Ancak basit bir test, var olan varsayımı kanıtlayabilir:

 #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;                                                                                                         
 }                                                                                                                     
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.