Bagaimana perbandingan pembanding bekerja di C? Apakah boleh membandingkan pointer yang tidak menunjuk ke array yang sama?

33

Dalam K&R (The C Programming Language 2nd Edition) bab 5 saya membaca yang berikut:

Pertama, pointer dapat dibandingkan dalam kondisi tertentu. Jika pdan qarahkan ke anggota array yang sama, hubungan itu seperti ==, !=, <, >=, dll bekerja dengan baik.

Yang tampaknya menyiratkan bahwa hanya pointer yang menunjuk ke array yang sama dapat dibandingkan.

Namun ketika saya mencoba kode ini

    char t = 't';
    char *pt = &t;
    char x = 'x';
    char *px = &x;

    printf("%d\n", pt > px);

1 dicetak ke layar.

Pertama-tama, saya pikir saya akan mendapatkan undefined atau beberapa tipe atau kesalahan, karena ptdan pxtidak menunjuk ke array yang sama (setidaknya dalam pemahaman saya).

Juga pt > pxkarena kedua pointer menunjuk ke variabel yang disimpan di stack, dan stack tumbuh turun, sehingga alamat memori tlebih besar dari x? Mengapa pt > pxitu benar?

Saya semakin bingung ketika malloc dibawa masuk. Juga di K&R di bab 8.7 berikut ini ditulis:

Masih ada satu asumsi, bagaimanapun, bahwa pointer ke blok yang berbeda dikembalikan oleh sbrkdapat dibandingkan secara bermakna. Ini tidak dijamin oleh standar yang memungkinkan perbandingan pointer hanya dalam sebuah array. Jadi versi mallocini portabel hanya di antara mesin-mesin yang perbandingan pointer umumnya bermakna.

Saya tidak punya masalah membandingkan pointer yang menunjuk ke ruang malloced di heap ke pointer yang menunjuk ke variabel stack.

Misalnya, kode berikut ini berfungsi dengan baik, dengan 1dicetak:

    char t = 't';
    char *pt = &t;
    char *px = malloc(10);
    strcpy(px, pt);
    printf("%d\n", pt > px);

Berdasarkan percobaan saya dengan kompiler saya, saya dituntun untuk berpikir bahwa setiap pointer dapat dibandingkan dengan pointer lainnya, terlepas dari mana mereka menunjuk secara individual. Selain itu, saya pikir pointer aritmatika antara dua pointer baik-baik saja, tidak peduli di mana mereka menunjuk secara individual karena aritmatika hanya menggunakan memori alamat toko pointer.

Namun, saya bingung dengan apa yang saya baca di K&R.

Alasan saya bertanya adalah karena prof. sebenarnya membuatnya menjadi pertanyaan ujian. Dia memberi kode berikut:

struct A {
    char *p0;
    char *p1;
};

int main(int argc, char **argv) {
    char a = 0;
    char *b = "W";
    char c[] = [ 'L', 'O', 'L', 0 ];

   struct A p[3];
    p[0].p0 = &a;
    p[1].p0 = b;
    p[2].p0 = c;

   for(int i = 0; i < 3; i++) {
        p[i].p1 = malloc(10);
        strcpy(p[i].p1, p[i].p0);
    }
}

Apa yang dievaluasi untuk:

  1. p[0].p0 < p[0].p1
  2. p[1].p0 < p[1].p1
  3. p[2].p0 < p[2].p1

Jawabannya adalah 0, 1, dan 0.

(Profesor saya tidak menyertakan penafian pada ujian bahwa pertanyaannya adalah untuk Ubuntu Linux 16.04, lingkungan pemrograman versi 64-bit)

(catatan editor: jika SO memperbolehkan lebih banyak tag, bagian terakhir itu akan menjamin , , dan mungkin . Jika inti dari pertanyaan / kelas adalah detail implementasi OS tingkat rendah secara khusus, daripada portable C.)

Shisui
sumber
17
Anda mungkin membingungkan apa yang berlaku di Cdengan apa yang aman di C. Membandingkan dua pointer dengan tipe yang sama selalu dapat dilakukan (memeriksa kesetaraan, misalnya), menggunakan pointer aritmatika dan membandingkan >dan <hanya aman ketika digunakan dalam array yang diberikan (atau blok memori).
Adrian Mole
13
Selain itu, Anda tidak boleh belajar C dari K&R. Sebagai permulaan, bahasa telah mengalami banyak perubahan sejak saat itu. Dan, jujur ​​saja, contoh kode di sana berasal dari waktu ketika kesederhanaan daripada keterbacaan dinilai.
paxdiablo
5
Tidak, itu tidak dijamin berfungsi. Ini dapat gagal dalam prakteknya pada mesin dengan model memori tersegmentasi. Lihat Apakah C memiliki setara dengan std :: kurang dari C ++? Pada sebagian besar mesin modern, itu akan terjadi meskipun UB.
Peter Cordes
6
@ Adam: Tutup, tapi ini sebenarnya UB (kecuali jika kompiler yang digunakan OP, GCC, memilih untuk mendefinisikannya. Mungkin). Tetapi UB tidak berarti "pasti meledak"; salah satu perilaku yang mungkin untuk UB bekerja seperti yang Anda harapkan !! Inilah yang membuat UB begitu jahat; itu dapat bekerja tepat di build debug dan gagal dengan optimasi diaktifkan, atau sebaliknya, atau istirahat tergantung pada kode di sekitarnya. Membandingkan petunjuk lain masih akan memberi Anda jawaban, tetapi bahasa tersebut tidak menentukan apa arti jawaban itu (jika ada). Tidak, menabrak diizinkan. Itu benar-benar UB.
Peter Cordes
3
@ Adam: Oh ya, jangan lupa bagian pertama dari komentar saya, saya salah membaca milik Anda. Tetapi Anda mengklaim Membandingkan petunjuk lain masih akan memberi Anda jawaban . Itu tidak benar. Itu akan menjadi hasil yang tidak ditentukan , bukan UB penuh. UB jauh lebih buruk dan berarti program Anda dapat melakukan segfault atau SIGILL jika eksekusi mencapai pernyataan itu dengan input tersebut (pada titik mana pun sebelum atau setelah itu benar-benar terjadi). (Hanya masuk akal pada x86-64 jika UB terlihat pada waktu kompilasi, tetapi secara umum apa pun bisa terjadi.) Bagian dari titik UB adalah membiarkan kompiler membuat asumsi "tidak aman" sambil menghasilkan ASM.
Peter Cordes

Jawaban:

33

Menurut standar C11 , operator relasional <, <=, >, dan >=hanya dapat digunakan pada pointer ke elemen array yang sama atau objek struct. Ini dijabarkan dalam bagian 6.5.8p5:

