Mendeklarasikan variabel di dalam loop, praktik baik atau praktik buruk?


266

Pertanyaan # 1: Apakah mendeklarasikan variabel di dalam loop merupakan praktik yang baik atau praktik yang buruk?

Saya telah membaca utas lainnya tentang apakah ada masalah kinerja (sebagian besar mengatakan tidak), dan bahwa Anda harus selalu mendeklarasikan variabel sedekat mungkin dengan tempat mereka akan digunakan. Yang saya pikirkan adalah apakah ini harus dihindari atau benar-benar disukai.

Contoh:

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

   cout << someString;
}

Pertanyaan # 2: Apakah sebagian besar penyusun menyadari bahwa variabel telah dideklarasikan dan hanya melewatkan bagian itu, atau apakah itu benar-benar membuat tempat untuk itu dalam memori setiap kali?


29
Dekatkan dengan penggunaannya, kecuali jika profil mengatakan sebaliknya.
Mooing Duck


3
@drnewman Saya memang membaca utas-utas itu, tetapi mereka tidak menjawab pertanyaan saya. Saya mengerti bahwa mendeklarasikan variabel di dalam loop berfungsi. Saya bertanya-tanya apakah itu praktik yang baik untuk dilakukan atau apakah itu sesuatu yang harus dihindari.
JeramyRR

Jawaban:


348

Ini adalah latihan yang bagus .

Dengan membuat variabel di dalam loop, Anda memastikan cakupannya dibatasi di dalam loop. Itu tidak dapat dirujuk atau dipanggil di luar loop.

Cara ini:

  • Jika nama variabel sedikit "generik" (seperti "i"), tidak ada risiko untuk mencampurnya dengan variabel lain dengan nama yang sama di suatu tempat nanti dalam kode Anda (juga dapat dikurangi dengan menggunakan -Wshadowinstruksi peringatan pada GCC)

  • Kompilator tahu bahwa ruang lingkup variabel terbatas di dalam loop, dan karena itu akan mengeluarkan pesan kesalahan yang tepat jika variabel tersebut dirujuk secara tidak sengaja di tempat lain.

  • Last but not least, beberapa optimasi khusus dapat dilakukan lebih efisien oleh kompiler (paling penting mendaftar alokasi), karena ia tahu bahwa variabel tidak dapat digunakan di luar loop. Misalnya, tidak perlu menyimpan hasilnya untuk digunakan kembali nanti.

Singkatnya, Anda benar untuk melakukannya.

Namun perlu dicatat bahwa variabel tidak seharusnya mempertahankan nilainya antara setiap loop. Dalam hal demikian, Anda mungkin perlu menginisialisasi setiap waktu. Anda juga dapat membuat blok yang lebih besar, mencakup loop, yang tujuan utamanya adalah mendeklarasikan variabel yang harus mempertahankan nilainya dari satu loop ke loop lainnya. Ini biasanya termasuk penghitung putaran itu sendiri.

{
    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 */
}

Untuk pertanyaan # 2: Variabel dialokasikan satu kali, ketika fungsi dipanggil. Bahkan, dari perspektif alokasi, itu (hampir) sama dengan mendeklarasikan variabel di awal fungsi. Satu-satunya perbedaan adalah ruang lingkup: variabel tidak dapat digunakan di luar loop. Bahkan dimungkinkan bahwa variabel tidak dialokasikan, hanya menggunakan kembali beberapa slot gratis (dari variabel lain yang ruang lingkupnya telah berakhir).

Dengan ruang lingkup terbatas dan lebih tepat datang optimasi yang lebih akurat. Tetapi yang lebih penting, itu membuat kode Anda lebih aman, dengan lebih sedikit status (yaitu variabel) yang perlu dikhawatirkan saat membaca bagian lain dari kode.

Ini benar bahkan di luar if(){...}blok. Biasanya, alih-alih:

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

