ทำไมอาร์เรย์ C ถึงไม่ติดตามความยาวของมัน


77

อะไรคือเหตุผลที่อยู่เบื้องหลังไม่แน่ชัดเก็บความยาวของอาร์เรย์กับอาร์เรย์ในC?

วิธีที่ฉันเห็นมันมีเหตุผลมากมายที่ต้องทำเช่นนั้นแต่มีไม่มากนักในการสนับสนุนมาตรฐาน (C89) ตัวอย่างเช่น

  1. การมีความยาวที่มีอยู่ในบัฟเฟอร์สามารถป้องกันไม่ให้บัฟเฟอร์มีค่าเกินได้
  2. สไตล์ Java arr.lengthมีทั้งชัดเจนและหลีกเลี่ยงโปรแกรมเมอร์จากการรักษาจำนวนมากintในกองหากจัดการกับหลายอาร์เรย์
  3. พารามิเตอร์ฟังก์ชั่นกลายเป็นจุดศูนย์กลางมากขึ้น

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

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

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

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


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

3
คุณอาจจะพบว่าเหตุใด Pascal ไม่ใช่ภาษาที่ฉันชอบในการเขียนโปรแกรมส่วน 2.1 ถึงจะลึกซึ้ง

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

6
ส่วนที่แท้จริงของคำตอบของคุณได้รับการตอบแล้วหลายครั้งในแบบที่ฉันจะได้ แต่ฉันสามารถแยกประเด็นที่แตกต่าง: "ทำไมขนาดmalloc()พื้นที่เอ็ดไม่สามารถร้องขอแบบพกพา?" นั่นเป็นสิ่งที่ทำให้ฉันสงสัยหลายครั้ง
glglgl

5
การลงคะแนนเพื่อเปิดใหม่ มีเหตุผลบางอย่างแม้ว่ามันจะเป็นเพียงแค่ "K&R ไม่ได้คิดถึงมัน"
Telastyn

คำตอบ:


106

อาร์เรย์ C จะติดตามความยาวของมันเนื่องจากความยาวของอาร์เรย์เป็นคุณสมบัติคงที่:

int xs[42];  /* a 42-element array */

ปกติคุณจะไม่สามารถสืบค้นความยาวนี้ได้ แต่คุณไม่จำเป็นต้องเป็นเพราะมันเป็นแบบคงที่ - เพียงแค่ประกาศXS_LENGTHความยาวของแมโครและคุณก็ทำเสร็จแล้ว

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

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


13
ฉันคิดว่าคุณหมายถึงว่าถ้าฉันไม่เข้าใจผิดคอมไพเลอร์ Cจะคอยติดตามความยาวของอาเรย์แบบคงที่ แต่สิ่งนี้ไม่ดีสำหรับฟังก์ชั่นที่เพิ่งได้รับพอยน์เตอร์
VF1

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

37
"คุณมักจะไม่สามารถสืบค้นความยาวนี้ได้" - ที่จริงแล้วคุณสามารถทำได้มันคือขนาดของโอเปอเรเตอร์ - sizeof (xs) จะคืนค่า 168 สมมติว่า int มีความยาวสี่ไบต์ ในการรับ 42 ให้ทำ: sizeof (xs) / sizeof (int)
tcrosley

15
@tcrosley ใช้งานได้เฉพาะภายในขอบเขตของการประกาศอาร์เรย์ - ลองส่ง xs เป็น param ไปยังฟังก์ชันอื่นแล้วดูว่า sizeof (xs) ให้อะไรคุณ ...
Gwyn Evans

26
@GwynEvans อีกครั้ง: พอยน์เตอร์ไม่ใช่อาร์เรย์ ดังนั้นถ้าคุณ“ ส่งอาร์เรย์เป็นพารามิเตอร์ไปยังฟังก์ชันอื่น” คุณจะไม่ผ่านอาร์เรย์ แต่เป็นตัวชี้ การอ้างว่าอาร์เรย์อยู่sizeof(xs)ที่ไหนxsจะเป็นสิ่งที่แตกต่างกันในขอบเขตอื่นนั้นเป็นเท็จอย่างโจ่งแจ้งเพราะการออกแบบของ C ไม่อนุญาตให้อาร์เรย์ออกจากขอบเขต หากsizeof(xs)ที่xsเป็นอาร์เรย์จะแตกต่างจากsizeof(xs)ที่xsเป็นตัวชี้ว่ามาเป็นแปลกใจเพราะคุณกำลังเปรียบเทียบแอปเปิ้ลกับส้ม
amon

