Apa yang terjadi pada variabel yang dideklarasikan dan tidak diinisialisasi di C? Apakah itu ada nilainya?

142

Jika di CI tulis:

int num;

Sebelum saya menetapkan apa pun num, apakah nilai numtak tentu?

atp
sumber
4
Um, bukankah itu variabel yang ditentukan , bukan yang dideklarasikan ? (Maaf jika itu C ++ saya bersinar ...)
sbi
6
Tidak. Saya bisa mendeklarasikan variabel tanpa mendefinisikannya: extern int x;Namun mendefinisikan selalu berarti mendeklarasikan. Ini tidak benar di C ++, dengan variabel anggota kelas statis seseorang dapat mendefinisikan tanpa mendeklarasikan, karena deklarasi harus dalam definisi kelas (bukan deklarasi!) Dan definisi harus di luar definisi kelas.
bdonlan
ee.hawaii.edu/~tep/EE160/Book/chap14/subsection2.1.1.4.html Sepertinya didefinisikan berarti Anda harus menginisialisasi juga.
ATP

Jawaban:

193

Variabel statis (ruang lingkup file dan fungsi statis) diinisialisasi ke nol:

int x; // zero
int y = 0; // also zero

void foo() {
    static int x; // also zero
}

Variabel non-statis (variabel lokal) tidak dapat ditentukan . Membacanya sebelum menetapkan nilai menghasilkan perilaku yang tidak terdefinisi .

void foo() {
    int x;
    printf("%d", x); // the compiler is free to crash here
}

Dalam praktiknya, mereka cenderung hanya memiliki beberapa nilai yang tidak masuk akal di sana pada awalnya - beberapa kompiler bahkan dapat menempatkan nilai tetap yang spesifik untuk membuatnya jelas saat mencari di debugger - tetapi secara tegas, kompilator bebas melakukan apa pun mulai dari menabrak hingga memanggil setan melalui saluran hidung Anda .

Adapun mengapa ini adalah perilaku tidak terdefinisi alih-alih hanya "nilai tidak ditentukan / sewenang-wenang", ada sejumlah arsitektur CPU yang memiliki bit bendera tambahan dalam representasi mereka untuk berbagai jenis. Contoh modern adalah Itanium, yang memiliki bit "Not a Thing" dalam registernya ; tentu saja, perancang standar C sedang mempertimbangkan beberapa arsitektur yang lebih tua.

Mencoba untuk bekerja dengan nilai dengan ini flag bit-bit dapat menghasilkan pengecualian CPU dalam sebuah operasi yang benar-benar tidak harus gagal (misalnya, penambahan integer, atau menugaskan ke variabel lain). Dan jika Anda pergi dan membiarkan variabel tidak diinisialisasi, compiler mungkin mengambil beberapa sampah acak dengan set bit flag ini - yang berarti menyentuh variabel yang tidak diinisialisasi itu mungkin mematikan.

bdonlan.dll
sumber
3
oh tidak, mereka tidak. Mereka mungkin, dalam mode debug, saat Anda tidak di depan pelanggan, pada bulan dengan R masuk, jika Anda beruntung
Martin Beckett
8
apa yang tidak? inisialisasi statis diperlukan oleh standar; lihat ISO / IEC 9899: 1999 6.7.8 # 10
bdonlan
3
contoh pertama baik-baik saja sejauh yang saya tahu. Saya kurang tahu mengapa kompiler mungkin macet di yang kedua :)
6
@Stuart: ada hal yang disebut "representasi perangkap", yang pada dasarnya adalah pola bit yang tidak menunjukkan nilai yang valid, dan dapat menyebabkan misalnya pengecualian perangkat keras pada waktu proses. Satu-satunya tipe C dimana ada jaminan bahwa pola bit apapun adalah nilai yang valid adalah char; semua yang lain dapat memiliki representasi jebakan. Atau - karena mengakses variabel yang tidak diinisialisasi adalah UB - kompiler yang sesuai mungkin hanya melakukan beberapa pemeriksaan dan memutuskan untuk memberi sinyal masalah.
Pavel Minaev
5
bdonian benar. C selalu ditentukan dengan cukup tepat. Sebelum C89 dan C99, makalah oleh dmr menentukan semua hal ini di awal tahun 1970-an. Bahkan dalam sistem tertanam paling kasar, hanya dibutuhkan satu memset () untuk melakukan sesuatu dengan benar, jadi tidak ada alasan untuk lingkungan yang tidak sesuai. Saya telah mengutip standar dalam jawaban saya.
DigitalRoss
57

