Mengapa operator panah (->) di C ada?

264

Operator dot ( .) digunakan untuk mengakses anggota struct, sedangkan operator panah ( ->) di C digunakan untuk mengakses anggota struct yang dirujuk oleh pointer yang dimaksud.

Pointer itu sendiri tidak memiliki anggota yang dapat diakses dengan operator titik (sebenarnya hanya angka yang menggambarkan lokasi dalam memori virtual sehingga tidak memiliki anggota). Jadi, tidak akan ada ambiguitas jika kita hanya mendefinisikan operator dot untuk secara otomatis melakukan dereferensi pointer jika digunakan pada sebuah pointer (informasi yang diketahui oleh kompiler pada waktu kompilasi afaik).

Jadi mengapa para pembuat bahasa memutuskan untuk membuat hal-hal lebih rumit dengan menambahkan operator yang tampaknya tidak perlu ini? Apa keputusan desain besar?

Askaga
sumber
1
Terkait: stackoverflow.com/questions/221346/… - juga, Anda dapat mengganti ->
Krease
16
@ Chris Yang itu tentang C ++ yang tentu saja membuat perbedaan besar. Tetapi karena kita berbicara tentang mengapa C dirancang dengan cara ini, mari kita berpura-pura kembali ke tahun 1970-an - sebelum C ++ ada.
Mysticial
5
Tebakan terbaik saya adalah, bahwa operator panah ada untuk mengekspresikan secara visual "awas! Anda berurusan dengan pointer di sini"
Chris
4
Sekilas, saya merasa pertanyaan ini sangat aneh. Tidak semua hal dirancang dengan baik. Jika Anda mempertahankan gaya ini sepanjang hidup Anda, dunia Anda akan penuh dengan pertanyaan. Jawaban yang mendapat suara terbanyak sangat informatif dan jelas. Tetapi itu tidak menyentuh titik kunci dari pertanyaan Anda. Ikuti gaya pertanyaan Anda, saya bisa mengajukan terlalu banyak pertanyaan. Misalnya, kata kunci 'int' adalah singkatan dari 'integer'; mengapa kata kunci 'ganda' juga lebih pendek?
junwanghe
1
@junwanghe Pertanyaan ini sebenarnya merupakan keprihatinan yang sah - mengapa .operator memiliki prioritas lebih tinggi daripada *operator? Jika tidak, kita dapat memiliki * ptr.member dan var.member.
milleniumbug

Jawaban:

358

Saya akan menginterpretasikan pertanyaan Anda sebagai dua pertanyaan: 1) mengapa ->ada, dan 2) mengapa .tidak secara otomatis mengubah referensi pointer. Jawaban untuk kedua pertanyaan memiliki akar sejarah.

Mengapa ->bahkan ada?

Dalam salah satu versi bahasa C yang paling pertama (yang akan saya rujuk sebagai CRM untuk " Manual Referensi C ", yang datang dengan Unix Edisi ke-6 pada bulan Mei 1975), operator ->memiliki makna yang sangat eksklusif, tidak identik dengan *dan .kombinasi

Bahasa C yang dijelaskan oleh CRM sangat berbeda dari bahasa C modern dalam banyak hal. Dalam CRM struct, anggota menerapkan konsep global byte offset , yang dapat ditambahkan ke nilai alamat apa pun tanpa batasan jenis. Yaitu semua nama semua anggota struct memiliki makna global independen (dan, oleh karena itu, harus unik). Misalnya Anda bisa mendeklarasikan

struct S {
  int a;
  int b;
};

dan nama aakan berarti offset 0, sedangkan nama bakan mewakili offset 2 (dengan asumsi inttipe ukuran 2 dan tanpa bantalan). Bahasa mengharuskan semua anggota semua struct di unit terjemahan memiliki nama yang unik atau memiliki nilai offset yang sama. Misalnya dalam unit terjemahan yang sama Anda juga dapat mendeklarasikan

struct X {
  int a;
  int x;
};

dan itu akan baik-baik saja, karena namanya aakan secara konsisten berarti offset 0. Tapi deklarasi tambahan ini

struct Y {
  int b;
  int a;
};

akan secara resmi tidak valid, karena berusaha "mendefinisikan kembali" asebagai offset 2 dan bsebagai offset 0.

Dan di sinilah ->operator masuk. Karena setiap nama anggota struct memiliki makna global mandiri, bahasa mendukung ekspresi seperti ini

int i = 5;
i->b = 42;  /* Write 42 into `int` at address 7 */
100->a = 0; /* Write 0 into `int` at address 100 */

Tugas pertama ditafsirkan oleh kompiler sebagai "ambil alamat 5, tambahkan offset 2ke sana dan tetapkan 42ke intnilai di alamat yang dihasilkan". Yaitu di atas akan menugaskan 42untuk intnilai di alamat 7. Perhatikan bahwa penggunaan ->ini tidak peduli dengan jenis ekspresi di sisi kiri. Sisi kiri ditafsirkan sebagai nilai numerik alamat (baik itu pointer atau integer).