Ketika dua pointer dibandingkan, hasilnya tergantung pada lokasi relatif di ruang alamat objek yang ditunjuk. Jika dua pointer ke objek jenis keduanya menunjuk ke objek yang sama, atau keduanya menunjuk satu melewati elemen terakhir dari objek array yang sama, mereka membandingkan sama. Jika objek yang ditunjuk adalah anggota dari objek agregat yang sama, pointer ke struktur yang dideklarasikan anggota kemudian membandingkan lebih besar dari pointer ke anggota yang dinyatakan sebelumnya dalam struktur, dan pointer ke elemen array dengan nilai subskrip yang lebih besar dibandingkan lebih besar dari pointer ke elemen dari array yang sama dengan nilai subskrip yang lebih rendah. Semua pointer ke anggota objek serikat yang sama dibandingkan sama.

Perhatikan bahwa perbandingan apa pun yang tidak memenuhi persyaratan ini memicu perilaku yang tidak terdefinisi , artinya (di antara hal-hal lain) yang tidak dapat Anda andalkan bergantung pada hasil yang dapat diulang.

Dalam kasus khusus Anda, untuk perbandingan antara alamat dua variabel lokal dan antara alamat lokal dan alamat dinamis, operasi tampaknya "berfungsi", namun hasilnya dapat berubah dengan membuat perubahan yang tampaknya tidak terkait dengan kode Anda atau bahkan mengkompilasi kode yang sama dengan pengaturan optimasi yang berbeda. Dengan perilaku yang tidak terdefinisi, hanya karena kodenya bisa crash atau menghasilkan kesalahan tidak berarti itu akan terjadi .

Sebagai contoh, prosesor x86 yang berjalan dalam mode nyata 8086 memiliki model memori tersegmentasi menggunakan segmen 16-bit dan offset 16-bit untuk membangun alamat 20-bit. Jadi dalam hal ini alamat tidak mengonversi persis ke integer.

Operator kesetaraan ==dan !=namun tidak memiliki pembatasan ini. Mereka dapat digunakan antara dua pointer ke tipe yang kompatibel atau pointer NULL. Jadi menggunakan ==atau !=dalam kedua contoh Anda akan menghasilkan kode C yang valid.

Namun, bahkan dengan ==dan !=Anda bisa mendapatkan beberapa hasil yang tidak terduga namun masih terdefinisi dengan baik. Lihat Bisakah perbandingan kesetaraan pointer yang tidak terkait mengevaluasi kebenarannya? untuk detail lebih lanjut tentang ini.

Mengenai pertanyaan ujian yang diberikan oleh profesor Anda, itu membuat sejumlah asumsi yang salah:

  • Model memori datar ada di mana ada korespondensi 1-ke-1 antara alamat dan nilai integer.
  • Nilai pointer yang dikonversi sesuai dengan tipe integer.
  • Bahwa implementasi hanya memperlakukan pointer sebagai bilangan bulat ketika melakukan perbandingan tanpa mengeksploitasi kebebasan yang diberikan oleh perilaku yang tidak ditentukan.
  • Bahwa stack digunakan dan variabel lokal disimpan di sana.
  • Tumpukan itu digunakan untuk menarik memori yang dialokasikan.
  • Bahwa tumpukan (dan karenanya variabel lokal) muncul pada alamat yang lebih tinggi daripada tumpukan (dan karenanya dialokasikan objek).
  • Konstanta string itu muncul di alamat yang lebih rendah dari heap.

Jika Anda menjalankan kode ini pada arsitektur dan / atau dengan kompiler yang tidak memenuhi asumsi ini maka Anda bisa mendapatkan hasil yang sangat berbeda.

Juga, kedua contoh juga menunjukkan perilaku yang tidak terdefinisi ketika mereka memanggil strcpy, karena operan yang tepat (dalam beberapa kasus) menunjuk ke satu karakter dan bukan string yang diakhiri dengan nol, menghasilkan fungsi yang membaca melewati batas-batas variabel yang diberikan.

dbush
sumber
3
@ Shisui Bahkan mengingat itu, Anda masih tidak harus bergantung pada hasilnya. Compiler dapat menjadi sangat agresif dalam hal optimasi dan akan menggunakan perilaku yang tidak terdefinisi sebagai kesempatan untuk melakukannya. Mungkin saja menggunakan kompiler yang berbeda dan / atau pengaturan optimasi yang berbeda dapat menghasilkan output yang berbeda.
dbush
2
@Shisui: Secara umum akan terjadi untuk bekerja pada mesin dengan model memori datar, seperti x86-64. Beberapa kompiler untuk sistem seperti itu bahkan mungkin mendefinisikan perilaku dalam dokumentasinya. Tetapi jika tidak, maka perilaku "gila" dapat terjadi karena kompilasi-waktu-kelihatan UB. (Dalam praktiknya saya tidak berpikir ada yang menginginkannya jadi itu bukan sesuatu yang dicari oleh para penyusun arus utama dan "cobalah untuk menghancurkan".)
Peter Cordes
1
Seperti jika kompilator melihat bahwa satu jalur eksekusi akan mengarah ke <antara mallochasil dan variabel lokal (penyimpanan otomatis, yaitu tumpukan), ia dapat mengasumsikan bahwa jalur eksekusi tidak pernah diambil dan hanya mengkompilasi seluruh fungsi ke ud2instruksi (menimbulkan ilegal -instruksi pengecualian yang akan ditangani oleh kernel dengan mengirimkan SIGILL ke proses). GCC / dentang melakukan ini dalam praktiknya untuk UB jenis lain, seperti jatuh dari kegagalan voidfungsi. godbolt.org turun sepertinya sekarang, tapi coba salin / tempel int foo(){int x=2;}dan perhatikan kekuranganret
Peter Cordes
4
@Shisui: TL: DR: ini bukan portable C, terlepas dari kenyataan bahwa itu berfungsi dengan baik di x86-64 Linux. Namun, membuat asumsi tentang hasil perbandingan itu gila. Jika Anda tidak berada di utas utama, tumpukan utas Anda akan dialokasikan secara dinamis menggunakan mekanisme yang sama mallocdigunakan untuk mendapatkan lebih banyak memori dari OS, jadi tidak ada alasan untuk menganggap bahwa vars lokal Anda (tumpukan benang) di atas mallocdialokasikan secara dinamis penyimpanan.
Peter Cordes
2
@PeterCordes: Apa yang diperlukan adalah mengenali berbagai aspek perilaku sebagai "didefinisikan secara opsional", sehingga implementasi dapat menentukannya atau tidak, sesuai waktu luang mereka, tetapi harus menunjukkan dengan cara yang dapat diuji (misalnya makro yang telah ditentukan) jika mereka tidak melakukannya. Selain itu, daripada mengkarakterisasi bahwa situasi apa pun di mana efek optimasi akan dapat diamati sebagai "Perilaku Tidak Terdefinisi", akan jauh lebih berguna untuk mengatakan bahwa pengoptimal mungkin menganggap aspek-aspek perilaku tertentu sebagai "tidak dapat diamati" jika mereka menunjukkan bahwa mereka lakukan itu. Misalnya, diberikan int x,y;, implementasi ...
supercat
12

