ด้วยอาร์เรย์ทำไมเป็น [5] == 5 [a]


1622

ตามที่โจเอลชี้ในพอดคาสต์ Stack Overflow # 34ในภาษาโปรแกรม C (aka: K & R) มีการกล่าวถึงคุณสมบัติของอาร์เรย์ใน C:a[5] == 5[a]

Joel บอกว่ามันเป็นเพราะเลขคณิตของตัวชี้ แต่ฉันก็ยังไม่เข้าใจ ทำไมa[5] == 5[a] ?


48
สิ่งที่ต้องการ [+] จะทำงานเช่น * (a ++) หรือ * (++ a) หรือไม่
Egon

45
@Egon: มันสร้างสรรค์มาก แต่น่าเสียดายที่นั่นไม่ใช่วิธีที่คอมไพเลอร์ทำงาน คอมไพเลอร์ตีความa[1]ว่าเป็นชุดของโทเค็นไม่ใช่สตริง: * ({จำนวนเต็มตำแหน่งของ} a {ตัวดำเนินการ} + {จำนวนเต็ม} 1) จะเหมือนกับ * ({จำนวนเต็ม} 1 {ตัวดำเนินการ} + {ตำแหน่งจำนวนเต็มของ} a) แต่จะไม่เหมือนกับ * ({จำนวนเต็มตำแหน่ง}} {โอเปอเรเตอร์} + {โอเปอเรเตอร์} +)
ดินาห์

11
รูปแบบผสมที่น่าสนใจเกี่ยวกับเรื่องนี้จะแสดงในการเข้าถึงอาร์เรย์ Illogicalที่คุณมีchar bar[]; int foo[];และfoo[i][bar]ใช้เป็นนิพจน์
Jonathan Leffler

5
@EldritchConundrum ทำไมคุณคิดว่า 'คอมไพเลอร์ไม่สามารถตรวจสอบว่าส่วนด้านซ้ายเป็นตัวชี้' หรือไม่ ใช่มันสามารถ มันเป็นความจริงที่a[b]= *(a + b)สำหรับทุก ๆ อย่างaและbแต่มันเป็นตัวเลือกฟรีสำหรับนักออกแบบภาษา+ที่จะกำหนดให้เปลี่ยนเป็นคอมมิชชันสำหรับทุกประเภท ไม่มีอะไรที่จะป้องกันไม่ให้พวกเขาจากการห้ามขณะที่ช่วยให้i + p p + i
อาชา

13
@Andrey One มักจะคาดหวังว่า+จะเป็นสับเปลี่ยนดังนั้นบางทีปัญหาที่แท้จริงคือการเลือกที่จะให้การดำเนินการตัวชี้คล้ายกับเลขคณิตแทนที่จะออกแบบตัวดำเนินการออฟเซ็ตแยกต่างหาก
Eldritch Conundrum

คำตอบ:


1924

มาตรฐาน C กำหนดตัว[]ดำเนินการดังนี้:

a[b] == *(a + b)

ดังนั้นa[5]จะประเมินเป็น:

*(a + 5)

และ5[a]จะประเมินเป็น:

*(5 + a)

aเป็นตัวชี้ไปยังองค์ประกอบแรกของอาร์เรย์ a[5]เป็นค่าที่ 5 องค์ประกอบเพิ่มเติมจากaซึ่งเป็นเช่นเดียวกับ*(a + 5)และจากคณิตศาสตร์ในโรงเรียนประถมศึกษาที่เรารู้ว่าผู้ที่มีความเท่าเทียมกัน (นอกจากเป็นสับเปลี่ยน )


325
ฉันสงสัยว่ามันไม่เหมือนกับ * ((5 * sizeof (a)) + a) อธิบายได้ดีมาก
John MacIntyre

92
@Dinah: จากมุมมองของ C-compiler คุณพูดถูก ไม่จำเป็นต้องใช้ขนาดและนิพจน์ที่ฉันกล่าวถึงเป็นชื่อเดียวกัน อย่างไรก็ตามคอมไพเลอร์จะคำนึงถึงขนาดของบัญชีเมื่อสร้างรหัสเครื่อง ถ้า a เป็น int int a[5]จะรวบรวมบางอย่างเช่นmov eax, [ebx+20]แทนที่จะเป็น[ebx+5]
Mehrdad Afshari

12
@Dinah: A เป็นที่อยู่พูด 0x1230 ถ้า a อยู่ใน int 32- บิต array, [0] อยู่ที่ 0x1230, [1] อยู่ที่ 0x1234, [2] ที่ 0x1238 ... a [5] ที่ x1244 เป็นต้นถ้าเราเพิ่ม 5 ถึง 0x1230 เราได้ 0x1235 ซึ่งผิด
James Curran

