Ringkasan :
Haruskah fungsi dalam C selalu memeriksa untuk memastikan itu bukan dereferencing NULL
pointer? Jika tidak, kapan tepat untuk melewatkan cek ini?
Detail :
Saya telah membaca beberapa buku tentang wawancara pemrograman dan saya bertanya-tanya apa tingkat validasi input yang sesuai untuk argumen fungsi di C? Jelas setiap fungsi yang mengambil input dari pengguna perlu melakukan validasi, termasuk memeriksa NULL
pointer sebelum mendereferensi itu. Tapi bagaimana dengan fungsi dalam file yang sama yang Anda tidak harapkan untuk mengekspos melalui API Anda?
Sebagai contoh, yang berikut ini muncul dalam kode sumber git:
static unsigned short graph_get_current_column_color(const struct git_graph *graph)
{
if (!want_color(graph->revs->diffopt.use_color))
return column_colors_max;
return graph->default_column_color;
}
Jika *graph
adalah NULL
maka pointer null akan dereferenced, mungkin menerjang program, tetapi mungkin menyebabkan beberapa perilaku tak terduga lainnya. Di sisi lain fungsinya static
dan jadi mungkin programmer sudah memvalidasi input. Saya tidak tahu, saya hanya memilihnya secara acak karena itu adalah contoh singkat dalam program aplikasi yang ditulis dalam C. Saya telah melihat banyak tempat lain di mana pointer digunakan tanpa memeriksa NULL. Pertanyaan saya secara umum tidak spesifik untuk segmen kode ini.
Saya melihat pertanyaan serupa yang diajukan dalam konteks pemberian pengecualian . Namun, untuk bahasa yang tidak aman seperti C atau C ++ tidak ada propagasi kesalahan otomatis dari pengecualian yang tidak ditangani.
Di sisi lain saya telah melihat banyak kode dalam proyek open source (seperti contoh di atas) yang tidak melakukan pemeriksaan pointer sebelum menggunakannya. Saya bertanya-tanya apakah ada yang memiliki pemikiran tentang pedoman kapan harus melakukan cek dalam suatu fungsi vs dengan asumsi bahwa fungsi itu dipanggil dengan argumen yang benar.
Saya tertarik pada pertanyaan ini secara umum untuk menulis kode produksi. Tetapi saya juga tertarik dengan konteks wawancara pemrograman. Misalnya banyak buku teks algoritma (seperti CLR) cenderung menyajikan algoritma dalam pseudocode tanpa pengecekan kesalahan. Namun, meskipun ini bagus untuk memahami inti dari suatu algoritma, itu jelas bukan praktik pemrograman yang baik. Jadi saya tidak ingin memberi tahu pewawancara bahwa saya melewatkan pengecekan kesalahan untuk menyederhanakan contoh kode saya (seperti buku teks). Tetapi saya juga tidak ingin muncul untuk menghasilkan kode yang tidak efisien dengan pengecekan kesalahan yang berlebihan. Misalnya graph_get_current_column_color
bisa dimodifikasi untuk memeriksa *graph
nol tetapi tidak jelas apa yang akan dilakukan jika *graph
itu nol, selain itu tidak boleh dereferensi.
sumber
Jawaban:
Pointer nol tidak valid dapat disebabkan oleh kesalahan pemrogram atau karena kesalahan runtime. Kesalahan runtime adalah sesuatu yang tidak bisa diperbaiki oleh programmer, seperti
malloc
gagal karena memori rendah atau jaringan menjatuhkan paket atau pengguna memasukkan sesuatu yang bodoh. Kesalahan pemrogram disebabkan oleh pemrogram yang menggunakan fungsinya secara tidak benar.Aturan umum yang saya lihat adalah bahwa kesalahan runtime harus selalu diperiksa, tetapi kesalahan programmer tidak harus diperiksa setiap waktu. Katakanlah beberapa programmer idiot langsung menelepon
graph_get_current_column_color(0)
. Ini akan segfault saat pertama kali dipanggil, tetapi begitu Anda memperbaikinya, perbaikan tersebut dikompilasi secara permanen. Tidak perlu memeriksa setiap kali dijalankan.Kadang-kadang, terutama di perpustakaan pihak ketiga, Anda akan melihat
assert
untuk memeriksa kesalahan pemrogram alih-alihif
pernyataan. Itu memungkinkan Anda untuk mengkompilasi dalam cek selama pengembangan, dan meninggalkannya dalam kode produksi. Saya juga sesekali melihat cek serampangan di mana sumber kesalahan programmer potensial jauh dari gejala.Jelas, Anda selalu dapat menemukan seseorang yang lebih bertele-tele, tetapi sebagian besar programmer C saya tahu lebih suka kode kurang berantakan daripada kode yang sedikit lebih aman. Dan "lebih aman" adalah istilah subyektif. Segfault terang-terangan selama pengembangan lebih disukai daripada kesalahan korupsi halus di lapangan.
sumber
Kernighan & Plauger, dalam "Perangkat Perangkat Lunak", menulis bahwa mereka akan memeriksa semuanya, dan, untuk kondisi yang mereka yakini tidak akan pernah terjadi, mereka akan membatalkan dengan pesan kesalahan "Tidak bisa terjadi".
Mereka melaporkan dengan cepat direndahkan dengan berapa kali mereka melihat "Tidak bisa terjadi" keluar di terminal mereka.
Anda harus SELALU memeriksa penunjuk untuk NULL sebelum Anda (berupaya) men-dereference-nya. SELALU . Jumlah kode yang Anda duplikat memeriksa NULL yang tidak terjadi, dan siklus prosesor yang Anda "buang", akan lebih dari yang dibayar dengan jumlah kerusakan yang Anda tidak perlu men-debug dari yang tidak lebih dari dump kecelakaan - jika Anda seberuntung itu.
Jika pointer tidak berubah di dalam sebuah loop, itu cukup untuk memeriksanya di luar loop, tetapi Anda kemudian harus "menyalinnya" ke dalam variabel lokal terbatas-lingkup, untuk digunakan oleh loop, yang menambahkan dekorasi const yang sesuai. Dalam hal ini, Anda HARUS memastikan bahwa setiap fungsi yang dipanggil dari loop body menyertakan dekorasi const yang diperlukan pada prototipe, ALL THE WAY DOWN. Jika Anda tidak, atau tidak bisa (karena misalnya vendor paket atau rekan kerja keras kepala), maka Anda harus memeriksa untuk NULL SETIAP WAKTU TI DAPAT DIUBAH , karena yakin sebagai COL Murphy adalah seorang optimis dapat disembuhkan, seseorang IS akan untuk zap ketika Anda tidak melihat.
Jika Anda berada di dalam suatu fungsi, dan penunjuk seharusnya bukan NULL masuk, Anda harus memverifikasinya.
Jika Anda menerimanya dari suatu fungsi, dan seharusnya tidak NULL keluar, Anda harus memverifikasinya. malloc () terkenal karena hal ini. (Nortel Networks, sekarang mati, memiliki standar pengkodean tertulis yang keras dan cepat tentang ini. Saya harus men-debug crash pada satu titik, yang saya telusuri kembali ke malloc () mengembalikan pointer NULL dan idiot coder tidak repot-repot memeriksa sebelum dia menulis untuk itu, karena dia hanya TAHU dia punya banyak memori ... saya mengatakan beberapa hal yang sangat jahat ketika saya akhirnya menemukannya.)
sumber
assert
, pasti. Saya tidak suka ide kode kesalahan jika Anda berbicara tentang mengubah kode yang ada untuk memasukkanNULL
cek.Anda dapat melewati pemeriksaan ketika Anda dapat meyakinkan diri sendiri bahwa pointer tidak mungkin nol.
Biasanya, pengecekan null pointer diimplementasikan dalam kode di mana null diharapkan muncul sebagai indikator bahwa suatu objek saat ini tidak tersedia. Null digunakan sebagai nilai sentinel, misalnya untuk mengakhiri daftar tertaut, atau bahkan array pointer. The
argv
vektor string disahkan menjadimain
diperlukan untuk menjadi null-diakhiri oleh pointer, sama dengan bagaimana string diakhiri oleh karakter null:argv[argc]
adalah pointer null, dan Anda dapat mengandalkan ini ketika parsing baris perintah.Jadi, situasi untuk memeriksa null adalah situasi di mana a merupakan nilai yang diharapkan. Pemeriksaan nol mengimplementasikan arti dari penunjuk nol, seperti menghentikan pencarian daftar tertaut. Mereka mencegah kode dari penereferensi pointer.
Dalam situasi di mana nilai pointer nol tidak diharapkan oleh desain, tidak ada gunanya memeriksanya. Jika nilai penunjuk yang tidak valid muncul, kemungkinan besar akan muncul non-nol, yang tidak dapat dibedakan dari nilai yang valid dengan cara portabel apa pun. Sebagai contoh, nilai pointer yang diperoleh dari pembacaan penyimpanan yang tidak diinisialisasi diartikan sebagai tipe pointer, sebuah pointer yang diperoleh melalui beberapa konversi teduh, atau sebuah pointer yang bertambah dari batas.
Tentang tipe data seperti
graph *
: ini dapat dirancang sehingga nilai nol adalah grafik yang valid: sesuatu tanpa tepi dan tanpa node. Dalam hal ini, semua fungsi yang mengambilgraph *
pointer harus berurusan dengan nilai itu, karena itu adalah nilai domain yang benar dalam representasi grafik. Di sisi lain, agraph *
bisa menjadi penunjuk ke objek mirip wadah yang tidak pernah nol jika kita memegang grafik; sebuah pointer nol kemudian dapat memberi tahu kita bahwa "objek grafik tidak ada; kami belum mengalokasikannya, atau kami membebaskannya; atau ini saat ini tidak memiliki grafik yang terkait". Penggunaan pointer yang terakhir ini adalah gabungan boolean / satelit: pointer menjadi non-null menunjukkan "Saya punya objek saudara ini", dan ia menyediakan objek itu.Kita mungkin menetapkan pointer ke null bahkan jika kita tidak membebaskan objek, hanya untuk memisahkan satu objek dari yang lain:
sumber
Biarkan saya menambahkan satu suara lagi ke fugue.
Seperti banyak jawaban lain, saya katakan - jangan repot-repot memeriksa pada saat ini; itu adalah tanggung jawab penelepon. Tetapi saya memiliki dasar untuk membangun dari pada kemanfaatan yang sederhana (dan kesombongan pemrograman C).
Saya mencoba mengikuti prinsip Donald Knuth membuat program serapuh mungkin. Jika terjadi kesalahan, buat crash besar , dan merujuk null pointer biasanya merupakan cara yang baik untuk melakukannya. Gagasan umum adalah crash atau infinite loop jauh lebih baik daripada membuat data yang salah. Dan itu mendapat perhatian programmer!
Tetapi referensi null pointer (terutama untuk struktur data besar) tidak selalu menyebabkan kerusakan. Mendesah. Itu benar. Dan di situlah Asserts masuk. Itu sederhana, dapat langsung menghentikan program Anda (yang menjawab pertanyaan, "Apa yang harus dilakukan metode jika menemui nol?"), Dan dapat dinyalakan / dimatikan untuk berbagai situasi (saya sarankan JANGAN mematikannya, karena lebih baik bagi pelanggan untuk mengalami kerusakan dan melihat pesan rahasia daripada memiliki data yang buruk).
Itu dua sen ku.
sumber
Saya biasanya hanya memeriksa ketika pointer ditugaskan, yang umumnya satu-satunya waktu saya benar-benar dapat melakukan sesuatu tentang hal itu dan mungkin pulih jika tidak valid.
Jika saya mendapatkan pegangan ke jendela misalnya, saya akan memeriksanya null benar dan kemudian sana-sini, dan melakukan sesuatu tentang kondisi nol, tapi saya tidak akan memeriksanya null setiap kali Saya menggunakan pointer, di masing-masing dan setiap fungsi pointer dilewatkan, jika tidak saya akan memiliki banyak kode penanganan kesalahan duplikat.
Fungsi seperti
graph_get_current_column_color
mungkin sama sekali tidak dapat melakukan sesuatu yang berguna untuk situasi Anda jika tidak menemukan pointer yang buruk, jadi saya akan pergi memeriksa NULL untuk peneleponnya.sumber
Saya akan mengatakan bahwa itu tergantung pada yang berikut:
Utilisasi CPU / Odds Pointer NULL Setiap kali Anda memeriksa NULL, butuh waktu. Untuk alasan ini saya mencoba membatasi cek saya di tempat penunjuk bisa memiliki nilainya berubah.
Sistem Preemptive Jika kode Anda sedang berjalan dan tugas lain dapat menghentikannya dan berpotensi mengubah nilainya, pemeriksaan yang baik untuk dilakukan.
Modul yang Digabungkan Tightly Jika sistem ini digabungkan dengan ketat maka masuk akal jika Anda memiliki lebih banyak pemeriksaan. Yang saya maksud dengan ini adalah jika ada struktur data yang dibagi antara beberapa modul, satu modul dapat mengubah sesuatu dari bawah modul lain. Dalam situasi ini masuk akal untuk memeriksa lebih sering.
Pemeriksaan Otomatis / Bantuan Perangkat Keras Hal terakhir yang harus dipertimbangkan adalah jika perangkat keras yang Anda jalankan memiliki semacam mekanisme yang dapat memeriksa NULL. Secara khusus saya mengacu pada deteksi Kesalahan Halaman. Jika sistem Anda memiliki deteksi kesalahan halaman, CPU itu sendiri dapat memeriksa akses NULL. Secara pribadi saya menemukan ini sebagai mekanisme terbaik karena selalu berjalan dan tidak bergantung pada programmer untuk melakukan pemeriksaan eksplisit. Ini juga memiliki manfaat praktis nol overhead. Jika ini tersedia, saya sarankan, debugging sedikit lebih sulit tetapi tidak terlalu.
Untuk menguji apakah tersedia buat program dengan pointer. Atur pointer ke 0 dan kemudian coba baca / tulis.
sumber
Menurut pendapat saya memvalidasi input (pra / pasca-kondisi, yaitu) adalah hal yang baik untuk mendeteksi kesalahan pemrograman, tetapi hanya jika itu menghasilkan kesalahan yang keras dan menjengkelkan, menunjukkan-berhenti dari jenis yang tidak dapat diabaikan.
assert
biasanya memiliki efek itu.Apa pun yang gagal ini dapat berubah menjadi mimpi buruk tanpa tim yang terkoordinasi dengan sangat hati-hati. Dan tentu saja idealnya semua tim dikoordinasikan dengan sangat hati-hati dan disatukan di bawah standar yang ketat, tetapi sebagian besar lingkungan tempat saya bekerja sangat jauh dari itu.
Sama seperti contoh, saya bekerja dengan beberapa rekan yang percaya bahwa seseorang harus secara religius memeriksa keberadaan null pointer, jadi mereka menaburkan banyak kode seperti ini:
... dan terkadang seperti itu bahkan tanpa mengembalikan / mengatur kode kesalahan. Dan ini berada dalam basis kode yang berumur beberapa dekade dengan banyak plugin pihak ketiga yang didapat. Itu juga merupakan basis kode yang dipenuhi dengan banyak bug, dan seringkali bug yang sangat sulit untuk ditelusuri ke akar penyebabnya karena mereka cenderung menabrak situs yang jauh dari sumber masalah.
Dan latihan ini adalah salah satu alasannya. Ini merupakan pelanggaran prasyarat mapan dari
move_vertex
fungsi di atas untuk melewatkan simpul nol padanya, namun fungsi seperti itu hanya diam-diam menerimanya dan tidak melakukan apa pun sebagai respons. Jadi apa yang cenderung terjadi adalah bahwa plugin mungkin memiliki kesalahan pemrogram yang menyebabkannya lulus nol ke fungsi tersebut, hanya untuk tidak mendeteksinya, hanya untuk melakukan banyak hal setelahnya, dan akhirnya sistem akan mulai terkelupas atau rusak.Tetapi masalah sebenarnya di sini adalah ketidakmampuan untuk dengan mudah mendeteksi masalah ini. Jadi saya pernah mencoba melihat apa yang akan terjadi jika saya mengubah kode analog di atas menjadi
assert
, seperti:... dan ngeri saya, saya menemukan bahwa pernyataan gagal kiri dan kanan bahkan ketika memulai aplikasi. Setelah saya memperbaiki beberapa situs panggilan pertama, saya melakukan beberapa hal lagi dan kemudian menerima lebih banyak kegagalan pernyataan. Saya terus berjalan sampai saya telah memodifikasi begitu banyak kode sehingga saya akhirnya mengembalikan perubahan saya karena mereka telah menjadi terlalu mengganggu dan dengan hati-hati menyimpan null pointer check, alih-alih mendokumentasikan bahwa fungsi memungkinkan menerima simpul nol.
Tapi itu bahayanya, meskipun skenario terburuk, gagal membuat pelanggaran pra / pasca-kondisi mudah terdeteksi. Anda kemudian dapat, selama bertahun-tahun, secara diam-diam mengakumulasi kode muatan kapal yang melanggar kondisi pra / pasca seperti saat terbang di bawah radar pengujian. Menurut pendapat saya null pointer seperti itu memeriksa di luar kegagalan pernyataan terang-terangan dan menjengkelkan benar-benar dapat melakukan jauh, jauh lebih berbahaya daripada baik.
Mengenai pertanyaan penting tentang kapan Anda harus memeriksa null pointer, saya percaya dalam menyatakan secara bebas jika itu dirancang untuk mendeteksi kesalahan programmer, dan tidak membiarkannya menjadi sunyi dan sulit dideteksi. Jika itu bukan kesalahan pemrograman dan sesuatu di luar kendali programmer, seperti kehabisan memori, maka masuk akal untuk memeriksa null dan menggunakan penanganan kesalahan. Selain itu, ini adalah pertanyaan desain dan berdasarkan pada apa fungsi Anda anggap sebagai kondisi pra / pasca yang valid.
sumber
Salah satu latihan adalah selalu melakukan pemeriksaan nol kecuali Anda telah memeriksanya; jadi jika input diteruskan dari fungsi A () ke B (), dan A () telah memvalidasi pointer dan Anda yakin B () tidak dipanggil di tempat lain, maka B () dapat mempercayai A () untuk memiliki membersihkan data.
sumber
NULL
pemeriksaan tambahan akan berbuat banyak. Pikirkan tentang ini: sekarangB()
periksaNULL
dan ... lakukan apa? Kembali-1
? Jika penelepon tidak memeriksaNULL
, kepercayaan apa yang dapat Anda miliki bahwa mereka akan berurusan dengan-1
kas nilai pengembalian?