Kapan pointer harus diperiksa untuk NULL dalam C?

18

Ringkasan :

Haruskah fungsi dalam C selalu memeriksa untuk memastikan itu bukan dereferencing NULLpointer? 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 NULLpointer 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 *graphadalah NULLmaka pointer null akan dereferenced, mungkin menerjang program, tetapi mungkin menyebabkan beberapa perilaku tak terduga lainnya. Di sisi lain fungsinya staticdan 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_colorbisa dimodifikasi untuk memeriksa *graphnol tetapi tidak jelas apa yang akan dilakukan jika *graphitu nol, selain itu tidak boleh dereferensi.

Gabriel Southern
sumber
7
Jika Anda menulis fungsi untuk API di mana penelepon tidak seharusnya memahami jeroan, ini adalah salah satu tempat di mana dokumentasi penting. Jika Anda mendokumentasikan bahwa suatu argumen harus berupa penunjuk yang valid dan non-NULL, mengeceknya menjadi tanggung jawab penelepon.
Blrfl
1
Lihat juga: stackoverflow.com/questions/4390007/...
Billy ONeal
Dengan melihat ke belakang pada tahun 2017, dengan mengingat pertanyaan dan sebagian besar jawaban ditulis pada tahun 2013, apakah ada jawaban yang membahas masalah perilaku yang tidak terdefinisi dalam perjalanan waktu karena mengoptimalkan kompiler?
rwong
Dalam kasus panggilan API mengharapkan argumen pointer yang valid, saya ingin tahu apa nilai pengujian hanya NULL? Setiap pointer yang tidak valid yang akan direferensikan akan sama buruknya dengan NULL dan segfault semuanya sama.
PaulHK

Jawaban:

15

Pointer nol tidak valid dapat disebabkan oleh kesalahan pemrogram atau karena kesalahan runtime. Kesalahan runtime adalah sesuatu yang tidak bisa diperbaiki oleh programmer, seperti mallocgagal 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 assertuntuk memeriksa kesalahan pemrogram alih-alih ifpernyataan. 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.

Karl Bielefeldt
sumber
Pertanyaannya agak subyektif tetapi ini sepertinya jawaban terbaik untuk saat ini. Terima kasih kepada semua orang yang telah memikirkan pertanyaan ini.
Gabriel Southern
1
Di iOS, malloc tidak akan pernah mengembalikan NULL. Jika tidak menemukan memori, maka ia akan meminta aplikasi Anda untuk melepaskan memori terlebih dahulu, lalu meminta sistem operasi (yang akan meminta aplikasi lain untuk melepaskan memori dan mungkin membunuh mereka), dan jika masih ada memori itu akan membunuh aplikasi Anda . Tidak perlu cek.
gnasher729
11

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.)

John R. Strohm
sumber
8
Jika Anda berada dalam fungsi yang membutuhkan pointer non-NULL, tetapi Anda tetap memeriksa dan itu NULL ... apa selanjutnya?
detly
1
@tetly baik menghentikan apa yang Anda lakukan dan mengembalikan kode kesalahan, atau membuat pernyataan
James
1
@ James - tidak memikirkan assert, pasti. Saya tidak suka ide kode kesalahan jika Anda berbicara tentang mengubah kode yang ada untuk memasukkan NULLcek.
detly
10
@Detly Anda tidak akan mendapatkan sejauh C dev jika Anda tidak suka kode kesalahan
James
5
@ JohnR.Strohm - ini C, ini pernyataan atau tidak sama sekali: P
detly
5

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 argvvektor string disahkan menjadi maindiperlukan 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.

while (*argv) {
   /* process argument string *argv */
   argv++; /* increment to next one */
}

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 mengambil graph *pointer harus berurusan dengan nilai itu, karena itu adalah nilai domain yang benar dalam representasi grafik. Di sisi lain, a graph *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:

tty_driver->tty = NULL; /* detach low level driver from the tty device */
Kaz
sumber
Argumen yang paling meyakinkan yang saya tahu bahwa pointer tidak boleh nol pada titik tertentu adalah untuk membungkus titik itu di "if (ptr! = NULL) {" dan yang sesuai "}". Di luar itu, Anda berada di wilayah verifikasi formal.
John R. Strohm
4

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.

Scott Biggs
sumber
1

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_colormungkin 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.

Apa namanya
sumber
1

Saya akan mengatakan bahwa itu tergantung pada yang berikut:

  1. Apakah pemanfaatan CPU penting? Setiap cek untuk NULL membutuhkan waktu.
  2. Berapa peluang bahwa penunjuknya adalah NULL? Apakah itu hanya digunakan pada fungsi sebelumnya. Mungkinkah nilai pointer telah diubah.
  3. Apakah sistem preemptive? Berarti bisakah suatu tugas berubah dan mengubah nilainya? Bisakah ISR masuk dan mengubah nilainya?
  4. Seberapa erat kodenya?
  5. Apakah ada semacam mekanisme otomatis yang akan memeriksa pointer NULL secara otomatis?

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.

barrem23
sumber
Saya tidak tahu apakah saya akan mengklasifikasikan segfault sebagai melakukan pemeriksaan NULL otomatis. Saya setuju bahwa memiliki perlindungan memori CPU memang membantu sehingga satu proses tidak dapat melakukan banyak kerusakan pada seluruh sistem, tetapi saya tidak akan menyebutnya perlindungan otomatis.
Gabriel Southern
1

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. assertbiasanya 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:

void vertex_move(Vertex* v)
{
     if (!v)
          return;
     ...
}

... 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_vertexfungsi 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:

void vertex_move(Vertex* v)
{
     assert(v && "Vertex should never be null!");
     ...
}

... 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
0

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.

Jay
sumber
1
... sampai dalam 6 bulan waktu seseorang datang dan menambahkan beberapa kode lagi yang memanggil B () (mungkin dengan asumsi bahwa siapa pun yang menulis B () pasti memeriksa NULL dengan benar). Maka Anda kacau, bukan? Aturan dasar - jika ada kondisi yang tidak valid untuk input ke suatu fungsi, maka periksa untuk itu, karena input di luar kendali fungsi.
Maximus Minimus
@ mh01 Jika Anda hanya menghancurkan kode acak (mis. membuat asumsi dan tidak membaca dokumentasi), maka saya tidak berpikir NULLpemeriksaan tambahan akan berbuat banyak. Pikirkan tentang ini: sekarang B()periksa NULLdan ... lakukan apa? Kembali -1? Jika penelepon tidak memeriksa NULL, kepercayaan apa yang dapat Anda miliki bahwa mereka akan berurusan dengan -1kas nilai pengembalian?
detly
1
Itulah tanggung jawab penelepon. Anda berurusan dengan tanggung jawab Anda sendiri, yang mencakup tidak mempercayai input sewenang-wenang / tidak diketahui / berpotensi tidak diverifikasi yang Anda berikan. Kalau tidak, Anda berada di kota cop-out. Jika penelepon tidak memeriksa maka penelepon telah mengacaukan; Anda memeriksa, pantat Anda sendiri tertutup, Anda dapat memberitahu siapa pun yang menulis kepada penelepon bahwa setidaknya Anda melakukan hal yang benar.
Maximus Minimus