Penempatan deklarasi variabel di C

129

Saya sudah lama berpikir bahwa dalam C, semua variabel harus dideklarasikan pada awal fungsi. Saya tahu bahwa di C99, aturannya sama seperti di C ++, tapi apa aturan penempatan deklarasi variabel untuk C89 / ANSI C?

Kode berikut berhasil dikompilasi dengan gcc -std=c89dan gcc -ansi:

#include <stdio.h>
int main() {
    int i;
    for (i = 0; i < 10; i++) {
        char c = (i % 95) + 32;
        printf("%i: %c\n", i, c);
        char *s;
        s = "some string";
        puts(s);
    }
    return 0;
}

Tidakkah seharusnya deklarasi cdan smenyebabkan kesalahan dalam mode C89 / ANSI?

mcjabberz
sumber
54
Hanya sebuah catatan: variabel dalam ansi C tidak harus dideklarasikan pada awal fungsi melainkan pada awal blok. Jadi, char c = ... di bagian atas for for loop Anda benar-benar legal di ansi C. Namun, char * s tidak akan.
Jason Coco

Jawaban:

149

Ini berhasil dikompilasi karena GCC memungkinkan deklarasi ssebagai ekstensi GNU, meskipun itu bukan bagian dari standar C89 atau ANSI. Jika Anda ingin mematuhi standar-standar itu dengan ketat, Anda harus melewati -pedanticbendera.

Deklarasi cpada awal { }blok adalah bagian dari standar C89; blok tidak harus berupa fungsi.

mipadi
sumber
41
Mungkin perlu dicatat bahwa hanya deklarasi dari sekstensi (dari sudut pandang C89). Deklarasi clegal sempurna di C89, tidak perlu ekstensi.
AnT
7
@ Andrewt: Ya, di C, deklarasi variabel harus @ awal blok dan bukan fungsi per se; tetapi orang membingungkan blok dengan fungsi karena itu adalah contoh utama dari sebuah blok.
legends2k
1
Saya memindahkan komentar dengan +39 suara menjadi jawabannya.
MarcH
78

Untuk C89, Anda harus mendeklarasikan semua variabel Anda di awal blok lingkup .

Jadi, char cdeklarasi Anda valid karena berada di bagian atas blok lingkup loop. Tapi, char *sdeklarasi itu harus menjadi kesalahan.

Kiley Hykawy
sumber
2
Cukup benar. Anda dapat mendeklarasikan variabel di awal {...} apa saja.
Artelius
5
@ Artelius Tidak cukup benar. Hanya jika curlies adalah bagian dari blok (tidak jika mereka adalah bagian dari pernyataan struct atau union atau penginisialisasi yang diperkuat.)
Jens
Hanya untuk menjadi bertele-tele, deklarasi yang salah harus setidaknya diberitahu sesuai dengan standar C. Jadi seharusnya ada kesalahan atau peringatan di gcc. Artinya, jangan percaya bahwa suatu program dapat dikompilasi untuk berarti bahwa itu sesuai.
jinawee
35

Pengelompokan deklarasi variabel di bagian atas blok adalah kemungkinan warisan karena keterbatasan kompiler C primitif yang lama. Semua bahasa modern merekomendasikan dan kadang-kadang bahkan menegakkan deklarasi variabel lokal pada titik terakhir: di mana mereka pertama kali diinisialisasi. Karena ini menghilangkan risiko menggunakan nilai acak secara tidak sengaja. Memisahkan deklarasi dan inisialisasi juga mencegah Anda menggunakan "const" (atau "final") ketika Anda bisa.

C ++ sayangnya terus menerima cara deklarasi lama dan teratas untuk kompatibilitas mundur dengan C (satu kompatibilitas C keluar dari yang lain ...) Tetapi C ++ mencoba untuk menjauh dari itu:

  • Desain referensi C ++ bahkan tidak memungkinkan pengelompokan blok atas.
  • Jika Anda memisahkan deklarasi dan inisialisasi objek lokal C ++ maka Anda membayar biaya konstruktor tambahan tanpa biaya. Jika konstruktor no-arg tidak ada maka sekali lagi Anda bahkan tidak diizinkan untuk memisahkan keduanya!

