Kebingungan tentang inisialisasi array di C

102

Dalam bahasa C, jika menginisialisasi array seperti ini:

int a[5] = {1,2};

maka semua elemen dari array yang tidak diinisialisasi secara eksplisit akan diinisialisasi secara implisit dengan nol.

Tapi, jika saya menginisialisasi array seperti ini:

int a[5]={a[2]=1};

printf("%d %d %d %d %d\n", a[0], a[1],a[2], a[3], a[4]);

keluaran:

1 0 1 0 0

Saya tidak mengerti, mengapa a[0]mencetak, 1bukan 0? Apakah ini perilaku yang tidak terdefinisi?

Catatan: Pertanyaan ini ditanyakan dalam sebuah wawancara.

msc
sumber
35
Ekspresi a[2]=1terevaluasi menjadi 1.
tkausl
14
Pertanyaan yang sangat dalam. Saya ingin tahu apakah pewawancara tahu jawabannya sendiri. Bukan saya. Memang seakan-akan nilai ekspresi a[2] = 1tersebut 1, tetapi saya tidak yakin apakah Anda diizinkan untuk mengambil hasil dari ekspresi penginisialisasi yang ditetapkan sebagai nilai elemen pertama. Fakta bahwa Anda telah menambahkan tanda pengacara berarti saya pikir kita membutuhkan jawaban yang mengutip standar.
Batsyeba
15
Nah jika itu pertanyaan favorit mereka, Anda mungkin telah menghindari peluru. Secara pribadi saya lebih suka latihan pemrograman tertulis (dengan akses ke kompiler dan debugger) untuk diambil selama beberapa jam daripada pertanyaan gaya "ace" seperti di atas. Saya dapat menebak jawaban, tetapi saya tidak berpikir itu akan memiliki dasar faktual yang nyata.
Batsyeba
1
@Bathsheba Saya akan melakukan yang sebaliknya, karena jawaban di sini sekarang menjawab kedua pertanyaan tersebut.
Selamat tinggal SE
1
@Batheba akan menjadi yang terbaik. Tetap saja saya akan memberikan penghargaan atas pertanyaan tersebut kepada OP, saat dia mengemukakan topik tersebut. Tapi ini bukan untuk saya untuk memutuskan apa yang saya rasa akan menjadi "hal yang benar".
Selamat tinggal SE

Jawaban:

95

TL; DR: Saya rasa perilaku dari int a[5]={a[2]=1};didefinisikan dengan baik, setidaknya di C99.

Bagian lucunya adalah bahwa satu-satunya bit yang masuk akal bagi saya adalah bagian yang Anda tanyakan: a[0]disetel ke 1karena operator penugasan mengembalikan nilai yang telah ditetapkan. Segala sesuatu yang lain tidak jelas.

Jika kodenya adalah int a[5] = { [2] = 1 }, semuanya akan mudah: Itu adalah pengaturan penginisialisasi yang ditunjuk a[2]untuk 1dan yang lainnya ke 0. Tapi dengan { a[2] = 1 }kita memiliki penginisialisasi yang tidak ditunjuk yang berisi ekspresi tugas, dan kita jatuh ke lubang kelinci.


