Pengindeksan pointer

11

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.

wolfPack88
sumber
3
standar C apa yang Anda referensikan? Aku bertanya karena per ingatan saya, "Numerical Recipes dalam C" telah diterbitkan dalam tahun 1990-an, di zaman kuno dari K & R dan mungkin ANSI C
nyamuk
2
Pertanyaan SO terkait: stackoverflow.com/questions/10473573/…
dan04
3
"Saya sudah mengujinya menggunakan gcc dan icc, dan kedua hasil itu tampaknya menunjukkan bahwa saya tidak mengkhawatirkan apa pun kecuali saya ingin benar-benar positif." Jangan pernah berasumsi bahwa karena kompiler Anda mengizinkannya, bahasa C mengizinkannya. Kecuali, tentu saja, Anda baik-baik saja dengan pemecahan kode Anda di masa depan.
Doval
5
Tanpa ingin menjadi sombong, "Numerical Recipies" umumnya dianggap sebagai buku yang berguna, cepat dan kotor, bukan paradigma pengembangan perangkat lunak atau analisis numerik. Lihatlah artikel Wikipedia di "Numerical Recipies" untuk ringkasan beberapa kritik.
Charles E. Grant
1
Selain itu, inilah mengapa kami mengindeks dari nol: cs.utexas.edu/~EWD/ewd08xx/EWD831.PDF
Russell Borogove

Jawaban:

16

Anda benar bahwa kode seperti

float a = malloc(size);
a--;

menghasilkan perilaku yang tidak terdefinisi, sesuai standar ANSI C, bagian 3.3.6:

Kecuali jika operan penunjuk dan titik hasil ke anggota objek array yang sama, atau melewati anggota terakhir dari objek array, perilaku tidak terdefinisi

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

Bart van Ingen Schenau
sumber
4
Tepatnya, dimungkinkan pada arsitektur multi-bank yang malloc dapat memberikan Anda alamat 0 di bank dan penurunan itu dapat menyebabkan jebakan CPU dengan underflow untuk satu.
Vality
1
Saya tidak setuju bahwa itu "beruntung". Saya pikir akan jauh lebih baik jika kompiler memancarkan kode yang langsung macet setiap kali Anda memanggil perilaku yang tidak ditentukan.
David Conrad
4
@ Davidvidon: Maka C bukan bahasa untuk Anda. Banyak perilaku tidak terdefinisi dalam C tidak dapat dengan mudah dideteksi atau hanya dengan performa yang hebat.
Bart van Ingen Schenau
Saya sedang berpikir untuk menambahkan "dengan saklar kompiler". Tentunya Anda tidak akan menginginkannya untuk kode yang dioptimalkan. Tapi, Anda benar, dan itulah sebabnya saya berhenti menulis C sepuluh tahun yang lalu.
David Conrad
@ BartartIngenSchenau tergantung pada apa yang Anda maksud dengan 'kinerja parah hit' ada eksekusi simbolis untuk C (misalnya dentang + klee) serta sanatizer (asan, tsan, ubsan, valgrind dll.) Yang cenderung sangat berguna untuk debugging.
Maciej Piechotka
10

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.

dan04
sumber
1
Itu masuk akal. Sayangnya, itu dua terlalu banyak jika sesuai dengan keinginan saya.
wolfPack88
3
Poin terakhir adalah IMHO yang paling bermasalah. Karena penyusun kali ini tidak hanya membiarkan apa pun yang dilakukan platform "secara alami" dalam kasus UB, tetapi pengoptimal secara agresif mengeksploitasinya , saya tidak akan mempermainkannya dengan begitu enteng.
Matteo Italia
3

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.

gnasher729
sumber
1

... hanya mengurangi pointer di luar rentang yang dialokasikan tampaknya sangat samar bagi saya. Apakah perilaku "diizinkan" ini dalam C?

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:

double *p = (double *)0xdeadbeef;
--p;  // p == 0xdeadbee7, assuming sizeof(double) == 8.
double d = p[0];

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, sehingga a[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 doubledipetakan ke 0xdeadbee7dan 0xdeadbeef. Sangat sedikit program yang perlu melakukan itu.

Saat Anda membuat alamat sesuatu, seperti dengan menggunakan &operator atau panggilan malloc(), 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 untuk malloc(), karena Anda telah membebani panggilan siapa pun free()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:

double *array_create(size_t size) {
    // Wasting one element, so don't allow it to be full-sized
    assert(size < SIZE_MAX);
    return malloc((size+1) * sizeof(double));
}

inline double array_index(double *array, size_t index) {
    assert(array != NULL);
    assert(index >= 1);  // This is a 1-based array
    return array[index];
}

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. -1adalah bilangan bulat, dan itu membuat p[-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):

Ketika ekspresi yang memiliki tipe integer ditambahkan atau dikurangi dari sebuah pointer, hasilnya memiliki tipe operan pointer.

... jika ekspresi Pmenunjuk ke ielemen -th dari objek array, ekspresi (P)+N(ekuivalen, N+(P)) dan (P)-N (di mana Nmemiliki nilai n) menunjuk ke, masing-masing, elemen i+n-th dan i−n-th dari objek array, asalkan ada .

Ini berarti bahwa hasil dari aritmatika pointer harus menunjuk pada elemen dalam array. Tidak dikatakan bahwa aritmatika harus dilakukan sekaligus. Karena itu:

double a[20];

// This points to element 9 of a; behavior is defined.
double d = a[-1 + 10];

double *p = a - 1;  // This is just a pointer.  No dereferencing.

double e = p[0];   // Does not point at any element of a; behavior is undefined.
double f = p[1];   // Points at element 0 of a; behavior is defined.

Apakah saya merekomendasikan melakukan hal-hal seperti ini? Saya tidak, dan jawaban saya menjelaskan alasannya.

Blrfl
sumber
8
-1 Definisi 'diizinkan' yang mencakup kode yang dinyatakan oleh standar C sebagai hasil yang tidak terdefinisi tidak bermanfaat.
Pete Kirkham
Orang lain telah menunjukkan bahwa itu adalah perilaku yang tidak terdefinisi, jadi Anda tidak boleh mengatakan bahwa itu "diizinkan". Namun, saran untuk mengalokasikan elemen 0 tambahan yang tidak digunakan adalah baik.
200_sukses
Ini benar-benar tidak benar, harap dicatat bahwa ini dilarang oleh standar C.
Vality
@PeteKirkham: Saya tidak setuju. Lihat addendum untuk jawaban saya.
Blrfl
4
@ Blrfl 6.5.6 dari standar ISO C11 menyatakan dalam kasus menambahkan integer ke sebuah pointer: "Jika kedua pointer operan dan titik hasil ke elemen objek array yang sama, atau melewati elemen terakhir objek array , evaluasi tidak akan menghasilkan luapan; jika tidak, perilaku tidak akan ditentukan. "
Vality