C99 mulai bergerak C ke arah yang sama ini.

Jika Anda khawatir tidak menemukan di mana variabel lokal dinyatakan maka itu berarti Anda memiliki masalah yang jauh lebih besar: blok penutup terlalu panjang dan harus dipecah.

https://wiki.sei.cmu.edu/confluence/display/c/DCL19-C.+Minimasikan+scope+of+variables+and+functions

Maret
sumber
Lihat juga bagaimana memaksa deklarasi variabel di bagian atas blok dapat membuat lubang keamanan: lwn.net/Articles/443037
MarcH
"C ++ sayangnya terus menerima cara deklarasi lama yang lama untuk kompatibilitas dengan C": IMHO, ini hanya cara bersih untuk melakukannya. Bahasa lain "memecahkan" masalah ini dengan selalu menginisialisasi dengan 0. Bzzt, yang hanya menutupi kesalahan logika jika Anda bertanya kepada saya. Dan ada beberapa kasus di mana Anda MEMBUTUHKAN deklarasi tanpa inisialisasi karena ada beberapa kemungkinan lokasi untuk inisialisasi. Dan itulah mengapa C ++'s RAII benar-benar sangat menyakitkan di pantat - Sekarang Anda perlu memasukkan keadaan tidak diinisialisasi "valid" di setiap objek untuk memungkinkan kasus-kasus ini.
Jo So
1
@ JoSo: Saya bingung mengapa Anda berpikir bahwa membaca variabel yang tidak diinisialisasi menghasilkan efek arbitrer akan membuat kesalahan pemrograman lebih mudah untuk dideteksi daripada membuat mereka menghasilkan nilai yang konsisten atau kesalahan deterministik? Perhatikan bahwa tidak ada jaminan bahwa pembacaan penyimpanan yang tidak disengaja akan berperilaku dengan cara yang konsisten dengan pola bit apa pun yang bisa dilakukan variabel, atau bahkan bahwa program seperti itu akan berperilaku dengan cara yang konsisten dengan hukum waktu dan hubungan sebab akibat yang biasa. Diberikan sesuatu seperti int y; ... if (x) { printf("X was true"); y=23;} return y;...
supercat
1
@ JoSo: Untuk pointer, terutama pada implementasi yang menjebak operasi null, semua-bit-nol sering merupakan nilai perangkap yang berguna. Selanjutnya, dalam bahasa yang secara eksplisit menentukan bahwa variabel default ke semua-bit-nol, bergantung pada nilai itu bukan kesalahan . Kompiler belum cenderung terlalu aneh dengan "optimisasi" mereka, tetapi penulis kompiler terus berusaha untuk menjadi semakin pintar. Opsi kompiler untuk menginisialisasi variabel dengan variabel pseudo-acak yang disengaja mungkin berguna untuk mengidentifikasi kesalahan, tetapi hanya meninggalkan penyimpanan memegang nilai terakhirnya kadang-kadang dapat menutupi kesalahan.
supercat
22

Dari sudut pandang rawatan, alih-alih sintaksis, setidaknya ada tiga jalur pemikiran:

  1. Nyatakan semua variabel di awal fungsi sehingga mereka akan berada di satu tempat dan Anda akan dapat melihat daftar lengkap secara sekilas.

  2. Nyatakan semua variabel sedekat mungkin ke tempat mereka pertama kali digunakan, jadi Anda akan tahu mengapa masing - masing diperlukan.

  3. Deklarasikan semua variabel di awal blok lingkup terdalam, jadi mereka akan keluar dari cakupan sesegera mungkin dan memungkinkan kompiler untuk mengoptimalkan memori dan memberi tahu Anda jika Anda secara tidak sengaja menggunakannya di tempat yang tidak Anda inginkan.