36
@ sr105: นั่นเป็นกรณีพิเศษสำหรับตัวดำเนินการ + โดยที่หนึ่งในตัวถูกดำเนินการเป็นตัวชี้และอีกจำนวนเต็ม มาตรฐานบอกว่าผลลัพธ์จะเป็นประเภทของตัวชี้ คอมไพเลอร์ / ต้องเป็น / ฉลาดพอ
aib

48
"จากคณิตศาสตร์ระดับประถมเรารู้ว่ามันเท่ากัน" - ฉันเข้าใจว่าคุณกำลังทำให้ง่ายขึ้น แต่ฉันอยู่กับคนที่รู้สึกแบบนี้ง่ายกว่า *(10 + (int *)13) != *((int *)10 + 13)มันไม่ได้เป็นระดับประถมศึกษาที่ กล่าวอีกนัยหนึ่งมีอะไรเกิดขึ้นที่นี่มากกว่าเลขคณิตของโรงเรียนประถม commutativity นั้นขึ้นอยู่กับช่วงวิกฤตของคอมไพเลอร์โดยรู้ว่าตัวถูกดำเนินการใดเป็นตัวชี้ (และขนาดของวัตถุ) ที่จะนำมันไปทางอื่น(1 apple + 2 oranges) = (2 oranges + 1 apple)แต่(1 apple + 2 oranges) != (1 orange + 2 apples)แต่
LarsH

288

เพราะการเข้าถึงอาเรย์ถูกกำหนดในแง่ของพอยน์เตอร์ a[i]หมายถึงค่าเฉลี่ย*(a + i)ซึ่งเปลี่ยนเป็น


42
ไม่ได้กำหนดอาร์เรย์ในแง่ของพอยน์เตอร์ แต่เข้าถึงได้
การแข่งขัน Lightness ใน Orbit

5
ฉันจะเพิ่ม "ดังนั้นจึงเท่ากับ*(i + a)ซึ่งสามารถเขียนเป็นi[a]"
Jim Balter

4
ฉันขอแนะนำให้คุณใส่เครื่องหมายอัญประกาศจากมาตรฐานซึ่งมีดังต่อไปนี้: 6.5.2.1: 2 นิพจน์ postfix ตามด้วยนิพจน์ในวงเล็บเหลี่ยม [] เป็นการกำหนดองค์ประกอบของวัตถุอาร์เรย์ คำจำกัดความของโอเปอเรเตอร์ตัวห้อย [] คือ E1 [E2] เหมือนกับ (* ((E1) + (E2))) เนื่องจากกฎการแปลงที่ใช้กับผู้ประกอบการไบนารี + ถ้า E1 เป็นวัตถุอาร์เรย์ (เท่ากันตัวชี้ไปยังองค์ประกอบเริ่มต้นของวัตถุอาร์เรย์) และ E2 เป็นจำนวนเต็ม E1 [E2] กำหนดองค์ประกอบ E2-th ของ E1 (นับจากศูนย์)
Vality

จะถูกต้องมากขึ้น: อาร์เรย์สลายตัวเป็นพอยน์เตอร์เมื่อคุณเข้าถึง
12431234123412341234123

Nitpick: มันไม่มีเหตุผลที่จะพูดว่า " *(a + i)สลับกัน" อย่างไรก็ตาม*(a + i) = *(i + a) = i[a]เนื่องจากการเติมนั้นสลับกัน
Andreas Rejbrand

231

ฉันคิดว่าคำตอบอื่นกำลังพลาดอยู่

ใช่p[i]คือโดยการนิยามที่เทียบเท่า*(p+i)ซึ่ง (เนื่องจากการบวกคือการสลับ) ที่เทียบเท่ากับ*(i+p)ซึ่ง (อีกครั้งโดยคำจำกัดความของ[]ผู้ประกอบการ) เทียบเท่ากับi[p]เทียบเท่ากับ

(และใน array[i]ชื่ออาร์เรย์จะถูกแปลงเป็นตัวชี้ไปยังองค์ประกอบแรกของอาร์เรย์โดยปริยาย)

แต่การสลับสับเปลี่ยนของการเติมนั้นไม่ใช่ทั้งหมดที่ชัดเจนในกรณีนี้

เมื่อตัวถูกดำเนินการทั้งสองเป็นประเภทเดียวกันหรือแม้กระทั่งประเภทตัวเลขที่แตกต่างกันซึ่งได้รับการส่งเสริมให้เป็นประเภททั่วไป commutativity จึงเหมาะสมอย่างสมบูรณ์: x + y == y + xทำให้รู้สึกดี:

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

คำอธิบายของมาตรฐาน C ของ+ผู้ปฏิบัติงาน ( N1570 6.5.6) พูดว่า:

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

สามารถพูดได้อย่างง่ายดายเพียง:

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

ในกรณีนี้ทั้งสองi + pและi[p]จะผิดกฎหมาย

ในแง่ C ++ เรามี+ตัวดำเนินการโอเวอร์โหลดสองชุดซึ่งสามารถอธิบายได้อย่างหลวม ๆ ดังนี้:

pointer operator+(pointer p, integer i);

และ

pointer operator+(integer i, pointer p);

สิ่งแรกเท่านั้นที่จำเป็นจริงๆ

เหตุใดจึงเป็นเช่นนี้

C ++ สืบทอดคำจำกัดความนี้จาก C ซึ่งได้มาจาก B (ความสับเปลี่ยนของการจัดทำดัชนีอาร์เรย์ถูกกล่าวถึงอย่างชัดเจนในการอ้างอิงผู้ใช้ 1972 ) ถึง B ) ซึ่งได้รับจากBCPL (คู่มือลงวันที่ 1967) ซึ่งอาจได้มาจาก ภาษาก่อนหน้า (CPL? Algol?)

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

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

และในช่วงหลายปีที่ผ่านมาการเปลี่ยนแปลงกฎนั้นจะทำให้รหัสที่มีอยู่เสียหาย (แม้ว่ามาตรฐาน ANSI C ปี 1989 อาจเป็นโอกาสที่ดี)

การเปลี่ยน C และ / หรือ C ++ เพื่อต้องการให้วางตัวชี้ทางด้านซ้ายและจำนวนเต็มทางด้านขวาอาจทำให้โค้ดที่มีอยู่บางตัวพัง แต่จะไม่มีการสูญเสียพลังการแสดงออกที่แท้จริง

ดังนั้นตอนนี้เรามีarr[3]และ3[arr]ความหมายว่าสิ่งเดียวกัน แต่รูปแบบหลังไม่ควรปรากฏนอกIOCCC


12
คำอธิบายที่ยอดเยี่ยมของคุณสมบัตินี้ จากมุมมองระดับสูงฉันคิดว่า3[arr]เป็นสิ่งประดิษฐ์ที่น่าสนใจ แต่ไม่ค่อยควรใช้ คำตอบที่ได้รับการยอมรับสำหรับคำถามนี้ (< stackoverflow.com/q/1390365/356> ) ซึ่งฉันถามกลับไปได้เปลี่ยนวิธีที่ฉันคิดเกี่ยวกับไวยากรณ์ แม้ว่ามักจะมีเทคนิคไม่ใช่วิธีที่ถูกและผิดในการทำสิ่งเหล่านี้ แต่ฟีเจอร์ชนิดนี้จะเริ่มจากที่คุณคิดแยกจากรายละเอียดการใช้งาน มีประโยชน์กับวิธีคิดที่แตกต่างกันซึ่งส่วนหนึ่งหายไปเมื่อคุณกำหนดรายละเอียดการใช้งาน
ไดน่า

3
นอกจากนี้ยังเป็นการสับเปลี่ยน สำหรับมาตรฐาน C เพื่อกำหนดเป็นอย่างอื่นจะแปลก นั่นเป็นสาเหตุที่ไม่สามารถพูดได้อย่างง่ายดายเช่น "นอกจากนี้ตัวถูกดำเนินการทั้งสองจะต้องมีประเภทคณิตศาสตร์หรือตัวถูกดำเนินการด้านซ้ายจะเป็นตัวชี้ไปยังวัตถุชนิดสมบูรณ์และตัวถูกดำเนินการด้านขวาจะมีประเภทจำนวนเต็ม" - นั่นไม่สมเหตุสมผลสำหรับคนส่วนใหญ่ที่เพิ่มสิ่งต่าง ๆ
iheanyi

9
@iheanyi: การเพิ่มมักจะสลับกัน - และมักจะใช้ตัวถูกดำเนินการสองประเภทเดียวกัน การเพิ่มตัวชี้ช่วยให้คุณเพิ่มตัวชี้และจำนวนเต็ม แต่ไม่ใช่ตัวชี้สองตัว IMHO นั้นเป็นกรณีพิเศษที่เพียงพอแล้วที่ต้องการให้ตัวชี้เป็นตัวถูกดำเนินการด้านซ้ายจะไม่เป็นภาระที่สำคัญ (บางภาษาใช้ "+" สำหรับการเรียงต่อสตริง; นั่นไม่ใช่การสลับอย่างแน่นอน)
Keith Thompson

3
@supercat ที่แย่ยิ่งกว่านั้น นั่นหมายความว่าบางครั้ง x + 1! = 1 + x ที่จะเป็นการละเมิดทรัพย์สินทางปัญญาของการเพิ่ม
iheanyi

