ทำไมดัชนีอาเรย์เชิงลบจึงสมเหตุสมผล


14

ฉันเจอประสบการณ์แปลก ๆ ในการเขียนโปรแกรม C พิจารณารหัสนี้:

int main(){
  int array1[6] = {0, 1, 2, 3, 4, 5};
  int array2[6] = {6, 7, 8, 9, 10, 11};

  printf("%d\n", array1[-1]);
  return 0;
}

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


2
ในขณะที่คำถามนี้ถูกกระตุ้นด้วยภาษา C เป็นภาษาที่เป็นรูปธรรม แต่ฉันคิดว่ามันสามารถเข้าใจได้ว่าเป็นคำถามเชิงแนวคิดที่เป็นแบบ ontopic ที่นี่ (ถ้าแทบจะไม่)
Raphael

7
@ ราฟาเอลฉันไม่เห็นด้วยและเชื่อว่ามันควรจะอยู่ใน SO วิธีนี้เป็นพฤติกรรมที่ไม่ได้กำหนดตำราเรียน (อ้างอิงหน่วยความจำนอกอาร์เรย์) และธงที่เหมาะสมในการรวบรวมควรเตือนเกี่ยวกับเรื่องนี้
วงล้อประหลาด

ฉันเห็นด้วยกับ @ratchetfreak ดูเหมือนว่าจะเป็นข้อบกพร่องของคอมไพเลอร์เนื่องจากช่วงดัชนีที่ถูกต้องคือ [0, 5] สิ่งใดก็ตามที่อยู่ภายนอกจะต้องมีข้อผิดพลาดในการรวบรวม / รันไทม์ โดยทั่วไปเวกเตอร์เป็นกรณีเฉพาะของฟังก์ชั่นที่มีดัชนีองค์ประกอบแรกขึ้นอยู่กับผู้ใช้ เนื่องจากสัญญา C คือองค์ประกอบเริ่มต้นที่ดัชนี 0 จึงเป็นข้อผิดพลาดในการเข้าถึงองค์ประกอบเชิงลบ
วาล

2
@ ราฟาเอล C มีลักษณะที่แตกต่างกันสองอย่างมากกว่าภาษาทั่วไปที่มีอาร์เรย์ที่สำคัญ หนึ่งคือ C มี subarrays และการอ้างถึงองค์ประกอบ-1ของ subarray เป็นวิธีที่ถูกต้องสมบูรณ์ในการอ้างถึงองค์ประกอบก่อนอาร์เรย์ในอาร์เรย์ขนาดใหญ่ อีกอย่างคือถ้าดัชนีไม่ถูกต้องโปรแกรมนั้นไม่ถูกต้อง แต่ในการใช้งานส่วนใหญ่คุณจะได้รับพฤติกรรมที่ไม่ดีเงียบไม่ใช่ข้อผิดพลาดนอกช่วง
Gilles 'หยุดความชั่วร้าย'

4
@Gilles หากเป็นประเด็นของคำถามนี่น่าจะเป็นStack Overflowแน่นอน
Raphael

คำตอบ:


27

การดำเนินการทำดัชนีอาร์เรย์a[i]ได้รับความหมายจากคุณสมบัติดังต่อไปนี้ของ C

  1. ไวยากรณ์เทียบเท่ากับa[i] *(a + i)ดังนั้นมันจึงเป็นที่ถูกต้องที่จะพูดที่จะได้รับองค์ประกอบที่5[a] 5a

  2. ชี้เลขคณิตกล่าวว่าได้รับการชี้pและจำนวนเต็มi, p + i ตัวชี้pขั้นสูงโดยi * sizeof(*p)ไบต์

  3. ชื่อของอาร์เรย์aจะเปลี่ยนไปเป็นตัวชี้ไปยังองค์ประกอบที่ 0 อย่างรวดเร็วa

