ทำไมไวยากรณ์ C สำหรับอาร์เรย์พอยน์เตอร์และฟังก์ชั่นจึงถูกออกแบบด้วยวิธีนี้


16

หลังจากได้เห็น (และถาม!) คำถามมากมายคล้ายกับ

อะไรint (*f)(int (*a)[5])หมายถึงใน C?

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

ทำไมไวยากรณ์ของ C จึงถูกออกแบบด้วยวิธีนี้

ตัวอย่างเช่นถ้าฉันออกแบบพอยน์เตอร์ฉันจะแปล "ตัวชี้ไปยังอาเรย์ของพอยน์เตอร์ 10 องค์ประกอบ" เป็น

int*[10]* p;

และไม่

int* (*p)[10];

ซึ่งฉันรู้สึกว่าคนส่วนใหญ่จะเห็นด้วยน้อยกว่ามาก

ดังนั้นฉันสงสัยว่าทำไมไวยากรณ์เอ่อไม่ได้ใช้งานง่าย? มีปัญหาเฉพาะหรือไม่ที่ไวยากรณ์แก้ปัญหา (อาจเป็นความกำกวม?) ที่ฉันไม่รู้?


2
คุณรู้ว่าไม่มีคำตอบที่แท้จริงสำหรับคำถามนี้ ขวา? สิ่งที่คุณจะได้รับก็แค่เดา
BЈовић

7
@VJo - อาจมีคำตอบ "ของจริง" (เช่นวัตถุประสงค์) - ผู้เขียนภาษาและคณะกรรมการมาตรฐานมีเหตุผลที่ชัดเจน (หรืออย่างน้อยก็อธิบาย) การตัดสินใจเหล่านี้จำนวนมาก
Detly

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

1
@Caleb: ตลกวิธีการที่คุณสรุปได้อย่างง่ายดายเพราะฉันได้เรียนรู้มันและฉันยังคงมีคำถามนี้ ...
user541686

1
cdeclคำสั่งเป็นประโยชน์มากสำหรับการถอดรหัสการประกาศ C ที่ซับซ้อน นอกจากนี้ยังมีอินเตอร์เฟซเว็บที่cdecl.org
Keith Thompson

คำตอบ:


16

ความเข้าใจของฉันเกี่ยวกับประวัติศาสตร์ของมันคือมันขึ้นอยู่กับสองประเด็นหลัก ...

ประการแรกผู้เขียนภาษาต้องการทำให้ไวยากรณ์เป็นศูนย์กลางมากกว่าที่จะพิมพ์เป็นศูนย์กลาง นั่นคือพวกเขาต้องการให้โปรแกรมเมอร์ดูการประกาศและคิดว่า "ถ้าฉันเขียนนิพจน์*func(arg)นั่นจะส่งผลให้intถ้าฉันเขียน*arg[N]ฉันจะมีการลอย" แทนที่จะ " funcต้องเป็นตัวชี้ไปยังฟังก์ชั่นนี้และคืนสิ่งนั้น "

รายการซีวิกิพีเดียบอกว่า:

แนวคิดของ Ritchie คือการประกาศตัวระบุในบริบทที่คล้ายคลึงกับการใช้งาน: "การประกาศสะท้อนการใช้งาน"

... อ้างถึง p122 ของ K & R2 ซึ่งอนิจจาฉันไม่ต้องไปหาใบเสนอราคาเพิ่มเติมสำหรับคุณ

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

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

ต้องบอกว่าฉันไม่ใช่ผู้เชี่ยวชาญด้านไวยากรณ์ภาษาหรือการเขียนคอมไพเลอร์ แต่ฉันรู้พอที่จะรู้ว่ามีอะไรให้รู้มากมาย)


2
"การทำให้คอมไพเลอร์ง่ายต่อการเขียน" ... ยกเว้น C มีชื่อเสียงในการแยกวิเคราะห์ได้ยาก (มีวงเงินเฉพาะ C ++)
Jan Hudec