0 jika statis atau global, tidak dapat ditentukan jika kelas penyimpanan otomatis

C selalu sangat spesifik tentang nilai awal objek. Jika global atau static, mereka akan menjadi nol. Jika auto, nilainya tidak pasti .

Ini adalah kasus pada penyusun pra-C89 dan ditentukan oleh K&R dan dalam laporan C asli DMR.

Ini adalah kasus di C89, lihat bagian 6.5.7 Inisialisasi .

Jika objek yang memiliki durasi penyimpanan otomatis tidak diinisialisasi secara eksplisit, nilainya tidak dapat ditentukan. Jika sebuah objek yang memiliki durasi penyimpanan statis tidak diinisialisasi secara eksplisit, itu diinisialisasi secara implisit seolah-olah setiap anggota yang memiliki tipe aritmatika ditugaskan 0 dan setiap anggota yang memiliki tipe pointer diberi konstanta pointer nol.

Ini adalah kasus di C99, lihat bagian 6.7.8 Inisialisasi .

Jika sebuah objek yang memiliki durasi penyimpanan otomatis tidak diinisialisasi secara eksplisit, nilainya tidak dapat ditentukan. Jika sebuah objek yang memiliki durasi penyimpanan statis tidak diinisialisasi secara eksplisit, maka:
- jika memiliki tipe penunjuk, ia diinisialisasi ke penunjuk null;
- jika memiliki tipe aritmatika, itu diinisialisasi ke nol (positif atau unsigned);
- jika merupakan agregat, setiap anggota diinisialisasi (secara rekursif) sesuai dengan aturan ini;
- jika itu adalah sebuah serikat pekerja, nama anggota pertama diinisialisasi (secara rekursif) sesuai dengan aturan ini.

Mengenai apa sebenarnya arti tak tentu , saya tidak yakin untuk C89, C99 mengatakan:

3.17.2
nilai tak tentu

baik nilai tak ditentukan atau representasi perangkap

Tetapi terlepas dari apa yang dikatakan standar, dalam kehidupan nyata, setiap halaman tumpukan sebenarnya dimulai sebagai nol, tetapi ketika program Anda melihat autonilai kelas penyimpanan apa pun , ia melihat apa pun yang tertinggal oleh program Anda sendiri saat terakhir kali menggunakan alamat tumpukan tersebut. Jika Anda mengalokasikan banyak autolarik, Anda akan melihatnya pada akhirnya mulai rapi dengan nol.

Anda mungkin bertanya-tanya, mengapa demikian? Jawaban SO yang berbeda berkaitan dengan pertanyaan itu, lihat: https://stackoverflow.com/a/2091505/140740

DigitalRoss
sumber
3
tak tentu biasanya (dulu?) artinya bisa melakukan apa saja. Bisa nol, bisa jadi nilai yang ada di sana, bisa merusak program, bisa membuat komputer memproduksi pancake blueberry dari slot CD. Anda sama sekali tidak memiliki jaminan. Itu mungkin menyebabkan kehancuran planet. Setidaknya sejauh spesifikasinya ... siapa pun yang membuat kompiler yang benar-benar melakukan hal seperti itu akan sangat disukai B-)
Brian Postow
Dalam draf C11 N1570, definisi indeterminate valuedapat ditemukan di 3.19.2.
pengguna3528438
Apakah itu selalu tergantung pada kompiler atau OS yang nilai apa yang ditetapkan untuk variabel statis? Misalnya, jika seseorang menulis OS atau kompiler saya sendiri, dan jika mereka juga menetapkan nilai awal secara default untuk statika sebagai tak tentu, apakah itu mungkin?
Aditya Singh
1
@AdityaSingh, OS dapat mempermudah kompilator tetapi pada akhirnya itu adalah tanggung jawab utama penyusun untuk menjalankan katalog kode C yang ada di dunia, dan tanggung jawab sekunder untuk memenuhi standar. Itu pasti akan mungkin untuk melakukannya secara berbeda, tetapi, mengapa? Selain itu, sulit untuk membuat data statis tidak dapat ditentukan, karena OS akan benar - benar ingin membidik halaman terlebih dahulu untuk alasan keamanan. (Variabel otomatis hanya tidak dapat diprediksi secara dangkal karena program Anda sendiri biasanya telah menggunakan alamat tumpukan tersebut pada titik sebelumnya.)
DigitalRoss
@BrianPostow Tidak, itu tidak benar. Lihat stackoverflow.com/a/40674888/584518 . Menggunakan nilai tak tentu menyebabkan perilaku tak ditentukan , bukan perilaku tak terdefinisi, kecuali kasus representasi jebakan.
Lundin
12