ผลการทำดัชนีอาเรย์เป็นกรณีพิเศษของการทำดัชนีชี้ ตั้งแต่ตัวชี้สามารถชี้ไปที่สถานที่ใด ๆ ภายในอาร์เรย์แสดงออกโดยพลการใด ๆ ที่มีลักษณะเหมือนp[-1]เป็นไม่ผิดโดยการตรวจสอบและเพื่อให้คอมไพเลอร์ไม่ได้ (ไม่สามารถ) พิจารณาการแสดงออกดังกล่าวทั้งหมดเป็นข้อผิดพลาด

ตัวอย่างของคุณa[-1]ที่aชื่อจริงของอาร์เรย์ไม่ถูกต้อง IIRC มันไม่ได้กำหนดถ้ามีค่าตัวชี้ที่มีความหมายซึ่งเป็นผลมาจากการแสดงออกa - 1ที่aรู้ว่าเป็นตัวชี้ไปยังองค์ประกอบที่ 0 ของอาร์เรย์ ดังนั้นคอมไพเลอร์ที่ฉลาดสามารถตรวจจับสิ่งนี้และตั้งค่าสถานะเป็นข้อผิดพลาด คอมไพเลอร์ตัวอื่นยังสามารถใช้งานร่วมกันได้ในขณะที่ให้คุณถ่ายภาพตัวเองโดยการให้พอยน์เตอร์ชี้ไปที่ช่องสแต็คแบบสุ่ม

คำตอบวิทยาศาสตร์คอมพิวเตอร์คือ:

  • ใน C ตัว[]ดำเนินการถูกกำหนดบนพอยน์เตอร์ไม่ใช่อาร์เรย์ โดยเฉพาะอย่างยิ่งมันถูกกำหนดไว้ในแง่ของเลขคณิตตัวชี้และความไม่สอดคล้องของตัวชี้

  • ใน C ตัวชี้เป็นนามธรรมสิ่งอันดับมีเงื่อนไขว่า(start, length, offset) 0 <= offset <= lengthเลขคณิตของตัวชี้ถูกยกขึ้นทางคณิตศาสตร์เป็นหลักในออฟเซ็ตโดยมีข้อแม้ว่าหากผลลัพธ์ของการดำเนินการละเมิดเงื่อนไขตัวชี้จะเป็นค่าที่ไม่ได้กำหนด De อ้างอิงตัวชี้เพิ่มข้อ จำกัด offset < lengthเพิ่มเติมที่

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


โปรดลองตอบคำถามทั่วไปไม่ใช่อย่างใดอย่างหนึ่งขึ้นอยู่กับนิสัยของภาษาการเขียนโปรแกรมเฉพาะใด ๆ
Raphael

6
@ ราฟาเอลคำถามอย่างชัดเจนเกี่ยวกับซีฉันคิดว่าฉันตอบคำถามเฉพาะว่าทำไมคอมไพเลอร์ C ได้รับอนุญาตให้รวบรวมการแสดงออกที่ดูเหมือนไม่มีความหมายภายในคำจำกัดความของซี
Hari

คำถามเกี่ยวกับ C โดยเฉพาะอย่างยิ่งเป็นที่น่ารังเกียจที่นี่; จดความคิดเห็นของฉันไว้กับคำถาม
Raphael

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

15

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

สิ่งนี้อาจดูบ้า แต่มีหลายสาเหตุที่ทำให้สิ่งนี้ได้รับอนุญาต:

  • การตรวจสอบว่าดัชนี i ถึง [-] นั้นมีราคาแพงหรือไม่
  • เทคนิคการเขียนโปรแกรมบางอย่างใช้ประโยชน์จากข้อเท็จจริงที่ a[-1]ถูกต้อง ตัวอย่างเช่นถ้าฉันรู้ว่าaไม่ใช่จุดเริ่มต้นของอาร์เรย์ แต่เป็นตัวชี้ไปที่กึ่งกลางของอาร์เรย์จากนั้นa[-1]ก็รับองค์ประกอบของอาร์เรย์ที่อยู่ทางด้านซ้ายของตัวชี้