38

สิ่งนี้เกี่ยวข้องกับคอมพิวเตอร์ที่มีอยู่ในขณะนั้น โปรแกรมที่คอมไพล์ไม่เพียง แต่ต้องทำงานบนคอมพิวเตอร์ที่มีทรัพยากร จำกัด แต่ที่สำคัญกว่านั้นคอมไพเลอร์เองก็ต้องทำงานบนเครื่องเหล่านี้ ในขณะที่ Thompson พัฒนา C เขาใช้ PDP-7 พร้อม RAM ขนาด 8k คุณสมบัติภาษาที่ซับซ้อนที่ไม่มีอะนาล็อกทันทีบนรหัสเครื่องจริงนั้นไม่ได้รวมอยู่ในภาษา

การอ่านอย่างละเอียดในประวัติของ Cทำให้เกิดความเข้าใจมากขึ้นในข้างต้น แต่มันไม่ได้เกิดจากข้อ จำกัด ของเครื่องจักรที่พวกเขามี:

ยิ่งไปกว่านั้นภาษา (C) แสดงพลังจำนวนมากในการอธิบายแนวคิดที่สำคัญตัวอย่างเช่นเวกเตอร์ที่มีความยาวแตกต่างกันไปในขณะใช้งานโดยมีกฎพื้นฐานเพียงเล็กน้อยเท่านั้น ... มันเป็นเรื่องที่น่าสนใจที่จะเปรียบเทียบวิธีการของ C กับภาษาสองภาษาร่วมกันคือ Algol 68 และ Pascal [Jensen 74] อาร์เรย์ในอัลกอล 68 มีขอบเขตคงที่หรือมีความยืดหยุ่น: จำเป็นต้องใช้กลไกจำนวนมากทั้งในการกำหนดภาษาและในคอมไพเลอร์เพื่อรองรับอาเรย์ที่มีความยืดหยุ่น (และไม่ใช่คอมไพเลอร์ทั้งหมด อาร์เรย์และสตริงและสิ่งนี้พิสูจน์ได้ว่า จำกัด [Kernighan 81]

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


4
คำถามนี้เป็นคำถามเดิม และความจริงที่ว่า C ถูกเก็บไว้อย่างจงใจ "สัมผัสเบา" เมื่อมันมาถึงการตรวจสอบสิ่งที่โปรแกรมเมอร์กำลังทำอยู่เป็นส่วนหนึ่งของการทำให้มันน่าสนใจสำหรับการเขียนระบบปฏิบัติการ
ClickRick

5
ลิงก์ที่ยอดเยี่ยมพวกเขาเปลี่ยนการจัดเก็บความยาวของสตริงอย่างชัดเจนเพื่อใช้ตัวคั่นto avoid the limitation on the length of a string caused by holding the count in an 8- or 9-bit slot, and partly because maintaining the count seemed, in our experience, less convenient than using a terminator- ดีมากสำหรับ :-)
Voo

5
อาเรย์ที่ไม่สิ้นสุดนั้นสอดคล้องกับวิธีการของโลหะซีซีจำไว้ว่าหนังสือK&R Cนั้นน้อยกว่า 300 หน้าด้วยการสอนภาษาการอ้างอิงและรายการการโทรมาตรฐาน หนังสือ O'Reilly Regex ของฉันมีความยาวเกือบสองเท่าของ K&R C.
Michael Shopsin

22

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

มีปัญหาอื่น - char*จำไว้ว่าซีจะไม่เชิงวัตถุดังนั้นถ้าคุณทำยาวคำนำหน้าสตริงทั้งหมดก็จะต้องได้รับการกำหนดให้เป็นคอมไพเลอร์ชนิดที่แท้จริงไม่ได้ หากเป็นชนิดพิเศษคุณจะไม่สามารถเปรียบเทียบสตริงกับสตริงคงที่เช่น:

String x = "hello";
if (strcmp(x, "hello") == 0) 
  exit;

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

ฉันคิดว่าท้ายที่สุดพวกเขาไม่ได้เลือกวิธีนำหน้าความยาวเหมือนกับที่พูดว่า Pascal


10
การตรวจสอบขอบเขตยังใช้เวลา เล็กน้อยในแง่ของวันนี้ แต่สิ่งที่คนให้ความสนใจเมื่อพวกเขาสนใจประมาณ 4 ไบต์
Steven Burnap