Masalah utama dengan membandingkan pointer ke dua array berbeda dari tipe yang sama adalah bahwa array itu sendiri tidak perlu ditempatkan dalam posisi relatif tertentu - satu bisa berakhir sebelum dan sesudah yang lain.

Pertama-tama, saya pikir saya akan mendapatkan undefined atau beberapa tipe atau kesalahan, karena pt suatu px tidak menunjuk ke array yang sama (setidaknya dalam pemahaman saya).

Tidak, hasilnya tergantung pada implementasi dan faktor tak terduga lainnya.

Juga pt> px karena kedua pointer menunjuk ke variabel yang disimpan di stack, dan stack tumbuh turun, sehingga alamat memori t lebih besar daripada x? Itu sebabnya pt> px benar?

Belum tentu ada setumpuk . Ketika ada, ia tidak perlu tumbuh. Itu bisa tumbuh. Ini bisa menjadi tidak berdampingan dalam beberapa cara yang aneh.

Selain itu, saya pikir pointer aritmatika antara dua pointer baik-baik saja, tidak peduli di mana mereka menunjuk secara individual karena aritmatika hanya menggunakan memori alamat toko pointer.

Mari kita lihat spesifikasi C , §6.5.8 di halaman 85 yang membahas operator relasional (yaitu operator perbandingan yang Anda gunakan). Perhatikan bahwa ini tidak berlaku untuk langsung !=atau ==perbandingan.

Ketika dua pointer dibandingkan, hasilnya tergantung pada lokasi relatif di ruang alamat objek yang ditunjuk. ... Jika objek yang ditunjuk adalah anggota dari objek agregat yang sama, ... pointer ke elemen array dengan nilai subskrip yang lebih besar membandingkan lebih besar dari pointer ke elemen array yang sama dengan nilai subskrip yang lebih rendah.

Dalam semua kasus lain, perilaku tidak terdefinisi.

Kalimat terakhir itu penting. Sementara saya memotong beberapa case yang tidak berhubungan untuk menghemat ruang, ada satu case yang penting bagi kami: dua array, bukan bagian dari struct / agregat objek 1 yang sama , dan kami membandingkan pointer ke dua array tersebut. Ini adalah perilaku yang tidak terdefinisi .

Sementara kompiler Anda baru saja memasukkan semacam instruksi mesin CMP (bandingkan) yang secara numerik membandingkan pointer, dan Anda beruntung di sini, UB adalah binatang yang sangat berbahaya. Secara harfiah apa pun bisa terjadi - kompiler Anda dapat mengoptimalkan seluruh fungsi termasuk efek samping yang terlihat. Itu bisa menelurkan setan hidung.

1 Pointer ke dalam dua array berbeda yang merupakan bagian dari struct yang sama dapat dibandingkan, karena ini berada di bawah klausa di mana dua array adalah bagian dari objek agregat yang sama (struct).

nanofarad
sumber
1
Lebih penting lagi, dengan tdan xdidefinisikan dalam fungsi yang sama, tidak ada alasan untuk menganggap apa pun tentang bagaimana kompiler yang menargetkan x86-64 akan meletakkan penduduk lokal dalam bingkai tumpukan untuk fungsi ini. Tumpukan yang tumbuh ke bawah tidak ada hubungannya dengan urutan deklarasi variabel dalam satu fungsi. Bahkan dalam fungsi-fungsi yang terpisah, jika satu bisa sejalan dengan yang lain maka fungsi lokal "anak" masih bisa bercampur dengan orang tua.
Peter Cordes
1
kompiler Anda dapat mengoptimalkan seluruh fungsi termasuk efek samping yang terlihat Bukan pernyataan yang berlebihan: untuk jenis UB lainnya (seperti jatuh dari ujung non- voidfungsi) g ++ dan clang ++ benar-benar melakukan itu dalam praktiknya: godbolt.org/z/g5vesB mereka berasumsi bahwa jalur eksekusi tidak diambil karena mengarah ke UB, dan kompilasi blok dasar seperti itu untuk instruksi ilegal. Atau tidak ada instruksi sama sekali, hanya diam-diam jatuh ke asm berikutnya apa pun jika fungsi itu pernah dipanggil. (Untuk beberapa alasan gcctidak hanya melakukan ini g++).
Peter Cordes
6

Lalu bertanya apa

p[0].p0 < p[0].p1
p[1].p0 < p[1].p1
p[2].p0 < p[2].p1

Mengevaluasi ke. Jawabannya adalah 0, 1, dan 0.

Pertanyaan-pertanyaan ini direduksi menjadi:

  1. Apakah tumpukan di atas atau di bawah tumpukan.
  2. Apakah tumpukan di atas atau di bawah bagian literal string dari program.
  3. sama seperti [1].

Dan jawaban untuk ketiganya adalah "implementasi didefinisikan". Pertanyaan prof Anda palsu; mereka mendasarkannya pada tata letak unix tradisional:

<empty>
text
rodata
rwdata
bss
< empty, used for heap >
...
stack
kernel

tetapi beberapa kesatuan modern (dan sistem alternatif) tidak sesuai dengan tradisi-tradisi itu. Kecuali mereka mengawali pertanyaan dengan "pada 1992"; pastikan untuk memberi -1 pada eval.

mevets
sumber
3
Tidak implementasi didefinisikan, tidak ditentukan! Pikirkan seperti ini, yang pertama dapat bervariasi antara implementasi tetapi implementasi harus mendokumentasikan bagaimana perilaku diputuskan. Yang terakhir berarti perilaku dapat bervariasi dengan cara apa pun dan implementasinya tidak harus memberi tahu Anda jongkok :-)
paxdiablo
1
@paxdiablo: Menurut Rationale oleh penulis Standar, "Perilaku tidak terdefinisi ... juga mengidentifikasi bidang-bidang yang memungkinkan ekstensi bahasa yang sesuai: pelaksana dapat menambah bahasa dengan memberikan definisi perilaku yang secara resmi tidak ditentukan." Rationale lebih lanjut mengatakan, "Tujuannya adalah untuk memberi programmer kesempatan berjuang untuk membuat program C yang kuat yang juga sangat portabel, tanpa tampaknya meremehkan program C yang sangat berguna yang kebetulan tidak menjadi portabel, dengan demikian kata keterangan secara ketat." Penulis kompiler komersial memahami hal ini, tetapi beberapa penulis kompiler lain tidak.
supercat
Ada aspek implementasi lain yang didefinisikan; perbandingan pointer ditandatangani , jadi tergantung pada mesin / os / compiler, beberapa alamat dapat ditafsirkan sebagai negatif. Sebagai contoh, mesin 32bit yang menempatkan stack pada 0xc << 28, kemungkinan akan menunjukkan variabel otomatis pada alamat lessor daripada heap atau rodata.
mevets
1
@mevets: Apakah Standar menentukan situasi di mana penandatanganan pointer dalam perbandingan dapat diamati? Saya berharap bahwa jika platform 16-bit memungkinkan objek yang lebih besar dari 32768 byte, dan arr[]merupakan objek yang demikian, Standar akan mengamanatkan bahwa arr+32768membandingkan lebih besar daripada arrbahkan jika perbandingan pointer yang ditandatangani akan melaporkan sebaliknya.
supercat
Saya tidak tahu; standar C mengorbit di lingkaran kesembilan Dante, berdoa untuk eutanasia. OP secara khusus merujuk K&R dan pertanyaan ujian. #UB adalah puing-puing dari kelompok kerja yang malas.
mevets
1

