ทำไมโอเปอเรเตอร์ลูกศร (->) ใน C จึงมีอยู่


264

ตัวดำเนินการ dot ( .) ใช้เพื่อเข้าถึงสมาชิกของ struct ในขณะที่โอเปอเรเตอร์ arrow ( ->) ใน C ใช้เพื่อเข้าถึงสมาชิกของ struct ที่อ้างอิงโดยตัวชี้ที่เป็นปัญหา

ตัวชี้ไม่มีสมาชิกใด ๆ ที่สามารถเข้าถึงได้ด้วยตัวดำเนินการ dot (จริง ๆ แล้วเป็นเพียงตัวเลขที่อธิบายตำแหน่งในหน่วยความจำเสมือนดังนั้นจึงไม่มีสมาชิกใด ๆ ) ดังนั้นจะไม่มีความกำกวมหากเราเพิ่งกำหนดตัวดำเนินการจุดเพื่อยกเลิกการลงทะเบียนตัวชี้โดยอัตโนมัติหากมีการใช้งานบนตัวชี้

เหตุใดผู้สร้างภาษาจึงตัดสินใจสร้างสิ่งที่ซับซ้อนมากขึ้นด้วยการเพิ่มโอเปอเรเตอร์ที่ไม่จำเป็น การตัดสินใจออกแบบครั้งใหญ่คืออะไร?


1
ที่เกี่ยวข้อง: stackoverflow.com/questions/221346/ … - นอกจากนี้คุณยังสามารถแทนที่ ->
Krease

16
@ Chris นั่นเกี่ยวกับ C ++ ซึ่งแน่นอนว่าสร้างความแตกต่างอย่างมาก แต่เนื่องจากเรากำลังพูดถึงว่าทำไม C จึงได้รับการออกแบบด้วยวิธีนี้เราจะแกล้งทำเป็นว่าเราย้อนกลับไปในทศวรรษ 1970 ก่อนที่ C ++ จะมีอยู่จริง
Mysticial

5
เดาที่ดีที่สุดของฉันคือว่าผู้ประกอบการที่ลูกศรที่มีอยู่เพื่อแสดงสายตา "ดูมันคุณซื้อขายอยู่กับตัวชี้นี่!"
คริส

4
ฉันรู้สึกว่าคำถามนี้แปลกมาก ไม่ใช่ทุกสิ่งที่ได้รับการออกแบบอย่างพิถีพิถัน หากคุณรักษาสไตล์นี้ไว้ตลอดชีวิตของคุณโลกของคุณจะเต็มไปด้วยคำถาม คำตอบที่ได้รับการโหวตมากที่สุดนั้นให้ข้อมูลและชัดเจนจริงๆ แต่มันก็ไม่ได้ทำให้ประเด็นสำคัญของคำถามของคุณ ทำตามรูปแบบคำถามของคุณฉันสามารถถามคำถามมากเกินไป ตัวอย่างเช่นคำหลัก 'int' คือตัวย่อของ 'จำนวนเต็ม' ทำไมคำหลัก 'คู่' ไม่สั้นลงเช่นกัน
Junwanghe

1
@junwanghe คำถามนี้แสดงให้เห็นถึงความกังวลที่ถูกต้อง - ทำไม.ผู้ประกอบการจึงมีความสำคัญมากกว่า*ผู้ประกอบการ? หากไม่เป็นเช่นนั้นเราสามารถมี * ptr.member และ var.member
milleniumbug

คำตอบ:


358

ฉันจะตีความคำถามของคุณเป็นสองคำถาม: 1) ทำไม->ถึงมีอยู่และ 2) เหตุใดจึง.ไม่อ่านตัวชี้โดยอัตโนมัติ คำตอบของคำถามทั้งสองนั้นมีรากฐานทางประวัติศาสตร์

ทำไม->ถึงมีอยู่จริง?

ในหนึ่งในภาษา C รุ่นแรก (ซึ่งฉันจะเรียกว่า CRM สำหรับ " คู่มืออ้างอิง C " ซึ่งมาพร้อมกับรุ่นที่ 6 Unix ในเดือนพฤษภาคมปี 1975) ผู้ประกอบการ->มีความหมายที่พิเศษมากไม่เหมือนกัน*และ.ผสมผสานกัน