Inilah yang saya temukan sejauh ini:

  • a harus berupa variabel lokal.

    6.7.8 Inisialisasi

    1. Semua ekspresi dalam penginisialisasi untuk objek yang memiliki durasi penyimpanan statis harus ekspresi konstan atau string literal.

    a[2] = 1bukan ekspresi konstan, jadi aharus memiliki penyimpanan otomatis.

  • a berada dalam ruang lingkup inisialisasi sendiri.

    6.2.1 Cakupan pengidentifikasi

    1. Tag struktur, gabungan, dan enumerasi memiliki cakupan yang dimulai tepat setelah kemunculan tag di penentu jenis yang mendeklarasikan tag. Setiap konstanta pencacahan memiliki ruang lingkup yang dimulai tepat setelah kemunculan pencacah yang menentukan dalam daftar pencacah. Pengenal lainnya memiliki cakupan yang dimulai tepat setelah deklaratornya selesai.

    Deklaratornya adalah a[5], jadi variabel berada dalam lingkup inisialisasi mereka sendiri.

  • a masih hidup dalam inisialisasi sendiri.

    6.2.4 Durasi penyimpanan benda

    1. Obyek yang identifier dinyatakan tanpa linkage dan tanpa specifier penyimpanan kelas staticmemiliki durasi penyimpanan otomatis .

    2. Untuk objek seperti itu yang tidak memiliki tipe array dengan panjang variabel, masa pakainya meluas dari entri ke dalam blok yang terkait hingga eksekusi blok itu berakhir dengan cara apa pun. (Memasuki blok tertutup atau memanggil fungsi menangguhkan, tetapi tidak berakhir, eksekusi blok saat ini.) Jika blok dimasukkan secara rekursif, instance baru dari objek dibuat setiap saat. Nilai awal objek tidak pasti. Jika sebuah inisialisasi ditentukan untuk objek, itu dilakukan setiap kali deklarasi dicapai dalam eksekusi blok; jika tidak, nilainya menjadi tak tentu setiap kali deklarasi tercapai.

  • Ada titik urutan setelahnya a[2]=1.

    6.8 Pernyataan dan blok

    1. Sebuah ekspresi penuh adalah ekspresi yang bukan merupakan bagian dari ekspresi lain atau dari deklarator a. Masing-masing dari berikut ini adalah ekspresi lengkap: penginisialisasi ; ekspresi dalam pernyataan ekspresi; ekspresi pengontrol dari pernyataan pemilihan ( ifatau switch); ekspresi pengontrol dari sebuah whileatau dopernyataan; setiap ekspresi (opsional) dari sebuah forpernyataan; ekspresi (opsional) dalam sebuah returnpernyataan. Akhir dari ekspresi lengkap adalah titik urutan.

    Perhatikan bahwa misalnya dalam int foo[] = { 1, 2, 3 }satu { 1, 2, 3 }bagian adalah daftar penjepit tertutup dari initializers, yang masing-masing memiliki titik urutan setelah.

  • Inisialisasi dilakukan dalam urutan daftar penginisialisasi.

    6.7.8 Inisialisasi

    1. Setiap daftar penginisialisasi yang diapit oleh kurung kurawal memiliki objek saat ini yang terkait . Jika tidak ada penunjukan, subobjek dari objek saat ini diinisialisasi sesuai dengan jenis objek saat ini: elemen array dalam urutan subskrip yang bertambah, anggota struktur dalam urutan deklarasi, dan anggota serikat yang bernama pertama. [...]

     

    1. Inisialisasi akan terjadi dalam urutan daftar penginisialisasi, setiap penginisialisasi disediakan untuk subobjek tertentu yang menggantikan penginisialisasi yang terdaftar sebelumnya untuk subobjek yang sama; semua subobjek yang tidak diinisialisasi secara eksplisit harus diinisialisasi secara implisit sama dengan objek yang memiliki durasi penyimpanan statis.
  • Namun, ekspresi penginisialisasi tidak selalu dievaluasi secara berurutan.

    6.7.8 Inisialisasi

    1. Urutan di mana efek samping terjadi di antara ekspresi daftar inisialisasi tidak ditentukan.

Namun, masih ada beberapa pertanyaan yang belum terjawab:

  • Apakah poin urutan bahkan relevan? Aturan dasarnya adalah:

    6.5 Ekspresi

    1. Antara titik urutan sebelumnya dan berikutnya, sebuah objek harus memiliki nilai simpanan yang dimodifikasi paling banyak satu kali dengan evaluasi ekspresi . Selanjutnya nilai prior harus dibaca hanya untuk menentukan nilai yang akan disimpan.

    a[2] = 1 adalah ekspresi, tetapi inisialisasi bukan.

    Ini sedikit bertentangan dengan Lampiran J:

    J.2 Perilaku tidak terdefinisi

    • Antara dua titik urutan, sebuah objek dimodifikasi lebih dari sekali, atau dimodifikasi dan nilai sebelumnya dibaca selain untuk menentukan nilai yang akan disimpan (6.5).

    Lampiran J mengatakan setiap modifikasi dihitung, tidak hanya modifikasi oleh ekspresi. Tetapi mengingat lampiran itu non-normatif, kita mungkin dapat mengabaikannya.

  • Bagaimana urutan inisialisasi subobjek sehubungan dengan ekspresi penginisialisasi? Apakah semua penginisialisasi dievaluasi terlebih dahulu (dalam beberapa urutan), kemudian subobjek diinisialisasi dengan hasil (dalam urutan daftar penginisialisasi)? Atau bisakah mereka disisipkan?