Pada hampir semua platform jarak jauh-modern, pointer dan integer memiliki hubungan pemesanan isomorfik, dan pointer ke objek terpisah tidak saling disisipkan. Sebagian besar penyusun mengekspos pemesanan ini kepada programmer ketika optimasi dinonaktifkan, tetapi Standar tidak membuat perbedaan antara platform yang memiliki pemesanan seperti itu dan yang tidak dan tidak mengharuskan implementasi apa pun mengekspos pemesanan seperti itu kepada programmer bahkan pada platform yang akan mendefinisikannya. Akibatnya, beberapa penulis kompiler melakukan berbagai macam optimisasi dan "optimisasi" berdasarkan pada asumsi bahwa kode tidak akan pernah membandingkan penggunaan operator relasional pada pointer ke objek yang berbeda.

Menurut Rationale yang diterbitkan, penulis Standar bermaksud bahwa implementasi memperluas bahasa dengan menentukan bagaimana mereka akan berperilaku dalam situasi Standar mencirikan sebagai "Perilaku Tidak Terdefinisi" (yaitu ketika Standar tidak memaksakan persyaratan ) ketika melakukan hal itu akan berguna dan praktis , tetapi beberapa penulis kompiler lebih suka berasumsi bahwa program tidak akan pernah mencoba mengambil manfaat dari apa pun di luar mandat Standar, daripada membiarkan program mengeksploitasi perilaku bermanfaat yang dapat didukung platform tanpa biaya tambahan.

Saya tidak mengetahui adanya kompiler yang dirancang secara komersial yang melakukan sesuatu yang aneh dengan perbandingan pointer, tetapi ketika kompiler pindah ke LLVM non-komersial untuk back end mereka, mereka semakin cenderung memproses kode yang tidak masuk akal yang perilakunya telah ditentukan sebelumnya. kompiler untuk platform mereka. Perilaku seperti itu tidak terbatas pada operator relasional, tetapi bahkan dapat mempengaruhi kesetaraan / ketidaksetaraan. Sebagai contoh, meskipun Standar menetapkan bahwa perbandingan antara pointer ke satu objek dan "just past" pointer ke objek yang sebelumnya akan membandingkan, kompiler berbasis gcc dan LLVM cenderung menghasilkan kode tidak masuk akal jika program melakukan seperti itu perbandingan.

Sebagai contoh situasi di mana bahkan perbandingan kesetaraan berperilaku tidak masuk akal dalam gcc dan dentang, pertimbangkan:

extern int x[],y[];
int test(int i)
{
    int *p = y+i;
    y[0] = 4;
    if (p == x+10)
        *p = 1;
    return y[0];
}

Baik dentang dan gcc akan menghasilkan kode yang akan selalu mengembalikan 4 bahkan jika xsepuluh elemen, ysegera mengikutinya, dan inol menghasilkan perbandingan yang benar dan p[0]ditulis dengan nilai 1. Saya pikir yang terjadi adalah bahwa satu langkah optimasi penulisan ulang fungsi seolah-olah *p = 1;diganti dengan x[10] = 1;. Kode yang terakhir akan menjadi setara jika kompiler diartikan *(x+10)sebagai setara dengan *(y+i), tetapi sayangnya tahap optimasi hilir mengakui bahwa akses ke x[10]hanya akan didefinisikan jika xmemiliki setidaknya 11 elemen, yang akan membuat tidak mungkin untuk mempengaruhi akses y.

Jika kompiler bisa mendapatkan "kreatif" dengan skenario kesetaraan pointer yang dijelaskan oleh Standar, saya tidak akan mempercayai mereka untuk menahan diri dari menjadi lebih kreatif dalam kasus di mana Standar tidak memaksakan persyaratan.

supercat
sumber
0

Ini sederhana: Membandingkan pointer tidak masuk akal karena lokasi memori untuk objek tidak pernah dijamin dalam urutan yang sama seperti yang Anda nyatakan. Pengecualian adalah array. & array [0] lebih rendah dari & array [1]. Itulah yang ditunjukkan oleh K&R. Dalam praktiknya, alamat anggota struct juga dalam urutan yang Anda nyatakan dalam pengalaman saya. Tidak ada jaminan untuk itu .... Pengecualian lain adalah jika Anda membandingkan pointer untuk yang sama. Ketika satu pointer sama dengan yang lain, Anda tahu itu menunjuk ke objek yang sama. Apapun itu. Pertanyaan ujian yang buruk jika Anda bertanya kepada saya. Bergantung pada Ubuntu Linux 16.04, lingkungan pemrograman versi 64-bit untuk pertanyaan ujian? Betulkah ?

Hans Lepoeter
sumber
Secara teknis, array tidak benar-benar pengecualian karena Anda tidak menyatakan arr[0], arr[1], dll secara terpisah. Anda mendeklarasikan arrsecara keseluruhan sehingga urutan elemen array individual adalah masalah yang berbeda dengan yang dijelaskan dalam pertanyaan ini.
paxdiablo
1
Elemen-elemen struktur dijamin berurutan, yang menjamin bahwa seseorang dapat menggunakan memcpyuntuk menyalin bagian yang berdekatan dari suatu struktur dan memengaruhi semua elemen di dalamnya dan tidak memengaruhi yang lain. Standar ceroboh tentang terminologi seperti apa aritmatika pointer dapat dilakukan dengan struktur atau malloc()penyimpanan yang dialokasikan. The offsetofmakro akan lebih berguna jika salah satu tidak bisa untuk jenis yang sama pointer aritmetika dengan byte dari struct sebagai dengan char[], tetapi Standard tidak tegas mengatakan bahwa byte struct (atau dapat digunakan sebagai) sebuah objek array.
supercat
-4

Apa Pertanyaan Provokatif!

Bahkan pemindaian sepintas terhadap respons dan komentar di utas ini akan mengungkapkan bagaimana emotifnya kueri Anda yang tampaknya sederhana dan lurus ke depan ternyata.

Seharusnya tidak mengejutkan.