3
@iheanyi: ฉันคิดว่าคุณหมายถึงทรัพย์สินที่สับเปลี่ยน; การเพิ่มยังไม่ได้เชื่อมโยงกันเนื่องจากการใช้งานส่วนใหญ่ (1LL + 1U) -2! = 1LL + (1U-2) อันที่จริงการเปลี่ยนแปลงจะทำให้บางสถานการณ์เชื่อมโยงกันซึ่งปัจจุบันไม่เช่น 3U + (UINT_MAX-2L) จะเท่ากับ (3U + UINT_MAX) -2 อะไรจะดีที่สุด แต่เป็นภาษาที่จะมีการเพิ่มประเภทที่แตกต่างกันใหม่สำหรับจำนวนเต็มเลื่อนตำแหน่งและ "ตัด" แหวนพีชคณิตเพื่อให้การเพิ่ม 2 ไปring16_tซึ่งถือ 65535 จะให้ผลผลิตring16_tที่มีมูลค่า 1, อิสระจากขนาดของ int
supercat

196

และแน่นอนว่า

 ("ABCD"[2] == 2["ABCD"]) && (2["ABCD"] == 'C') && ("ABCD"[2] == 'C')

เหตุผลหลักสำหรับเรื่องนี้คือย้อนกลับไปในยุค 70 เมื่อ C ถูกออกแบบคอมพิวเตอร์ไม่ได้มีหน่วยความจำมาก (64KB มีจำนวนมาก) ดังนั้นคอมไพเลอร์ C จึงไม่ทำการตรวจสอบไวยากรณ์มากนัก ดังนั้น " X[Y]" จึงแปลเป็น " *(X+Y)" ค่อนข้างสุ่มสี่สุ่มห้า

นอกจากนี้ยังอธิบายถึงไวยากรณ์ " +=" และ " ++" ทุกอย่างในแบบฟอร์ม " A = B + C" มีแบบฟอร์มที่รวบรวมไว้เหมือนกัน แต่ถ้า B เป็นวัตถุเดียวกับ A ดังนั้นการเพิ่มประสิทธิภาพระดับแอสเซมบลีก็พร้อมใช้งาน แต่คอมไพเลอร์ไม่สว่างพอที่จะจดจำได้ดังนั้นผู้พัฒนาจึงต้อง ( A += C) ในทำนองเดียวกันถ้าCเคย1มีการเพิ่มประสิทธิภาพระดับแอสเซมบลีที่แตกต่างกันและนักพัฒนาต้องทำให้ชัดเจนเนื่องจากคอมไพเลอร์ไม่รู้จัก (คอมไพเลอร์ล่าสุดทำดังนั้นไวยากรณ์เหล่านั้นส่วนใหญ่ไม่จำเป็นวันนี้)


127
ที่จริงแล้วมันประเมินว่าเป็นเท็จ คำแรก "ABCD" [2] == 2 ["ABCD"] ประเมินว่าเป็นจริงหรือ 1 และ 1! = 'C': D
Jonathan Leffler

8
@ โจนาธาน: ความคลุมเครือเดียวกันนำไปสู่การแก้ไขชื่อเดิมของโพสต์นี้ เราเป็นเครื่องหมายเท่ากับความเท่าเทียมกันทางคณิตศาสตร์, รหัสไวยากรณ์หรือรหัสหลอก ฉันโต้แย้งความเท่าเทียมกันทางคณิตศาสตร์ แต่เนื่องจากเรากำลังพูดถึงรหัสเราไม่สามารถหลบหนีได้ว่าเรากำลังดูทุกอย่างในรูปแบบของรหัสไวยากรณ์
Dinah

19
นี่ไม่ใช่ตำนานหรอกเหรอ? ฉันหมายความว่าตัวดำเนินการ + = และ ++ ถูกสร้างขึ้นเพื่อทำให้คอมไพเลอร์ง่ายขึ้นหรือไม่ โค้ดบางตัวมีความชัดเจนมากขึ้นและเป็นประโยชน์ในการมีไวยากรณ์ไม่ว่าคอมไพเลอร์จะทำอะไรกับมัน
Thomas Padron-McCarthy

6
+ = และ ++ มีประโยชน์ที่สำคัญอีกประการหนึ่ง หากด้านซ้ายมือเปลี่ยนแปลงตัวแปรบางตัวขณะประเมินผลการเปลี่ยนแปลงจะทำเพียงครั้งเดียว a = a + ... ; จะทำมันสองครั้ง
Johannes Schaub - litb

8
ไม่ - "ABCD" [2] == * ("ABCD" + 2) = * ("CD") = 'C' การยกเลิกการอ้างอิงสตริงจะให้อักขระไม่ใช่สตริงย่อย
MSalters

55

ดูเหมือนจะไม่มีใครพูดถึงปัญหาของไดน่าด้วยsizeof:

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