Saya pikir int a[5] = { a[2] = 1 }dieksekusi sebagai berikut:

  1. Penyimpanan untuk adialokasikan ketika blok berisi dimasukkan. Isinya tidak dapat ditentukan pada saat ini.
  2. Penginisialisasi (satu-satunya) dijalankan ( a[2] = 1), diikuti dengan titik urutan. Ini toko 1di a[2]dan kembali 1.
  3. Itu 1digunakan untuk menginisialisasi a[0](penginisialisasi pertama menginisialisasi subobjek pertama).

Tapi di sini hal-hal kabur karena unsur-unsur yang tersisa ( a[1], a[2], a[3], a[4]) seharusnya diinisialisasi untuk 0, tetapi tidak jelas kapan: Apakah itu terjadi sebelum a[2] = 1dievaluasi? Jika demikian, a[2] = 1akankah "menang" dan menimpa a[2], tetapi apakah tugas tersebut memiliki perilaku yang tidak ditentukan karena tidak ada titik urutan antara inisialisasi nol dan ekspresi tugas? Apakah poin urutan bahkan relevan (lihat di atas)? Atau apakah nol inisialisasi terjadi setelah semua penginisialisasi dievaluasi? Jika demikian, a[2]harus berakhir menjadi 0.

Karena standar C tidak secara jelas mendefinisikan apa yang terjadi di sini, saya yakin perilakunya tidak ditentukan (oleh kelalaian).

melpomene
sumber
1
Alih-alih tidak terdefinisi, saya berpendapat bahwa itu tidak ditentukan , yang membuat hal-hal terbuka untuk interpretasi oleh implementasi.
Beberapa programmer,
1
"kita jatuh ke lubang kelinci" LOL! Tidak pernah mendengar itu untuk UB atau hal-hal yang tidak ditentukan.
BЈовић
2
@Someprogrammerdude Saya rasa itu tidak dapat ditentukan (" perilaku di mana Standar Internasional ini memberikan dua kemungkinan atau lebih dan tidak memberlakukan persyaratan lebih lanjut yang dipilih dalam hal apa pun ") karena standar tidak benar-benar memberikan kemungkinan di antaranya untuk memilih. Ini hanya tidak mengatakan apa yang terjadi, yang saya percaya termasuk di bawah " Perilaku tidak terdefinisi [...] ditunjukkan dalam Standar Internasional ini [...] dengan menghilangkan definisi eksplisit apa pun dari perilaku. "
melpomene
2
@ BЈовић Ini juga merupakan deskripsi yang sangat bagus tidak hanya untuk perilaku tidak terdefinisi, tetapi juga untuk perilaku yang ditentukan yang memerlukan utas seperti ini untuk menjelaskan.
gnasher729
1
@JohnBollinger Perbedaannya adalah Anda tidak dapat benar-benar menginisialisasi a[0]subobjek sebelum mengevaluasi penginisialisasinya, dan mengevaluasi penginisialisasi apa pun menyertakan titik urutan (karena ini adalah "ekspresi penuh"). Oleh karena itu, saya yakin memodifikasi subobjek yang kami inisialisasi adalah permainan yang adil.
melpomene
22

Saya tidak mengerti, mengapa a[0]mencetak, 1bukan 0?