Inarguably, kesalahpahaman di sekitar konsep dan penggunaan dari pointer merupakan dominan penyebab serius kegagalan dalam pemrograman pada umumnya.

Pengakuan atas kenyataan ini sudah terbukti dalam ubikuitas bahasa yang dirancang khusus untuk mengatasi, dan lebih baik untuk menghindari tantangan yang diperkenalkan oleh pointer sama sekali. Pikirkan C ++ dan turunan lainnya dari C, Java dan relasinya, Python dan skrip lainnya - hanya sebagai yang lebih menonjol dan lazim, dan kurang lebih teratur dalam menangani masalah ini.

Mengembangkan pemahaman yang lebih dalam tentang prinsip-prinsip yang mendasarinya, oleh karena itu harus relevan dengan setiap individu yang bercita-cita untuk keunggulan dalam pemrograman - terutama di tingkat sistem .

Saya membayangkan inilah tepatnya yang ditunjukkan oleh guru Anda.

Dan sifat C membuatnya menjadi kendaraan yang nyaman untuk eksplorasi ini. Kurang jelas daripada perakitan - meskipun mungkin lebih mudah dipahami - dan masih jauh lebih eksplisit daripada bahasa berdasarkan abstraksi yang lebih dalam dari lingkungan eksekusi.

Dirancang untuk memfasilitasi terjemahan deterministik dari maksud programmer ke dalam instruksi yang dapat dipahami mesin, C adalah bahasa tingkat sistem . Meskipun diklasifikasikan sebagai tingkat tinggi, itu benar-benar termasuk dalam kategori 'sedang'; tetapi karena tidak ada seperti itu, penunjukan 'sistem' harus cukup.

Karakteristik ini sebagian besar bertanggung jawab untuk menjadikannya bahasa pilihan untuk driver perangkat , kode sistem operasi , dan implementasi yang disematkan . Lebih jauh, alternatif yang lebih disukai dalam aplikasi di mana efisiensi optimal adalah yang terpenting; di mana itu berarti perbedaan antara kelangsungan hidup dan kepunahan, dan oleh karena itu merupakan keharusan sebagai lawan dari kemewahan. Dalam kasus seperti itu, kenyamanan portabilitas yang menarik kehilangan semua daya pikatnya, dan memilih untuk kinerja yang kurang berkilau dari penyebut yang paling tidak umum menjadi pilihan merugikan yang tak terduga .

Apa yang membuat C - dan beberapa turunannya - cukup istimewa, adalah bahwa ia memungkinkan penggunanya mengendalikan sepenuhnya - ketika itu yang mereka inginkan - tanpa memaksakan tanggung jawab terkait kepada mereka ketika mereka tidak. Namun demikian, tidak pernah menawarkan lebih dari tertipis dari isolasi dari mesin , karenanya penggunaan yang tepat menuntut menuntut pemahaman konsep pointer .

Pada dasarnya, jawaban atas pertanyaan Anda sangat sederhana dan manis - sebagai konfirmasi atas kecurigaan Anda. Disediakan , bagaimanapun, salah satu yang melekat syarat penting untuk setiap konsep dalam pernyataan ini:

  • Tindakan memeriksa, membandingkan, dan memanipulasi pointer selalu dan tentu saja valid, sedangkan kesimpulan yang diperoleh dari hasil tergantung pada validitas nilai yang terkandung, dan karenanya tidak perlu.

Yang pertama keduanya selalu aman dan berpotensi tepat , sedangkan yang terakhir hanya bisa tepat ketika telah ditetapkan sebagai aman . Anehnya - bagi sebagian orang - jadi menetapkan validitas yang terakhir tergantung pada dan menuntut yang pertama.

Tentu saja, bagian dari kebingungan muncul dari efek rekursi yang secara inheren hadir dalam prinsip penunjuk - dan tantangan yang ditimbulkan dalam membedakan konten dari alamat.

Anda telah menduga dengan benar ,

Saya dituntun untuk berpikir bahwa pointer apa pun dapat dibandingkan dengan pointer lain, terlepas dari mana mereka menunjuk secara individual. Selain itu, saya pikir pointer aritmatika antara dua pointer baik-baik saja, tidak peduli di mana mereka menunjuk secara individual karena aritmatika hanya menggunakan memori alamat toko pointer.

Dan beberapa kontributor telah menegaskan: pointer hanya angka. Terkadang sesuatu lebih dekat ke bilangan kompleks , tetapi masih tidak lebih dari angka.

Perasaan lucu di mana pertikaian ini diterima di sini mengungkapkan lebih banyak tentang sifat manusia daripada pemrograman, tetapi tetap layak dicatat dan dielaborasi. Mungkin kita akan melakukannya nanti ...

Ketika satu komentar mulai mengisyaratkan; semua kebingungan dan kekhawatiran ini berasal dari kebutuhan untuk membedakan apa yang valid dari apa yang aman , tetapi itu adalah penyederhanaan yang berlebihan. Kita juga harus membedakan mana yang fungsional dan apa yang dapat diandalkan , apa yang praktis dan apa yang pantas , dan lebih jauh lagi: apa yang pantas dalam keadaan tertentu dari apa yang mungkin pantas dalam arti yang lebih umum . Apalagi; perbedaan antara kesesuaian dan kesopanan .

Menjelang itu, pertama kita perlu menghargai tepat apa pointer adalah .

  • Anda telah menunjukkan cengkeraman yang kuat pada konsep, dan seperti yang lain mungkin menemukan ilustrasi ini sangat sederhana, tetapi tingkat kebingungan jelas di sini menuntut kesederhanaan dalam klarifikasi.

Seperti yang telah ditunjukkan oleh beberapa orang: istilah pointer hanyalah nama khusus untuk apa yang sekadar indeks , dan dengan demikian tidak lebih dari angka lainnya .

Ini harus sudah jelas dengan mempertimbangkan fakta bahwa semua komputer arus utama kontemporer adalah mesin biner yang tentu saja bekerja secara eksklusif dengan dan pada angka . Komputasi kuantum dapat mengubah itu, tetapi itu sangat tidak mungkin, dan itu belum dewasa.