18
@StevenBurnap: มันไม่สำคัญเลยแม้แต่วันนี้ถ้าคุณอยู่ในวงวนที่อยู่เหนือทุกพิกเซลที่มีภาพ 200 MB โดยทั่วไปถ้าคุณเขียน C คุณต้องการที่จะไปเร็วและคุณไม่ต้องการเสียเวลาในการตรวจสอบที่ถูกผูกไว้ที่ไร้ประโยชน์ทุกการวนซ้ำเมื่อforวนรอบของคุณถูกตั้งค่าให้เคารพขอบเขตแล้ว
Matteo Italia

4
@ VF1 "ย้อนกลับไปในวัน" มันอาจจะเป็นสองไบต์ (ธันวาคม PDP / 11 ใคร?)
คลิก Rick

7
มันไม่ใช่แค่ "ย้อนยุค" สำหรับซอฟต์แวร์ที่ C กำหนดเป้าหมายไว้ในฐานะ "ภาษาแอสเซมบลีแบบพกพา" เช่น OS kernals, ไดรเวอร์อุปกรณ์, ซอฟต์แวร์เรียลไทม์แบบฝัง ฯลฯ เป็นต้น การสูญเสียคำแนะนำครึ่งโหลในการตรวจสอบขอบเขตนั้นมีความสำคัญและในหลาย ๆ กรณีคุณต้อง "ไม่อยู่ในขอบเขต" (คุณจะเขียนดีบักเกอร์ได้อย่างไรถ้าคุณไม่สามารถเข้าถึงที่เก็บข้อมูลโปรแกรมแบบสุ่มได้)
James Anderson

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

11

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


6
"การออกแบบจะแตกต่าง" ไม่ใช่เหตุผลกับการออกแบบที่แตกต่าง
VF1

7
@ VF1: คุณเคยตั้งโปรแกรมใน Standard Pascal หรือไม่? ความสามารถของ C ในการยืดหยุ่นอย่างสมเหตุสมผลกับอาร์เรย์คือการปรับปรุงอย่างมากในการประกอบ (ไม่มีความปลอดภัยใด ๆ ) และภาษา typesafe รุ่นแรก (overafill typesafety รวมถึงขอบเขต array ที่แน่นอน)
MSalters

5
ความสามารถในการแบ่งอาร์เรย์นี้เป็นข้อโต้แย้งที่ยิ่งใหญ่สำหรับการออกแบบ C89

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

3
มีอีกทางเลือกหนึ่งในการออกแบบที่น่าสนใจที่อนุญาตให้มีการแบ่งเป็น: อย่าเก็บความยาวไว้ข้างอาร์เรย์ สำหรับตัวชี้ใด ๆ ไปยังอาร์เรย์ให้เก็บความยาวด้วยตัวชี้ (เมื่อคุณมีอาร์เรย์ C จริงขนาดจะเป็นค่าคงที่เวลารวบรวมและพร้อมใช้งานสำหรับคอมไพเลอร์) ใช้พื้นที่เพิ่มขึ้น แต่อนุญาตให้มีการแบ่งส่วนในขณะที่รักษาความยาว สนิมทำสิ่งนี้สำหรับ&[T]ประเภทเช่น

8

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

void ClearTwoElements(int *ptr)
{
  ptr[-2] = 0;
  ptr[2] = 0;
}
void blah(void)
{
  static int foo[10] = {1,2,3,4,5,6,7,8,9,10};
  ClearTwoElements(foo+2);
  ClearTwoElements(foo+7);
  ClearTwoElements(foo+1);
  ClearTwoElements(foo+8);
}

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

void ClearTwoElements(int *ptr)
{
  int* array_end = ARRAY_END(ptr);
  if ((array_end - ARRAY_BASE(ptr)) < 10 ||
      (ARRAY_BASE(ptr)+4) <= ADDRESS(ptr) ||          
      (array_end - 4) < ADDRESS(ptr)))
    trap();
  *(ADDRESS(ptr) - 4) = 0;
  *(ADDRESS(ptr) + 4) = 0;
}

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

void ClearTwoElements(int arr[], int index)
{
  arr[index-2] = 0;
  arr[index+2] = 0;
}
void blah(void)
{
  static int foo[10] = {1,2,3,4,5,6,7,8,9,10};
  ClearTwoElements(foo,2);
  ClearTwoElements(foo,7);
  ClearTwoElements(foo,1);
  ClearTwoElements(foo,8);
}

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