lebih aman untuk menulis:

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

Perbedaannya mungkin tampak kecil, terutama pada contoh kecil seperti itu. Tetapi pada basis kode yang lebih besar, ini akan membantu: sekarang tidak ada risiko untuk memindahkan sejumlah resultnilai dari f1()ke f2()blok. Masing result- masing sangat terbatas pada ruang lingkupnya sendiri, menjadikan perannya lebih akurat. Dari sudut pandang pengulas, ini jauh lebih baik, karena ia memiliki variabel status jarak jauh yang kurang perlu dikhawatirkan dan dilacak.

Bahkan kompiler akan membantu lebih baik: dengan asumsi bahwa, di masa depan, setelah beberapa perubahan kode yang salah, resulttidak diinisialisasi dengan benar f2(). Versi kedua hanya akan menolak untuk bekerja, menyatakan pesan kesalahan yang jelas pada waktu kompilasi (jauh lebih baik daripada waktu berjalan). Versi pertama tidak akan menemukan apa pun, hasilnya f1()hanya akan diuji untuk kedua kalinya, menjadi bingung untuk hasilnya f2().

Informasi pelengkap

Alat open-source CppCheck (alat analisis statis untuk kode C / C ++) memberikan beberapa petunjuk yang sangat baik mengenai ruang lingkup variabel yang optimal.

Menanggapi komentar tentang alokasi: Aturan di atas benar dalam C, tetapi mungkin tidak untuk beberapa kelas C ++.

Untuk tipe dan struktur standar, ukuran variabel diketahui pada waktu kompilasi. Tidak ada yang namanya "konstruksi" di C, jadi ruang untuk variabel hanya akan dialokasikan ke stack (tanpa inisialisasi), ketika fungsi dipanggil. Itu sebabnya ada biaya "nol" ketika mendeklarasikan variabel di dalam satu loop.

Namun, untuk kelas C ++, ada hal konstruktor ini yang saya tahu jauh tentang. Saya kira alokasi mungkin tidak akan menjadi masalah, karena kompiler harus cukup pintar untuk menggunakan kembali ruang yang sama, tetapi inisialisasi kemungkinan terjadi pada setiap perulangan loop.


4
Jawaban yang luar biasa. Ini persis apa yang saya cari, dan bahkan memberi saya wawasan tentang sesuatu yang tidak saya sadari. Saya tidak menyadari bahwa ruang lingkup tetap di dalam loop saja. Terima kasih atas tanggapannya!
JeramyRR

22
"Tapi itu tidak akan pernah lebih lambat daripada mengalokasikan di awal fungsi." Ini tidak selalu benar. Variabel akan dialokasikan satu kali, tetapi masih akan dibangun dan dihancurkan sebanyak yang diperlukan. Yang dalam hal kode contoh, adalah 11 kali. Mengutip komentar Mooing, "Tempatkan mereka dekat dengan penggunaannya, kecuali jika profil mengatakan sebaliknya."
IronMensan

4
@ JeramyRR: Sama sekali tidak - kompiler tidak memiliki cara untuk mengetahui apakah objek memiliki efek samping yang berarti dalam konstruktor atau destruktornya.
ildjarn

2
@ Besi: Di ​​sisi lain, ketika Anda mendeklarasikan item pertama, Anda hanya mendapatkan banyak panggilan ke operator penugasan; yang biasanya berharga hampir sama dengan membangun dan menghancurkan objek.
Billy ONeal

4
@ BillyONeal: Untuk stringdan vectorsecara khusus, operator penugasan dapat menggunakan kembali buffer yang dialokasikan setiap loop, yang (tergantung pada loop Anda) mungkin penghematan waktu yang sangat besar.
Mooing Duck

22

Secara umum, ini adalah praktik yang sangat baik untuk tetap dekat.

Dalam beberapa kasus, akan ada pertimbangan seperti kinerja yang membenarkan menarik variabel keluar dari loop.