Secara teknis, seperti yang telah Anda catat, pointer adalah alamat yang lebih akurat ; wawasan yang jelas yang secara alami memperkenalkan analogi yang bermanfaat dari menghubungkan mereka dengan 'alamat' rumah, atau plot di jalan.

  • Dalam model memori datar : seluruh memori sistem disusun dalam satu urutan linier tunggal: semua rumah di kota terletak di jalan yang sama, dan setiap rumah diidentifikasi secara unik dengan jumlahnya saja. Sederhana dan menyenangkan.

  • Dalam skema tersegmentasi : organisasi hierarkis jalan bernomor diperkenalkan di atas rumah bernomor sehingga diperlukan alamat komposit.

    • Beberapa implementasi masih lebih berbelit-belit, dan totalitas 'jalan' yang berbeda tidak perlu dijumlahkan dengan urutan yang berdekatan, tetapi tidak ada yang mengubah apa pun tentang yang mendasarinya.
    • Kami harus dapat menguraikan setiap tautan hierarkis tersebut kembali menjadi organisasi yang rata. Semakin kompleks organisasi, semakin banyak rintangan yang harus kita lewati untuk melakukannya, tetapi itu harus dimungkinkan. Memang, ini juga berlaku untuk 'mode nyata' di x86.
    • Kalau tidak, pemetaan tautan ke lokasi tidak akan bersifat bijektif , karena eksekusi yang andal - pada tingkat sistem - menuntut bahwa itu HARUS .
      • banyak alamat tidak boleh dipetakan ke lokasi memori tunggal, dan
      • alamat tunggal tidak boleh memetakan ke beberapa lokasi memori.

Membawa kami ke putaran lebih lanjut yang mengubah teka-teki menjadi kusut yang begitu rumit . Di atas, itu bijaksana untuk menyarankan bahwa pointer adalah alamat, demi kesederhanaan dan kejelasan. Tentu saja ini tidak benar. Sebuah pointer adalah bukan alamat; pointer adalah referensi ke alamat , itu berisi alamat . Seperti olahraga amplop referensi ke rumah. Merenungkan ini dapat membuat Anda melihat sekilas apa yang dimaksud dengan saran rekursi yang terkandung dalam konsep. Masih; kami hanya memiliki begitu banyak kata, dan berbicara tentang alamat referensi ke alamatdan semacamnya, segera menghentikan sebagian besar otak pada pengecualian kode-op yang tidak valid . Dan sebagian besar, niat sudah siap dikumpulkan dari konteks, jadi mari kita kembali ke jalan.

Pekerja pos di kota imajiner kita ini sangat mirip dengan yang kita temukan di dunia 'nyata'. Tidak ada yang cenderung menderita stroke ketika Anda berbicara atau menanyakan tentang alamat yang tidak valid , tetapi setiap yang terakhir akan menolak ketika Anda meminta mereka untuk bertindak berdasarkan informasi itu.

Misalkan hanya ada 20 rumah di jalan tunggal kami. Lebih lanjut berpura-pura bahwa beberapa jiwa yang salah arah, atau disleksia telah mengarahkan sebuah surat, yang sangat penting, ke nomor 71. Sekarang, kita dapat bertanya kepada pembawa pesan kita Frank, apakah ada alamat seperti itu, dan dia akan dengan sederhana dan tenang melaporkan: tidak . Kita bahkan bisa berharap dia memperkirakan seberapa jauh di luar jalan lokasi ini akan terletak jika memang ada: kira-kira 2,5 kali lebih jauh dari akhir. Semua ini tidak akan membuatnya putus asa. Namun, jika kita memintanya untuk mengirimkan surat ini, atau untuk mengambil item dari tempat itu, dia kemungkinan besar akan terus terang tentang ketidaksenangannya , dan penolakan untuk mematuhinya.

Pointer itu adil alamat, dan alamat hanyalah angka.

Verifikasi output dari yang berikut:

void foo( void *p ) {
   printf(“%p\t%zu\t%d\n”, p, (size_t)p, p == (size_t)p);
}

Sebutkan pada pointer sebanyak yang Anda suka, valid atau tidak. Silakan lakukan posting temuan Anda jika gagal pada platform Anda, atau Anda (kontemporer) compiler mengeluh.

Sekarang, karena pointer yang hanya nomor, itu pasti berlaku untuk membandingkan mereka. Di satu sisi inilah yang ditunjukkan oleh guru Anda. Semua pernyataan berikut ini benar-benar valid - dan layak! - C, dan ketika dikompilasi akan berjalan tanpa menemui masalah , meskipun pointer tidak perlu diinisialisasi dan nilai-nilai yang dikandungnya mungkin tidak terdefinisi :

  • Kami hanya menghitung result secara eksplisit demi kejelasan , dan mencetaknya untuk memaksa kompiler menghitung apa yang seharusnya menjadi kode mati yang mubazir.
void foo( size_t *a, size_t *b ) {
   size_t result;
   result = (size_t)a;
   printf(“%zu\n”, result);
   result = a == b;
   printf(“%zu\n”, result);
   result = a < b;
   printf(“%zu\n”, result);
   result = a - b;
   printf(“%zu\n”, result);
}

Tentu saja, program ini salah bentuk ketika a atau b tidak terdefinisi (baca: tidak diinisialisasi dengan benar ) pada titik pengujian, tetapi itu sama sekali tidak relevan dengan bagian diskusi kita ini. Cuplikan ini, seperti juga pernyataan berikut, dijamin - dengan 'standar' - untuk dikompilasi dan dijalankan dengan sempurna, meskipun IN- validitas pointer yang terlibat.

Masalah hanya muncul ketika pointer tidak valid ditereferensi . Ketika kami meminta Frank untuk mengambil atau mengirim di alamat yang tidak valid dan tidak ada.

Diberikan pointer sembarang:

int *p;

Sementara pernyataan ini harus mengkompilasi dan menjalankan:

printf(“%p”, p);

... sebagaimana mestinya:

size_t foo( int *p ) { return (size_t)p; }

... berikut dua, kontras, akan tetap mudah mengkompilasi, tetapi gagal di eksekusi kecuali pointer adalah sah - yang kita disini hanya berarti bahwa itu referensi alamat dimana aplikasi ini telah diberikan akses :

printf(“%p”, *p);
size_t foo( int *p ) { return *p; }

Seberapa halus perubahannya? Perbedaannya terletak pada perbedaan antara nilai pointer - yang merupakan alamat, dan nilai konten: dari rumah di nomor itu. Tidak ada masalah muncul sampai pointer dereferenced ; sampai upaya dilakukan untuk mengakses alamat yang ditautkan. Dalam mencoba mengirimkan atau mengambil paket di luar bentangan jalan ...

Dengan perluasan, prinsip yang sama harus berlaku untuk contoh yang lebih kompleks, termasuk kebutuhan yang disebutkan di atas untuk menetapkan validitas yang diperlukan:

int* validate( int *p, int *head, int *tail ) { 
    return p >= head && p <= tail ? p : NULL; 
}

Perbandingan relasional dan aritmatika menawarkan utilitas yang identik untuk menguji kesetaraan, dan pada prinsipnya valid - pada prinsipnya. Namun , apa hasil dari perhitungan seperti itu ditunjukkan, adalah masalah yang sama sekali berbeda - dan justru masalah yang dibahas oleh kutipan yang Anda sertakan.