Agaknya a[2]=1menginisialisasi a[2]terlebih dahulu, dan hasil ekspresi digunakan untuk menginisialisasi a[0].

Dari N2176 (draft C17):

6.7.9 Inisialisasi

  1. Evaluasi ekspresi daftar inisialisasi tidak ditentukan urutannya sehubungan dengan satu sama lain dan dengan demikian urutan di mana efek samping terjadi tidak ditentukan. 154)

Jadi nampaknya keluaran 1 0 0 0 0juga akan dimungkinkan.

Kesimpulan: Jangan menulis penginisialisasi yang mengubah variabel yang diinisialisasi dengan cepat.

pengguna694733
sumber
1
Bagian itu tidak berlaku: Hanya ada satu ekspresi penginisialisasi di sini, jadi tidak perlu diurutkan dengan apa pun.
melpomene
@melpomene Ada {...}ekspresi yang menginisialisasi a[2]ke 0, dan a[2]=1sub-ekspresi yang menginisialisasi a[2]ke 1.
pengguna694733
1
{...}adalah daftar penginisialisasi yang diperkuat. Itu bukan ekspresi.
melpomene
@melpomene Ok, Anda mungkin ada di sana. Tapi saya masih berpendapat masih ada 2 efek samping yang bersaing sehingga paragraf tetap bertahan.
pengguna694733
@melpomene ada dua hal yang harus diurutkan: penginisialisasi pertama, dan pengaturan elemen lain ke 0
MM
6

Saya pikir standar C11 mencakup perilaku ini dan mengatakan bahwa hasilnya tidak ditentukan , dan menurut saya C18 tidak membuat perubahan apa pun yang relevan di area ini.

Bahasa standar tidak mudah diurai. Bagian yang relevan dari standar adalah §6.7.9 Inisialisasi . Sintaksnya didokumentasikan sebagai:

initializer:
                assignment-expression
                { initializer-list }
                { initializer-list , }
initializer-list:
                designationopt initializer
                initializer-list , designationopt initializer
designation:
                designator-list =
designator-list:
                designator
                designator-list designator
designator:
                [ constant-expression ]
                . identifier

Perhatikan bahwa salah satu termnya adalah assignment-expression , dan karena itu a[2] = 1adalah ekspresi assignment, itu diperbolehkan di dalam penginisialisasi untuk array dengan durasi non-statis:

§4 Semua ekspresi dalam penginisialisasi untuk objek yang memiliki durasi penyimpanan statis atau utas harus ekspresi konstan atau literal string.

Salah satu paragraf kuncinya adalah:

§19 Inisialisasi akan terjadi dalam urutan daftar penginisialisasi, setiap penginisialisasi disediakan untuk subobjek tertentu yang menggantikan penginisialisasi yang terdaftar sebelumnya untuk subobjek yang sama; 151) semua subobjek yang tidak diinisialisasi secara eksplisit harus diinisialisasi secara implisit sama dengan objek yang memiliki durasi penyimpanan statis.

151) Penginisialisasi apa pun untuk subobjek yang diganti sehingga tidak digunakan untuk menginisialisasi subobjek itu mungkin tidak dievaluasi sama sekali.

Dan paragraf kunci lainnya adalah:

§23 Evaluasi dari ekspresi daftar inisialisasi diurutkan secara tidak pasti terhadap satu sama lain dan dengan demikian urutan di mana efek samping yang terjadi tidak ditentukan. 152)

152) Secara khusus, urutan evaluasi tidak harus sama dengan urutan inisialisasi sub-objek.

Saya cukup yakin bahwa paragraf §23 menunjukkan bahwa notasi dalam pertanyaan:

int a[5] = { a[2] = 1 };

mengarah ke perilaku yang tidak ditentukan. Penugasan ke a[2]adalah efek samping, dan urutan evaluasi ekspresi diurutkan secara tidak pasti terkait satu sama lain. Akibatnya, menurut saya tidak ada cara untuk menarik standar dan mengklaim bahwa kompilator tertentu menangani ini dengan benar atau salah.