หากอาร์เรย์มีขนาดรันไทม์ดังนั้นตัวชี้ไปยังอาร์เรย์จะแตกต่างจากตัวชี้ไปยังองค์ประกอบของอาร์เรย์ ส่วนหลังอาจไม่สามารถแปลงให้เป็นอดีตได้เลย (โดยไม่ต้องสร้างอาร์เรย์ใหม่) []ไวยากรณ์อาจยังคงมีอยู่สำหรับพอยน์เตอร์ แต่มันจะแตกต่างจากอาเรย์ "จริง" สมมุติฐานเหล่านี้และปัญหาที่คุณอธิบายอาจไม่มีอยู่จริง
hyde

@hyde: คำถามคือควรอนุญาตให้ใช้เลขคณิตในพอยน์เตอร์ซึ่งไม่ทราบที่อยู่ฐานวัตถุ นอกจากนี้ฉันลืมปัญหาอื่น: อาร์เรย์ภายในโครงสร้าง คิดเกี่ยวกับมันฉันไม่แน่ใจว่าจะมีประเภทตัวชี้ที่สามารถชี้ไปยังอาร์เรย์ที่เก็บไว้ในโครงสร้างโดยไม่ต้องใช้ตัวชี้แต่ละตัวเพื่อรวมไม่เพียง แต่ที่อยู่ของตัวชี้ แต่ยังถูกกฎหมายบนและล่าง ช่วงที่สามารถเข้าถึงได้
supercat

จุดแยก ฉันคิดว่านี่จะช่วยลดคำตอบของ amon ได้
VF1

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

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

7

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

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


3
C ไม่ได้ "ออกแบบ" มากนักตามที่วิวัฒนาการมา เริ่มแรกการประกาศเช่นint f[5];นั้นจะไม่สร้างfเป็นอาร์เรย์ห้ารายการ int CANT_ACCESS_BY_NAME[5]; int *f = CANT_ACCESS_BY_NAME;แทนมันก็เท่ากับ การประกาศก่อนหน้านี้สามารถดำเนินการได้โดยไม่ต้องคอมไพเลอร์ต้อง "เข้าใจ" ครั้งอาร์เรย์ มันแค่เอาท์พุทคำสั่งของแอสเซมเบลอร์เพื่อจัดสรรพื้นที่แล้วก็ลืมได้ว่าfเคยมีอะไรเกี่ยวข้องกับอาเรย์ พฤติกรรมที่ไม่สอดคล้องกันของประเภทอาเรย์เกิดจากสิ่งนี้
supercat

1
ปรากฎว่าไม่มีโปรแกรมเมอร์รู้ว่าพวกเขากำลังทำอะไรในระดับที่ C ต้องการ
CodesInChaos

7

คำตอบสั้น ๆ :

เนื่องจาก C เป็นภาษาการเขียนโปรแกรมระดับต่ำจึงคาดหวังให้คุณดูแลปัญหาเหล่านี้ด้วยตัวคุณเอง แต่สิ่งนี้จะเพิ่มความยืดหยุ่นที่มากขึ้นในวิธีที่คุณใช้งาน

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

retval = my_func(my_array, my_array_length);

หรือคุณสามารถใช้ struct กับตัวชี้และความยาวหรือวิธีอื่น ๆ

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

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


1
+1 "และถ้าโค้ดทั้งหมดที่คุณเขียนได้ทราบความยาวของอาเรย์แล้วคุณไม่จำเป็นต้องผ่านความยาวเป็นตัวแปรเลย"
林果皞

ถ้าเฉพาะตัวชี้ความยาว + struct ได้รับการอบเข้ากับภาษาและห้องสมุดมาตรฐาน ช่องโหว่ด้านความปลอดภัยจำนวนมากสามารถหลีกเลี่ยงได้
CodesInChaos

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

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

5

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

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

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


1
You might say just store the length in the memory just before the data. What if the array isn't pointing to memory, but something like a UART buffer?คุณช่วยอธิบายเพิ่มเติมอีกหน่อยได้ไหม นอกจากนี้สิ่งที่อาจเกิดขึ้นบ่อยเกินไปหรือเป็นเพียงกรณีที่หายาก?
มาห์ดี