1
@JanHudec - ดี ... ใช่ นั่นไม่ใช่คำสั่งที่กันน้ำ แต่ในขณะที่ C เป็นไปไม่ได้ที่จะแยกวิเคราะห์เป็นไวยากรณ์ที่ไม่มีบริบทเมื่อบุคคลคนหนึ่งมีวิธีในการแยกวิเคราะห์มันจะหยุดขั้นตอนนี้ยาก และความจริงคือมันเป็นที่อุดมสมบูรณ์ในช่วงแรกเนื่องจากคนที่ความสามารถในการคอมไพเลอร์ปังออกได้อย่างง่ายดายเพื่อ K & R จะต้องหลงสมดุล (ในริชาร์ดกาเบรียลชื่อดังเรื่อง Rise of "Worse is Better"เขา
ยอมแพ้

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

12

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

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


เกี่ยวกับพอยน์เตอร์ของอาร์เรย์โดยเฉพาะตัวอย่างที่สองของคุณควรเป็นint (*p)[10];(ใช่ซินแท็คซ์สับสนมาก) ฉันอาจจะอ่านว่าเป็น "int pointer to array สิบ" ... ซึ่งค่อนข้างสมเหตุสมผล หากไม่ใช่สำหรับวงเล็บคอมไพเลอร์จะตีความว่ามันเป็นอาร์เรย์ของสิบพอยน์เตอร์แทนซึ่งจะทำให้การประกาศมีความหมายที่แตกต่างกันโดยสิ้นเชิง

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

ตัวอย่างชัดเจน:

int func (int (*arr_ptr)[10])
{
  return 0;
}

int main()
{
  int array[10];
  int (*arr_ptr)[10]  = &array;
  int (*func_ptr)(int(*)[10]) = &func;

  func_ptr(arr_ptr);
}

ตัวอย่างที่ไม่ชัดเจนที่เทียบเท่า:

typedef int array_t[10];
typedef int (*funcptr_t)(array_t*);


int func (array_t* arr_ptr)
{
  return 0;
}

int main()
{
  int        array[10];
  array_t*   arr_ptr  = &array; /* non-obscure array pointer */
  funcptr_t  func_ptr = &func;  /* non-obscure function pointer */

  func_ptr(arr_ptr);
}

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


ในที่สุดคำตอบที่สมเหตุสมผล :-) ฉันอยากรู้ว่าไวยากรณ์เฉพาะจะลดขนาดซอร์สโค้ดอย่างไร แต่ก็เป็นความคิดที่สมเหตุสมผลและสมเหตุสมผล ขอบคุณ +1
user541686

ฉันจะบอกว่ามันมีขนาดเล็กกว่าซอร์สโค้ดน้อยลงและมีมากขึ้นเกี่ยวกับการเขียนคอมไพเลอร์ แต่ +1 อย่างแน่นอนสำหรับ "typdef away the weirdness" สุขภาพจิตของฉันดีขึ้นอย่างมากในวันที่ฉันรู้ว่าฉันสามารถทำได้
Detly

2
[จำเป็นต้องมี] ในสิ่งที่ขนาดซอร์สโค้ด ฉันไม่เคยได้ยินข้อ จำกัด เช่นนี้มาก่อน
Sean McMillan

1
ฉันเขียนโปรแกรมในยุค 70 ในภาษาโคบอล, แอสเซมเบลอร์, CORAL และ PL / 1 บนชุด IBM, DEC และ XEROX และฉันไม่เคยเจอข้อ จำกัด ขนาดซอร์สโค้ดเลย ข้อ จำกัด เกี่ยวกับขนาดอาร์เรย์ขนาดที่สามารถใช้งานได้ขนาดชื่อโปรแกรม - แต่ไม่เคยมีขนาดซอร์สโค้ด
James Anderson

1
@ Sean McMillan: ฉันไม่คิดว่าขนาดซอร์สโค้ดเป็นข้อ จำกัด (พิจารณาว่าในขณะนั้นภาษา verbose เช่น Pascal ค่อนข้างเป็นที่นิยม) และแม้ว่าจะเป็นกรณีนี้ฉันคิดว่ามันคงจะง่ายมากในการแยกวิเคราะห์ซอร์สโค้ดล่วงหน้าและแทนที่คีย์เวิร์ดแบบยาวด้วยโค้ดไบต์เดียวแบบสั้น (เช่นเช่นตัวแปลภาษาพื้นฐานบางตัวที่เคยใช้) ดังนั้นฉันจึงพบว่าอาร์กิวเมนต์ "C เป็นคำย่อเนื่องจากมีการประดิษฐ์ในช่วงเวลาที่หน่วยความจำน้อยลงมี" อ่อนแอเล็กน้อย
Giorgio

7

มันค่อนข้างง่าย: int *pหมายความว่า*pเป็น int; int a[5]หมายความว่าa[i]เป็น int

int (*f)(int (*a)[5])

หมายความว่า*fเป็นฟังก์ชั่น*aเป็นอาร์เรย์จำนวนเต็มห้าจำนวนดังนั้นfฟังก์ชั่นจะใช้เป็นตัวชี้ไปยังอาร์เรย์ที่มีจำนวนเต็มห้าจำนวนและส่งคืน int อย่างไรก็ตามใน C มันไม่มีประโยชน์ที่จะส่งตัวชี้ไปยังอาร์เรย์

การประกาศ C ไม่ค่อยมีความซับซ้อนเท่านี้

นอกจากนี้คุณสามารถชี้แจงโดยใช้ typedefs:

typedef int vec5[5];
int (*f)(vec5 *a);

4
ขอโทษถ้าสิ่งนี้ฟังดูหยาบคาย (ฉันไม่ได้ตั้งใจให้เป็น) แต่ฉันคิดว่าคุณพลาดจุดทั้งหมดของคำถาม ... : \
user541686

2
@ Mehrdad: ฉันไม่สามารถบอกคุณได้สิ่งที่อยู่ในใจของ Kernighan และ Ritchie; ฉันบอกคุณตรรกะหลังไวยากรณ์ ฉันไม่รู้เกี่ยวกับคนส่วนใหญ่ แต่ฉันไม่คิดว่าไวยากรณ์ที่คุณแนะนำนั้นชัดเจนกว่า
kevin cline

