Jika saya mendefinisikan variabel dari jenis tertentu (yang, sejauh yang saya tahu, hanya mengalokasikan data untuk konten variabel), bagaimana cara melacak jenis variabel itu?
Jika saya mendefinisikan variabel dari jenis tertentu (yang, sejauh yang saya tahu, hanya mengalokasikan data untuk konten variabel), bagaimana cara melacak jenis variabel itu?
Jawaban:
Variabel (atau lebih umum: "objek" dalam arti C) tidak menyimpan tipenya pada saat runtime. Sejauh menyangkut kode mesin, hanya ada memori yang tidak diketik. Sebagai gantinya, operasi pada data ini menginterpretasikan data sebagai tipe tertentu (misalnya sebagai float atau sebagai pointer). Jenis-jenis ini hanya digunakan oleh kompiler.
Sebagai contoh, kita mungkin memiliki struct atau kelas struct Foo { int x; float y; };
dan variabel Foo f {}
. Bagaimana cara auto result = f.y;
kompilasi akses lapangan ? Compiler tahu itu f
adalah objek bertipe Foo
dan tahu tata letak Foo
-objects. Bergantung pada detail platform-spesifik, ini mungkin dikompilasi sebagai "Ambil pointer ke awal f
, tambahkan 4 byte, lalu muat 4 byte dan interpretasikan data ini sebagai float." Dalam banyak set instruksi kode mesin (termasuk x86-64 ) ada instruksi prosesor yang berbeda untuk memuat float atau int.
Salah satu contoh di mana sistem tipe C ++ tidak dapat melacak tipe untuk kita adalah seperti gabungan union Bar { int as_int; float as_float; }
. Serikat pekerja berisi hingga satu objek dari berbagai jenis. Jika kita menyimpan sebuah objek dalam sebuah union, ini adalah tipe aktif dari union. Kita hanya harus mencoba untuk mendapatkan tipe itu kembali dari serikat pekerja, hal lain apa pun adalah perilaku yang tidak terdefinisi. Entah kita "tahu" saat memprogram apa jenis aktifnya, atau kita dapat membuat gabungan yang ditandai di mana kita menyimpan tag jenis (biasanya enum) secara terpisah. Ini adalah teknik umum dalam C, tetapi karena kita harus menjaga penyatuan dan tag jenis dalam sinkronisasi, ini cukup rentan kesalahan. Sebuah void*
pointer mirip dengan serikat tapi hanya bisa memegang benda pointer, kecuali fungsi pointer.
C ++ menawarkan dua mekanisme yang lebih baik untuk menangani objek dari tipe yang tidak dikenal: Kita dapat menggunakan teknik berorientasi objek untuk melakukan penghapusan tipe (hanya berinteraksi dengan objek melalui metode virtual sehingga kita tidak perlu tahu tipe sebenarnya), atau kita bisa gunakan std::variant
, semacam serikat tipe-aman.
Ada satu kasus di mana C ++ tidak menyimpan jenis objek: jika kelas objek memiliki metode virtual ("tipe polimorfik", alias antarmuka.). Target panggilan metode virtual tidak diketahui pada waktu kompilasi dan diselesaikan pada saat dijalankan berdasarkan pada jenis objek yang dinamis (“pengiriman dinamis”). Kebanyakan kompiler mengimplementasikan ini dengan menyimpan tabel fungsi virtual ("vtable") di awal objek. Vtable juga dapat digunakan untuk mendapatkan jenis objek saat runtime. Kita kemudian dapat menggambar perbedaan antara tipe statis ekspresi waktu kompilasi yang diketahui, dan tipe dinamis suatu objek saat runtime.
C ++ memungkinkan kita untuk memeriksa tipe dinamis suatu objek dengan typeid()
operator yang memberi kita std::type_info
objek. Entah kompiler mengetahui jenis objek pada waktu kompilasi, atau kompiler telah menyimpan informasi jenis yang diperlukan di dalam objek dan dapat mengambilnya saat runtime.
void*
).
typeid(e)
mengintrospeksi jenis ekspresi statis e
. Jika tipe statis adalah tipe polimorfik, ekspresi akan dievaluasi dan tipe dinamis objek tersebut diambil. Anda tidak dapat menunjuk tipid pada memori tipe yang tidak dikenal dan mendapatkan informasi yang berguna. Misalnya typeid dari serikat menggambarkan serikat, bukan objek di serikat. Typeid dari void*
hanya pointer kosong. Dan tidak mungkin untuk melakukan dereferensi void*
untuk mendapatkan isinya. Di C ++ tidak ada tinju kecuali diprogram secara eksplisit seperti itu.
Jawaban lain menjelaskan dengan baik aspek teknis, tetapi saya ingin menambahkan beberapa umum "bagaimana memikirkan kode mesin".
Kode mesin setelah kompilasi cukup bodoh, dan itu benar-benar hanya mengasumsikan bahwa semuanya berfungsi sebagaimana mestinya. Katakanlah Anda memiliki fungsi sederhana seperti
bool isEven(int i) { return i % 2 == 0; }
Dibutuhkan int, dan mengeluarkan bool.
Setelah Anda mengompilasinya, Anda dapat menganggapnya sebagai sesuatu seperti juicer jeruk otomatis ini:
Dibutuhkan jeruk, dan mengembalikan jus. Apakah itu mengenali jenis objek yang masuk? Tidak, mereka hanya dianggap jeruk. Apa yang terjadi jika apel mendapat jeruk, bukan jeruk? Mungkin itu akan pecah. Tidak masalah, karena pemilik yang bertanggung jawab tidak akan mencoba menggunakannya dengan cara ini.
Fungsi di atas serupa: ia dirancang untuk mengambil int, dan itu dapat merusak atau melakukan sesuatu yang tidak relevan ketika diberi makan sesuatu yang lain. Itu (biasanya) tidak masalah, karena kompiler (umumnya) memeriksa bahwa itu tidak pernah terjadi - dan memang tidak pernah terjadi dalam kode yang terbentuk dengan baik. Jika kompiler mendeteksi kemungkinan bahwa suatu fungsi akan mendapatkan nilai yang diketik salah, ia menolak untuk mengkompilasi kode dan mengembalikan kesalahan ketik sebagai gantinya.
Peringatannya adalah bahwa ada beberapa kasus kode yang salah bentuk yang akan dilewati oleh kompiler. Contohnya adalah:
void*
untuk orange*
ketika ada sebuah apel di ujung lain dari pointer,Seperti yang dikatakan, kode yang dikompilasi sama seperti mesin juicer - tidak tahu apa yang diprosesnya, ia hanya menjalankan instruksi. Dan jika instruksinya salah, itu rusak. Itu sebabnya masalah di atas dalam C + + mengakibatkan crash yang tidak terkendali.
void*
memaksa foo*
, promosi aritmatika yang biasa, union
tipe hukuman, NULL
vs nullptr
, bahkan hanya memiliki pointer buruk adalah UB, dll. Tapi saya tidak berpikir daftar semua hal itu secara materi akan meningkatkan jawaban Anda, jadi mungkin lebih baik untuk meninggalkan apa adanya.
void*
tidak secara implisit dikonversi ke foo*
, dan union
ketik punning tidak didukung (memiliki UB).
Variabel memiliki sejumlah properti mendasar dalam bahasa seperti C:
Dalam kode sumber Anda , lokasi, (5), bersifat konseptual, dan lokasi ini disebut dengan namanya, (1). Jadi, deklarasi variabel digunakan untuk membuat lokasi dan ruang untuk nilai, (6), dan di baris sumber lainnya, kami merujuk ke lokasi itu dan nilai yang dimilikinya dengan memberi nama variabel dalam beberapa ekspresi.
Menyederhanakan hanya sedikit, setelah program Anda diterjemahkan ke dalam kode mesin oleh kompiler, lokasi, (5), adalah beberapa lokasi memori atau register CPU, dan ekspresi kode sumber apa pun yang merujuk variabel diterjemahkan ke dalam urutan kode mesin yang merujuk memori itu atau lokasi register CPU.
Jadi, ketika terjemahan selesai dan program berjalan pada prosesor, nama-nama variabel secara efektif dilupakan dalam kode mesin, dan, instruksi yang dihasilkan oleh kompiler hanya merujuk ke lokasi variabel yang ditugaskan (daripada ke mereka nama). Jika Anda men-debug dan meminta debugging, lokasi variabel yang terkait dengan nama, ditambahkan ke metadata untuk program, meskipun prosesor masih melihat instruksi kode mesin menggunakan lokasi (bukan metadata itu). (Ini adalah penyederhanaan berlebihan karena beberapa nama ada dalam metadata program untuk keperluan menghubungkan, memuat, dan mencari dinamis - masih prosesor hanya menjalankan instruksi kode mesin yang diperintahkan untuk program, dan dalam kode mesin ini nama-nama tersebut memiliki telah dikonversi ke lokasi.)
Hal yang sama juga berlaku untuk tipe, cakupan, dan masa pakai. Instruksi kode mesin yang dihasilkan kompiler mengetahui versi mesin lokasi, yang menyimpan nilai. Properti lainnya, seperti tipe, dikompilasi ke dalam kode sumber yang diterjemahkan sebagai instruksi spesifik yang mengakses lokasi variabel. Misalnya, jika variabel yang dimaksud adalah byte 8-bit yang ditandatangani vs. byte 8-bit yang tidak ditandatangani, maka ekspresi dalam kode sumber yang mereferensikan variabel tersebut akan diterjemahkan ke dalam, katakanlah, beban byte yang ditandatangani vs. beban byte yang tidak ditandatangani, sesuai kebutuhan untuk memenuhi aturan bahasa (C). Jenis variabel dengan demikian dikodekan ke dalam terjemahan kode sumber ke dalam instruksi mesin, yang memerintahkan CPU bagaimana menafsirkan memori atau lokasi register CPU masing-masing dan setiap kali menggunakan lokasi variabel.
Intinya adalah bahwa kita harus memberi tahu CPU apa yang harus dilakukan melalui instruksi (dan lebih banyak instruksi) dalam set instruksi kode mesin prosesor. Prosesor mengingat sangat sedikit tentang apa yang baru saja dilakukan atau diberi tahu - prosesor hanya menjalankan instruksi yang diberikan, dan itu adalah tugas programmer kompiler atau bahasa assembly untuk memberikan rangkaian urutan instruksi lengkap untuk memanipulasi variabel dengan benar.
Prosesor secara langsung mendukung beberapa tipe data mendasar, seperti byte / word / int / lama ditandatangani / tidak ditandatangani, float, dobel, dll. Prosesor umumnya tidak akan mengeluh atau keberatan jika Anda secara bergantian memperlakukan lokasi memori yang sama seperti ditandatangani atau tidak ditandatangani, untuk contoh, meskipun itu biasanya kesalahan logika dalam program. Ini adalah tugas pemrograman untuk menginstruksikan prosesor pada setiap interaksi dengan variabel.
Di luar tipe-tipe primitif fundamental, kita harus menyandikan hal-hal dalam struktur data dan menggunakan algoritma untuk memanipulasinya dalam hal primitif tersebut.
Dalam C ++, objek yang terlibat dalam hierarki kelas untuk polimorfisme memiliki pointer, biasanya di awal objek, yang merujuk pada struktur data kelas-spesifik, yang membantu pengiriman virtual, casting, dll.
Singkatnya, prosesor tidak mengetahui atau tidak mengingat tujuan penggunaan lokasi penyimpanan - prosesor menjalankan instruksi kode mesin dari program yang memberitahukan cara memanipulasi penyimpanan dalam register CPU dan memori utama. Pemrograman, kemudian, adalah tugas perangkat lunak (dan pemrogram) untuk menggunakan penyimpanan secara bermakna, dan untuk menyajikan serangkaian instruksi kode mesin yang konsisten kepada prosesor yang dengan setia menjalankan program secara keseluruhan.
useT1(&unionArray[i].member1); useT2(&unionArray[j].member2); useT1(&unionArray[i].member1);
, dentang dan gcc cenderung mengasumsikan bahwa pointer ke unionArray[j].member2
tidak dapat mengakses unionArray[i].member1
meskipun keduanya berasal dari yang sama unionArray[]
.
jika saya mendefinisikan variabel dari tipe tertentu bagaimana cara melacak tipe variabel itu.
Ada dua fase yang relevan di sini:
Kompiler C mengkompilasi kode C ke bahasa mesin. Kompiler memiliki semua informasi yang dapat diperoleh dari file sumber Anda (dan perpustakaan, dan hal-hal lain apa pun yang diperlukan untuk melakukan tugasnya). Kompiler C melacak apa artinya apa. Kompiler C tahu bahwa jika Anda mendeklarasikan variabel menjadi char
, itu adalah char.
Itu melakukan ini dengan menggunakan apa yang disebut "tabel simbol" yang berisi daftar nama-nama variabel, jenisnya, dan informasi lainnya. Ini adalah struktur data yang agak rumit, tetapi Anda bisa menganggapnya sebagai sekadar melacak apa arti nama yang dapat dibaca manusia. Dalam output biner dari kompiler, tidak ada nama variabel seperti ini yang muncul lagi (jika kita mengabaikan informasi debug opsional yang mungkin diminta oleh programmer).
Output dari compiler - executable yang dikompilasi - adalah bahasa mesin, yang dimuat ke dalam RAM oleh OS Anda, dan dieksekusi langsung oleh CPU Anda. Dalam bahasa mesin, tidak ada gagasan "ketik" sama sekali - itu hanya memiliki perintah yang beroperasi pada beberapa lokasi dalam RAM. The perintah memang memiliki jenis tetap mereka beroperasi dengan (yaitu, mungkin ada perintah bahasa mesin "menambahkan dua bilangan bulat 16-bit ini disimpan pada lokasi RAM 0x100 dan 0x521"), tetapi tidak ada informasi di mana saja dalam sistem yang byte di lokasi tersebut sebenarnya mewakili bilangan bulat. Tidak ada perlindungan dari kesalahan ketik sama sekali di sini.
char *ptr = 0x123
dalam C). Saya percaya penggunaan kata "penunjuk" harus cukup jelas dalam konteks ini. Jika tidak, silakan beri saya informasi lebih lanjut dan saya akan menambahkan kalimat pada jawabannya.
Ada beberapa kasus khusus yang penting di mana C ++ tidak menyimpan tipe saat runtime.
Solusi klasik adalah serikat terdiskriminasi: struktur data yang berisi salah satu dari beberapa jenis objek, ditambah bidang yang mengatakan jenis apa yang dikandungnya saat ini. Versi templated ada di pustaka standar C ++ sebagai std::variant
. Biasanya, tag akan menjadi enum
, tetapi jika Anda tidak memerlukan semua bit penyimpanan untuk data Anda, itu mungkin bitfield.
Kasus umum lainnya adalah pengetikan dinamis. Ketika Anda class
memiliki virtual
fungsi, program akan menyimpan pointer ke fungsi itu dalam tabel fungsi virtual , yang akan diinisialisasi untuk setiap instance class
ketika dibangun. Biasanya, itu berarti satu tabel fungsi virtual untuk semua instance kelas, dan setiap instance memegang pointer ke tabel yang sesuai. (Ini menghemat waktu dan memori karena tabel akan jauh lebih besar dari satu penunjuk tunggal.) Saat Anda memanggil virtual
fungsi itu melalui penunjuk atau referensi, program akan mencari penunjuk fungsi di tabel virtual. (Jika ia tahu tipe persisnya pada waktu kompilasi, ia dapat melewati langkah ini.) Ini memungkinkan kode untuk memanggil implementasi tipe turunan alih-alih kelas dasar.
Hal yang membuat ini relevan di sini adalah: masing ofstream
- masing berisi pointer ke ofstream
tabel virtual, masing ifstream
- masing ke ifstream
tabel virtual, dan sebagainya. Untuk hierarki kelas, penunjuk tabel virtual dapat berfungsi sebagai tag yang memberi tahu program apa yang dimiliki objek kelas!
Meskipun standar bahasa tidak memberi tahu orang-orang yang merancang kompiler bagaimana mereka harus mengimplementasikan runtime di bawah tenda, ini adalah bagaimana Anda dapat mengharapkan dynamic_cast
dan typeof
bekerja.