1
มีการสนทนาที่ละเอียดถี่ถ้วนในเรื่องความคิดเห็นของคำตอบที่ยอมรับได้ ฉันอ้างถึงการสนทนาดังกล่าวในการแก้ไขคำถามเดิม แต่ไม่ได้ตอบข้อกังวลของคุณในเรื่องขนาดที่ถูกต้องของคุณโดยตรง ไม่แน่ใจว่าจะทำสิ่งนี้ได้อย่างไรใน SO ฉันควรแก้ไขอีกครั้งในแหล่งกำเนิด คำถาม?
ไดน่า

50

เพื่อตอบคำถามอย่างแท้จริง มันไม่เป็นความจริงเสมอไปx == x

double zero = 0.0;
double a[] = { 0,0,0,0,0, zero/zero}; // NaN
cout << (a[5] == 5[a] ? "true" : "false") << endl;

พิมพ์

false

27
จริง "น่าน" ไม่เท่ากับตัวเอง: เป็นcout << (a[5] == a[5] ? "true" : "false") << endl; false
TrueY

8
@TrueY: เขาระบุว่าเฉพาะสำหรับกรณี NaN (และเฉพาะที่x == xไม่เป็นความจริงเสมอไป) ฉันคิดว่านั่นเป็นความตั้งใจของเขา ดังนั้นเขาจึงถูกต้องทางเทคนิค (และอาจเป็นไปได้ว่าพวกเขาพูดถูกต้องที่สุด!)
ทิม 13as

3
คำถามเกี่ยวกับ C รหัสของคุณไม่ใช่รหัส C นอกจากนี้ยังมีNANใน<math.h>ซึ่งดีกว่า0.0/0.0เพราะ0.0/0.0เป็น UB เมื่อ__STDC_IEC_559__ไม่ได้กำหนด (การใช้งานส่วนใหญ่ไม่ได้กำหนด__STDC_IEC_559__แต่ในการใช้งานส่วนใหญ่0.0/0.0จะยังคงทำงาน)
124312341234123412341234123

26

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

int a[] = { 2 , 3 , 3 , 2 , 4 };
int s = sizeof a / sizeof *a;  //  s == 5

for(int i = 0 ; i < s ; ++i) {  

           cout << a[a[a[i]]] << endl;
           // ... is equivalent to ... 
           cout << i[a][a][a] << endl;  // but I prefer this one, it's easier to increase the level of indirection (without loop)

}

แน่นอนฉันค่อนข้างแน่ใจว่าไม่มีกรณีการใช้งานสำหรับรหัสจริง แต่ฉันคิดว่ามันน่าสนใจอยู่ดี :)


เมื่อคุณเห็นi[a][a][a]คุณคิดว่า i เป็นตัวชี้ไปยังอาร์เรย์หรืออาร์เรย์ของตัวชี้ไปยังอาร์เรย์หรืออาร์เรย์ ... และaเป็นดัชนี เมื่อคุณเห็นa[a[a[i]]]คุณคิดว่า a เป็นตัวชี้ไปยังอาร์เรย์หรืออาร์เรย์และiเป็นดัชนี
12431234123412341234123

1
ว้าว! มันยอดเยี่ยมมากในการใช้งานคุณสมบัติ "โง่" นี้ อาจเป็นประโยชน์ในการประกวดอัลกอริทึมในบางปัญหา))
Serge Breusov

26

คำถาม / คำตอบที่ดี

เพียงแค่ต้องการชี้ให้เห็นว่าตัวชี้และอาร์เรย์ C นั้นไม่เหมือนกันแม้ว่าในกรณีนี้ความแตกต่างนั้นไม่จำเป็น

พิจารณาการประกาศต่อไปนี้:

int a[10];
int* p = a;

ในa.outสัญลักษณ์aอยู่ที่ที่อยู่ซึ่งเป็นจุดเริ่มต้นของอาร์เรย์และสัญลักษณ์pอยู่ที่ที่เก็บตัวชี้และค่าของตัวชี้ที่ตำแหน่งหน่วยความจำนั้นเป็นจุดเริ่มต้นของอาร์เรย์


2
ไม่ทางเทคนิคพวกเขาไม่เหมือนกัน ถ้าคุณนิยาม b เป็น int * const และทำให้มันชี้ไปที่อาร์เรย์มันจะยังคงเป็นตัวชี้ซึ่งหมายความว่าในตารางสัญลักษณ์ b หมายถึงตำแหน่งหน่วยความจำที่เก็บที่อยู่ซึ่งจะชี้ไปยังจุดที่อาร์เรย์อยู่ .
PolyThinker