Itu tergantung pada durasi penyimpanan variabel. Variabel dengan durasi penyimpanan statis selalu secara implisit diinisialisasi dengan nol.

Sedangkan untuk variabel otomatis (lokal), variabel yang tidak diinisialisasi memiliki nilai yang tidak dapat ditentukan . Nilai tak tentu, antara lain, berarti bahwa "nilai" apa pun yang mungkin "Anda lihat" dalam variabel itu tidak hanya tidak dapat diprediksi, bahkan tidak dijamin akan stabil . Misalnya, dalam prakteknya (yaitu mengabaikan UB sebentar) kode ini

int num;
int a = num;
int b = num;

tidak menjamin variabel tersebut adan bakan menerima nilai yang identik. Menariknya, ini bukanlah konsep teoretis yang terlalu mendasari, ini langsung terjadi dalam praktik sebagai konsekuensi dari pengoptimalan.

Jadi secara umum, jawaban populer bahwa "ia diinisialisasi dengan sampah apa pun yang ada di memori" bahkan tidak benar. Perilaku variabel yang tidak diinisialisasi berbeda dengan variabel yang diinisialisasi dengan sampah.

Semut
sumber
Saya tidak mengerti (yah, saya sangat bisa ) mengapa ini memiliki suara positif yang jauh lebih sedikit daripada yang dari DigitalRoss hanya satu menit setelahnya: D
Antti Haapala
8

Contoh Ubuntu 15.10, Kernel 4.2.0, x86-64, GCC 5.2.1

Standar yang cukup, mari kita lihat implementasinya :-)

Variabel lokal

Standar: perilaku tidak terdefinisi.

Implementasi: program mengalokasikan ruang tumpukan, dan tidak pernah memindahkan apa pun ke alamat itu, jadi apa pun yang ada sebelumnya digunakan.

#include <stdio.h>
int main() {
    int i;
    printf("%d\n", i);
}

kompilasi dengan:

gcc -O0 -std=c99 a.c

keluaran:

0

dan mendekompilasi dengan:

objdump -dr a.out

untuk:

0000000000400536 <main>:
  400536:       55                      push   %rbp
  400537:       48 89 e5                mov    %rsp,%rbp
  40053a:       48 83 ec 10             sub    $0x10,%rsp
  40053e:       8b 45 fc                mov    -0x4(%rbp),%eax
  400541:       89 c6                   mov    %eax,%esi
  400543:       bf e4 05 40 00          mov    $0x4005e4,%edi
  400548:       b8 00 00 00 00          mov    $0x0,%eax
  40054d:       e8 be fe ff ff          callq  400410 <printf@plt>
  400552:       b8 00 00 00 00          mov    $0x0,%eax
  400557:       c9                      leaveq
  400558:       c3                      retq

Dari pengetahuan kami tentang konvensi panggilan x86-64:

  • %rdiadalah argumen printf pertama, dengan demikian string "%d\n"di alamat0x4005e4

  • %rsiadalah argumen printf kedua i.

    Itu berasal dari -0x4(%rbp), yang merupakan variabel lokal 4-byte pertama.

    Pada titik ini, rbpdi halaman pertama tumpukan telah dialokasikan oleh kernel, jadi untuk memahami nilai itu kita akan melihat ke dalam kode kernel dan mencari tahu apa yang ditetapkan untuk itu.

    TODO apakah kernel menyetel memori itu ke sesuatu sebelum menggunakannya kembali untuk proses lain ketika suatu proses mati? Jika tidak, proses baru akan dapat membaca memori program selesai lainnya, membocorkan data. Lihat: Apakah nilai yang tidak diinisialisasi pernah menjadi risiko keamanan?

Kami kemudian juga dapat bermain dengan modifikasi tumpukan kami sendiri dan menulis hal-hal menyenangkan seperti:

#include <assert.h>

int f() {
    int i = 13;
    return i;
}

int g() {
    int i;
    return i;
}

int main() {
    f();
    assert(g() == 13);
}

Variabel lokal di -O3

Analisis implementasi pada: Apa arti <value dioptimalkan out> di gdb?

Variabel global

Standar: 0

Implementasi: .bssbagian.

#include <stdio.h>
int i;
int main() {
    printf("%d\n", i);
}

gcc -00 -std=c99 a.c

mengkompilasi ke:

0000000000400536 <main>:
  400536:       55                      push   %rbp
  400537:       48 89 e5                mov    %rsp,%rbp
  40053a:       8b 05 04 0b 20 00       mov    0x200b04(%rip),%eax        # 601044 <i>
  400540:       89 c6                   mov    %eax,%esi
  400542:       bf e4 05 40 00          mov    $0x4005e4,%edi
  400547:       b8 00 00 00 00          mov    $0x0,%eax
  40054c:       e8 bf fe ff ff          callq  400410 <printf@plt>
  400551:       b8 00 00 00 00          mov    $0x0,%eax
  400556:       5d                      pop    %rbp
  400557:       c3                      retq
  400558:       0f 1f 84 00 00 00 00    nopl   0x0(%rax,%rax,1)
  40055f:       00

# 601044 <i>mengatakan itu idi alamat 0x601044dan:

readelf -SW a.out

mengandung:

[25] .bss              NOBITS          0000000000601040 001040 000008 00  WA  0   0  4

yang mengatakan 0x601044tepat di tengah .bssbagian, yang dimulai pada 0x601040dan panjangnya 8 byte.

Standar ELF kemudian menjamin bahwa bagian yang dinamai .bssbenar-benar diisi dengan angka nol:

.bssBagian ini menyimpan data yang tidak diinisialisasi yang berkontribusi pada gambar memori program. Menurut definisi, sistem menginisialisasi data dengan nol saat program mulai dijalankan. Bagian tidak menggunakan ruang file, seperti yang ditunjukkan oleh tipe bagian SHT_NOBITS,.

Selain itu, jenisnya SHT_NOBITSefisien dan tidak menempati ruang pada file yang dapat dieksekusi:

sh_sizeAnggota ini memberikan ukuran bagian dalam byte. Kecuali jika tipe SHT_NOBITSbagiannya adalah , bagian tersebut menempati sh_size byte dalam file. Bagian tipe SHT_NOBITSmungkin memiliki ukuran bukan nol, tetapi tidak menempati ruang di file.

Kemudian terserah pada kernel Linux untuk mengosongkan wilayah memori tersebut saat memuat program ke dalam memori saat program dimulai.

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
sumber
4

Itu tergantung. Jika definisi tersebut global (di luar fungsi apa pun) maka numakan diinisialisasi ke nol. Jika itu lokal (di dalam fungsi) maka nilainya tidak dapat ditentukan. Secara teori, bahkan mencoba membaca nilai memiliki perilaku yang tidak ditentukan - C memungkinkan kemungkinan bit yang tidak berkontribusi pada nilai, tetapi harus disetel dengan cara tertentu agar Anda bahkan mendapatkan hasil yang ditentukan dari membaca variabel.

Jerry Coffin
sumber
1

Karena komputer memiliki kapasitas penyimpanan yang terbatas, variabel otomatis biasanya akan disimpan dalam elemen penyimpanan (baik register atau RAM) yang sebelumnya telah digunakan untuk tujuan arbitrer lainnya. Jika variabel seperti itu digunakan sebelum nilai diberikan padanya, penyimpanan itu dapat menampung apa pun yang dipegangnya sebelumnya, sehingga konten variabel tidak dapat diprediksi.

Sebagai tambahan, banyak kompiler mungkin menyimpan variabel dalam register yang lebih besar dari tipe terkait. Meskipun kompiler akan diminta untuk memastikan bahwa nilai apa pun yang ditulis ke variabel dan dibaca kembali akan dipotong dan / atau diperpanjang tanda ke ukuran yang tepat, banyak kompiler akan melakukan pemotongan seperti itu ketika variabel ditulis dan berharap itu akan terjadi. dilakukan sebelum variabel dibaca. Pada kompiler seperti itu, seperti:

uint16_t hey(uint32_t x, uint32_t mode)
{ uint16_t q; 
  if (mode==1) q=2; 
  if (mode==3) q=4; 
  return q; }

 uint32_t wow(uint32_t mode) {
   return hey(1234567, mode);
 }

mungkin menghasilkan wow()penyimpanan nilai 1234567 ke dalam register 0 dan 1, dan memanggil foo(). Karena xtidak diperlukan dalam "foo", dan karena fungsi seharusnya meletakkan nilai kembaliannya ke register 0, kompilator dapat mengalokasikan register 0 ke q. Jika mode1 atau 3, register 0 akan dimuat dengan 2 atau 4, masing-masing, tetapi jika itu adalah nilai lain, fungsi dapat mengembalikan apa pun yang ada di register 0 (yaitu nilai 1234567) meskipun nilai itu tidak dalam kisaran dari uint16_t.