ภาษา C ที่อธิบายโดย CRM นั้นแตกต่างจาก C สมัยใหม่อย่างมากหลายประการ ในสมาชิก struct CRM นำแนวคิดสากลของการชดเชยไบต์ซึ่งสามารถเพิ่มไปยังค่าที่อยู่ใด ๆ โดยไม่มีข้อ จำกัด ประเภท คือชื่อทั้งหมดของสมาชิก struct ทั้งหมดมีความหมายระดับโลกที่เป็นอิสระ (และดังนั้นจึงต้องไม่ซ้ำกัน) ตัวอย่างเช่นคุณสามารถประกาศ

struct S {
  int a;
  int b;
};

และชื่อaจะหมายถึงออฟเซ็ต 0 ในขณะที่ชื่อbจะย่อมาจากออฟเซ็ต 2 (สมมติว่าเป็นintประเภทของขนาด 2 และไม่มีการเว้นวรรค) ภาษาที่ต้องการสมาชิกทั้งหมดของ structs ทั้งหมดในหน่วยการแปลอาจมีชื่อเฉพาะหรือยืนสำหรับค่าออฟเซ็ตเดียวกัน เช่นในหน่วยการแปลเดียวกันคุณสามารถประกาศเพิ่มเติมได้

struct X {
  int a;
  int x;
};

และนั่นก็โอเคเนื่องจากชื่อaนั้นจะตรงข้ามกับออฟเซต 0 แต่การประกาศเพิ่มเติมนี้

struct Y {
  int b;
  int a;
};

จะไม่ถูกต้องอย่างเป็นทางการเนื่องจากพยายาม "กำหนดใหม่" aเป็นออฟเซ็ต 2 และbออฟเซ็ต 0

และนี่คือที่ที่->โอเปอเรเตอร์เข้ามาเนื่องจากชื่อสมาชิก struct ทุกคนมีความหมายพอเพียงทั่วโลกภาษาที่รองรับการแสดงออกเช่นนี้

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

การแปลครั้งแรกถูกแปลโดยคอมไพเลอร์ว่า "รับที่อยู่5เพิ่มออฟเซ็ต2และกำหนด42ให้กับintค่าที่ที่อยู่ผลลัพธ์" คือข้างต้นจะกำหนด42ที่จะคุ้มค่าที่อยู่int 7โปรดทราบว่าการใช้งานนี้->ไม่ได้ใส่ใจเกี่ยวกับประเภทของการแสดงออกทางด้านซ้าย ด้านซ้ายมือถูกตีความว่าเป็นที่อยู่ตัวเลข rvalue (ไม่ว่าจะเป็นตัวชี้หรือจำนวนเต็ม)

กลอุบายแบบนี้เป็นไปไม่ได้ด้วย*และ.ผสมผสาน คุณทำไม่ได้

(*i).b = 42;

เนื่องจาก*iเป็นนิพจน์ที่ไม่ถูกต้องอยู่แล้ว ตัว*ดำเนินการเนื่องจากถูกแยกจากจะ.กำหนดข้อกำหนดชนิดที่เข้มงวดมากขึ้นบนตัวถูกดำเนินการ เพื่อให้ความสามารถในการแก้ไขข้อ จำกัด นี้ CRM แนะนำ->ผู้ประกอบการซึ่งเป็นอิสระจากประเภทของตัวถูกดำเนินการทางซ้าย

ดังที่ Keith ระบุไว้ในความคิดเห็นความแตกต่างระหว่าง->และชุดค่าผสม*+ .นี้คือสิ่งที่ CRM อ้างถึงว่า "การผ่อนคลายข้อกำหนด" ใน 7.1.8: ยกเว้นการผ่อนคลายข้อกำหนดที่E1เป็นประเภทตัวชี้การแสดงออกE1−>MOSนั้นเทียบเท่ากับ(*E1).MOS

