ปริศนานี้มีสามชิ้น
ส่วนแรกคือช่องว่างใน C และ C ++ โดยปกติไม่มีนัยสำคัญนอกเหนือจากการแยกโทเค็นที่อยู่ติดกันซึ่งแยกไม่ออก
ในระหว่างขั้นตอนก่อนการประมวลผลข้อความต้นทางจะถูกแบ่งออกเป็นลำดับของโทเค็นเช่นตัวระบุเครื่องหมายวรรคตอนตัวอักษรตัวเลขตัวอักษรสตริง ฯลฯ ลำดับของโทเค็นนั้นจะถูกวิเคราะห์ในภายหลังสำหรับไวยากรณ์และความหมาย โทเค็นเป็น "โลภ" และจะสร้างโทเค็นที่ถูกต้องยาวที่สุดเท่าที่จะทำได้ ถ้าคุณเขียนสิ่งที่ชอบ
inttest;
tokenizer เพียงเห็นสองราชสกุล - ระบุinttestตาม ;punctuator ไม่รู้จักintเป็นคีย์เวิร์ดแยกต่างหากในขั้นตอนนี้ (ซึ่งจะเกิดขึ้นภายหลังในกระบวนการ) ดังนั้นเพื่อให้บรรทัดที่จะอ่านเป็นการประกาศจำนวนเต็มชื่อtestเราต้องใช้ช่องว่างเพื่อแยกโทเค็นตัวระบุ:
int test;
*ตัวละครที่ไม่เป็นส่วนหนึ่งของตัวระบุใด ๆ เป็นโทเค็นแยกต่างหาก (เครื่องหมายวรรคตอน) ในตัวของมันเอง ดังนั้นถ้าคุณเขียน
int*test;
คอมไพเลอร์เห็น 4 ราชสกุลที่แยกต่างหาก - int, *, และtest ;ดังนั้นช่องว่างจึงไม่มีความสำคัญในการประกาศตัวชี้และทั้งหมด
int *test;
int* test;
int*test;
int * test;
ตีความไปในทางเดียวกัน
ส่วนที่สองของปริศนาคือการประกาศทำงานจริงใน C และ C ++ 1อย่างไร การประกาศจะถูกแบ่งออกเป็นสองชิ้นหลัก - ลำดับของspecifiers ประกาศ (specifiers อุปกรณ์เก็บข้อมูลชนิด specifiers ประเภทบ่น ฯลฯ ) ตามด้วยรายการคั่นด้วยเครื่องหมายจุลภาค (อาจจะเริ่มต้น) declarators ในการประกาศ
unsigned long int a[10]={0}, *p=NULL, f(void);
specifiers ประกาศที่มีunsigned long intและ declarators มีa[10]={0}, และ*p=NULL f(void)ผู้ประกาศจะแนะนำชื่อของสิ่งที่กำลังประกาศ ( a, pและf) พร้อมกับข้อมูลเกี่ยวกับอาร์เรย์ของสิ่งนั้นตัวชี้และฟังก์ชัน ผู้ประกาศอาจมีตัวเริ่มต้นที่เกี่ยวข้องด้วย
ประเภทของaคือ "อาร์เรย์ 10 องค์ประกอบของunsigned long int" ประเภทที่ระบุไว้อย่างเต็มที่โดยการรวมกันของ specifiers ประกาศและ declarator ={0}และค่าเริ่มต้นที่ระบุไว้กับการเริ่มต้น ในทำนองเดียวกันประเภทของการpเป็น "ตัวชี้ไปunsigned long int" และอีกประเภทที่ระบุไว้โดยการรวมกันของ specifiers ประกาศและ declarator NULLและจะเริ่มต้นที่จะ และประเภทของfคือ "ฟังก์ชันที่ส่งคืนunsigned long int" โดยใช้เหตุผลเดียวกัน
นี่คือกุญแจสำคัญ - ไม่มีตัวระบุประเภท "pointer-to" เหมือนกับที่ไม่มีตัวระบุประเภท "array-of" เช่นเดียวกับที่ไม่มีตัวระบุประเภท "function-return" เราไม่สามารถประกาศอาร์เรย์เป็น
int[10] a;
เพราะถูกดำเนินการของ[]ผู้ประกอบการคือไม่a intในทำนองเดียวกันในการประกาศ
int* p;
ถูกดำเนินการ*คือไม่p intแต่เนื่องจากตัวดำเนินการ indirection เป็นแบบยูนารีและช่องว่างไม่มีนัยสำคัญคอมไพเลอร์จะไม่บ่นถ้าเราเขียนด้วยวิธีนี้ อย่างไรก็ตามมันก็มักจะint (*p);ตีความว่าเป็น
เพราะฉะนั้นถ้าคุณเขียน
int* p, q;
ตัวถูกดำเนินการของ*is pดังนั้นมันจะถูกตีความว่า
int (*p), q;
ดังนั้นทั้งหมด
int *test1, test2;
int* test1, test2;
int * test1, test2;
ทำสิ่งเดียวกัน - ในทุกกรณีที่สามtest1คือตัวถูกดำเนินการของ*และทำให้มีประเภท "ชี้int" ในขณะที่มีประเภทtest2int
ผู้ประกาศสามารถซับซ้อนได้โดยพลการ คุณสามารถมีอาร์เรย์ของพอยน์เตอร์:
T *a[N];
คุณสามารถมีตัวชี้ไปยังอาร์เรย์:
T (*a)[N];
คุณสามารถมีฟังก์ชันที่ส่งคืนพอยน์เตอร์:
T *f(void);
คุณสามารถมีตัวชี้ไปยังฟังก์ชัน:
T (*f)(void)
คุณสามารถมีอาร์เรย์ของตัวชี้ไปยังฟังก์ชัน:
T (*a[N])(void)
คุณสามารถมีฟังก์ชันที่ส่งคืนตัวชี้ไปยังอาร์เรย์:
T (*f(void))[N];
คุณสามารถมีฟังก์ชันที่ส่งคืนตัวชี้ไปยังอาร์เรย์ของตัวชี้ไปยังฟังก์ชันที่ส่งคืนตัวชี้ไปที่T:
T *(*(*f(void))[N])(void)
แล้วคุณมีsignal:
void (int)))(int);
ซึ่งอ่านว่า
signal -- signal
signal( ) -- is a function taking
signal( ) -- unnamed parameter
signal(int ) -- is an int
signal(int, ) -- unnamed parameter
signal(int, (*) ) -- is a pointer to
signal(int, (*)( )) -- a function taking
signal(int, (*)( )) -- unnamed parameter
signal(int, (*)(int)) -- is an int
signal(int, void (*)(int)) -- returning void
(*signal(int, void (*)(int))) -- returning a pointer to
(*signal(int, void (*)(int)))( ) -- a function taking
(*signal(int, void (*)(int)))( ) -- unnamed parameter
(*signal(int, void (*)(int)))(int) -- is an int
void (*signal(int, void (*)(int)))(int)
และนี่เป็นเพียงแค่รอยขีดข่วนบนพื้นผิวของสิ่งที่เป็นไปได้ แต่โปรดสังเกตว่า array-ness, pointer-ness และ function-ness เป็นส่วนหนึ่งของ declarator เสมอไม่ใช่ type specifier
สิ่งหนึ่งที่ต้องระวัง - constสามารถปรับเปลี่ยนทั้งประเภทตัวชี้และประเภทชี้ไปที่:
const int *p;
int const *p;
ทั้งสองข้อข้างต้นประกาศpเป็นตัวชี้ไปยังconst intวัตถุ คุณสามารถเขียนค่าใหม่เพื่อpตั้งค่าให้ชี้ไปที่วัตถุอื่น:
const int x = 1;
const int y = 2;
const int *p = &x;
p = &y;
แต่คุณไม่สามารถเขียนไปยังวัตถุที่ชี้ไปที่:
*p = 3;
อย่างไรก็ตาม
int * const p;
ประกาศpเป็นconstตัวชี้ไปยัง non-const int; คุณสามารถเขียนถึงสิ่งที่pชี้ไป
int x = 1;
int y = 2;
int * const p = &x;
*p = 3;
แต่คุณไม่สามารถกำหนดpให้ชี้ไปที่วัตถุอื่นได้:
p = &y
ซึ่งนำเราไปสู่ปริศนาชิ้นที่สาม - ทำไมการประกาศจึงมีโครงสร้างในลักษณะนี้
เจตนาคือโครงสร้างของการประกาศควรสะท้อนโครงสร้างของนิพจน์ในรหัสอย่างใกล้ชิด ("การประกาศเลียนแบบการใช้") ตัวอย่างเช่นสมมติว่าเรามีอาร์เรย์ของพอยน์เตอร์ที่จะintตั้งชื่อapและเราต้องการเข้าถึงintค่าที่ชี้โดยiองค์ประกอบ 'th เราจะเข้าถึงค่านั้นดังนี้:
printf( "%d", *ap[i] );
แสดงออก *ap[i]มีประเภทint; ดังนั้นคำประกาศapจึงเขียนเป็น
int *ap[N];
declarator มีรูปแบบเดียวกันกับการแสดงออก*ap[N] *ap[i]ตัวดำเนินการ*และ[]ทำงานในลักษณะเดียวกันในการประกาศว่าพวกเขาทำในนิพจน์ - []มีลำดับความสำคัญสูงกว่ายูนารี*ดังนั้นตัวถูกดำเนินการ*จึงเป็นap[N](แยกวิเคราะห์เป็น*(ap[N]))
อีกตัวอย่างหนึ่งสมมติว่าเรามีตัวชี้ไปยังอาร์เรย์ของintชื่อpaและเราต้องการเข้าถึงค่าของiองค์ประกอบ 'th เราจะเขียนว่าเป็น
printf( "%d", (*pa)[i] )
ประเภทของนิพจน์(*pa)[i]คือintดังนั้นการประกาศจึงถูกเขียนเป็น
int (*pa)[N];
อีกครั้งใช้กฎความสำคัญและความสัมพันธ์เดียวกัน ในกรณีนี้เราไม่ต้องการที่จะ dereference i'องค์ประกอบของวันที่paเราต้องการที่จะเข้าถึงi' องค์ประกอบของสิ่ง TH pa จุดมาเพื่อให้เรามีอย่างชัดเจนกลุ่มผู้ประกอบการด้วย*pa
*, []และ()ผู้ประกอบการเป็นส่วนหนึ่งของการแสดงออกในรหัสเพื่อให้พวกเขาเป็นส่วนหนึ่งของdeclaratorในการประกาศ ตัวประกาศจะบอกวิธีใช้อ็อบเจกต์ในนิพจน์ หากคุณมีคำประกาศเช่นint *p;นี้จะบอกคุณว่านิพจน์*pในโค้ดของคุณจะให้intค่า โดยขยายมันจะบอกคุณว่าการแสดงออกpมีผลเป็นค่าของชนิด "ชี้int" int *หรือ
แล้วสิ่งต่าง ๆ เช่นการแสดงและsizeofการแสดงออกที่เราใช้สิ่งที่เหมือน(int *)หรือsizeof (int [10])หรือสิ่งนั้น? ฉันจะอ่านสิ่งที่ชอบได้อย่างไร
void foo( int *, int (*)[10] );
ไม่มีตัวประกาศไม่ใช่*และ[]ตัวดำเนินการแก้ไขประเภทโดยตรงหรือไม่
ไม่มี - ยังคงมีผู้ประกาศเพียงแค่มีตัวระบุที่ว่างเปล่า (เรียกว่าผู้ประกาศนามธรรม ) ถ้าเราเป็นตัวแทนของตัวระบุว่างเปล่ากับλสัญลักษณ์แล้วเราสามารถอ่านสิ่งเหล่านั้นเป็น(int *λ), sizeof (int λ[10])และ
void foo( int *λ, int (*λ)[10] );
และพวกเขาปฏิบัติเหมือนคำประกาศอื่น ๆ int *[10]แทนอาร์เรย์ 10 พอยน์เตอร์ในขณะที่int (*)[10]แทนตัวชี้ไปยังอาร์เรย์
และตอนนี้ส่วนที่แสดงความคิดเห็นของคำตอบนี้ ฉันไม่ชอบรูปแบบ C ++ ในการประกาศตัวชี้อย่างง่ายเป็น
T* p;
และพิจารณาว่าเป็นการปฏิบัติที่ไม่ดีด้วยเหตุผลต่อไปนี้:
- ไม่สอดคล้องกับไวยากรณ์
- แนะนำความสับสน (เป็นหลักฐานโดยคำถามนี้ทั้งหมดที่ซ้ำกันคำถามนี้คำถามเกี่ยวกับความหมายของการ
T* p, q;ซ้ำกันทั้งหมดเพื่อให้ผู้คำถาม ฯลฯ );
- ไม่สอดคล้องกันภายใน - การประกาศอาร์เรย์ของพอยน์เตอร์ที่
T* a[N]ไม่สมดุลกับการใช้งาน (เว้นแต่คุณจะมีนิสัยชอบเขียน* a[i])
- ไม่สามารถนำไปใช้กับประเภทตัวชี้ไปยังอาร์เรย์หรือตัวชี้ต่อฟังก์ชันได้ (เว้นแต่คุณจะสร้าง typedef เพียงเพื่อให้คุณสามารถใช้รูป
T* pแบบได้อย่างหมดจดซึ่ง ... ไม่ );
- เหตุผลในการทำเช่นนั้น - "เป็นการเน้นย้ำถึงตัวชี้ของวัตถุ" - เป็นการปลอมแปลง ไม่สามารถใช้กับอาร์เรย์หรือประเภทฟังก์ชันได้และฉันคิดว่าคุณสมบัติเหล่านั้นสำคัญพอ ๆ กับการเน้น
ท้ายที่สุดมันบ่งบอกถึงการคิดสับสนเกี่ยวกับการทำงานของระบบประเภทสองภาษา
มีเหตุผลที่ดีในการประกาศรายการแยกกัน การหลีกเลี่ยงการปฏิบัติที่ไม่ดี ( T* p, q;) ไม่ใช่หนึ่งในนั้น หากคุณเขียนผู้ประกาศของคุณอย่างถูกต้อง ( T *p, q;) คุณมีโอกาสน้อยที่จะทำให้เกิดความสับสน
ฉันคิดว่ามันคล้ายกับการจงใจเขียนforลูปง่ายๆทั้งหมดของคุณเป็น
i = 0;
for( ; i < N; )
{
...
i++
}
ถูกต้องตามหลักไวยากรณ์ แต่สร้างความสับสนและเจตนามีแนวโน้มที่จะตีความผิด อย่างไรก็ตามการT* p;ประชุมนั้นยึดติดอยู่ในชุมชน C ++ และฉันใช้มันในรหัส C ++ ของฉันเองเพราะความสม่ำเสมอในฐานรหัสเป็นสิ่งที่ดี แต่มันทำให้ฉันคันทุกครั้งที่ทำ
- ฉันจะใช้คำศัพท์ภาษา C - คำศัพท์ C ++ นั้นแตกต่างกันเล็กน้อย แต่แนวคิดส่วนใหญ่เหมือนกัน