6
กล่าวอีกนัยหนึ่งก็ไม่ควรใช้ ระยะเวลา อะไรชื่อของคุณคือ Donald Knuth และคุณพยายามบันทึกคำแนะนำอีก 17 คำ โดยทั้งหมดไปข้างหน้า
Raphael

ขอบคุณสำหรับการตอบกลับ แต่ฉันไม่เข้าใจ BTW ฉันจะอ่านมันซ้ำแล้วซ้ำอีกจนกว่าฉันจะเข้าใจ .. :)
Mohammed Fawzan

2
@Raphael: การดำเนินงานของรุ่นโคล่าวัตถุที่ใช้ -1 ตำแหน่งในการจัดเก็บ vtable นี้: piumarta.com/software/cola/objmodel2.pdf ดังนั้นเขตข้อมูลจะถูกเก็บไว้ในส่วนบวกของวัตถุและ vtable ในเชิงลบ ฉันจำรายละเอียดไม่ได้ แต่ฉันคิดว่ามันต้องเกี่ยวข้องกับความมั่นคง
Dave Clarke

@ DeZéroToxin: อาร์เรย์เป็นเพียงตำแหน่งในหน่วยความจำเท่านั้นโดยมีบางตำแหน่งที่อยู่ถัดจากอาร์เรย์ซึ่งเป็นส่วนหนึ่งในเชิงตรรกะของอาร์เรย์ แต่จริงๆแล้วอาร์เรย์เป็นเพียงตัวชี้
Dave Clarke

1
@ ราฟาเอลa[-1]ทำให้รู้สึกที่สมบูรณ์แบบสำหรับบางกรณีaในกรณีนี้มันผิดกฎหมายธรรมดา (แต่ไม่ถูกคอมไพเลอร์)
vonbrand

4

ตามที่คำตอบอื่น ๆ อธิบายนี่คือพฤติกรรมที่ไม่ได้กำหนดใน C. พิจารณาว่า C ถูกกำหนด (และส่วนใหญ่จะใช้) เป็น "แอสเซมเบลอร์ระดับสูง" ผู้ใช้ C ให้ความสำคัญกับความเร็วที่ไม่ลดทอนลงและการตรวจสอบสิ่งที่รันไทม์นั้นส่วนใหญ่เป็นคำถามเพื่อประโยชน์ในการใช้งาน บางคนสร้างซีที่ดูไร้สาระสำหรับคน comming จากภาษาอื่น ๆ ให้ความรู้สึกที่สมบูรณ์แบบใน C a[-1]เช่นนี้ ใช่มันไม่สมเหตุสมผลเสมอไป (


1
ฉันชอบคำตอบนี้ ให้เหตุผลที่แท้จริงว่าทำไมจึงไม่เป็นไร
darxsys

3

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


2

C ไม่ได้พิมพ์อย่างยิ่ง คอมไพเลอร์ C มาตรฐานจะไม่ตรวจสอบขอบเขตของอาร์เรย์ สิ่งอื่น ๆ ที่อาร์เรย์ใน C คืออะไร แต่บล็อกติดกันของหน่วยความจำและการสร้างดัชนีเริ่มต้นที่ 0 ดังนั้นดัชนี -1 a[0]เป็นที่ตั้งของสิ่งบิตรูปแบบก่อน

ภาษาอื่น ๆ ใช้ประโยชน์จากดัชนีลบในทางที่ดี ใน Python a[-1]จะส่งคืนองค์ประกอบสุดท้ายจะส่งคืนองค์ประกอบa[-2]ที่สองไปยังอีกต่อไปและอื่น ๆ


2
ดัชนีการพิมพ์และอาร์เรย์ที่เกี่ยวข้องมีความสัมพันธ์กันอย่างไร มีภาษาที่มีประเภทของธรรมชาติที่ดัชนีอาร์เรย์จะต้องเป็นธรรมชาติ?
Raphael

@ ราฟาเอลเท่าที่ฉันรู้การพิมพ์ที่แข็งแกร่งหมายถึงข้อผิดพลาดประเภทที่ถูกจับ อาเรย์เป็นประเภท IndexOutOfBounds เป็นข้อผิดพลาดดังนั้นในภาษาที่พิมพ์อย่างยิ่งที่จะรายงานใน C นี้จะไม่ นั่นคือสิ่งที่ฉันหมายถึง.
saadtaame

ในภาษาที่ฉันรู้ดัชนีของอาเรย์เป็นประเภทintดังนั้นa[-5]โดยทั่วไปint i; ... a[i] = ...;จะพิมพ์อย่างถูกต้อง ตรวจพบข้อผิดพลาดของดัชนีที่รันไทม์เท่านั้น แน่นอนผู้แปลที่ฉลาดอาจตรวจพบการละเมิดบางอย่าง
Raphael

@ ราฟาเอลฉันกำลังพูดถึงประเภทข้อมูลอาร์เรย์โดยรวมไม่ใช่ประเภทดัชนี นั่นอธิบายว่าทำไม C อนุญาตให้ผู้ใช้เขียน [-5] ใช่ -5 เป็นประเภทดัชนีที่ถูกต้อง แต่ไม่มีขอบเขตและเป็นข้อผิดพลาด ไม่มีการเอ่ยถึงการคอมไพล์หรือการตรวจสอบชนิดรันไทม์ในคำตอบของฉัน
saadtaame

1

ในคำง่าย ๆ :

ตัวแปรทั้งหมด (รวมถึงอาร์เรย์) ใน C จะถูกเก็บไว้ในหน่วยความจำ สมมติว่าคุณมี "หน่วยความจำ" 14 ไบต์และคุณเริ่มต้นต่อไปนี้:

int a=0;
int array1[6] = {0, 1, 2, 3, 4, 5};

นอกจากนี้ให้พิจารณาขนาดของ int เป็น 2 ไบต์ จากนั้นสมมุติว่าใน 2 ไบต์แรกของหน่วยความจำจำนวนเต็ม a จะถูกบันทึกไว้ใน 2 ไบต์ถัดไปจำนวนเต็มของตำแหน่งแรกของอาร์เรย์จะถูกบันทึกไว้ (นั่นหมายถึงอาร์เรย์ [0])

จากนั้นเมื่อคุณบอกว่าอาร์เรย์ [-1] เหมือนกับการอ้างถึงจำนวนเต็มที่บันทึกไว้ในหน่วยความจำซึ่งอยู่ก่อนอาร์เรย์ [0] ซึ่งในสมมุติฐานของเราคือจำนวนเต็ม a ในความเป็นจริงนี่ไม่ใช่วิธีที่ตัวแปรจัดเก็บในหน่วยความจำ


0
//:Example of negative index:
//:A memory pool with a heap and a stack:

unsigned char memory_pool[64] = {0};

unsigned char* stack = &( memory_pool[ 64 - 1] );
unsigned char* heap  = &( memory_pool[ 0     ] );

int stack_index =    0;
int  heap_index =    0;

//:reserve 4 bytes on stack:
stack_index += 4;

//:reserve 8 bytes on heap:
heap_index  += 8;

//:Read back all reserved memory from stack:
for( int i = 0; i < stack_index; i++ ){
    unsigned char c = stack[ 0 - i ];
    //:do something with c
};;
//:Read back all reserved memory from heap:
for( int i = 0; i < heap_index; i++ ){
    unsigned char c = heap[ 0 + i ];
    //:do something with c
};;

ยินดีต้อนรับสู่ CS.SE! เรากำลังมองหาคำตอบที่มาพร้อมกับคำอธิบายหรือคำอธิบายของการอ่าน เราไม่ได้เป็นเว็บไซต์ที่เข้ารหัสและเราไม่ต้องการคำตอบที่เป็นเพียงส่วนหนึ่งของรหัส คุณอาจพิจารณาว่าคุณสามารถแก้ไขคำตอบของคุณเพื่อให้ข้อมูลประเภทนั้นได้หรือไม่ ขอขอบคุณ!
DW
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.