4
จุดที่ดีมาก ฉันจำได้ว่ามีข้อผิดพลาดที่น่ารังเกียจมากเมื่อฉันกำหนดสัญลักษณ์ทั่วโลกเป็นถ่าน [100] ในโมดูลเดียวประกาศว่าเป็นถ่านภายนอก * ในโมดูลอื่น หลังจากเชื่อมโยงมันเข้าด้วยกันแล้วโปรแกรมก็ทำตัวแปลกมาก เนื่องจากโมดูลที่ใช้การประกาศ extern ได้ใช้ไบต์เริ่มต้นของอาร์เรย์เป็นตัวชี้เพื่อถ่าน
Giorgio

1
แต่เดิมใน BCPL ปู่ย่าตายายของ C อาร์เรย์เป็นตัวชี้ นั่นคือสิ่งที่คุณได้รับเมื่อคุณเขียน (ฉันได้ทับศัพท์ C) int a[10]คือตัวชี้ที่เรียกว่า 'a' ซึ่งชี้ไปยังที่เก็บเพียงพอสำหรับจำนวนเต็ม 10 ตัวที่อื่น ดังนั้น a + i และ j + i จึงมีรูปแบบเดียวกัน: เพิ่มเนื้อหาของที่ตั้งหน่วยความจำสองแห่ง อันที่จริงฉันคิดว่า BCPL นั้นไม่มีความเป็นตัวตนดังนั้นพวกเขาจึงเหมือนกัน และการปรับขนาดแบบไม่ได้ใช้เนื่องจาก BCPL นั้นเป็นระบบเชิงคำล้วนๆ (บนเครื่องที่ใช้คำพูดด้วย)
dave

ฉันคิดว่าวิธีที่ดีที่สุดที่จะเข้าใจความแตกต่างคือการเปรียบเทียบint*p = a;กับint b = 5; ในภายหลัง "b" และ "5" เป็นทั้งจำนวนเต็ม แต่ "b" เป็นตัวแปรในขณะที่ "5" เป็นค่าคงที่ ในทำนองเดียวกัน "p" & "a" เป็นทั้งที่อยู่ของอักขระ แต่ "a" เป็นค่าคงที่
James Curran

20

สำหรับพอยน์เตอร์ใน C เรามี

a[5] == *(a + 5)

และนอกจากนี้ยังมี

5[a] == *(5 + a)

ดังนั้นจึงเป็นเรื่องจริงที่ a[5] == 5[a].


15

ไม่ใช่คำตอบ แต่เป็นเพียงอาหารสำหรับความคิด หากคลาสนั้นมีตัวดำเนินการ index / subscript มากเกินไปนิพจน์0[x]จะไม่ทำงาน:

class Sub
{
public:
    int operator [](size_t nIndex)
    {
        return 0;
    }   
};

int main()
{
    Sub s;
    s[0];
    0[s]; // ERROR 
}

เนื่องจากเราไม่มีสิทธิ์เข้าถึงคลาสintจึงไม่สามารถทำได้:

class int
{
   int operator[](const Sub&);
};

2
class Sub { public: int operator[](size_t nIndex) const { return 0; } friend int operator[](size_t nIndex, const Sub& This) { return 0; } };
Ben Voigt

1
คุณลองรวบรวมมันจริง ๆ ? มีชุดของตัวดำเนินการที่ไม่สามารถนำไปใช้นอกคลาส (เช่นเป็นฟังก์ชันที่ไม่คงที่)!
Ajay

3
อ๊ะคุณพูดถูก " operator[]จะเป็นฟังก์ชันสมาชิกแบบคงที่ที่มีพารามิเตอร์ตัวเดียว" ผมคุ้นเคยกับข้อ จำกัด ในการที่ไม่ได้คิดว่ามันจะนำไปใช้กับoperator= []
Ben Voigt

1
แน่นอนถ้าคุณเปลี่ยนคำจำกัดความของ[]โอเปอเรเตอร์มันจะไม่เทียบเท่าอีก ... ถ้าa[b]เท่ากับ*(a + b)และคุณเปลี่ยนสิ่งนี้คุณจะต้องทำงานหนักเกินไปint::operator[](const Sub&);และintไม่ใช่คลาส ...
Luis Colorado

7
นี้ ... ไม่ได้ ... C
MD XF

11

มันมีคำอธิบายที่ดีมากในการสอนเกี่ยวกับตัวชี้และการจัดเรียงใน C โดย Ted Jensen

Ted Jensen อธิบายว่า:

ในความเป็นจริงนี่คือความจริงคือทุกที่ที่เขียนa[i]มันสามารถถูกแทนที่ด้วย*(a + i) ไม่มีปัญหาใด ๆ ในความเป็นจริงคอมไพเลอร์จะสร้างรหัสเดียวกันทั้งสองกรณี ดังนั้นเราจึงเห็นว่าตัวคำนวณเลขคณิตนั้นเหมือนกับการจัดทำดัชนีอาร์เรย์ ไวยากรณ์อย่างใดอย่างหนึ่งให้ผลลัพธ์เดียวกัน

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