ต่อมาใน K&R C คุณสมบัติหลายอย่างที่อธิบายไว้ใน CRM ได้รับการทำใหม่อย่างมีนัยสำคัญ แนวคิดของ "struct member เป็น global identifier" ถูกลบออกอย่างสมบูรณ์ และฟังก์ชั่นการใช้งานของ->ผู้ปฏิบัติงานก็เหมือนกันกับฟังก์ชั่น*และการ.รวมกัน

เหตุใดจึงไม่.อ่านตัวชี้โดยอัตโนมัติได้

อีกครั้งในรุ่น CRM ของภาษาถูกดำเนินการด้านซ้ายของ.ผู้ประกอบการจะต้องเป็นlvalue นั่นเป็นข้อกำหนดเพียงข้อเดียวที่กำหนดไว้ในตัวถูกดำเนินการ (และนั่นคือสิ่งที่ทำให้แตกต่างจาก->ที่อธิบายไว้ข้างต้น) โปรดทราบว่า CRM ไม่ต้องการให้ตัวถูกดำเนินการด้านซ้าย.มีประเภท struct มันแค่ต้องการให้มันเป็น lvalue, lvalue ใด ๆ ซึ่งหมายความว่าในรุ่น CRM ของ C คุณสามารถเขียนโค้ดแบบนี้ได้

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

struct T c;
c.b = 55;

ในกรณีนี้คอมไพเลอร์จะเขียน55เป็นintมูลค่าในตำแหน่งที่ไบต์ชดเชยที่ 2 ในบล็อกหน่วยความจำอย่างต่อเนื่องที่รู้จักในฐานะcแม้ว่าประเภทได้ข้อมูลไม่มีชื่อstruct T bคอมไพเลอร์จะไม่สนใจเกี่ยวกับชนิดที่แท้จริงของcเลย สิ่งที่มันสนใจก็cคือ lvalue: บล็อกหน่วยความจำที่เขียนได้บางประเภท

ตอนนี้ทราบว่าถ้าคุณทำเช่นนี้

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

รหัสจะถือว่าถูกต้อง (เนื่องจากsยังเป็น lvalue) และคอมไพเลอร์จะพยายามเขียนข้อมูลลงในตัวชี้sด้วยตัวเองที่ byte-offset 2 ไม่จำเป็นต้องพูดสิ่งต่าง ๆ เช่นนี้อาจส่งผลให้หน่วยความจำล้น แต่ภาษา ไม่ได้กังวลกับเรื่องดังกล่าว

นั่นคือภาษาที่คุณเสนอเกี่ยวกับการใช้งานมากเกินไป.สำหรับตัวชี้ประเภทนั้นจะไม่ทำงาน: โอเปอเรเตอร์.มีความหมายเฉพาะเจาะจงมากเมื่อใช้กับพอยน์เตอร์ (ที่มีพอยน์เตอร์ lvalue หรือ lvalues ​​ใด ๆ เลย) มันเป็นฟังก์ชั่นที่แปลกมากอย่างไม่ต้องสงสัย แต่มันอยู่ที่นั่นในเวลา

แน่นอนว่าฟังก์ชั่นแปลก ๆ นี้ไม่ได้เป็นเหตุผลที่ดีนักในการแนะนำตัว.ดำเนินการโอเวอร์โหลดสำหรับตัวชี้ (ตามที่คุณแนะนำ) ในเวอร์ชัน C - K&R C. ที่นำกลับมาทำใหม่ แต่ยังไม่ได้ทำ บางทีในเวลานั้นอาจมีรหัสดั้งเดิมที่เขียนในเวอร์ชัน CRM ของ C ที่ต้องรองรับ

(URL สำหรับคู่มืออ้างอิง 1975 C อาจไม่เสถียรสำเนาอื่นอาจมีความแตกต่างเล็กน้อยอยู่ที่นี่ )


10
และส่วน 7.1.8 ของคู่มืออ้างอิง C ที่อ้างถึงกล่าวว่า "ยกเว้นการผ่อนคลายข้อกำหนดที่ E1 เป็นประเภทตัวชี้การแสดงออก '' E1−> MOS '' จะเทียบเท่ากับ '' (* E1) .MOS 'อย่างแน่นอน '."
Keith Thompson

