ปริศนานี้มีสามชิ้น
ส่วนแรกคือช่องว่างใน 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
" ในขณะที่มีประเภทtest2
int
ผู้ประกาศสามารถซับซ้อนได้โดยพลการ คุณสามารถมีอาร์เรย์ของพอยน์เตอร์:
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 ++ นั้นแตกต่างกันเล็กน้อย แต่แนวคิดส่วนใหญ่เหมือนกัน