Saya biasanya lebih suka opsi pertama, karena saya menemukan yang lain sering memaksa saya untuk mencari kode deklarasi. Mendefinisikan semua variabel di muka juga membuatnya lebih mudah untuk menginisialisasi dan melihatnya dari debugger.

Kadang-kadang saya akan mendeklarasikan variabel dalam blok lingkup yang lebih kecil, tetapi hanya untuk Alasan Bagus, yang saya miliki sangat sedikit. Salah satu contoh mungkin setelah a fork(), untuk mendeklarasikan variabel yang hanya dibutuhkan oleh proses anak. Bagi saya, indikator visual ini merupakan pengingat tentang tujuan mereka.

Adam Liss
sumber
27
Saya menggunakan opsi 2 atau 3 sehingga lebih mudah untuk menemukan variabel - karena fungsi tidak boleh terlalu besar sehingga Anda tidak dapat melihat deklarasi variabel.
Jonathan Leffler
8
Opsi 3 adalah non-masalah, kecuali jika Anda menggunakan kompiler dari tahun 70-an.
edgar.holleis
15
Jika Anda menggunakan IDE yang layak, Anda tidak perlu pergi berburu kode, karena harus ada perintah-IDE untuk menemukan deklarasi untuk Anda. (F3 dalam Eclipse)
edgar.holleis
4
Saya tidak mengerti bagaimana Anda bisa memastikan inisialisasi dalam opsi 1, mungkin kali Anda hanya bisa mendapatkan nilai awal nanti di blok, dengan memanggil fungsi lain, atau melakukan caclulation, mungkin.
Plumenator
4
@Plumenator: opsi 1 tidak memastikan inisialisasi; Saya memilih untuk menginisialisasi mereka pada deklarasi, baik untuk nilai "benar" mereka atau untuk sesuatu yang akan menjamin kode berikutnya akan rusak jika mereka tidak diatur dengan tepat. Saya mengatakan "memilih" karena preferensi saya telah berubah ke # 2 sejak saya menulis ini, mungkin karena saya menggunakan Java lebih dari C sekarang, dan karena saya memiliki alat dev yang lebih baik.
Adam Liss
6

Seperti dicatat oleh orang lain, GCC permisif dalam hal ini (dan mungkin kompiler lain, tergantung pada argumen yang mereka panggil) bahkan ketika dalam mode 'C89', kecuali jika Anda menggunakan pemeriksaan 'pedantic'. Sejujurnya, tidak ada banyak alasan bagus untuk tidak memakai obat; kode modern berkualitas harus selalu dikompilasi tanpa peringatan (atau sangat sedikit di mana Anda tahu Anda melakukan sesuatu yang spesifik yang mencurigakan ke kompiler sebagai kesalahan yang mungkin terjadi), jadi jika Anda tidak dapat membuat kode Anda dikompilasi dengan pengaturan yang bertele-tele mungkin perlu perhatian.

C89 mensyaratkan bahwa variabel harus dideklarasikan sebelum pernyataan lain dalam setiap lingkup, standar kemudian memungkinkan deklarasi lebih dekat untuk digunakan (yang dapat lebih intuitif dan lebih efisien), terutama deklarasi simultan dan inisialisasi variabel kontrol loop dalam loop 'untuk'.

Gaidheal
sumber
0

Seperti telah dicatat, ada dua aliran pemikiran tentang ini.

1) Nyatakan semuanya di atas fungsi karena tahun 1987.

2) Deklarasikan paling dekat dengan penggunaan pertama dan dalam ruang lingkup sekecil mungkin.

Jawaban saya untuk ini adalah DO KEDUA! Biarkan saya jelaskan:

Untuk fungsi yang panjang, 1) membuat refactoring sangat sulit. Jika Anda bekerja dalam basis kode di mana pengembang menentang gagasan subrutin, maka Anda akan memiliki 50 deklarasi variabel di awal fungsi dan beberapa dari mereka mungkin hanya menjadi "i" untuk for-loop yang sangat bagian bawah fungsi.

