Pointer
mengacu pada sel memori, dan penunjuk referensi berarti membaca nilai sel yang ditentukan. Nilai dari pointer itu sendiri adalah alamat sel memori. Standar bahasa C tidak menentukan formulir untuk mewakili alamat memori. Ini adalah poin yang sangat penting, karena arsitektur yang berbeda dapat menggunakan model pengalamatan yang berbeda. Sebagian besar arsitektur modern menggunakan ruang alamat linear atau serupa. Namun, bahkan pertanyaan ini tidak ditentukan secara ketat, karena alamat dapat berupa fisik atau virtual. Beberapa arsitektur menggunakan representasi non-numerik sama sekali. Jadi, Symbolics Lisp Machine beroperasi dengan tupel formulir
(objek, offset) sebagai alamat.
Beberapa waktu kemudian, setelah publikasi terjemahan tentang Habré, penulis membuat modifikasi besar pada teks artikel. Memperbarui terjemahan pada Habré bukanlah ide yang baik, karena beberapa komentar akan kehilangan maknanya atau akan terlihat tidak pada tempatnya. Saya tidak ingin menerbitkan teks sebagai artikel baru. Oleh karena itu, kami baru saja memperbarui terjemahan artikel di viva64.com, dan di sini kami meninggalkan semuanya apa adanya. Jika Anda seorang pembaca baru, saya sarankan membaca terjemahan yang lebih baru di situs kami dengan mengklik tautan di atas. |
Standar tidak menetapkan bentuk penyajian pointer, tetapi menetapkan - pada tingkat yang lebih besar atau lebih kecil - operasi dengan mereka. Di bawah ini kami mempertimbangkan operasi ini dan fitur dari definisi mereka dalam standar. Mari kita mulai dengan contoh berikut:
#include <stdio.h> int main(void) { int a, b; int *p = &a; int *q = &b + 1; printf("%p %p %d\n", (void *)p, (void *)q, p == q); return 0; }
Jika kami mengkompilasi kode GCC ini dengan level optimasi 1 dan menjalankan program di Linux x86-64, itu akan mencetak yang berikut:
0x7fff4a35b19c 0x7fff4a35b19c 0
Perhatikan bahwa pointer
p dan
q merujuk ke alamat yang sama. Namun, hasil dari ekspresi
p == q salah , dan ini sekilas tampak aneh. Bukankah dua pointer ke alamat yang sama harus sama?
Inilah cara standar C mendefinisikan hasil pemeriksaan dua petunjuk untuk persamaan:
C11 § 6.5.9 paragraf 6
Dua pointer sama jika dan hanya jika keduanya nol, baik menunjuk ke objek yang sama (termasuk pointer ke objek dan sub-objek pertama di objek) atau fungsi, atau arahkan ke posisi setelah elemen terakhir array, atau satu pointer merujuk ke posisi setelah elemen terakhir dari array, dan yang lainnya merujuk ke awal array lain segera setelah yang pertama di ruang alamat yang sama. |
Pertama-tama, muncul pertanyaan: apa itu "objek
" ? Karena kita berbicara tentang bahasa C, jelas bahwa di sini objek tidak ada hubungannya dengan objek dalam bahasa OOP seperti C ++. Dalam standar C, konsep ini tidak sepenuhnya didefinisikan:
C11 § 3.15
Objek adalah area penyimpanan runtime yang isinya dapat digunakan untuk mewakili nilai
CATATAN Ketika disebutkan, suatu objek dapat dianggap memiliki jenis tertentu; lihat 6.3.2.1. |
Mari kita perbaiki. Variabel integer 16-bit adalah kumpulan data dalam memori yang dapat mewakili nilai integer 16-bit. Oleh karena itu, variabel semacam itu adalah objek. Akankah dua pointer sama jika salah satu dari mereka merujuk ke byte pertama dari integer yang diberikan, dan yang kedua ke byte kedua dari angka yang sama? Komite standardisasi bahasa, tentu saja, tidak bermaksud sama sekali. Tetapi di sini perlu dicatat bahwa dalam hal ini dia tidak memiliki penjelasan yang jelas, dan kita dipaksa untuk menebak apa yang sebenarnya dimaksud.
Ketika kompiler menghalangi
Mari kita kembali ke contoh pertama kita. Pointer
p diperoleh dari objek
a , dan pointer
q dari objek
b . Dalam kasus kedua, aritmatika alamat digunakan, yang didefinisikan untuk operator plus dan minus sebagai berikut:
C11 § 6.5.6 klausa 7
Ketika digunakan dengan operator ini, sebuah penunjuk ke objek yang bukan merupakan elemen dari array berperilaku seperti penunjuk ke awal array dengan panjang satu elemen, tipe yang sesuai dengan jenis objek asli. |
Karena setiap pointer ke objek yang bukan array
sebenarnya menjadi pointer ke array dengan panjang satu elemen, standar mendefinisikan aritmatika alamat hanya untuk pointer ke array - ini adalah poin 8. Kami tertarik pada bagian berikut:
C11 § 6.5.6 klausa 8
Jika ekspresi integer ditambahkan atau dikurangi dari pointer, pointer yang dihasilkan adalah jenis yang sama dengan pointer asli. Jika pointer sumber merujuk ke elemen array dan array memiliki panjang yang cukup, maka sumber dan elemen yang dihasilkan dipisahkan satu sama lain sehingga perbedaan antara indeks mereka sama dengan nilai ekspresi integer. Dengan kata lain, jika ekspresi P menunjuk ke elemen ke-l dari array, ekspresi (P) + N (atau N + (P) ) dan (P) -N (dimana N memiliki nilai n) menunjukkan masing-masing (i + n) elemen th dan (i - n) dari array, asalkan ada. Selain itu, jika ekspresi P menunjuk ke elemen terakhir dari array, maka ekspresi (P) +1 menunjukkan posisi setelah elemen terakhir dari array, dan jika ekspresi Q menunjukkan posisi setelah elemen terakhir dari array, maka ekspresi (Q) -1 menunjukkan elemen terakhir array Jika sumber dan pointer yang dihasilkan merujuk ke elemen array yang sama atau ke posisi setelah elemen terakhir array, maka overflow dikecualikan; jika tidak, perilaku tidak terdefinisi. Jika pointer yang dihasilkan merujuk ke posisi setelah elemen terakhir dari array, operator unary * tidak dapat diterapkan padanya. |
Oleh karena itu, hasil dari ekspresi
& b + 1 pastilah alamat, dan oleh karena itu
p dan
q adalah pointer yang valid. Biarkan saya mengingatkan Anda bagaimana kesetaraan dua pointer dalam standar didefinisikan: "
Dua pointer sama jika dan hanya jika [...] satu pointer merujuk ke posisi setelah elemen terakhir dari array, dan yang lainnya ke awal array lain segera setelah yang pertama di sama. address space " (C11 § 6.5.9 klausa 6). Inilah tepatnya yang kita amati dalam contoh kita. Pointer q mengacu pada posisi setelah objek b, segera diikuti oleh objek a, ke mana pointer p merujuk. Jadi, apakah ada bug di GCC? Kontradiksi ini digambarkan pada tahun 2014 sebagai
bug # 61502 , tetapi pengembang GCC tidak menganggapnya sebagai bug dan karenanya tidak akan memperbaikinya.
Masalah serupa ditemui pada tahun 2016 oleh programmer Linux. Pertimbangkan kode berikut:
extern int _start[]; extern int _end[]; void foo(void) { for (int *i = _start; i != _end; ++i) { } }
Simbol
_mulai dan
_end tentukan batas-batas area memori. Karena mereka ditransfer ke file eksternal, kompiler tidak tahu bagaimana sebenarnya array berada di memori. Untuk alasan ini, ia harus berhati-hati di sini dan melanjutkan dari asumsi bahwa mereka mengikuti satu sama lain di ruang alamat. Namun, GCC mengkompilasi kondisi loop sehingga selalu benar, yang membuat loop tidak terbatas. Masalah ini dijelaskan di sini dalam
posting ini
di LKML - sebuah fragmen kode serupa digunakan di sana. Tampaknya dalam kasus ini, penulis GCC tetap memperhitungkan komentar dan mengubah perilaku kompiler. Setidaknya saya tidak dapat mereproduksi kesalahan ini dalam GCC versi 7.3.1 di Linux x86_64.
Solusi - dalam laporan bug # 260?
Kasus kami dapat mengklarifikasi laporan bug
# 260 . Ini lebih tentang nilai-nilai yang tidak pasti, tetapi Anda dapat menemukan komentar penasaran dari panitia di dalamnya:
Implementasi kompiler [...] juga dapat membedakan pointer yang diperoleh dari objek yang berbeda, bahkan jika pointer ini memiliki set bit yang sama.Jika kita mengambil komentar ini secara harfiah, maka logis bahwa hasil dari ekspresi
p == q adalah "false," karena
p dan
q diperoleh dari objek yang berbeda yang tidak terhubung dengan cara apa pun. Sepertinya kita semakin dekat dengan kebenaran - atau tidak? Sejauh ini, kami telah berurusan dengan operator kesetaraan, tetapi bagaimana dengan operator hubungan?
Petunjuk terakhir ada pada operator relasi?
Definisi
< ,
<= ,
> dan
> = operator hubungan dalam konteks perbandingan pointer berisi satu pemikiran yang aneh:
C11 § 6.5.8 paragraf 5
Hasil membandingkan dua pointer tergantung pada posisi relatif dari objek yang ditunjukkan dalam ruang alamat. Jika dua pointer ke tipe objek merujuk ke objek yang sama, atau keduanya merujuk ke posisi setelah elemen terakhir dari array yang sama, maka pointer tersebut sama. Jika objek yang ditunjukkan adalah anggota dari objek komposit yang sama, maka pointer ke anggota struktur yang dideklarasikan lebih dari pointer ke anggota yang dideklarasikan sebelumnya, dan pointer ke elemen array dengan indeks lebih tinggi lebih dari pointer ke elemen array yang sama dengan indeks lebih rendah. Semua petunjuk untuk anggota dari asosiasi yang sama adalah sama. Jika ekspresi P menunjuk ke elemen array, dan ekspresi Q menunjuk ke elemen terakhir dari array yang sama, maka nilai pointer-ekspresi Q + 1 lebih besar dari nilai ekspresi P. Dalam semua kasus lain, perilaku tidak didefinisikan. |
Menurut definisi ini, hasil membandingkan pointer ditentukan hanya jika pointer diperoleh dari objek yang
sama . Kami menunjukkan ini dengan dua contoh.
int *p = malloc(64 * sizeof(int)); int *q = malloc(64 * sizeof(int)); if (p < q)
Di sini, pointer
p dan
q merujuk ke dua objek berbeda yang tidak saling berhubungan. Oleh karena itu, hasil perbandingannya tidak ditentukan. Namun dalam contoh berikut:
int *p = malloc(64 * sizeof(int)); int *q = p + 42; if (p < q) foo();
pointer
p dan
q merujuk ke objek yang sama dan, oleh karena itu, saling berhubungan. Jadi, mereka dapat dibandingkan - kecuali
malloc mengembalikan nilai nol.
Ringkasan
Standar C11 tidak cukup menggambarkan perbandingan pointer. Titik paling bermasalah yang kami temui adalah paragraf 6 § 6.5.9, di mana secara eksplisit diizinkan untuk membandingkan dua petunjuk yang merujuk dua array yang berbeda. Ini bertentangan dengan komentar dari laporan bug # 260. Namun, di sana kita berbicara tentang makna yang tidak terbatas, dan saya tidak ingin membangun alasan saya hanya berdasarkan komentar ini dan menafsirkannya dalam konteks lain. Ketika membandingkan pointer, operator hubungan didefinisikan sedikit berbeda dari operator kesetaraan - yaitu, operator hubungan didefinisikan hanya jika kedua pointer diperoleh dari objek yang
sama .
Jika kita mengabaikan teks standar dan bertanya apakah mungkin untuk membandingkan dua petunjuk yang diperoleh dari dua objek yang berbeda, maka dalam hal apa pun jawabannya kemungkinan besar akan "tidak". Contoh di awal artikel menunjukkan masalah teoretis. Karena variabel
a dan
b memiliki durasi penyimpanan otomatis, asumsi kami tentang penempatannya di memori tidak dapat diandalkan. Dalam beberapa kasus, kita dapat menebak, tetapi jelas bahwa kode semacam itu tidak dapat porting dengan aman, dan Anda dapat mengetahui arti dari program hanya dengan menyusun dan menjalankan atau membongkar kode, dan ini bertentangan dengan paradigma pemrograman yang serius.
Namun, secara umum, saya tidak puas dengan kata-kata dalam standar C11, dan karena beberapa orang telah mengalami masalah ini, pertanyaannya tetap: mengapa tidak merumuskan aturan lebih jelas?
Selain itu
Pointer ke posisi setelah elemen terakhir dari array
Adapun aturan tentang membandingkan dan mengatasi aritmatika pointer ke posisi setelah elemen terakhir dari array, Anda sering dapat menemukan pengecualian untuk itu. Asumsikan bahwa standar tidak akan memungkinkan membandingkan dua petunjuk yang diperoleh dari array yang
sama , meskipun setidaknya satu dari mereka merujuk pada posisi di luar akhir array. Maka kode berikut tidak akan berfungsi:
const int num = 64; int x[num]; for (int *i = x; i < &x[num]; ++i) { }
Menggunakan loop, kita berkeliling seluruh
x array, yang terdiri dari 64 elemen, yaitu loop body harus dieksekusi tepat 64 kali. Tetapi pada kenyataannya, kondisi diperiksa 65 kali - satu kali lebih banyak dari jumlah elemen dalam array. Dalam 64 iterasi pertama, pointer
saya selalu merujuk ke bagian dalam array
x , sedangkan ekspresi
& x [num] selalu menunjukkan posisi setelah elemen terakhir dari array. Pada iterasi ke-65, pointer
i juga akan merujuk ke posisi di luar akhir array
x , karena kondisi loop menjadi salah. Ini adalah cara yang nyaman untuk mem-bypass seluruh array, dan bergantung pada pengecualian pada aturan ketidakpastian dalam perilaku ketika membandingkan pointer tersebut. Perhatikan bahwa standar hanya menggambarkan perilaku ketika membandingkan pointer; dereferencing adalah masalah terpisah.
Apakah mungkin untuk mengubah contoh kita sehingga tidak satu pun pointer menunjuk ke posisi setelah elemen terakhir dari array
x ? Itu mungkin, tetapi akan lebih sulit. Kita harus mengubah kondisi loop dan melarang penambahan variabel
i pada iterasi terakhir.
const int num = 64; int x[num]; for (int *i = x; i <= &x[num-1]; ++i) { if (i == &x[num-1]) break; }
Kode ini penuh dengan seluk-beluk teknis, yang repot dengan yang mengalihkan dari tugas utama. Selain itu, cabang tambahan muncul di badan loop. Jadi saya merasa masuk akal bahwa standar memungkinkan pengecualian ketika membandingkan pointer posisi setelah elemen terakhir dari sebuah array.
Catatan Tim PVS-StudioKetika mengembangkan alat analisa kode PVS-Studio, kita kadang-kadang harus berurusan dengan masalah halus untuk membuat diagnosa lebih akurat atau untuk memberikan konsultasi terperinci kepada klien kami. Artikel ini tampaknya menarik bagi kami, karena menyentuh masalah di mana kita sendiri tidak sepenuhnya merasa percaya diri. Karena itu, kami meminta penulis untuk mengirim terjemahannya. Kami berharap bahwa lebih banyak programmer C dan C ++ akan mengenalnya dan memahami bahwa itu tidak begitu sederhana dan ketika penganalisa tiba-tiba menampilkan pesan aneh, Anda tidak perlu terburu-buru menganggapnya sebagai false positive :).Artikel ini pertama kali diterbitkan dalam bahasa Inggris di stefansf.de. Terjemahan diterbitkan dengan izin dari penulis.