1
เหตุใดจึงไม่*iเป็นค่าเริ่มต้นบางชนิด (int?) ที่ที่อยู่ 5 จากนั้น (* i) .b จะทำงานในลักษณะเดียวกัน
Random832

5
@Leo: บางคนคิดว่าภาษา C เป็นแอสเซมเบลอร์ระดับสูงกว่า ในช่วงเวลานั้นในประวัติศาสตร์ C ภาษาจริง ๆ แล้วเป็นแอสเซมเบลอร์ระดับสูงขึ้นอย่างแน่นอน
AnT

29
ฮะ. ดังนั้นสิ่งนี้จึงอธิบายได้ว่าทำไมโครงสร้างจำนวนมากใน UNIX (เช่นstruct stat) นำหน้าฟิลด์ของพวกเขา (เช่นst_mode)
icktoofay

5
@ perfectionm1ng: ดูเหมือนว่า bell-labs.com ถูกยึดครองโดย Alcatel-Lucent และหน้าต้นฉบับหายไป ฉันอัพเดทลิงค์ไปยังเว็บไซต์อื่นแม้ว่าฉันจะไม่สามารถบอกได้ว่าเว็บไซต์นั้นจะอยู่นานแค่ไหน อย่างไรก็ตาม googling สำหรับ "ritchie c manual manual" มักจะพบเอกสาร
AnT

46

นอกเหนือจากเหตุผลในอดีต (ดีและรายงานไปแล้ว) แล้วยังมีปัญหาเล็กน้อยเกี่ยวกับตัวดำเนินการลำดับความสำคัญ: ตัวดำเนินการ dot มีลำดับความสำคัญสูงกว่าตัวดำเนินการดาวดังนั้นหากคุณมี struct ที่มีตัวชี้ไปยัง struct ที่มีตัวชี้ไปยัง struct ...

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

a->b->c->d

แต่ข้อที่สองอ่านได้ชัดเจนกว่า ตัวดำเนินการลูกศรมีลำดับความสำคัญสูงสุด (เช่นเดียวกับจุด) และส่วนเชื่อมโยงจากซ้ายไปขวา ฉันคิดว่าสิ่งนี้ชัดเจนกว่าการใช้ตัวดำเนินการ dot ทั้งสำหรับพอยน์เตอร์เพื่อ struct และ struct เนื่องจากเรารู้ชนิดจากนิพจน์โดยไม่ต้องดูการประกาศซึ่งอาจอยู่ในไฟล์อื่น


2
ด้วยชนิดข้อมูลที่ซ้อนกันซึ่งมีทั้ง structs และ pointers to structs สิ่งนี้จะทำให้ยากขึ้นเมื่อคุณคิดที่จะเลือกโอเปอเรเตอร์ที่เหมาะสมสำหรับการเข้าถึงข้อมูลของสมาชิกแต่ละคน คุณอาจท้ายด้วย ab-> c-> d หรือ a-> bc-> d (ฉันมีปัญหานี้เมื่อใช้ไลบรารี freetype - ฉันต้องค้นหามันเป็นซอร์สโค้ดตลอดเวลา) นอกจากนี้ยังไม่ได้อธิบายว่าทำไมมันเป็นไปไม่ได้ที่จะให้คอมไพเลอร์ dereference ตัวชี้โดยอัตโนมัติเมื่อจัดการกับพอยน์เตอร์
Askaga

3
ในขณะที่ข้อเท็จจริงที่คุณระบุถูกต้องพวกเขาไม่ตอบคำถามดั้งเดิมของฉันในทางใดทางหนึ่ง คุณอธิบายความเท่าเทียมกันของ a-> และ * (a) สัญกรณ์ (ซึ่งได้รับการอธิบายแล้วหลายครั้งในคำถามอื่น ๆ ) เช่นเดียวกับการให้ถ้อยคำที่คลุมเครือเกี่ยวกับการออกแบบภาษาโดยพลการ ฉันไม่พบคำตอบที่เป็นประโยชน์ของคุณดังนั้นการลงคะแนน
Askaga