Karena itu saya mengembangkan deklarasi-at-the-top-PTSD dari ini dan mencoba melakukan opsi 2) secara agama.

Saya kembali ke opsi satu karena satu hal: fungsi pendek. Jika fungsi Anda cukup pendek, maka Anda akan memiliki beberapa variabel lokal dan karena fungsi ini pendek, jika Anda menempatkannya di bagian atas fungsi, mereka akan tetap dekat dengan penggunaan pertama.

Juga, anti-pola "menyatakan dan mengatur ke NULL" ketika Anda ingin mendeklarasikan di atas tetapi Anda belum membuat beberapa perhitungan yang diperlukan untuk inisialisasi diselesaikan karena hal-hal yang perlu Anda inisialisasi kemungkinan akan diterima sebagai argumen.

Jadi sekarang pemikiran saya adalah bahwa Anda harus mendeklarasikan di bagian atas fungsi dan sedekat mungkin dengan penggunaan pertama. Jadi KEDUA! Dan cara untuk melakukannya adalah dengan subrutin yang dibagi dengan baik.

Tetapi jika Anda sedang mengerjakan fungsi yang panjang, maka letakkan hal-hal yang paling dekat dengan penggunaan pertama karena dengan cara itu akan lebih mudah untuk mengekstrak metode.

Resep saya adalah ini. Untuk semua variabel lokal, ambil variabel dan pindahkan deklarasi ke bawah, kompilasi, lalu pindahkan deklarasi ke sesaat sebelum kesalahan kompilasi. Itu penggunaan pertama. Lakukan ini untuk semua variabel lokal.

int foo = 0;
<code that uses foo>

int bar = 1;
<code that uses bar>

<code that uses foo>

Sekarang, tentukan blok lingkup yang dimulai sebelum deklarasi dan pindahkan ujungnya hingga program dikompilasi

{
    int foo = 0;
    <code that uses foo>
}

int bar = 1;
<code that uses bar>

>>> First compilation error here
<code that uses foo>

Ini tidak dikompilasi karena ada beberapa kode lagi yang menggunakan foo. Kita dapat melihat bahwa kompiler dapat melalui kode yang menggunakan bilah karena tidak menggunakan foo. Pada titik ini, ada dua pilihan. Yang mekanis adalah dengan hanya memindahkan "}" ke bawah hingga kompilasi, dan pilihan lainnya adalah memeriksa kode dan menentukan apakah urutannya dapat diubah menjadi:

{
    int foo = 0;
    <code that uses foo>
}

<code that uses foo>

int bar = 1;
<code that uses bar>

Jika pesanan dapat diaktifkan, itu mungkin yang Anda inginkan karena mempersingkat masa pakai nilai sementara.

Hal lain yang perlu diperhatikan, apakah nilai foo perlu dipertahankan antara blok kode yang menggunakannya, atau mungkinkah itu hanya foo yang berbeda di keduanya. Sebagai contoh

int i;

for(i = 0; i < 8; ++i){
    ...
}

<some stuff>

for(i = 3; i < 32; ++i){
    ...
}

Situasi ini membutuhkan lebih dari prosedur saya. Pengembang harus menganalisis kode untuk menentukan apa yang harus dilakukan.

Tetapi langkah pertama adalah menemukan penggunaan pertama. Anda dapat melakukannya secara visual tetapi kadang-kadang, itu hanya lebih mudah untuk menghapus deklarasi, mencoba untuk mengkompilasi dan hanya meletakkannya kembali di atas penggunaan pertama. Jika penggunaan pertama itu di dalam pernyataan if, letakkan di sana dan periksa apakah kompilasi. Kompiler kemudian akan mengidentifikasi kegunaan lain. Cobalah untuk membuat blok lingkup yang mencakup kedua kegunaan.