Untuk menghindari keharusan compiler melakukan pekerjaan ekstra guna memastikan bahwa variabel yang tidak diinisialisasi sepertinya tidak pernah menyimpan nilai di luar domain mereka, dan menghindari keharusan untuk menentukan perilaku tak tentu secara berlebihan, Standar mengatakan bahwa penggunaan variabel otomatis yang tidak diinisialisasi adalah Undefined Behavior. Dalam beberapa kasus, konsekuensi dari hal ini bahkan mungkin lebih mengejutkan daripada nilai yang berada di luar kisaran jenisnya. Misalnya, diberikan:

void moo(int mode)
{
  if (mode < 5)
    launch_nukes();
  hey(0, mode);      
}

kompiler dapat menyimpulkan bahwa karena memanggil moo()dengan mode yang lebih besar dari 3 pasti akan menyebabkan program memanggil Undefined Behavior, kompilator dapat menghilangkan kode apa pun yang hanya relevan jika mode4 atau lebih besar, seperti kode yang biasanya mencegah peluncuran nuklir dalam kasus seperti itu. Perhatikan bahwa baik Standard, maupun filosofi compiler modern, akan peduli dengan fakta bahwa nilai yang dikembalikan dari "hey" diabaikan - tindakan mencoba mengembalikannya memberikan lisensi compiler tak terbatas untuk menghasilkan kode arbitrer.

supercat
sumber
0

Jawaban dasarnya adalah, ya itu tidak ditentukan.

Jika Anda melihat perilaku aneh karena ini, mungkin tergantung di mana ia dideklarasikan. Jika di dalam suatu fungsi di stack maka isinya kemungkinan besar akan berbeda setiap kali fungsi dipanggil. Jika itu adalah ruang lingkup statis atau modul, itu tidak ditentukan tetapi tidak akan berubah.

simon
sumber
0

Jika kelas penyimpanan statis atau global maka selama pemuatan, BSS menginisialisasi variabel atau lokasi memori (ML) ke 0 kecuali variabel awalnya diberi nilai tertentu. Dalam kasus variabel lokal yang tidak diinisialisasi, representasi perangkap ditetapkan ke lokasi memori. Jadi jika salah satu register Anda yang berisi info penting ditimpa oleh compiler, program tersebut mungkin macet.

tetapi beberapa kompiler mungkin memiliki mekanisme untuk menghindari masalah seperti itu.

Saya bekerja dengan seri nec v850 ketika saya menyadari Ada representasi perangkap yang memiliki pola bit yang mewakili nilai yang tidak ditentukan untuk tipe data kecuali untuk char. Ketika saya mengambil karakter yang tidak diinisialisasi saya mendapat nilai default nol karena representasi perangkap. Ini mungkin berguna untuk any1 yang menggunakan necv850es

hanish
sumber
Sistem Anda tidak sesuai jika Anda mendapatkan representasi trap saat menggunakan unsigned char. Mereka secara eksplisit tidak diperbolehkan mengandung representasi trap, C17 6.2.6.1/5.
Lundin
-2

Nilai num adalah nilai sampah dari memori utama (RAM). lebih baik jika Anda menginisialisasi variabel segera setelah membuat.

Shrikant Singh
sumber
-4

Sejauh yang saya lakukan itu sebagian besar tergantung pada kompiler tetapi secara umum sebagian besar kasus nilai sebelumnya diasumsikan sebagai 0 oleh pelengkap.
Saya mendapat nilai sampah dalam kasus VC ++ sementara TC memberi nilai 0. Saya mencetaknya seperti di bawah ini

int i;
printf('%d',i);
Rajeev Kumar
sumber
Jika Anda mendapatkan nilai deterministik seperti misalnya 0kompilator Anda kemungkinan besar akan melakukan langkah-langkah ekstra untuk memastikan nilai tersebut (dengan menambahkan kode untuk menginisialisasi variabel). Beberapa kompiler melakukan ini saat melakukan kompilasi "debug", tetapi memilih nilai 0untuk ini adalah ide yang buruk karena akan menyembunyikan kesalahan dalam kode Anda (hal yang lebih tepat akan menjamin angka yang sangat tidak mungkin seperti 0xBAADF00Datau yang serupa). Saya pikir sebagian besar kompilator hanya akan meninggalkan sampah apa pun yang terjadi untuk menempati memori sebagai nilai variabel (mis. Itu secara umum tidak diasumsikan sebagai 0).
skyking