ฉันเห็นด้วย - เป็นเรื่องผิดปกติที่จะเห็นการประกาศที่ซับซ้อน
Caleb

การออกแบบของ C declarations ถือกำเนิดtypedef, const, volatileความสามารถและการเริ่มต้นสิ่งที่อยู่ในการประกาศ ความสับสนที่น่ารำคาญหลายประการของไวยากรณ์การประกาศ (เช่นint const *p, *q;ควรผูกconstกับประเภทหรือการประกาศ) ไม่สามารถเกิดขึ้นในภาษาตามที่ออกแบบมาตั้งแต่แรก ฉันหวังว่าภาษาจะเพิ่มเครื่องหมายโคลอนระหว่างประเภทและ declarand แต่อนุญาตให้ละเว้นเมื่อใช้ประเภท "คำสงวน" ในตัวโดยไม่มีตัวระบุ ความหมายของint: const *p,*q;และint const *: p,*q;จะชัดเจน
supercat

3

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

ลองอ่านนิพจน์ประเภท

int* (*p)[10];

ดังนั้นองค์ประกอบภายในสุดคือ p ซึ่งเป็นตัวแปร

p

หมายความว่า: p เป็นตัวแปร

ก่อนที่ตัวแปรจะมี * ผู้ประกอบการ * จะถูกใส่ก่อนนิพจน์ที่อ้างถึงเสมอดังนั้น

(*p)

หมายถึง: ตัวแปร p เป็นตัวชี้ หากไม่มี () ตัวดำเนินการ [] ทางด้านขวาจะมีลำดับความสำคัญสูงกว่าเช่น

**p[]

จะถูกแยกวิเคราะห์เป็น

*(*(p[]))

ขั้นตอนต่อไปคือ []: เนื่องจากไม่มีเพิ่มเติม () ดังนั้น [] จึงมีความสำคัญมากกว่าด้านนอก * ดังนั้น

(*p)[]

หมายถึง: (ตัวแปร p เป็นตัวชี้) ไปยังอาร์เรย์ จากนั้นเรามี * ที่สอง:

* (*p)[]

หมายถึง: ((ตัวแปร p เป็นตัวชี้) ไปยังอาร์เรย์) ของพอยน์เตอร์

ในที่สุดคุณก็มีตัวดำเนินการ int (ชื่อประเภท) ซึ่งมีลำดับความสำคัญต่ำสุด:

int* (*p)[]

หมายถึง: ((((ตัวแปร p เป็นตัวชี้) ไปยังอาร์เรย์) ของตัวชี้) เป็นจำนวนเต็ม

ดังนั้นทั้งระบบจะขึ้นอยู่กับการแสดงออกของประเภทกับผู้ประกอบการและผู้ประกอบการแต่ละคนมีกฎความสำคัญของตัวเอง สิ่งนี้ช่วยในการกำหนดประเภทที่ซับซ้อนมาก


0

มันไม่ยากนักเมื่อคุณเริ่มคิดและ C ไม่เคยเป็นภาษาที่ง่ายมาก และint*[10]* pไม่ง่ายกว่าจริง ๆint* (*p)[10] และ k แบบไหนที่จะเป็นint*[10]* p, k;


2
k จะเป็นการตรวจสอบโค้ดที่ล้มเหลวฉันสามารถทำงานในสิ่งที่คอมไพเลอร์จะทำฉันอาจจะใส่ใจ แต่ฉันไม่สามารถทำงานในสิ่งที่โปรแกรมเมอร์ตั้งใจ - ล้มเหลว ............
mattnz

และเหตุใด k จึงไม่สามารถตรวจสอบโค้ดได้
Dainius

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

1
โดยพื้นฐานแล้วมี 3 (ในกรณีนี้) การประกาศตัวแปรประเภทต่าง ๆ ในแถวเดียวกันเช่น int * p, int i [10] และ int k นั่นเป็นสิ่งที่ยอมรับไม่ได้ ยอมรับการประกาศประเภทเดียวกันหลายครั้งได้หากตัวแปรมีรูปแบบความสัมพันธ์บางอย่างเช่นความกว้าง int ความสูงความลึก โปรดจำไว้ว่าโปรแกรมหลายคนใช้ int * p ดังนั้นอะไรคือ i ใน 'int * p, i;'
mattnz

1
สิ่งที่ @attattz กำลังพยายามพูดคือคุณสามารถฉลาดได้เท่าที่คุณต้องการ แต่มันก็ไร้ความหมายเมื่อเจตนาของคุณไม่ชัดเจนและ / หรือรหัสของคุณเขียนได้ไม่ดี / อ่านไม่ได้ สิ่งประเภทนี้มักส่งผลให้รหัสที่ใช้งานไม่ได้และเสียเวลา บวกpointer to intและintไม่ได้เป็นประเภทเดียวกันดังนั้นจึงควรประกาศแยกต่างหาก ระยะเวลา ฟังผู้ชาย เขามีตัวแทน 18k ด้วยเหตุผล
Braden ที่ดีที่สุด
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.