Semacam ini tipu daya itu tidak mungkin dengan *dan .kombinasi. Anda tidak bisa melakukannya

(*i).b = 42;

karena *isudah merupakan ekspresi yang tidak valid. The *operator, karena terpisah dari ., memberlakukan persyaratan jenis yang lebih ketat pada operand. Untuk memberikan kemampuan untuk mengatasi keterbatasan ini, CRM memperkenalkan ->operator, yang independen dari jenis operan di sebelah kiri.

Seperti yang disebutkan Keith dalam komentar, perbedaan antara ->dan *+ .kombinasi inilah yang disebut CRM sebagai "pelonggaran persyaratan" dalam 7.1.8: Kecuali untuk pelonggaran persyaratan yang E1bertipe pointer, ekspresi E1−>MOSpersis sama dengan(*E1).MOS

Kemudian, di K&R C banyak fitur yang awalnya dijelaskan dalam CRM secara signifikan ulang. Gagasan "struct member sebagai global offset identifier" sepenuhnya dihapus. Dan fungsi ->operator menjadi sepenuhnya identik dengan fungsi *dan .kombinasi.

Mengapa .dereference pointer tidak bisa secara otomatis?

Sekali lagi, dalam versi CRM bahasa operan kiri .Operator diperlukan untuk menjadi lvalue . Itulah satu - satunya persyaratan yang dikenakan pada operan itu (dan itulah yang membuatnya berbeda dari ->, seperti dijelaskan di atas). Perhatikan bahwa CRM tidak memerlukan operan kiri .untuk memiliki tipe struct. Itu hanya diperlukan untuk menjadi nilai, nilai apa pun . Ini berarti bahwa dalam versi CRM C Anda dapat menulis kode seperti ini

struct S { int a, b; };
struct T { float x, y, z; };

struct T c;
c.b = 55;

Dalam hal ini kompiler akan menulis 55ke intnilai yang diposisikan pada byte-offset 2 di blok memori kontinu yang dikenal sebagai c, meskipun tipe struct Ttidak memiliki bidang bernama b. Kompiler tidak akan peduli dengan tipe yang sebenarnya csama sekali. Yang dipedulikannya chanyalah nilai tinggi: semacam blok memori yang bisa ditulis.

Sekarang perhatikan bahwa jika Anda melakukan ini

S *s;
...
s.b = 42;

kode akan dianggap valid (karena sjuga merupakan lvalue) dan kompiler hanya akan mencoba untuk menulis data ke dalam pointer situ sendiri , pada byte-offset 2. Tidak perlu dikatakan, hal-hal seperti ini dapat dengan mudah mengakibatkan memori overrun, tetapi bahasa tidak peduli dengan masalah seperti itu.

Yaitu dalam versi bahasa ide yang Anda usulkan tentang operator overloading .untuk tipe pointer tidak akan berfungsi: operator .sudah memiliki makna yang sangat spesifik ketika digunakan dengan pointer (dengan pointer nilai atau dengan nilai apa pun sama sekali). Fungsionalitasnya sangat aneh, tidak diragukan lagi. Tetapi itu ada di sana pada saat itu.

Tentu saja, fungsi yang aneh ini bukan alasan yang sangat kuat untuk tidak memperkenalkan .operator kelebihan beban untuk pointer (seperti yang Anda sarankan) dalam versi ulang C - K&R C. Tapi itu belum dilakukan. Mungkin pada waktu itu ada beberapa kode lama yang ditulis dalam versi CRM C yang harus didukung.

(URL untuk Manual Referensi C 1975 mungkin tidak stabil. Salinan lain, mungkin dengan beberapa perbedaan halus, ada di sini .)

AnT
sumber
10
Dan bagian 7.1.8 dari C Reference Manual yang dikutip mengatakan, "Kecuali untuk relaksasi persyaratan bahwa E1 adalah tipe penunjuk, ungkapan '' E1−> MOS '' persis sama dengan '' (* E1) .MOS ' '. "
Keith Thompson
1
Mengapa itu tidak *imenjadi nilai dari beberapa tipe default (int?) Di alamat 5? Maka (* i) .b akan bekerja dengan cara yang sama.
Random832
5
@ Leo: Ya, beberapa orang menyukai bahasa C sebagai assembler tingkat tinggi. Pada periode itu dalam sejarah C bahasa sebenarnya adalah assembler tingkat yang lebih tinggi.
AnT
29
Hah. Jadi ini menjelaskan mengapa banyak struktur di UNIX (misalnya, struct stat) awali bidangnya (misalnya, st_mode).
icktoofay
5
@ perfectionm1ng: Sepertinya bell-labs.com telah diambil alih oleh Alcatel-Lucent dan halaman aslinya hilang. Saya memperbarui tautan ke situs lain, meskipun saya tidak bisa mengatakan berapa lama akan tetap terjaga. Lagi pula, googling untuk "manual ritchie c reference" biasanya menemukan dokumen.
AnT
46