Setelah bagian mekanik ini dilakukan, maka menjadi lebih mudah untuk menganalisis di mana data berada. Jika variabel digunakan dalam blok lingkup besar, analisis situasi dan lihat apakah Anda hanya menggunakan variabel yang sama untuk dua hal yang berbeda (seperti "i" yang digunakan untuk dua untuk loop). Jika penggunaannya tidak terkait, buat variabel baru untuk masing-masing penggunaan yang tidak terkait ini.

Philippe Carphin
sumber
0

Anda harus mendeklarasikan semua variabel di bagian atas atau "secara lokal" dalam fungsi. Jawabannya adalah:

Tergantung pada jenis sistem yang Anda gunakan:

1 / Sistem Tertanam (terutama yang terkait dengan kehidupan seperti Airplane atau Car): Ini memungkinkan Anda untuk menggunakan memori dinamis (misalnya: calloc, malloc, baru ...). Bayangkan Anda bekerja di proyek yang sangat besar, dengan 1000 insinyur. Bagaimana jika mereka mengalokasikan memori dinamis baru dan lupa menghapusnya (ketika tidak digunakan lagi)? Jika sistem tertanam berjalan untuk waktu yang lama, itu akan menyebabkan stack overflow dan perangkat lunak akan rusak. Tidak mudah memastikan kualitasnya (cara terbaik adalah melarang memori dinamis).

Jika sebuah Pesawat terbang dalam 30 hari dan tidak mematikan, apa yang terjadi jika perangkat lunak rusak (ketika pesawat masih di udara)?

2 / Sistem lain seperti web, PC (memiliki ruang memori besar):

Anda harus mendeklarasikan variabel "lokal" untuk mengoptimalkan penggunaan memori. Jika sistem ini berjalan untuk waktu yang lama dan terjadi stack overflow (karena seseorang lupa untuk menghapus memori dinamis). Lakukan saja hal sederhana untuk mengatur ulang PC: P Tidak berdampak pada nyawa

Dang_Ho
sumber
Saya tidak yakin ini benar. Saya kira Anda mengatakan lebih mudah untuk mengaudit kebocoran memori jika Anda mendeklarasikan semua variabel lokal Anda di satu tempat? Itu mungkin benar, tetapi saya tidak begitu yakin saya membelinya. Adapun poin (2), Anda mengatakan mendeklarasikan variabel secara lokal akan "mengoptimalkan penggunaan memori"? Secara teori ini mungkin. Kompiler dapat memilih untuk mengubah ukuran frame tumpukan selama fungsi untuk meminimalkan penggunaan memori, tapi saya tidak mengetahui ada yang melakukan ini. Pada kenyataannya, kompiler hanya akan mengubah semua deklarasi "lokal" menjadi "mulai fungsi di belakang layar."
QuinnFreedman
1 / Sistem tertanam kadang-kadang tidak memungkinkan memori dinamis, jadi jika Anda mendeklarasikan semua variabel di atas fungsi. Ketika kode sumber dibangun, ia dapat menghitung jumlah byte yang mereka butuhkan di stack untuk menjalankan program. Tetapi dengan memori dinamis, kompiler tidak dapat melakukan hal yang sama.
Dang_Ho
2 / Jika Anda mendeklarasikan variabel secara lokal, variabel itu hanya ada di dalam kurung buka / tutup "{}". Jadi kompiler dapat melepaskan ruang variabel jika variabel itu "di luar lingkup". Itu mungkin lebih baik daripada mendeklarasikan semuanya di bagian atas fungsi.
Dang_Ho
Saya pikir Anda bingung tentang memori statis vs dinamis. Memori statis dialokasikan pada tumpukan. Semua variabel yang dideklarasikan dalam suatu fungsi, di mana pun mereka dideklarasikan, dialokasikan secara statis. Memori dinamis dialokasikan pada tumpukan dengan sesuatu seperti malloc(). Meskipun saya belum pernah melihat perangkat yang tidak mampu melakukannya, adalah praktik terbaik untuk menghindari alokasi dinamis pada sistem tertanam ( lihat di sini ). Tapi itu tidak ada hubungannya dengan di mana Anda mendeklarasikan variabel Anda dalam suatu fungsi.
QuinnFreedman
1
Meskipun saya setuju bahwa ini akan menjadi cara yang masuk akal untuk beroperasi, bukan itu yang terjadi dalam praktik. Berikut ini adalah kumpulan sebenarnya untuk sesuatu yang sangat mirip dengan contoh Anda: godbolt.org/z/mLhE9a . Seperti yang Anda lihat, pada baris 11, sub rsp, 1008mengalokasikan ruang untuk seluruh array di luar pernyataan if. Ini berlaku untuk clangdan gccpada setiap versi dan level pengoptimalan yang saya coba.
QuinnFreedman
-1

