Menu

Union di C: Memori Bersama, Union vs Struct, dan Tagged Union

Cara union di C menyimpan beberapa tipe di byte yang sama: kenapa ukurannya sebesar anggota terbesarnya, kenapa kamu harus membaca anggota yang terakhir kamu tulis, dan bagaimana tagged union (enum ditambah union) membuatnya aman.

Halaman ini dilengkapi editor yang bisa dijalankan. Edit, jalankan, dan lihat output-nya seketika.

Struct berkata "semua ini, bersama-sama". union berkata "tepat satu dari ini, pada satu waktu". Anggota-anggotanya ditumpuk di alamat yang sama, jadi union hanya sebesar anggota terbesarnya, dan menulis satu anggota akan menghancurkan anggota lainnya.

Kedengarannya seperti jebakan, dan tanpa pengecekan memang begitu. Tetapi itu juga cara C menyatakan sebuah nilai yang bisa berupa salah satu dari beberapa jenis, seperti token yang berupa angka atau string, atau pesan yang berupa klik atau tekanan tombol, tanpa harus membayar untuk semuanya sekaligus.

Mendeklarasikan dan Memakai Union

Sintaksnya persis meniru struct; hanya kata kuncinya yang berubah.

Anggota diakses dengan . (atau -> lewat pointer), persis seperti struct. Bedanya, hanya anggota yang paling akhir ditulis yang berisi nilai bermakna. Setelah v.f = 3.5f, membaca v.i tidak memberimu 3, melainkan integer apa pun yang kebetulan terbentuk dari pola bit 3.5f.

Ukuran: Anggota Terbesar yang Menang

Bandingkan tata letak memori struct dan union dengan anggota yang sama:

Di mesin pada umumnya, struct-nya berukuran 24 byte (4 untuk int, 8 untuk double, 1 untuk char, ditambah padding), sedangkan union-nya 8 byte, yaitu ukuran double-nya, dibulatkan ke atas demi alignment. Ketiga alamat yang dicetak identik, dan itulah seluruh cerita tentang union dalam satu baris output.

Perhatikan cast %p ke void *. printf mengharapkan persis itu untuk %p; mengirim tipe pointer lain adalah undefined behavior walaupun biasanya tampak berjalan. Lihat format specifier.

Menginisialisasi Union

Inisialisasi dengan kurung kurawal tanpa designator akan menginisialisasi anggota pertama:

Bentuk designated adalah yang sebaiknya dipakai. {42} diam-diam bergantung pada urutan anggota, jadi mengubah urutan deklarasi nanti akan mengubah anggota mana yang diinisialisasi. Ini bug yang sungguh menjengkelkan, karena tidak ada yang tampak berbeda pada kodenya.

Masalah Sebenarnya: Anggota Mana yang Aktif?

Union tidak mencatat anggota mana yang terakhir kamu tulis. Isinya hanya byte; pengetahuan itu ada di kepalamu, dan justru di situlah pengetahuan sering hilang.

Angka yang tercetak besar dan aneh: pola bit 1.0f yang dibaca sebagai int. Tidak ada yang crash, tidak ada peringatan, dan programnya diam-diam salah. Union melakukan persis apa yang dijanjikannya; kesalahannya ada pada kita yang lupa anggota mana yang sedang aktif.

Solusinya: Tagged Union

Solusi standarnya adalah memasangkan union dengan enum yang mencatat anggota yang aktif, lalu membungkus keduanya dalam sebuah struct. Kombinasi itu disebut tagged union (atau discriminated union), dan begitulah seharusnya kamu menulis hampir setiap union di kode aplikasi.

Setiap pembacaan kini melewati switch pada kind, jadi mustahil membaca anggota yang tidak pernah ditulis, selama setiap penulisan juga mengisi tag-nya. Membungkus penulisan dalam fungsi konstruktor kecil (value_from_int, value_from_string) adalah cara yang umum agar hal itu tidak mungkin terlupa.

Penghematan memorinya nyata: setiap Value di sini memakan 24 byte payload ditambah tag, bukan 4 + 4 + 24 untuk struct yang menampung ketiganya. Dengan seratus ribu nilai seperti itu, selisihnya terasa.

Compile dengan -Wall menambahkan jaring pengaman kedua: jika nanti kamu menambahkan VAL_BOOL ke enum dan lupa membuat case untuknya, GCC akan memberi peringatan soal nilai enum yang tidak ditangani.