Dalam contoh Anda, program membuat dan menghancurkan string setiap kali. Beberapa perpustakaan menggunakan optimasi string kecil (SSO), sehingga alokasi dinamis dapat dihindari dalam beberapa kasus.

Misalkan Anda ingin menghindari kreasi / alokasi yang berlebihan itu, Anda akan menuliskannya sebagai:

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

atau Anda dapat menarik keluar konstan:

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

Apakah sebagian besar penyusun menyadari bahwa variabel telah dideklarasikan dan hanya melewatkan bagian itu, atau apakah itu benar-benar membuat tempat untuk itu di memori setiap kali?

Itu dapat menggunakan kembali ruang yang dikonsumsi variabel , dan itu bisa menarik invarian keluar dari loop Anda. Dalam kasus array char const (di atas) - array yang bisa ditarik keluar. Namun, konstruktor dan destruktor harus dieksekusi pada setiap iterasi dalam kasus suatu objek (seperti std::string). Dalam hal ini std::string, 'spasi' itu termasuk pointer yang berisi alokasi dinamis yang mewakili karakter. Jadi ini:

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

akan memerlukan penyalinan berlebihan dalam setiap kasus, dan alokasi dinamis dan gratis jika variabel berada di atas ambang batas untuk jumlah karakter SSO (dan SSO diimplementasikan oleh perpustakaan std Anda).

Melakukan ini:

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

masih memerlukan salinan fisik karakter di setiap iterasi, tetapi formulir dapat menghasilkan satu alokasi dinamis karena Anda menetapkan string dan implementasinya akan melihat bahwa tidak perlu mengubah ukuran alokasi dukungan string. Tentu saja, Anda tidak akan melakukan itu dalam contoh ini (karena beberapa alternatif unggul telah diperlihatkan), tetapi Anda dapat mempertimbangkannya ketika string atau konten vektor bervariasi.

Jadi apa yang Anda lakukan dengan semua opsi itu (dan banyak lagi)? Tetap sangat dekat sebagai default - sampai Anda memahami biayanya dengan baik dan tahu kapan Anda harus menyimpang.


1
Mengenai tipe data dasar seperti float atau int, akankah mendeklarasikan variabel di dalam loop menjadi lebih lambat daripada menyatakan bahwa variabel di luar loop karena harus mengalokasikan ruang untuk variabel setiap iterasi?
Kasparov92

2
@ Kasparov92 Jawaban singkatnya adalah "Tidak. Abaikan optimasi itu dan letakkan di loop jika memungkinkan untuk meningkatkan keterbacaan / lokalitas. Kompiler dapat melakukan optimasi mikro untuk Anda." Secara lebih rinci, yang pada akhirnya bagi kompiler untuk memutuskan, berdasarkan apa yang terbaik untuk platform, tingkat optimisasi, dll. Int / float biasa di dalam sebuah loop biasanya akan ditempatkan pada stack. Kompiler tentu dapat memindahkannya di luar loop dan menggunakan kembali penyimpanan jika ada optimasi dalam melakukan itu. Untuk tujuan praktis, ini akan menjadi optimasi yang sangat sangat sangat kecil ...
justin

1
@ Kasparov92 ... (lanjutan) yang hanya akan Anda pertimbangkan di lingkungan / aplikasi tempat setiap siklus dihitung. Dalam hal ini, Anda mungkin ingin mempertimbangkan untuk menggunakan perakitan.
justin

14

Untuk C ++ tergantung pada apa yang Anda lakukan. OK, itu kode bodoh tapi bayangkan

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;

}

Anda akan menunggu 55 detik sampai Anda mendapatkan output dari myFunc. Hanya karena setiap loop kontruktor dan destruktor bersama-sama membutuhkan 5 detik untuk selesai.

Anda akan membutuhkan 5 detik hingga Anda mendapatkan output dari myOtherFunc.