16
@effeffe ผู้ใช้ OP บอกว่าภาษาสามารถตีความได้อย่างง่ายดายa.b.c.dว่าเป็นการ(*(*(*a).b).c).dแสดงผลตัว->ดำเนินการที่ไร้ประโยชน์ ดังนั้นเวอร์ชันของ OP ( a.b.c.d) จึงสามารถอ่านได้อย่างเท่าเทียมกัน (เทียบกับa->b->c->d) นั่นเป็นเหตุผลที่คำตอบของคุณไม่ตอบคำถามของ OP
Shahbaz

4
@Shahbaz นั่นอาจจะเป็นกรณีสำหรับโปรแกรมเมอร์ Java, C / C ++ Programmer จะเข้าใจa.b.c.dและa->b->c->dเป็นสองมากสิ่งที่แตกต่าง: ที่แรกก็คือการเข้าถึงหน่วยความจำเดียวที่จะซ้อนกันย่อยวัตถุ (มีเพียงวัตถุหน่วยความจำเพียงครั้งเดียวในกรณีนี้ ) ที่สองคือการเข้าถึงหน่วยความจำที่สามไล่ตัวชี้ผ่านวัตถุที่แตกต่างกันสี่รายการ นั่นเป็นความแตกต่างอย่างมากในรูปแบบหน่วยความจำและฉันเชื่อว่า C ถูกต้องในการแยกความแตกต่างระหว่างสองกรณีนี้อย่างชัดเจน
cmaster - คืนสถานะโมนิก้า

2
@Shahbaz ฉันไม่ได้หมายความว่าในฐานะที่เป็นการดูถูกของโปรแกรมเมอร์ Java พวกเขาจะใช้ภาษาที่มีพอยน์เตอร์โดยนัย หากฉันถูกนำขึ้นมาเป็นโปรแกรมเมอร์ Java ฉันอาจจะคิดแบบเดียวกัน ... อย่างไรก็ตามฉันคิดว่าตัวดำเนินการมากเกินไปที่เราเห็นใน C นั้นน้อยกว่าความเหมาะสม อย่างไรก็ตามฉันยอมรับว่าพวกเราทุกคนล้วน แต่เป็นนักคณิตศาสตร์ที่เสียเปรียบผู้ประกอบการของพวกเขามากเกินไปสำหรับทุกสิ่ง ฉันเข้าใจถึงแรงจูงใจของพวกเขาด้วยเนื่องจากชุดของสัญลักษณ์ที่มีอยู่ค่อนข้าง จำกัด ฉันเดาว่าในท้ายที่สุดมันเป็นแค่คำถามที่คุณวาดเส้น ...
cmaster - reinstate monica

19

C ยังทำงานได้ดีโดยไม่ทำสิ่งที่คลุมเครือ

แน่ใจว่าจุดนั้นมากเกินไปเพื่อหมายถึงทั้งสองสิ่ง แต่ลูกศรทำให้แน่ใจว่าโปรแกรมเมอร์รู้ว่าเขากำลังทำงานอยู่บนตัวชี้เหมือนกับเมื่อคอมไพเลอร์ไม่ยอมให้คุณผสมสองประเภทที่เข้ากันไม่ได้


4
นี่คือคำตอบที่ง่ายและถูกต้อง C ส่วนใหญ่พยายามที่จะหลีกเลี่ยงการบรรทุกเกินพิกัดซึ่ง IMO เป็นหนึ่งในสิ่งที่ดีที่สุดเกี่ยวกับซี
jforberg

10
หลายสิ่งใน C นั้นคลุมเครือและคลุมเครือ มีการแปลงประเภทโดยนัยตัวดำเนินการทางคณิตศาสตร์มีการโหลดมากเกินไปการทำดัชนีแบบโยงโซ่ทำบางสิ่งที่แตกต่างกันโดยสิ้นเชิงขึ้นอยู่กับว่าคุณกำลังทำดัชนีอาร์เรย์แบบหลายมิติหรืออาร์เรย์ของตัวชี้และสิ่งใด ๆ t)
PSkocik
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.