Dalam C, array adalah buffer yang bersebelahan, sebuah rangkaian linear lokasi memori yang tidak terputus. Perbandingan dan aritmatika diterapkan pada petunjuk bahwa lokasi referensi dalam rangkaian singular semacam itu secara alami, dan jelas bermakna dalam kaitannya satu sama lain, dan dengan 'larik' ini (yang hanya diidentifikasi oleh pangkalan). Hal yang sama berlaku untuk setiap blok yang dialokasikan melalui malloc, atau sbrk. Karena hubungan ini implisit , kompiler dapat membangun hubungan yang valid di antara mereka, dan karena itu dapat yakin bahwa perhitungan akan memberikan jawaban yang diantisipasi.

Pertunjukan senam yang sama pada pointer yang referensi yang berbeda blok atau array tidak menawarkan apapun seperti yang melekat , dan jelas utilitas. Terlebih lagi karena hubungan apa pun yang ada pada satu saat dapat dibatalkan oleh realokasi yang mengikuti, di mana itu sangat mungkin berubah, bahkan dapat dibalik. Dalam kasus seperti itu kompiler tidak dapat memperoleh informasi yang diperlukan untuk membangun kepercayaan yang dimilikinya pada situasi sebelumnya.

Anda , bagaimanapun, sebagai programmer, mungkin memiliki pengetahuan seperti itu! Dan dalam beberapa kasus wajib mengeksploitasi itu.

Ada ADALAH , oleh karena itu, keadaan di mana BAHKAN INI sepenuhnya VALID dan sempurna PROPER.

Bahkan, itulah yang mallocharus dilakukan sendiri secara internal ketika saatnya tiba untuk mencoba menggabungkan blok reklamasi - pada sebagian besar arsitektur. Hal yang sama berlaku untuk pengalokasi sistem operasi, seperti itu di belakang sbrk; jika lebih jelas , sering , pada entitas yang lebih berbeda , lebih kritis - dan relevan juga pada platform di mana ini mallocmungkin tidak. Dan berapa banyak dari mereka yang tidak ditulis dalam C?

Validitas, keamanan, dan keberhasilan suatu tindakan tidak dapat dihindari adalah konsekuensi dari tingkat wawasan yang menjadi dasar pemikiran dan penerapannya.

Dalam kutipan yang Anda tawarkan, Kernighan dan Ritchie membahas masalah yang terkait erat, namun tetap terpisah. Mereka mendefinisikan yang keterbatasan dari bahasa , dan menjelaskan bagaimana Anda dapat memanfaatkan kemampuan compiler untuk melindungi Anda dengan setidaknya mendeteksi konstruksi berpotensi keliru. Mereka menggambarkan panjangnya mekanisme yang bisa - dirancang - untuk digunakan untuk membantu Anda dalam tugas pemrograman Anda. Kompiler adalah pelayan Anda , Anda adalah tuannya. Namun, seorang guru yang bijak adalah seorang yang akrab dengan kemampuan berbagai pelayannya.

Dalam konteks ini, perilaku tidak terdefinisi berfungsi untuk menunjukkan potensi bahaya dan kemungkinan bahaya; bukan untuk menyiratkan malapetaka yang sudah dekat, ireversibel, atau akhir dunia seperti yang kita kenal. Ini hanya berarti bahwa kita - 'yang berarti kompiler' - tidak dapat membuat dugaan tentang apa hal ini mungkin, atau mewakili dan karena alasan ini kami memilih untuk mencuci tangan masalah ini. Kami tidak akan bertanggung jawab atas kesalahan yang mungkin terjadi akibat penggunaan, atau salah penggunaan fasilitas ini .

Akibatnya, ia hanya mengatakan: 'Di luar titik ini, koboi : Anda sendirian ...'

Profesor Anda berusaha menunjukkan nuansa yang lebih halus kepada Anda.

Perhatikan betapa hati - hati mereka dalam membuat contoh mereka; dan bagaimana rapuh itu masih adalah. Dengan mengambil alamat a, di

p[0].p0 = &a;

kompiler dipaksa untuk mengalokasikan penyimpanan aktual untuk variabel, daripada menempatkannya dalam register. Ini menjadi variabel otomatis, namun, programmer tidak memiliki kendali atas tempat yang ditugaskan, dan karenanya tidak dapat membuat dugaan yang valid tentang apa yang akan mengikutinya. Itulah sebabnya a harus ditetapkan sama dengan nol agar kode berfungsi seperti yang diharapkan.

Hanya mengubah baris ini:

char a = 0;

untuk ini:

char a = 1;  // or ANY other value than 0

menyebabkan perilaku program menjadi tidak terdefinisi . Minimal, jawaban pertama sekarang adalah 1; tetapi masalahnya jauh lebih jahat.

Sekarang kode mengundang bencana.

Meskipun masih benar-benar valid dan bahkan sesuai dengan standar , itu sekarang tidak terbentuk dan meskipun yakin untuk dikompilasi, mungkin gagal dalam eksekusi dengan berbagai alasan. Untuk saat ini ada beberapa masalah - tidak ada dimana compiler adalah mampu untuk mengenali.

strcpyakan mulai dari alamat a, dan melanjutkan melampaui ini untuk mengkonsumsi - dan mentransfer - byte demi byte, sampai bertemu dengan nol.

The p1pointer telah diinisialisasi ke blok tepat 10 bytes.

  • Jika akebetulan ditempatkan di ujung blok dan proses tidak memiliki akses ke yang berikut, pembacaan berikutnya - dari p0 [1] - akan memperoleh segfault. Skenario ini tidak mungkin pada arsitektur x86, tetapi dimungkinkan.

  • Jika area di luar alamat a dapat diakses, tidak akan terjadi kesalahan baca, tetapi program masih belum disimpan dari kemalangan.

  • Jika nol byte terjadi dalam sepuluh dimulai pada alamat a, itu masih dapat bertahan, karena itu strcpyakan berhenti dan setidaknya kita tidak akan menderita pelanggaran tulis.

  • Jika tidak salah untuk membaca salah, tetapi tidak ada byte nol terjadi dalam rentang 10 ini, strcpyakan terus berlanjut dan berusaha untuk menulis di luar blok yang dialokasikan oleh malloc.

    • Jika area ini tidak dimiliki oleh proses, segfault harus segera dipicu.

    • Masih lebih bencana - dan halus situasi --- muncul ketika blok berikut ini dimiliki oleh proses, untuk maka kesalahan tidak dapat dideteksi, tidak ada sinyal dapat diangkat, dan sehingga mungkin 'muncul' masih 'bekerja' , sementara itu sebenarnya akan menimpa data lain, struktur manajemen pengalokasi Anda, atau bahkan kode (dalam lingkungan operasi tertentu).

Ini adalah mengapa pointer terkait bug bisa begitu sulit untuk melacak . Bayangkan baris-baris ini terkubur dalam ribuan baris kode terkait yang rumit, yang telah ditulis orang lain, dan Anda diarahkan untuk menyelidiki.

Meskipun demikian , program tersebutmasih harus dikompilasi, karena tetap valid sempurna dan sesuai standar C.