ถ้าฉันออกแบบมันฟังก์ชั่นอาร์กิวเมนต์ที่เขียนว่าT[]จะไม่เทียบเท่าT*แต่จะส่ง tuple ของตัวชี้และขนาดไปยังฟังก์ชัน อาร์เรย์ที่มีขนาดคงที่สามารถสลายตัวเป็นชิ้นส่วนอาร์เรย์แทนที่จะสลายไปยังตัวชี้ตามที่ทำใน C. ข้อดีหลักของวิธีนี้คือไม่ปลอดภัยโดยตัวมันเอง แต่เป็นแบบแผนที่ทุกอย่างรวมถึงไลบรารีมาตรฐานสามารถ สร้าง.
CodesInChaos

1

จากการพัฒนาภาษา C :

ดูเหมือนว่าโครงสร้างควรแมปด้วยวิธีที่ใช้งานง่ายไปยังหน่วยความจำในเครื่อง แต่ในโครงสร้างที่มีอาร์เรย์ไม่มีสถานที่ที่ดีในการซ่อนตัวชี้ที่ประกอบด้วยฐานของอาร์เรย์หรือวิธีที่สะดวกในการจัดเรียงให้เป็น เริ่มต้น ตัวอย่างเช่นรายการไดเรกทอรีของระบบ Unix ยุคแรก ๆ อาจอธิบายไว้ใน C เป็น
struct {
    int inumber;
    char    name[14];
};
ฉันต้องการโครงสร้างไม่เพียง แต่จะอธิบายลักษณะวัตถุนามธรรม แต่ยังอธิบายคอลเลกชันของบิตที่อาจถูกอ่านจากไดเรกทอรี คอมไพเลอร์สามารถซ่อนตัวชี้ไปยังnameที่ความหมายเรียกร้องที่ไหน? แม้ว่าโครงสร้างจะมีความคิดที่เป็นนามธรรมมากขึ้นและพื้นที่สำหรับพอยน์เตอร์อาจซ่อนอยู่ได้ฉันจะจัดการกับปัญหาทางเทคนิคของการเริ่มต้นพอยน์เตอร์เหล่านี้ได้อย่างไรเมื่อทำการจัดสรรวัตถุที่ซับซ้อนบางทีโครงสร้างที่ระบุที่มีอาร์เรย์

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

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

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

T *p = &a[0][0];

for ( size_t i = 0; i < rows; i++ )
  for ( size_t j = 0; j < cols; j++ )
    do_something_with( *p++ );

-2

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

รหัสต่อไปนี้คัดลอกข้อมูลบางส่วนจาก src ไปยัง dst ในกลุ่มขนาดใหญ่ที่ไม่ทราบว่าเป็นสตริงอักขระจริง

char src[] = "Hello, world";
char dst[1024];
int *my_array = src; /* What? Compiler warning, but the code is valid. */
int *other_array = dst;
int i;
for (i = 0; i <= sizeof(src)/sizeof(int); i++)
    other_array[i] = my_array[i]; /* Oh well, we've copied some extra bytes */
printf("%s\n", dst);

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


2
ฉันไม่คิดว่าคุณจะตอบคำถาม
Robert Harvey

2
สิ่งที่คุณพูดนั้นเป็นความจริง แต่ผู้ที่ต้องการทราบว่าทำไมถึงเป็นเช่นนี้

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

8
มันผิดที่จะบอกว่า C ไม่มีอาร์เรย์พิจารณาว่าคุณไม่สามารถสร้างไบนารีเดียวกันกับโครงสร้างอื่น ๆ ได้ (อย่างน้อยก็ไม่ใช่ถ้าคุณพิจารณาใช้ #defines เพื่อกำหนดขนาดอาร์เรย์) อาร์เรย์ใน C คือ "ลำดับของข้อมูลอย่างต่อเนื่อง" ไม่มีอะไรที่หวานชื่นเกี่ยวกับมัน การใช้พอยน์เตอร์เหมือนพวกเขาคืออาร์เรย์คือน้ำตาลประโยคที่นี่ (แทนที่จะเป็นตัวชี้ทางคณิตศาสตร์ที่ชัดเจน) ไม่ใช่อาร์เรย์
hyde

2
struct Foo { int arr[10]; }ใช่พิจารณารหัสนี้: arrเป็นอาร์เรย์ไม่ใช่ตัวชี้
Steven Burnap
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.