Saat ini saya sedang membaca buku berjudul "Numerical Recipes in C". Dalam buku ini, penulis merinci bagaimana algoritme tertentu secara inheren bekerja lebih baik jika kami memiliki indeks yang dimulai dengan 1 (saya tidak sepenuhnya mengikuti argumennya dan itu bukan inti dari posting ini), tetapi C selalu mengindeks arraynya mulai dengan 0 Untuk menyiasatinya, ia menyarankan untuk menurunkan pointer setelah alokasi, misalnya:
float *a = malloc(size);
a--;
Ini, katanya, secara efektif akan memberi Anda pointer yang memiliki indeks dimulai dengan 1, yang kemudian akan dibebaskan dengan:
free(a + 1);
Sejauh yang saya ketahui, ini adalah perilaku yang tidak terdefinisi oleh standar C. Ini tampaknya buku yang sangat terkenal dalam komunitas HPC, jadi saya tidak ingin mengabaikan apa yang dia katakan, tetapi hanya mengurangi pointer di luar rentang yang dialokasikan tampaknya sangat samar bagi saya. Apakah perilaku "diizinkan" ini dalam C? Saya telah mengujinya menggunakan gcc dan icc, dan kedua hasil tersebut menunjukkan bahwa saya tidak mengkhawatirkan apa-apa, tetapi saya ingin benar-benar positif.
Jawaban:
Anda benar bahwa kode seperti
menghasilkan perilaku yang tidak terdefinisi, sesuai standar ANSI C, bagian 3.3.6:
Untuk kode seperti ini, kualitas kode C dalam buku (ketika saya menggunakannya pada akhir 1990-an) tidak dianggap sangat tinggi.
Masalah dengan perilaku yang tidak terdefinisi adalah bahwa tidak peduli apa hasil yang dihasilkan oleh kompiler, hasil itu secara definisi benar (bahkan jika itu sangat merusak dan tidak dapat diprediksi).
Untungnya, sangat sedikit penyusun yang berupaya untuk benar-benar menyebabkan perilaku yang tidak terduga untuk kasus-kasus seperti itu dan
malloc
implementasi tipikal pada mesin yang digunakan untuk HPC memiliki beberapa data pembukuan tepat sebelum alamat dikembalikan, sehingga penurunan biasanya akan memberi Anda petunjuk ke data pembukuan tersebut. Bukan ide yang baik untuk menulis di sana, tetapi hanya membuat pointer tidak berbahaya pada sistem tersebut.Perlu diketahui bahwa kode dapat rusak ketika lingkungan runtime diubah atau ketika kode porting ke lingkungan yang berbeda.
sumber
Secara resmi, itu adalah perilaku yang tidak ditentukan untuk memiliki titik penunjuk di luar array (kecuali satu melewati akhir), bahkan jika itu tidak pernah direferensikan .
Dalam praktiknya, jika prosesor Anda memiliki model memori datar (tidak seperti yang aneh seperti x86-16 ), dan jika kompiler tidak memberi Anda kesalahan runtime atau optimasi yang salah jika Anda membuat penunjuk yang tidak valid, maka kode akan berfungsi baik baik saja.
sumber
Pertama, itu perilaku yang tidak terdefinisi. Beberapa kompiler yang mengoptimalkan saat ini menjadi sangat agresif tentang perilaku yang tidak terdefinisi. Sebagai contoh, karena a-- dalam hal ini adalah perilaku yang tidak terdefinisi, kompiler dapat memutuskan untuk menyimpan instruksi dan siklus prosesor dan tidak mengurangi a. Yang secara resmi benar dan legal.
Mengabaikan itu, Anda mungkin mengurangi 1, atau 2, atau 1980. Misalnya jika saya memiliki data keuangan untuk tahun 1980 hingga 2013, saya mungkin mengurangi 1980. Sekarang jika kita menggunakan float * a = malloc (size); pasti ada beberapa konstanta besar k sehingga a - k adalah pointer nol. Dalam hal ini, kami benar-benar berharap ada kesalahan.
Sekarang ambil struct besar, katakan ukuran megabyte. Alokasikan p pointer yang menunjuk ke dua struct. p - 1 mungkin menjadi pointer nol. p - 1 mungkin membungkus (jika struct adalah megabyte, dan blok malloc adalah 900 KB dari awal ruang alamat). Jadi bisa saja tanpa niat jahat dari kompiler yang p - 1> p. Segalanya mungkin menjadi menarik.
sumber
Diizinkan? Iya. Ide bagus? Tidak biasanya.
C adalah singkatan untuk bahasa assembly, dan dalam bahasa assembly tidak ada pointer, hanya alamat memori. Pointer C adalah alamat memori yang memiliki perilaku tambahan yang bertambah atau berkurang berdasarkan ukuran yang mereka tunjukkan ketika dikenakan aritmatika. Ini membuat berikut ini baik-baik saja dari perspektif sintaksis:
Array sebenarnya bukan benda dalam C; mereka hanya petunjuk ke rentang memori yang berdekatan yang berperilaku seperti array. The
[]
operator adalah singkatan untuk melakukan aritmetik pointer dan dereferencing, sehinggaa[x]
benar-benar berarti*(a + x)
.Ada alasan yang sah untuk melakukan hal di atas, seperti beberapa perangkat I / O yang memiliki beberapa pasangan yang
double
dipetakan ke0xdeadbee7
dan0xdeadbeef
. Sangat sedikit program yang perlu melakukan itu.Saat Anda membuat alamat sesuatu, seperti dengan menggunakan
&
operator atau panggilanmalloc()
, Anda ingin menjaga pointer asli tetap utuh sehingga Anda tahu bahwa apa yang ditunjukkannya sebenarnya sesuatu yang valid. Mengurangkan penunjuk berarti bahwa sedikit kode yang salah dapat mencoba mereduksikannya, mendapatkan hasil yang salah, mengalahkan sesuatu atau, tergantung pada lingkungan Anda, melakukan pelanggaran segmentasi. Hal ini terutama berlaku untukmalloc()
, karena Anda telah membebani panggilan siapa punfree()
untuk mengingat untuk melewati nilai asli dan bukan versi yang diubah yang akan menyebabkan semua kehilangan.Jika Anda membutuhkan array berbasis 1 di C, Anda dapat melakukannya dengan aman dengan mengorbankan alokasi satu elemen tambahan yang tidak akan pernah digunakan:
Perhatikan bahwa ini tidak melakukan apa pun untuk melindungi dari melampaui batas atas, tetapi itu cukup mudah untuk ditangani.
Tambahan:
Beberapa bab dan ayat dari draft C99 (maaf, hanya itu yang bisa saya tautkan):
§6.5.2.1.1 mengatakan bahwa ekspresi kedua ("lainnya") yang digunakan dengan operator subskrip adalah tipe integer.
-1
adalah bilangan bulat, dan itu membuatp[-1]
valid dan karenanya juga membuat pointer&(p[-1])
valid. Ini tidak menyiratkan bahwa mengakses memori di lokasi itu akan menghasilkan perilaku yang ditentukan, tetapi pointer masih merupakan pointer yang valid.§6.5.2.2 mengatakan bahwa operator subkrip array mengevaluasi setara dengan menambahkan nomor elemen ke pointer, oleh karena
p[-1]
itu setara dengan*(p + (-1))
. Masih valid, tetapi mungkin tidak menghasilkan perilaku yang diinginkan.§6.5.6.8 mengatakan (penekanan milikku):
Ini berarti bahwa hasil dari aritmatika pointer harus menunjuk pada elemen dalam array. Tidak dikatakan bahwa aritmatika harus dilakukan sekaligus. Karena itu:
Apakah saya merekomendasikan melakukan hal-hal seperti ini? Saya tidak, dan jawaban saya menjelaskan alasannya.
sumber