Jenis kesalahan ini, tidak ada standar dan tidak ada kompiler dapat melindungi mereka yang tidak waspada. Saya membayangkan itulah yang ingin mereka ajarkan kepada Anda.

Orang paranoid terus berusaha untuk mengubah dengan sifat dari C untuk membuang kemungkinan-kemungkinan bermasalah dan menyelamatkan kita dari diri kita sendiri; tapi itu tidak jujur . Ini adalah tanggung jawab yang harus kita terima ketika kita memilih untuk mengejar kekuasaan dan memperoleh kebebasan yang ditawarkan kontrol mesin yang lebih langsung dan komprehensif . Promotor dan pengejar kesempurnaan dalam kinerja tidak akan pernah menerima apa pun yang kurang.

Portabilitas dan sifat umum yang diwakilinya merupakan pertimbangan yang terpisah secara mendasar dan semua yangberusaha ditangani oleh standar :

Dokumen ini menentukan bentuk dan menetapkan interpretasi program yang diekspresikan dalam bahasa pemrograman C. Tujuannya adalah untuk mempromosikan portabilitas , keandalan, perawatan, dan pelaksanaan program bahasa C yang efisien pada berbagai sistem komputasi .

Itulah sebabnya sangat tepat untuk membedakannya dari definisi dan spesifikasi teknis bahasa itu sendiri. Bertentangan dengan apa yang banyak orang percayai, generalitas adalah antitesis terhadap pengecualian dan keteladanan .

Untuk menyimpulkan:

  • Memeriksa dan memanipulasi pointer sendiri selalu valid dan sering bermanfaat . Interpretasi hasil, mungkin, atau mungkin tidak bermakna, tetapi malapetaka tidak pernah diundang sampai penunjuk dereferensi ; hingga upaya dilakukan untuk mengakses alamat yang ditautkan.

Kalau ini tidak benar, pemrograman seperti yang kita tahu - dan menyukainya - tidak akan mungkin terjadi.

Ghii Velte
sumber
3
Sayangnya jawaban ini secara inheren tidak valid. Anda tidak dapat memberi alasan apa pun tentang perilaku tidak terdefinisi. Perbandingan tidak perlu dilakukan di tingkat mesin.
Antti Haapala
6
Ghii, sebenarnya tidak. Jika Anda melihat C11 Annex J dan 6.5.8, tindakan perbandingan itu sendiri adalah UB. Dereferencing adalah masalah terpisah.
paxdiablo
6
Tidak, UB masih bisa berbahaya bahkan sebelum sebuah pointer direferensikan. Kompiler bebas untuk sepenuhnya mengoptimalkan fungsi dengan UB menjadi satu NOP tunggal, meskipun ini jelas mengubah perilaku yang terlihat.
nanofarad
2
@Ghii, Annex J (bit yang saya sebutkan) adalah daftar hal-hal yang merupakan perilaku yang tidak terdefinisi , jadi saya tidak yakin bagaimana itu mendukung argumen Anda :-) 6.5.8 secara eksplisit menyebut perbandingan sebagai UB. Untuk komentar Anda ke supercat, tidak ada perbandingan yang terjadi ketika Anda mencetak pointer sehingga Anda mungkin benar bahwa itu tidak akan crash. Tapi bukan itu yang ditanyakan OP. 3.4.3juga merupakan bagian yang harus Anda perhatikan: ini mendefinisikan UB sebagai perilaku "yang tidak ada persyaratan Standar Internasional ini ".
paxdiablo
3
@GhiiVelte, Anda terus menyatakan hal-hal yang jelas-jelas salah, meskipun itu ditunjukkan kepada Anda. Ya, cuplikan yang Anda poskan harus dikompilasi tetapi anggapan Anda bahwa itu berjalan tanpa hambatan adalah salah. Saya sarankan Anda benar-benar membaca standar, terutama (dalam hal ini) C11 6.5.6/9, dengan mengingat bahwa kata "wajib" menunjukkan persyaratanL "Ketika dua pointer dikurangi, keduanya akan menunjuk ke elemen dari objek array yang sama, atau satu melewati yang terakhir elemen dari objek array ".
paxdiablo
-5

Pointer hanya bilangan bulat, seperti yang lainnya di komputer. Anda benar-benar dapat membandingkannya dengan <dan >dan menghasilkan hasil tanpa menyebabkan program mogok. Yang mengatakan, standar tidak menjamin bahwa hasil tersebut memiliki arti luar perbandingan array.

Dalam contoh Anda variabel alokasi stack, kompiler bebas untuk mengalokasikan variabel-variabel tersebut ke register atau alamat memori stack, dan dalam urutan apa pun itu pilih. Perbandingan seperti <dan >karenanya tidak akan konsisten di seluruh kompiler atau arsitektur. Namun, ==dan !=tidak begitu dibatasi, membandingkan kesetaraan pointer adalah operasi yang valid dan bermanfaat.

nickelpro
sumber
2
Kata stack muncul persis nol kali dalam standar C11. Dan perilaku tidak terdefinisi berarti apa pun bisa terjadi (termasuk crash program).
paxdiablo
1
@ paxdiablo Apakah saya mengatakan itu?
nickelpro
2
Anda menyebutkan variabel yang dialokasikan stack. Tidak ada tumpukan dalam standar, itu hanya detail implementasi. Masalah yang lebih serius dengan jawaban ini adalah anggapan Anda dapat membandingkan pointer dengan tidak ada kemungkinan crash - itu hanya salah.
paxdiablo
1
@nickelpro: Jika seseorang ingin menulis kode yang kompatibel dengan pengoptimal dalam gcc dan dentang, perlu untuk melewati banyak lingkaran konyol. Kedua pengoptimal akan secara agresif mencari peluang untuk menarik kesimpulan tentang hal-hal apa yang akan diakses oleh pointer setiap kali ada cara Standar dapat diputar untuk membenarkan mereka (dan bahkan kadang-kadang ketika tidak ada). Diberikan int x[10],y[10],*p;, jika kode mengevaluasi y[0], kemudian mengevaluasi p>(x+5)dan menulis *ptanpa memodifikasi puntuk sementara, dan akhirnya mengevaluasi y[0]lagi, ...
supercat
1
nickelpro, setuju untuk setuju untuk tidak setuju tetapi jawaban Anda pada dasarnya masih salah. Saya menyamakan pendekatan Anda dengan pendekatan orang-orang yang menggunakan (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')alih-alih isalpha()karena implementasi waras apa yang membuat karakter-karakter itu terputus? Intinya adalah bahwa, bahkan jika tidak ada implementasi yang Anda tahu memiliki masalah, Anda harus membuat kode sebanyak mungkin jika Anda menghargai portabilitas. Saya sangat menghargai label "standar pakar", terima kasih untuk itu. Saya dapat memasukkan CV saya :-)
paxdiablo