ทีนี้ถ้าดูที่นิพจน์สุดท้ายนี้ส่วนหนึ่งของมัน .. (a + i)เป็นการเพิ่มอย่างง่ายโดยใช้ตัวดำเนินการ + และกฎของสถานะ C ที่นิพจน์ดังกล่าวสลับกัน นั่นคือ (ก + i) (i + a)เป็นเหมือน ดังนั้นเราสามารถเขียนได้อย่างง่ายดายเพียงเป็น*(i + a) *(a + i)แต่*(i + a)อาจมาจากi[a]! จากทั้งหมดนี้ความจริงที่อยากรู้อยากเห็นที่มา:

char a[20];

การเขียน

a[3] = 'x';

เหมือนกับการเขียน

3[a] = 'x';

4
a + i ไม่ใช่การเพิ่มที่ง่ายเนื่องจากเป็นตัวชี้ทางคณิตศาสตร์ หากขนาดขององค์ประกอบของ a คือ 1 (ถ่าน) ดังนั้นใช่มันเหมือนกับจำนวนเต็ม + แต่ถ้ามัน (เช่น) จำนวนเต็มมันอาจจะเท่ากับ + 4 * i
Alex Brown

@AlexBrown ใช่มันเป็นตัวชี้เลขคณิตซึ่งเป็นสาเหตุที่ประโยคสุดท้ายของคุณผิดเว้นแต่ว่าคุณจะให้ 'a' เป็น a (char *) (สมมติว่า int เป็น 4 ตัวอักษร) ฉันไม่เข้าใจจริง ๆ ว่าทำไมผู้คนจำนวนมากถึงค้างอยู่กับผลลัพธ์ที่แท้จริงของตัวชี้เลขคณิต จุดประสงค์ทั้งหมดของตัวชี้ทางคณิตศาสตร์คือทำให้นามธรรมค่าตัวชี้พื้นฐานและให้โปรแกรมเมอร์คิดเกี่ยวกับวัตถุที่ถูกจัดการมากกว่าค่าที่อยู่
jschultz410

8

ฉันรู้ว่าตอบคำถามแล้ว แต่ฉันไม่สามารถต่อต้านการแบ่งปันคำอธิบายนี้

ฉันจำหลักการออกแบบคอมไพเลอร์ได้สมมติว่าaมีintอาร์เรย์และขนาดเท่ากับint2 ไบต์และที่อยู่ฐานสำหรับaคือ 1,000

วิธีa[5]จะทำงาน ->

Base Address of your Array a + (5*size of(data type for array a))
i.e. 1000 + (5*2) = 1010

ดังนั้น,

ในทำนองเดียวกันเมื่อรหัส c แบ่งออกเป็นรหัสที่อยู่ 3 รหัส 5[a]จะกลายเป็น ->

Base Address of your Array a + (size of(data type for array a)*5)
i.e. 1000 + (2*5) = 1010 

ดังนั้นโดยพื้นฐานแล้วข้อความทั้งสองจะชี้ไปที่ตำแหน่งเดียวกันในหน่วยความจำและด้วยเหตุนี้ a[5] = 5[a] .

คำอธิบายนี้ยังเป็นสาเหตุที่ดัชนีเชิงลบในอาร์เรย์ทำงานใน C

เช่นถ้าฉันเข้าถึงa[-5]มันจะให้ฉัน

Base Address of your Array a + (-5 * size of(data type for array a))
i.e. 1000 + (-5*2) = 990

มันจะคืนให้ฉันที่ตำแหน่ง 990


6

ในอาร์เรย์ C , arr[3]และ3[arr]ที่เหมือนกันและสัญลักษณ์ชี้เทียบเท่าของพวกเขาไป*(arr + 3) *(3 + arr)แต่ในทางตรงกันข้าม[arr]3หรือ[3]arrไม่ถูกต้องและจะส่งผลให้เกิดข้อผิดพลาดทางไวยากรณ์เป็น(arr + 3)*และ(3 + arr)*ไม่ใช่การแสดงออกที่ถูกต้อง เหตุผลคือผู้ดำเนินการปฏิเสธความรับผิดชอบควรวางไว้ก่อนที่อยู่จะให้ผลลัพธ์โดยนิพจน์ไม่ใช่หลังจากที่อยู่


6

ในคอมไพเลอร์

a[i]
i[a]
*(a+i)

วิธีที่แตกต่างในการอ้างถึงองค์ประกอบในอาร์เรย์! (ไม่แปลกเลย)


5