Anonymous Union

C11 mengizinkan anggota union tanpa nama di dalam struct, sehingga anggota-anggotanya naik ke namespace struct luarnya:

Karena union-nya sendiri tidak bernama, kamu menulis s->circle.r, bukan s->as.circle.r. Lebih pendek untuk dibaca, dengan konsekuensi menyembunyikan fakta bahwa ada union di sana, dan itu tidak masalah selama tag-nya ada tepat di sampingnya.

Kapan Union Benar-benar Berguna

Empat pemakaian yang sering muncul:

  • Nilai varian. Interpreter, parser JSON dan konfigurasi, serta antrean pesan semuanya membawa nilai yang tipenya baru ditentukan saat program berjalan. Tagged union adalah representasi kanoniknya.
  • Record yang hemat memori. Jika sebuah struct punya beberapa field yang saling eksklusif dan jumlahnya jutaan, menumpuknya langsung menghemat memori.
  • Tata letak protokol dan hardware. Paket yang isinya bergantung pada sebuah byte header cocok dipetakan ke tagged union, begitu juga register perangkat.
  • Memeriksa byte. Menumpuk sebuah nilai dengan array unsigned char[] memungkinkanmu melihat byte-byte individunya, misalnya untuk menentukan endianness:

Ini disebut type punning: sengaja membaca kembali byte sebagai tipe lain. Membaca lewat union seperti ini secara eksplisit diizinkan di C dengan hasil yang implementation-defined (berbeda dengan cast pointer antartipe yang tidak berhubungan, yang melanggar aturan aliasing), dan memeriksa byte sebagai unsigned char selalu aman. Menafsirkan ulang int sebagai float adalah perkara lain: hasilnya sepenuhnya bergantung pada representasi di platform-mu, jadi jauhkan dari kode yang harus portabel.

Kesalahan Umum

  • Membaca anggota yang tidak kamu tulis. Bahaya utamanya. Pakai tag.
  • Menganggap union melakukan konversi. Tidak. u.i = 3; float f = u.f; menafsirkan ulang bit; int i = 3; float f = i; melakukan konversi. Lihat type casting.
  • Menaruh pointer di dalam union lalu kehilangan jejaknya. Jika satu cabang berisi char * yang kamu alokasikan, menimpa union dengan anggota lain akan membocorkannya, karena tidak ada lagi yang menunjuk ke buffer itu. Bebaskan dulu sebelum berganti cabang.
  • Berharap compiler akan mengecek. Tidak akan. Union adalah salah satu dari sedikit fitur C di mana bahasanya sama sekali tidak memberi bantuan selain soal ukuran dan alignment; tag-nya adalah satu-satunya pagar pengamanmu.

Pertanyaan yang sering diajukan

Apa itu union di C?

Union adalah tipe yang semua anggotanya berbagi memori yang sama. Menulis satu anggota akan menimpa anggota lainnya, jadi sebuah union hanya berisi tepat satu anggotanya pada satu waktu. Union dideklarasikan seperti struct, tetapi dengan kata kunci union: union Value { int i; float f; };.

Apa perbedaan union dan struct di C?

Struct memberi setiap anggota penyimpanannya sendiri dan menampung semuanya sekaligus, jadi ukurannya setidaknya sebesar jumlah ukuran anggotanya. Union menumpuk setiap anggota di alamat yang sama dan hanya menampung satu pada satu waktu, jadi ukurannya sebesar anggota terbesarnya.

Berapa ukuran union di C?

Cukup besar untuk anggota terbesarnya, dibulatkan ke atas demi alignment. Union berisi int (4 byte) dan double (8 byte) berukuran 8 byte, bukan 12. sizeof adalah cara untuk mengeceknya di platform-mu sendiri.

Apa yang terjadi jika kamu membaca anggota union yang tidak kamu tulis?

Kamu menafsirkan ulang byte yang sama sebagai tipe lain. Menulis u.i = 1 lalu membaca u.f tidak melakukan konversi: yang terbaca adalah pola bit integer itu sebagai float, sehingga hasilnya angka yang tidak bermakna. Di C standar, itu paling baik pun hanya unspecified, dan karena itulah pola tagged union ada: simpan sebuah tag di sampingnya yang menyatakan anggota mana yang aktif.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai