โค้ดชิ้นนี้กำหนดขนาดอาร์เรย์โดยไม่ใช้ sizeof () ได้อย่างไร


134

จากคำถามสัมภาษณ์ C ฉันพบคำถามที่ระบุว่า "จะหาขนาดของอาร์เรย์ใน C โดยไม่ใช้ตัวดำเนินการ sizeof ได้อย่างไร" โดยมีวิธีแก้ไขดังนี้ มันใช้งานได้ แต่ฉันไม่เข้าใจว่าทำไม

#include <stdio.h>

int main() {
    int a[] = {100, 200, 300, 400, 500};
    int size = 0;

    size = *(&a + 1) - a;
    printf("%d\n", size);

    return 0;
}

ตามที่คาดไว้ผลตอบแทน 5

แก้ไข: ผู้คนชี้ให้เห็นคำตอบนี้แต่ไวยากรณ์แตกต่างกันเล็กน้อยนั่นคือวิธีการจัดทำดัชนี

size = (&arr)[1] - arr;

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


13
หาไม่เจอ แต่ดูเหมือนจะพูดอย่างเคร่งครัด ภาคผนวก J.2ระบุไว้อย่างชัดเจน: ตัวถูกดำเนินการของตัวดำเนินการ unary * มีค่าที่ไม่ถูกต้องเป็นพฤติกรรมที่ไม่ได้กำหนด ที่นี่&a + 1ไม่ได้ชี้ไปที่วัตถุใด ๆ ที่ถูกต้องดังนั้นจึงไม่ถูกต้อง
ยูจีนช.



@AlmaDo ดีไวยากรณ์ไม่แตกต่างกันเล็กน้อยเช่นส่วนการจัดทำดัชนีดังนั้นฉันเชื่อว่าคำถามนี้ยังคงใช้ได้ในตัวเอง แต่ฉันอาจจะผิด ขอบคุณที่ชี้ให้ดู!
janojlic

1
@janojlicz พวกเขาเหมือนกันเป็นหลักเพราะ(ptr)[x]เหมือนกับ*((ptr) + x).
SS Anne

คำตอบ:


137

เมื่อคุณเพิ่ม 1 ลงในตัวชี้ผลลัพธ์คือตำแหน่งของวัตถุถัดไปในลำดับของวัตถุชนิดชี้ไปที่ (เช่นอาร์เรย์) หากpชี้ไปที่intวัตถุก็p + 1จะชี้ไปที่ถัดไปintตามลำดับ ถ้าpชี้ไปที่อาร์เรย์ 5 องค์ประกอบของint(ในกรณีนี้คือนิพจน์&a) p + 1จะชี้ไปที่อาร์เรย์ 5 องค์ประกอบintถัดไปในลำดับ

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

นิพจน์&aให้ที่อยู่ของaและมีประเภทint (*)[5](ตัวชี้ไปที่อาร์เรย์ 5 องค์ประกอบของint) นิพจน์&a + 1ให้ที่อยู่ของอาร์เรย์ 5 องค์ประกอบintถัดไปต่อไปนี้aและยังมีประเภทint (*)[5]ด้วย การแสดงออก*(&a + 1)dereferences ผลมาจากการ&a + 1เช่นว่าผลตอบแทนถัวเฉลี่ยที่อยู่ของแรกintดังต่อไปนี้องค์ประกอบสุดท้ายของaและมีประเภทint [5]ซึ่งในบริบทนี้ "สูญสลาย" int *เพื่อแสดงออกของพิมพ์

ในทำนองเดียวกันการแสดงออกa"สลายตัว" int *เพื่อชี้ไปยังองค์ประกอบแรกของอาร์เรย์และมีประเภท

รูปภาพอาจช่วยได้:

int [5]  int (*)[5]     int      int *

+---+                   +---+
|   | <- &a             |   | <- a
| - |                   +---+
|   |                   |   | <- a + 1
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
+---+                   +---+
|   | <- &a + 1         |   | <- *(&a + 1)
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
+---+                   +---+

นี่คือสองมุมมองของการจัดเก็บเดียวกัน - ด้านซ้ายเรากำลังดูอยู่ว่ามันเป็นลำดับของอาร์เรย์ 5 องค์ประกอบของในขณะที่ทางด้านขวาเรากำลังดูอยู่ว่ามันเป็นลำดับของint intฉันยังแสดงนิพจน์ต่างๆและประเภทของมันด้วย

โปรดทราบว่าการแสดงออก*(&a + 1)ส่งผลให้เกิดพฤติกรรมที่ไม่ได้กำหนดไว้ :

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

C 2011 Online Draft , 6.5.6 / 9


13
ข้อความ“ ห้ามใช้” นั้นเป็นทางการ: C 2018 6.5.6 8.
Eric Postpischil

@EricPostpischil: คุณมีลิงก์ไปยังร่างพรีผับปี 2018 (คล้ายกับ N1570.pdf) ไหม
John Bode

1
@JohnBode: คำตอบนี้มีการเชื่อมโยงไปยังเครื่อง Wayback ฉันตรวจสอบมาตรฐานอย่างเป็นทางการในสำเนาที่ซื้อมา
Eric Postpischil

7
ดังนั้นถ้าใครเขียนsize = (int*)(&a + 1) - a;โค้ดนี้จะถูกต้องสมบูรณ์? : o
Gizmo

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

35

บรรทัดนี้มีความสำคัญที่สุด:

size = *(&a + 1) - a;

อย่างที่คุณเห็นขั้นแรกจะใช้ที่อยู่aและเพิ่มที่อยู่เข้าไป จากนั้นจึง dereferences ตัวชี้และลบค่าดั้งเดิมของaมัน

เลขคณิตชี้ใน C 5ทำให้เกิดนี้เพื่อกลับจำนวนขององค์ประกอบในอาร์เรย์หรือ เพิ่มหนึ่งและ&aเป็นตัวชี้ไปยังอาร์เรย์ต่อไป 5 หลังจากint aหลังจากนั้นโค้ดนี้จะยกเลิกการอ้างอิงตัวชี้ที่เป็นผลลัพธ์และลบa(ประเภทอาร์เรย์ที่สลายตัวเป็นตัวชี้) จากนั้นให้จำนวนองค์ประกอบในอาร์เรย์

รายละเอียดเกี่ยวกับการทำงานของตัวชี้ทางคณิตศาสตร์:

สมมติว่าคุณมีตัวชี้xyzที่ชี้ไปยังชนิดและมีค่าint (int *)160เมื่อคุณลบจำนวนใด ๆ ออกจากxyzC จะระบุว่าจำนวนจริงที่ลบออกxyzคือจำนวนนั้นคูณด้วยขนาดของประเภทที่ชี้ไป ตัวอย่างเช่นถ้าคุณหักออก5จากxyzค่าของxyzที่เกิดจะเป็นxyz - (sizeof(*xyz) * 5)ถ้าคำนวณตัวชี้ไม่ได้ใช้

เนื่องจากaเป็นอาร์เรย์5 intประเภทค่าผลลัพธ์จะเป็น 5 อย่างไรก็ตามสิ่งนี้จะใช้ไม่ได้กับตัวชี้เฉพาะกับอาร์เรย์เท่านั้น 1ถ้าคุณพยายามที่นี้กับตัวชี้ผลที่ได้จะเป็น

นี่คือตัวอย่างเล็กน้อยที่แสดงที่อยู่และวิธีการที่ไม่ได้กำหนดไว้ ด้านซ้ายมือจะแสดงที่อยู่:

a + 0 | [a[0]] | &a points to this
a + 1 | [a[1]]
a + 2 | [a[2]]
a + 3 | [a[3]]
a + 4 | [a[4]] | end of array
a + 5 | [a[5]] | &a+1 points to this; accessing past array when dereferenced

ซึ่งหมายความว่ารหัสถูกลบaจาก&a[5](หรือa+5) 5ทำให้

โปรดทราบว่านี่เป็นพฤติกรรมที่ไม่ได้กำหนดและไม่ควรใช้ในสถานการณ์ใด ๆ อย่าคาดหวังว่าพฤติกรรมนี้จะสอดคล้องกันในทุกแพลตฟอร์มและอย่าใช้ในโปรแกรมการผลิต


27

อืมฉันสงสัยว่านี่เป็นสิ่งที่จะไม่ได้ผลในช่วงแรก ๆ ของ C. มันฉลาดมาก

ทำตามขั้นตอนทีละขั้นตอน:

  • &a รับตัวชี้ไปยังวัตถุประเภท int [5]
  • +1 รับวัตถุถัดไปโดยสมมติว่ามีอาร์เรย์ของสิ่งเหล่านั้น
  • * แปลงที่อยู่นั้นเป็นตัวชี้ชนิดเป็น int ได้อย่างมีประสิทธิภาพ
  • -a ลบพอยน์เตอร์ int สองตัวส่งคืนจำนวนอินสแตนซ์ int ระหว่างพวกเขา

ฉันไม่แน่ใจว่ามันถูกกฎหมายอย่างสมบูรณ์ (ในที่นี้ฉันหมายถึงกฎหมายภาษาทนายความ - จะไม่ได้ผลในทางปฏิบัติ) เนื่องจากมีการดำเนินการบางประเภทที่เกิดขึ้น ตัวอย่างเช่นคุณได้รับ "อนุญาต" ให้ลบพอยน์เตอร์สองตัวเมื่อชี้ไปที่องค์ประกอบในอาร์เรย์เดียวกันเท่านั้น ถูกสังเคราะห์โดยการเข้าถึงอาร์เรย์อีกแม้ว่าอาร์เรย์ผู้ปกครองจึงไม่จริงชี้ลงในอาร์เรย์เดียวกับ*(&a+1) aนอกจากนี้ในขณะที่คุณได้รับอนุญาตให้สังเคราะห์ตัวชี้ผ่านองค์ประกอบสุดท้ายของอาร์เรย์และคุณสามารถถือว่าวัตถุใด ๆ เป็นอาร์เรย์ของ 1 องค์ประกอบการดำเนินการของ dereferencing ( *) จะไม่ "อนุญาต" บนตัวชี้ที่สังเคราะห์นี้แม้ว่าจะเป็น ไม่มีพฤติกรรมในกรณีนี้!

ฉันสงสัยว่าในช่วงแรก ๆ ของ C (ไวยากรณ์ K&R มีใครบ้าง?) อาร์เรย์จะสลายตัวเป็นตัวชี้เร็วกว่ามากดังนั้นจึง*(&a+1)อาจส่งคืนที่อยู่ของตัวชี้ถัดไปของประเภท int ** เท่านั้น คำจำกัดความที่เข้มงวดมากขึ้นของ C ++ สมัยใหม่ทำให้ตัวชี้ประเภทอาร์เรย์มีอยู่และทราบขนาดอาร์เรย์และอาจเป็นไปตามมาตรฐาน C โค้ดฟังก์ชัน C ทั้งหมดใช้ตัวชี้เป็นอาร์กิวเมนต์เท่านั้นดังนั้นความแตกต่างที่มองเห็นได้ทางเทคนิคจึงมีน้อย แต่ฉันคาดเดาที่นี่เท่านั้น

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

จุดอ่อนที่เป็นไปได้อีกประการหนึ่งคือคอมไพเลอร์ C อาจจัดแนวอาร์เรย์ด้านนอก ลองนึกภาพว่านี่คืออาร์เรย์ 5 ตัวอักษร ( char arr[5]) หรือไม่เมื่อโปรแกรมดำเนินการ&a+1มันเป็นการเรียกใช้ลักษณะการทำงาน "array of array" คอมไพลเลอร์อาจตัดสินใจว่าอาร์เรย์ของอาร์เรย์ 5 ตัวอักษร ( char arr[][5]) ถูกสร้างขึ้นเป็นอาร์เรย์ของอาร์เรย์ 8 ตัวอักษร ( char arr[][8]) เพื่อให้อาร์เรย์ด้านนอกจัดแนวอย่างสวยงาม โค้ดที่เรากำลังคุยกันตอนนี้จะรายงานขนาดอาร์เรย์เป็น 8 ไม่ใช่ 5 ฉันไม่ได้บอกว่าคอมไพเลอร์เฉพาะจะทำเช่นนี้ แต่อาจทำได้


พอใช้. อย่างไรก็ตามด้วยเหตุผลที่ยากจะอธิบายทุกคนใช้ sizeof () / sizeof ()?
Gem Taylor

5
คนส่วนใหญ่ทำ ตัวอย่างเช่นsizeof(array)/sizeof(array[0])ให้จำนวนองค์ประกอบในอาร์เรย์
SS Anne

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

1
การลบพอยน์เตอร์ไม่ได้ จำกัด เพียงแค่พอยน์เตอร์สองตัวในอาร์เรย์เดียวกันเท่านั้นพอยน์เตอร์ยังได้รับอนุญาตให้อยู่ที่ปลายอาร์เรย์อีกด้วย &a+1ถูกกำหนด ดังที่ John Bollinger ตั้งข้อสังเกต*(&a+1)ไม่ใช่เพราะมันพยายามที่จะหักล้างวัตถุที่ไม่มีอยู่จริง
Eric Postpischil

5
คอมไพเลอร์ไม่สามารถใช้char [][5]as char arr[][8]. อาร์เรย์เป็นเพียงวัตถุที่ทำซ้ำในนั้น ไม่มีช่องว่างภายใน เพิ่มเติมนี้จะทำลาย (Non-กฎเกณฑ์) 2 ตัวอย่างใน C 2018 6.5.3.4 7 sizeof array / sizeof array[0]ซึ่งบอกเราเราสามารถคำนวณจำนวนขององค์ประกอบในอาร์เรย์ที่มี
Eric Postpischil
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.