Tentu saja, ini adalah contoh gila.

Tapi itu menggambarkan bahwa itu mungkin menjadi masalah kinerja ketika setiap loop konstruksi yang sama dilakukan ketika konstruktor dan / atau destruktor membutuhkan waktu.


2
Nah, secara teknis dalam versi kedua Anda akan mendapatkan output hanya dalam 2 detik, karena Anda belum merusak objeknya .....
Chrys

12

Saya tidak memposting untuk menjawab pertanyaan JeremyRR (karena sudah dijawab); alih-alih, saya mengirim hanya untuk memberikan saran.

Untuk JeremyRR, Anda bisa melakukan ini:

{
  string someString = "testing";   

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

  // The variable is in scope.
}

// The variable is no longer in scope.

Saya tidak tahu apakah Anda sadar (saya tidak melakukannya ketika pertama kali memulai pemrograman), bahwa tanda kurung (selama mereka berpasangan) dapat ditempatkan di mana saja di dalam kode, tidak hanya setelah "jika", "untuk", " sementara ", dll.

Kode saya dikompilasi dalam Microsoft Visual C ++ 2010 Express, jadi saya tahu itu berfungsi; juga, saya telah mencoba menggunakan variabel di luar tanda kurung yang didefinisikan dan saya menerima kesalahan, jadi saya tahu bahwa variabel "dihancurkan".

Saya tidak tahu apakah ini praktik yang buruk untuk menggunakan metode ini, karena banyak tanda kurung yang tidak berlabel dapat dengan cepat membuat kode tidak dapat dibaca, tetapi mungkin beberapa komentar dapat menghapus semuanya.


4
Bagi saya, ini adalah jawaban yang sangat sah yang membawa saran yang terkait langsung dengan pertanyaan. Anda memiliki suara saya!
Alexis Leclerc

0

Ini adalah praktik yang sangat baik, karena semua jawaban di atas memberikan aspek teoretis yang sangat baik dari pertanyaan, izinkan saya memberikan sekilas kode, saya mencoba menyelesaikan DFS melalui GEEKSFORGEEKS, saya menghadapi masalah optimisasi ...... Jika Anda mencoba untuk pecahkan kode dengan mendeklarasikan integer di luar loop akan memberikan Anda Kesalahan Optimasi ..

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

Sekarang letakkan bilangan bulat di dalam loop ini akan memberi Anda jawaban yang benar ...

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

ini sepenuhnya mencerminkan apa yang dikatakan pak @justin dalam komentar ke-2 .... coba ini di sini https://practice.geeksforgeeks.org/problems/depth-first-traversal-for-a-graph/1 . coba saja .... Anda akan mendapatkannya. Mohon bantuan ini.


Saya tidak berpikir ini berlaku untuk pertanyaan. Jelas, dalam kasus Anda di atas itu penting. Pertanyaannya adalah berhadapan dengan kasus ketika definisi variabel dapat didefinisikan di tempat lain tanpa mengubah perilaku kode.
pcarter

Dalam kode yang Anda posting, masalahnya bukanlah definisi tetapi bagian inisialisasi. flagharus diinisialisasi ulang pada 0 setiap whileiterasi. Itu masalah logika, bukan masalah definisi.
Martin Véronneau

0

Bab 4.8 Struktur Blok dalam Bahasa Pemrograman K&R 2.Ed. :

Variabel otomatis yang dinyatakan dan diinisialisasi dalam sebuah blok diinisialisasi setiap kali blok dimasukkan.

Saya mungkin tidak melihat deskripsi yang relevan dalam buku seperti:

Variabel otomatis yang dideklarasikan dan diinisialisasi dalam sebuah blok dialokasikan hanya satu kali sebelum blok dimasukkan.

Tetapi tes sederhana dapat membuktikan asumsi yang dimiliki:

 #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;                                                                                                         
 }                                                                                                                     
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.