Di luar alasan historis (baik dan sudah dilaporkan), ada juga sedikit masalah dengan prioritas operator: operator titik memiliki prioritas lebih tinggi daripada operator bintang, jadi jika Anda memiliki pointer yang berisi pointer ke struct yang berisi pointer ke struct ... Keduanya setara:

(*(*(*a).b).c).d

a->b->c->d

Tapi yang kedua jelas lebih mudah dibaca. Operator panah memiliki prioritas tertinggi (sama seperti titik) dan rekan kiri ke kanan. Saya pikir ini lebih jelas daripada menggunakan operator titik baik untuk pointer ke struct dan struct, karena kita tahu tipe dari ekspresi tanpa harus melihat pada deklarasi, yang bahkan bisa di file lain.

effeffe
sumber
2
Dengan tipe data bersarang yang berisi struct dan pointer ke struct, ini dapat membuat hal-hal lebih sulit karena Anda harus berpikir tentang memilih operator yang tepat untuk setiap akses submember. Anda mungkin berakhir dengan ab-> c-> d atau a-> bc-> d (saya mengalami masalah ini saat menggunakan perpustakaan jenis bebas - saya harus mencari kode sumbernya sepanjang waktu). Ini juga tidak menjelaskan mengapa kompiler tidak dapat membiarkan dereference pointer secara otomatis ketika berhadapan dengan pointer.
Askaga
3
Meskipun fakta yang Anda nyatakan benar, mereka tidak menjawab pertanyaan asli saya dengan cara apa pun. Anda menjelaskan persamaan a-> dan * (a). notasi (yang telah dijelaskan berulang kali dalam pertanyaan lain) serta memberikan pernyataan yang tidak jelas tentang desain bahasa yang agak arbitrer. Saya tidak menemukan jawaban Anda sangat membantu, oleh karena itu downvote.
Askaga
16
@effeffe, OP mengatakan bahwa bahasa tersebut dapat dengan mudah diartikan a.b.c.dsebagai (*(*(*a).b).c).d, membuat ->operator tidak berguna. Jadi versi OP ( a.b.c.d) sama-sama dapat dibaca (dibandingkan dengan a->b->c->d). Itu sebabnya jawaban Anda tidak menjawab pertanyaan OP.
Shahbaz
4
@ Shahbaz Itu mungkin kasus untuk programmer java, seorang programmer C / C ++ akan memahami a.b.c.ddan a->b->c->dsebagai dua hal yang sangat berbeda: Yang pertama adalah akses memori tunggal ke sub-objek bersarang (hanya ada satu objek memori tunggal dalam kasus ini ), yang kedua adalah tiga akses memori, mengejar pointer melalui empat objek yang berbeda. Itu perbedaan besar dalam tata letak memori, dan saya percaya bahwa C benar dalam membedakan kedua kasus ini dengan sangat jelas.
cmaster - mengembalikan monica
2
@ Shahbaz Saya tidak bermaksud bahwa sebagai penghinaan dari programmer java, mereka hanya digunakan untuk bahasa dengan pointer sepenuhnya implisit. Seandainya saya dibesarkan sebagai programmer java, saya mungkin akan berpikir dengan cara yang sama ... Bagaimanapun, saya benar-benar berpikir bahwa operator kelebihan yang kita lihat di C kurang optimal. Namun, saya mengakui bahwa kita semua telah dimanjakan oleh para ahli matematika yang secara bebas membebani operator mereka untuk hampir semua hal. Saya juga memahami motivasi mereka, karena rangkaian simbol yang tersedia agak terbatas. Saya kira, pada akhirnya itu hanya pertanyaan di mana Anda menarik garis ...
cmaster - mengembalikan monica
19

C juga melakukan pekerjaan dengan baik karena tidak membuat sesuatu yang ambigu.

Tentu saja dot bisa kelebihan beban artinya dua hal, tetapi panah memastikan bahwa programmer tahu bahwa dia beroperasi pada pointer, seperti ketika kompiler tidak akan membiarkan Anda mencampur dua jenis yang tidak kompatibel.

mukunda
sumber
4
Ini adalah jawaban yang sederhana dan benar. C kebanyakan mencoba untuk menghindari kelebihan muatan yang IMO adalah salah satu hal terbaik tentang C.
jforberg
10
Banyak hal dalam C yang ambigu dan kabur. Ada konversi tipe implisit, operator matematika kelebihan beban, pengindeksan berantai melakukan sesuatu yang sama sekali berbeda tergantung pada apakah Anda mengindeks array multidimensi atau array pointer dan apa pun bisa menjadi makro menyembunyikan apa pun (konvensi penamaan huruf besar membantu di sana tetapi C tidak t).
PSkocik