Jonathan Leffler
sumber
Hanya ada satu ekspresi daftar inisialisasi, jadi §23 tidak relevan.
melpomene
2

Pemahaman saya a[2]=1mengembalikan nilai 1 jadi kode menjadi

int a[5]={a[2]=1} --> int a[5]={1}

int a[5]={1} berikan nilai untuk a [0] = 1

Karenanya mencetak 1 untuk [0]

Sebagai contoh

char str[10]={‘H’,‘a’,‘i’};


char str[0] = H’;
char str[1] = a’;
char str[2] = i;
Karthika
sumber
2
Ini adalah pertanyaan [pengacara bahasa], tapi ini bukan jawaban yang sesuai dengan standar, sehingga membuatnya tidak relevan. Selain itu, ada juga 2 jawaban yang lebih mendalam yang tersedia dan jawaban Anda sepertinya tidak menambahkan apa-apa.
Selamat tinggal SE
Saya ragu. Apakah konsep yang saya posting salah? Bisakah Anda menjelaskan kepada saya dengan ini?
Karthika
1
Anda hanya berspekulasi untuk alasan, sementara ada jawaban yang sangat bagus sudah diberikan dengan bagian standar yang relevan. Hanya mengatakan bagaimana itu bisa terjadi bukanlah tentang apa pertanyaannya. Ini tentang apa yang menurut standar harus terjadi.
Selamat tinggal SE
Tetapi orang yang memposting pertanyaan di atas menanyakan alasannya dan mengapa itu terjadi? Jadi hanya saya yang menjatuhkan jawaban ini, tetapi konsepnya benar, bukan?
Karthika
OP bertanya " Apakah itu perilaku yang tidak terdefinisi? ". Jawaban Anda tidak mengatakan.
melpomene
1

Saya mencoba memberikan jawaban singkat dan sederhana untuk teka-teki tersebut: int a[5] = { a[2] = 1 };

  1. Pertama a[2] = 1diatur. Itu berarti array mengatakan:0 0 1 0 0
  2. Tapi lihatlah, mengingat Anda melakukannya di dalam { }tanda kurung, yang digunakan untuk menginisialisasi array secara berurutan, ia mengambil nilai pertama (yaitu 1) dan menyetelnya ke a[0]. Seolah-olah int a[5] = { a[2] };akan tetap, di mana kita sudah mendapatkannya a[2] = 1. Array yang dihasilkan sekarang adalah:1 0 1 0 0

Contoh lain: int a[6] = { a[3] = 1, a[4] = 2, a[5] = 3 };- Meskipun urutannya agak sewenang-wenang, dengan asumsi itu bergerak dari kiri ke kanan, itu akan berjalan dalam 6 langkah berikut:

0 0 0 1 0 0
1 0 0 1 0 0
1 0 0 1 2 0
1 2 0 1 2 0
1 2 0 1 2 3
1 2 3 1 2 3
Pertarungan
sumber
1
A = B = C = 5bukan deklarasi (atau inisialisasi). Ini adalah ekspresi normal yang diurai A = (B = (C = 5))karena =operatornya adalah asosiatif yang benar. Itu tidak terlalu membantu menjelaskan cara kerja inisialisasi. Array sebenarnya mulai ada ketika blok yang didefinisikan di dalamnya dimasukkan, yang bisa lama sebelum definisi sebenarnya dieksekusi.
melpomene
1
" Ini berjalan dari kiri ke kanan, masing-masing dimulai dengan pernyataan internal " tidak benar. Standar C secara eksplisit mengatakan " Urutan di mana setiap efek samping yang terjadi di antara ekspresi daftar inisialisasi tidak ditentukan. "
melpomene
1
" Anda cukup menguji kode dari contoh saya dan melihat apakah hasilnya konsisten. " Bukan begitu cara kerjanya. Anda sepertinya tidak mengerti apa itu perilaku tidak terdefinisi. Segala sesuatu di C memiliki perilaku tidak terdefinisi secara default; hanya saja beberapa bagian memiliki perilaku yang ditentukan oleh standar. Untuk membuktikan bahwa sesuatu telah mendefinisikan perilaku, Anda harus mengutip standar dan menunjukkan di mana standar mendefinisikan apa yang harus terjadi. Dengan tidak adanya definisi seperti itu, perilaku tidak terdefinisi.
melpomene
1
Penegasan dalam poin (1) adalah lompatan besar atas pertanyaan kunci di sini: apakah inisialisasi implisit elemen a [2] ke 0 terjadi sebelum efek samping a[2] = 1ekspresi penginisialisasi diterapkan? Hasil yang diamati adalah seolah-olah, tetapi standar tampaknya tidak menentukan bahwa seharusnya demikian. Itulah pusat kontroversi, dan jawaban ini sepenuhnya mengabaikannya.
John Bollinger
1
"Perilaku tidak terdefinisi" adalah istilah teknis dengan arti sempit. Ini tidak berarti "perilaku yang tidak kami yakini". Wawasan utama di sini adalah bahwa tidak ada pengujian, tanpa kompilator, yang dapat menunjukkan program tertentu berperilaku baik atau tidak sesuai standar , karena jika program memiliki perilaku tidak terdefinisi, kompilator diperbolehkan melakukan apa saja - termasuk bekerja dengan cara yang dapat diprediksi dan masuk akal dengan sempurna. Ini bukan hanya masalah kualitas implementasi di mana penulis kompilator mendokumentasikan hal-hal - itu adalah perilaku yang tidak ditentukan atau ditentukan oleh implementasi.
Jeroen Mostert
0

Tugas a[2]= 1adalah ekspresi yang memiliki nilai 1, dan pada dasarnya Anda menulis int a[5]= { 1 };(dengan efek samping yang a[2]ditetapkan 1juga).

Yves Daoust
sumber
Tetapi tidak jelas kapan efek samping dievaluasi dan perilakunya mungkin berubah bergantung pada kompilator. Juga standar tampaknya menyatakan bahwa ini adalah perilaku yang tidak terdefinisi sehingga penjelasan untuk realisasi spesifik kompilator tidak membantu.
Selamat tinggal SE
@KamiKaze: tentu, nilai 1 mendarat di sana secara tidak sengaja.
Yves Daoust
0

Aku percaya itu int a[5]={ a[2]=1 }; adalah contoh yang baik bagi seorang programmer yang menembak dirinya sendiri ke kakinya sendiri.

Saya mungkin tergoda untuk berpikir bahwa yang Anda maksud adalah int a[5]={ [2]=1 }; yang akan menjadi elemen pengaturan penginisialisasi C99 yang ditunjuk 2 ke 1 dan sisanya ke nol.

Dalam kasus yang jarang terjadi yang benar-benar Anda maksudkan int a[5]={ 1 }; a[2]=1;, itu akan menjadi cara penulisan yang lucu. Bagaimanapun, inilah inti dari kode Anda, meskipun beberapa di sini menunjukkan bahwa itu tidak didefinisikan dengan baik ketika penulisan ke a[2]benar-benar dijalankan. Perangkap di sini adalah bahwa a[2]=1bukan penginisialisasi yang ditunjuk tetapi tugas sederhana yang dengan sendirinya memiliki nilai 1.

Sven
sumber
Sepertinya topik pengacara bahasa ini meminta referensi dari draf standar. Itulah mengapa Anda mendapat suara negatif (saya tidak melakukannya seperti yang Anda lihat, saya mendapat suara negatif karena alasan yang sama). Saya pikir apa yang Anda tulis benar-benar baik-baik saja tetapi sepertinya semua pengacara bahasa di sini berasal dari komite atau semacamnya. Jadi mereka tidak meminta bantuan sama sekali, mereka mencoba memeriksa apakah draf mencakup kasus atau tidak dan sebagian besar orang di sini terpicu jika Anda memberikan jawaban seperti Anda membantu mereka. Saya rasa saya akan menghapus jawaban saya :) Jika aturan topik ini dijelaskan dengan jelas yang akan membantu
Abdurrahim