Saya akan mengutip beberapa pernyataan dari manual untuk versi gcc 4.7.0 untuk penjelasan yang jelas.

"Kompilator dapat menerima beberapa standar dasar, seperti 'c90' atau 'c ++ 98', dan dialek GNU dari standar-standar itu, seperti 'gnu90' atau 'gnu ++ 98'. Dengan menentukan standar dasar, kompiler akan menerima semua program yang mengikuti standar itu dan yang menggunakan ekstensi GNU yang tidak bertentangan dengannya. Misalnya, '-std = c90' mematikan fitur GCC tertentu yang tidak kompatibel dengan ISO C90, seperti asm dan jenis kata kunci, tetapi tidak ekstensi GNU lain yang tidak memiliki arti dalam ISO C90, seperti menghilangkan istilah tengah ekspresi?: "

Saya pikir titik kunci dari pertanyaan Anda adalah mengapa tidak gcc sesuai dengan C89 bahkan jika opsi "-std = c89" digunakan. Saya tidak tahu versi gcc Anda, tetapi saya pikir tidak akan ada perbedaan besar. Pengembang gcc telah memberi tahu kami bahwa opsi "-std = c89" hanya berarti ekstensi yang bertentangan dengan C89 dimatikan. Jadi, itu tidak ada hubungannya dengan beberapa ekstensi yang tidak memiliki arti di C89. Dan ekstensi yang tidak membatasi penempatan deklarasi variabel milik ekstensi yang tidak bertentangan dengan C89.

Sejujurnya, semua orang akan berpikir bahwa itu harus sesuai C89 sepenuhnya pada pandangan pertama dari opsi "-std = c89". Tapi ternyata tidak. Adapun masalah yang menyatakan semua variabel di awal lebih baik atau lebih buruk hanyalah masalah kebiasaan.

junwanghe
sumber
mematuhi tidak berarti tidak menerima ekstensi: selama kompiler mengkompilasi program yang valid dan menghasilkan diagnostik yang diperlukan untuk orang lain, itu sesuai.
Ingat Monica
@ Marc Lehmann, ya, Anda benar ketika kata "menyesuaikan" digunakan untuk membedakan kompiler. Tetapi ketika kata "menyesuaikan" digunakan untuk menggambarkan beberapa penggunaan, Anda dapat mengatakan "Penggunaan tidak sesuai dengan standar." Dan semua pemula memiliki pendapat bahwa penggunaan yang tidak sesuai dengan standar harus menyebabkan kesalahan.
junwanghe
@Marc Lehmann, omong-omong, tidak ada diagnostik saat gcc melihat penggunaan yang tidak sesuai dengan standar C89.
junwanghe
Jawaban Anda masih salah, karena mengklaim "gcc tidak sesuai" tidak sama dengan "beberapa program pengguna tidak sesuai". Penggunaan kepatuhan Anda benar-benar salah. Selain itu, ketika saya masih pemula, saya tidak setuju dengan pendapat Anda, jadi itu juga salah. Terakhir, tidak ada persyaratan untuk compiler yang menyesuaikan untuk mendiagnosis kode yang tidak sesuai, dan pada kenyataannya, ini tidak mungkin untuk diterapkan.
Ingat Monica