ประวัติเล็กน้อยตอนนี้ ในภาษาอื่น ๆ BCPL มีอิทธิพลอย่างมากต่อการพัฒนาของ C ในช่วงแรก หากคุณประกาศอาร์เรย์ใน BCPL ด้วยสิ่งที่ชอบ:

let V = vec 10

ที่จัดสรรหน่วยความจำ 11 คำไม่ใช่ 10 โดยปกติ V เป็นคำแรกและมีที่อยู่ของคำต่อไปนี้ทันที ซึ่งแตกต่างจาก C การตั้งชื่อ V ไปที่ตำแหน่งนั้นและหยิบที่อยู่ขององค์ประกอบ zeroeth ของอาร์เรย์ ดังนั้นการหาอาร์เรย์ใน BCPL แสดงว่า

let J = V!5

จำเป็นต้องทำจริงๆJ = !(V + 5)(ใช้ไวยากรณ์ BCPL) เนื่องจากจำเป็นต้องดึงข้อมูล V เพื่อรับที่อยู่ฐานของอาร์เรย์ ดังนั้นV!5และ5!Vมีความหมายเหมือนกัน จากการสังเกตการณ์เล็ก ๆ น้อย ๆ WAFL (Warwick Functional Language) เขียนใน BCPL และหน่วยความจำที่ดีที่สุดของฉันมักจะใช้ไวยากรณ์หลังมากกว่าที่จะใช้เพื่อเข้าถึงโหนดที่ใช้เป็นที่เก็บข้อมูล ได้รับมาจากที่ไหนสักแห่งระหว่าง 35 และ 40 ปีที่ผ่านมาดังนั้นความทรงจำของฉันเป็นสนิมเล็กน้อย :)

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

โปรดทราบว่า!ใน BCPL นั้นเป็นทั้งตัวดำเนินการนำหน้า unary และตัวดำเนินการแบบไบนารีมัดทั้งสองกรณีทำในทางอ้อม เพียงว่ารูปแบบไบนารีรวมถึงการเพิ่มตัวถูกดำเนินการทั้งสองก่อนที่จะทำการอ้อม ด้วยคำที่เน้นลักษณะของ BCPL (และ B) สิ่งนี้ทำให้รู้สึกได้จริงมาก ข้อ จำกัด ของ "ตัวชี้และจำนวนเต็ม" จำเป็นใน C เมื่อได้รับชนิดข้อมูลและsizeofกลายเป็นเรื่อง


1

นี่เป็นคุณสมบัติที่เป็นไปได้เพราะการสนับสนุนทางภาษา

คอมไพเลอร์ตีความa[i]เป็น*(a+i)และการแสดงออกประเมิน5[a] *(5+a)นับตั้งแต่การเปลี่ยนเป็นปรากฎว่าทั้งคู่มีค่าเท่ากัน trueดังนั้นประเมินแสดงออกถึงความ


แม้ว่าจะซ้ำซ้อนนี้ชัดเจนกระชับและสั้น
Bill K

0

ในซี

 int a[]={10,20,30,40,50};
 int *p=a;
 printf("%d\n",*p++);//output will be 10
 printf("%d\n",*a++);//will give an error

ตัวชี้เป็น "ตัวแปร"

ชื่ออาร์เรย์คือ "ช่วยในการจำ" หรือ "คำพ้อง"

p++;ถูกต้อง แต่a++ไม่ถูกต้อง

a[2] เท่ากับ 2 [a] เพราะการดำเนินการภายในของทั้งสองนี้คือ

"ตัวชี้คณิตศาสตร์" คำนวณภายในเป็น

*(a+3) เท่ากับ *(3+a)


-4

ประเภทตัวชี้

1) ตัวชี้ไปยังข้อมูล

int *ptr;

2) ตัวชี้ const กับข้อมูล

int const *ptr;

3) ตัวชี้ const เพื่อ const ข้อมูล

int const *const ptr;

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

ความแตกต่างที่สำคัญที่ฉันพบคือ ...

เราสามารถเริ่มต้นตัวชี้อีกครั้งโดยที่อยู่ แต่ไม่ใช่กรณีเดียวกันกับอาร์เรย์

======
และกลับไปที่คำถามของคุณ ...
a[5]ไม่มีอะไร แต่*(a + 5)
คุณสามารถเข้าใจได้ง่ายโดย
a - ประกอบด้วยที่อยู่ (ผู้คนเรียกมันว่าเป็นที่อยู่พื้นฐาน) เหมือนกับตัวชี้ประเภท (2) ในรายการของเรา
[]- ผู้ประกอบการนั้นสามารถ *เปลี่ยนได้ด้วยตัวชี้

ดังนั้นในที่สุด ...

a[5] == *(a +5) == *(5 + a) == 5[a] 
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.