C ไม่ใช่เรื่องยาก: void (* (* f []) ()) ()


188

ฉันเพิ่งเห็นภาพวันนี้และคิดว่าฉันชอบคำอธิบาย ดังนั้นนี่คือภาพ:

บางรหัสค

ฉันพบว่ามันสับสนและสงสัยว่ารหัสดังกล่าวเป็นประโยชน์ ฉันไปที่รูปภาพและพบรูปภาพอื่นในรายการ reddit นี้และนี่คือรูปภาพนั้น:

คำอธิบายที่น่าสนใจ

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

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


9
คำอธิบายจะง่ายขึ้นได้อย่างไร ครอบคลุมทุกแง่มุมของคำจำกัดความของ fw / คำสองคำสำหรับแต่ละประเด็นสำคัญ
Scott Hunter

30
บางที C ยาก? ที่จริงแล้วประกาศfเป็นอาร์เรย์ของตัวชี้ไปยังฟังก์ชันที่สามารถรับอาร์กิวเมนต์ใด ๆ .. ถ้าเป็นvoid (*(*f[])(void))(void);เช่นนั้นใช่มันจะเป็นฟังก์ชันที่ไม่มีอาร์กิวเมนต์ ...
txtechhelp

18
ในทางปฏิบัติอย่าใช้รหัสคลุมเครือเช่นนั้น ใช้ typedef สำหรับลายเซ็น
Basile Starynkevitch

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

20
หากคุณเหล่ตาของคุณดูเหมือนว่า LISP
user2023861

คำตอบ:


117

มีกฎที่เรียกว่า"กฎตามเข็มนาฬิกา / เกลียวกฎ"เพื่อช่วยค้นหาความหมายของการประกาศที่ซับซ้อน

จากc-faq :

มีสามขั้นตอนง่าย ๆ ในการปฏิบัติตาม:

  1. เริ่มต้นด้วยองค์ประกอบที่ไม่รู้จักย้ายไปในทิศทางเกลียว / ตามเข็มนาฬิกา; เมื่อพบองค์ประกอบต่อไปนี้แทนที่ด้วยงบภาษาอังกฤษที่เกี่ยวข้อง:

    [X]หรือ[]
    => ขนาดอาร์เรย์ X ของ ... หรือขนาดอาร์เรย์ที่ไม่ได้กำหนดของ ...

    (type1, type2)
    => ฟังก์ชั่นผ่าน type1 และ type2 ส่งคืน ...

    *
    => ตัวชี้ไปยัง ...

  2. ทำเช่นนี้ในทิศทางที่เป็นเกลียว / ตามเข็มนาฬิกาจนกว่าโทเค็นทั้งหมดจะได้รับการครอบคลุม

  3. แก้ไขทุกอย่างในวงเล็บก่อนเสมอ!

คุณสามารถตรวจสอบลิงค์ด้านบนเพื่อดูตัวอย่าง

โปรดทราบด้วยว่าเพื่อช่วยให้คุณมีเว็บไซต์ที่เรียกว่า:

http://www.cdecl.org

คุณสามารถป้อนคำประกาศ C และจะให้ความหมายเป็นภาษาอังกฤษ สำหรับ

void (*(*f[])())()

มันเอาท์พุท:

ประกาศ f เป็นอาร์เรย์ของตัวชี้ไปยังฟังก์ชันที่ส่งคืนตัวชี้ไปยังฟังก์ชันที่คืนค่าเป็นโมฆะ

แก้ไข:

ตามที่ระบุไว้ในความคิดเห็นโดยRandom832กฎเกลียวไม่ได้อยู่อาร์เรย์ของอาร์เรย์และจะนำไปสู่ผลลัพธ์ที่ผิดในการประกาศเหล่านั้น (ส่วนใหญ่) ตัวอย่างเช่นสำหรับint **x[1][2];กฎเกลียวละเว้นความจริงที่ว่ามีความสำคัญสูงกว่า[]*

เมื่ออยู่ข้างหน้าอาร์เรย์อาร์เรย์หนึ่งสามารถเพิ่มวงเล็บที่ชัดเจนก่อนที่จะใช้กฎเกลียว ตัวอย่างเช่น: int **x[1][2];เหมือนกับint **(x[1][2]);(ยังใช้ได้กับ C) เนื่องจากลำดับความสำคัญและกฎของเกลียวนั้นอ่านอย่างถูกต้องว่า "x คืออาร์เรย์ 1 ของอาร์เรย์ 2 ของตัวชี้ไปยังตัวชี้ไปยัง int" ซึ่งเป็นการประกาศภาษาอังกฤษที่ถูกต้อง

หมายเหตุว่าปัญหานี้ยังได้รับการคุ้มครองในเรื่องนี้คำตอบโดยเจมส์ Kanze (แหลมออกโดยhaccksในความคิดเห็น)


5
ฉันหวังว่า cdecl.org จะดีกว่านี้
Grady Player

8
ไม่มี "กฎเกลียว" ... "int *** foo [] [] []" กำหนดอาร์เรย์อาร์เรย์ของพอยน์เตอร์พอยน์เตอร์ให้กับพอยน์เตอร์ "เกลียว" นั้นมาจากความจริงที่ว่าการประกาศนี้เกิดขึ้นกับกลุ่มสิ่งที่อยู่ในวงเล็บในลักษณะที่ทำให้พวกเขาสลับกัน มันคือทุกอย่างทางด้านขวาแล้วทิ้งไว้ในวงเล็บแต่ละชุด
Random832

1
@ Random832 มี "กฎเกลียว" และครอบคลุมถึงกรณีที่คุณกล่าวถึงเช่นพูดถึงวิธีการจัดการกับวงเล็บ / อาร์เรย์ ฯลฯ แน่นอนว่าไม่ใช่กฎมาตรฐาน C แต่ช่วยในการจำที่ดีสำหรับการหาวิธีการจัดการ ด้วยการประกาศที่ซับซ้อน IMHO มันมีประโยชน์อย่างมากและช่วยให้คุณประหยัดเมื่อมีปัญหาหรือเมื่อcdecl.orgไม่สามารถแยกวิเคราะห์การประกาศ แน่นอนหนึ่งไม่ควรละเมิดการประกาศดังกล่าว แต่เป็นการดีที่จะรู้ว่าพวกเขาจะแยกวิเคราะห์ได้อย่างไร
vsoftco

5
@ vsoftco แต่มันไม่ใช่ "เคลื่อนที่ไปในทิศทางที่เป็นเกลียว / ตามเข็มนาฬิกา" ถ้าคุณหมุนไปรอบ ๆ เมื่อถึงวงเล็บ
Random832

2
ouah คุณควรจะพูดถึงว่ากฎเกลียวไม่เป็นสากล
haccks

105

ชนิดของกฎ "หมุนวน" ที่ตกลงมาจากกฎที่มีลำดับความสำคัญต่อไปนี้:

T *a[]    -- a is an array of pointer to T
T (*a)[]  -- a is a pointer to an array of T
T *f()    -- f is a function returning a pointer to T
T (*f)()  -- f is a pointer to a function returning T

ห้อย[]และสายงาน()ผู้ประกอบการมีความสำคัญสูงกว่าเอก*เพื่อให้*f()มีการแยกวิเคราะห์เป็น*(f())และจะแยกเป็น *a[]*(a[])

ดังนั้นหากคุณต้องการชี้ไปยังอาร์เรย์หรือชี้ไปยังฟังก์ชั่นได้แล้วคุณจะต้องไปยังกลุ่มอย่างชัดเจน*กับตัวระบุเป็นในหรือ(*a)[](*f)()

จากนั้นคุณก็รู้aและfสามารถแสดงออกได้ซับซ้อนกว่าตัวบ่งชี้ ในT (*a)[N], aอาจจะเป็นตัวระบุง่ายหรือมันอาจจะเป็นฟังก์ชั่นเช่น(*f())[N]( a-> f()) หรือมันอาจจะเป็นอาร์เรย์เช่น(*p[M])[N]( a-> p[M]) หรือมันอาจจะเป็นอาร์เรย์ของตัวชี้ไปที่ฟังก์ชั่นเช่น(*(*p[M])())[N]( a-> (*p[M])()) เป็นต้น

มันจะดีถ้าโอเปอเรเตอร์ทางอ้อม*เป็น postfix แทนที่จะเป็น unary ซึ่งจะทำให้การประกาศค่อนข้างง่ายต่อการอ่านจากซ้ายไปขวา ( void f[]*()*();ไหลแน่นอนดีกว่าvoid (*(*f[])())()) แต่ไม่ใช่

เมื่อคุณเจอการประกาศที่มีขนดกแบบนี้ให้เริ่มต้นด้วยการค้นหาตัวระบุซ้ายสุดและใช้กฎที่มีลำดับความสำคัญเหนือกว่า

         f              -- f
         f[]            -- is an array
        *f[]            -- of pointers  ([] has higher precedence than *)
       (*f[])()         -- to functions
      *(*f[])()         -- returning pointers
     (*(*f[])())()      -- to functions
void (*(*f[])())();     -- returning void

signalฟังก์ชั่นในห้องสมุดมาตรฐานน่าจะเป็นประเภทตัวอย่างสำหรับชนิดของความบ้านี้:

       signal                                       -- signal
       signal(                          )           -- is a function with parameters
       signal(    sig,                  )           --    sig
       signal(int sig,                  )           --    which is an int and
       signal(int sig,        func      )           --    func
       signal(int sig,       *func      )           --    which is a pointer
       signal(int sig,      (*func)(int))           --    to a function taking an int                                           
       signal(int sig, void (*func)(int))           --    returning void
      *signal(int sig, void (*func)(int))           -- returning a pointer
     (*signal(int sig, void (*func)(int)))(int)     -- to a function taking an int
void (*signal(int sig, void (*func)(int)))(int);    -- and returning void

ณ จุดนี้คนส่วนใหญ่พูดว่า "use typedefs" ซึ่งแน่นอนว่าเป็นตัวเลือก:

typedef void outerfunc(void);
typedef outerfunc *innerfunc(void);

innerfunc *f[N];

แต่...

คุณจะใช้ fในการแสดงออกอย่างไร คุณรู้ว่ามันเป็นชุดพอยน์เตอร์ แต่คุณจะใช้มันอย่างไรเพื่อรันฟังก์ชั่นที่ถูกต้อง? คุณต้องไปที่ typedefs และไขปริศนาไวยากรณ์ที่ถูกต้อง ในทางตรงข้ามรุ่น "เปลือยกาย" นั้นดูเหมือนจะสวยมาก แต่มันบอกคุณได้อย่างชัดเจนว่าจะใช้งานอย่างไร fในนิพจน์ (กล่าวคือ(*(*f[i])())();สมมติว่าไม่มีฟังก์ชันใดใช้อาร์กิวเมนต์)


7
ขอบคุณที่ให้ตัวอย่าง 'สัญญาณ' แสดงให้เห็นว่าสิ่งต่าง ๆ เหล่านี้ปรากฏขึ้นในป่า
Justsalt

นั่นเป็นตัวอย่างที่ดี
Casey

ฉันชอบfต้นไม้การชะลอตัวของคุณอธิบายความสำคัญ ... ด้วยเหตุผลบางอย่างฉันมักจะถูกไล่ออกจาก ASCII-art โดยเฉพาะอย่างยิ่งเมื่อมันมาถึงการอธิบายสิ่งต่าง ๆ :)
txtechhelp

1
สมมติว่าไม่มีฟังก์ชั่นใช้อาร์กิวเมนต์ : จากนั้นคุณต้องใช้voidในวงเล็บฟังก์ชั่นมิฉะนั้นจะสามารถใช้อาร์กิวเมนต์ใด ๆ
haccks

1
@haccks: สำหรับการประกาศใช่; ฉันกำลังพูดถึงการเรียกใช้ฟังก์ชัน
John Bode

57

ใน C การประกาศสะท้อนการใช้งาน - นั่นคือวิธีการที่กำหนดไว้ในมาตรฐาน การประกาศ:

void (*(*f[])())()

คือการยืนยันว่าการแสดงออกผลิตเป็นผลมาจากประเภท(*(*f[i])())() voidซึ่งหมายความว่า:

  • f ต้องเป็นอาร์เรย์เนื่องจากคุณสามารถสร้างดัชนีได้:

    f[i]
  • องค์ประกอบของfต้องเป็นพอยน์เตอร์เนื่องจากคุณสามารถอ้างอิงได้:

    *f[i]
  • พอยน์เตอร์เหล่านั้นจะต้องเป็นพอยน์เตอร์ของฟังก์ชั่นที่ไม่มีข้อโต้แย้งเนื่องจากคุณสามารถเรียกพวกมันได้:

    (*f[i])()
  • ผลลัพธ์ของฟังก์ชั่นเหล่านั้นจะต้องเป็นตัวชี้เนื่องจากคุณสามารถตรวจสอบได้:

    *(*f[i])()
  • คำแนะนำเหล่านั้นต้องยังเป็นตัวชี้ไปยังฟังก์ชั่นการขัดแย้งใดเนื่องจากคุณสามารถเรียกพวกเขา:

    (*(*f[i])())()
  • ตัวชี้ฟังก์ชั่นเหล่านั้นจะต้องกลับมา void

"กฎเกลียว" เป็นเพียงช่วยในการจำที่ให้วิธีที่แตกต่างของการทำความเข้าใจในสิ่งเดียวกัน


3
วิธีที่ดีในการดูว่าฉันไม่เคยเห็นมาก่อน +1
59

4
ดี การกระทำแบบนี้มันคือเรื่องจริงที่เรียบง่าย ที่จริงค่อนข้างง่ายกว่าสิ่งที่ต้องการvector< function<function<void()>()>* > fโดยเฉพาะอย่างยิ่งถ้าคุณเพิ่มในstd::s (แต่ก็เป็นตัวอย่างที่ถูกออกแบบมา ... แม้จะf :: [IORef (IO (IO ()))]ดูแปลก ๆ )
leftaroundabout

1
@TimoDenk: การประกาศa[x]แสดงให้เห็นว่าการแสดงออกที่ถูกต้องเมื่อa[i] i >= 0 && i < xในขณะที่a[]ใบขนาดที่ไม่ได้ระบุและดังนั้นจึงเป็นเหมือนกับที่*aมันแสดงให้เห็นว่าการแสดงออกa[i](หรือเท่ากัน*(a + i)) เป็นที่ถูกต้องสำหรับบางiช่วงของ
Jon Purdy

4
นี่เป็นวิธีที่ง่ายที่สุดในการคิดเกี่ยวกับประเภท C ขอบคุณสำหรับสิ่งนี้
Alex Ozer

4
ฉันรักสิ่งนี้! ง่ายกว่าที่จะให้เหตุผลมากกว่าเกลียวที่งี่เง่า (*f[])()เป็นประเภทที่คุณสามารถจัดทำดัชนีจากนั้นยกเลิกการอ้างอิงจากนั้นเรียกดังนั้นจึงเป็นอาร์เรย์ของพอยน์เตอร์สำหรับฟังก์ชั่น
Lynn

32

ดังนั้น "การอ่านแบบวน" นี้เป็นสิ่งที่ถูกต้อง?

การใช้กฎหมุนวนหรือใช้cdeclนั้นไม่ถูกต้องเสมอไป ทั้งสองล้มเหลวในบางกรณี กฎเกลียวทำงานหลายกรณี แต่มันไม่เป็นสากล

หากต้องการถอดรหัสการประกาศที่ซับซ้อนให้จดจำกฎง่าย ๆ สองข้อนี้:

  • อ่านคำประกาศจากภายในสู่ภายนอกเสมอ : เริ่มจากด้านในสุดหากมีวงเล็บ ค้นหาตัวระบุที่กำลังประกาศและเริ่มถอดรหัสการประกาศจากที่นั่น

  • เมื่อมีตัวเลือกให้เลือกเสมอ[]และ()มากกว่า* : หาก*นำหน้าตัวระบุและ[]ตามหลังตัวระบุจะแสดงอาร์เรย์ไม่ใช่ตัวชี้ เช่นเดียวกันหาก*นำหน้าตัวระบุและ()ตามหลังตัวระบุจะแทนฟังก์ชันไม่ใช่ตัวชี้ (วงเล็บสามารถใช้เพื่อแทนที่ลำดับความสำคัญปกติของ[]และ()มากกว่า*)

กฎนี้เกี่ยวข้องกับการซิกแซกจากด้านหนึ่งของตัวระบุไปยังอีกด้านหนึ่ง

ตอนนี้ถอดรหัสการประกาศอย่างง่าย

int *a[10];

การใช้กฎ:

int *a[10];      "a is"  
     ^  

int *a[10];      "a is an array"  
      ^^^^ 

int *a[10];      "a is an array of pointers"
    ^

int *a[10];      "a is an array of pointers to `int`".  
^^^      

ลองถอดรหัสการประกาศที่ซับซ้อนเช่นกัน

void ( *(*f[]) () ) ();  

โดยใช้กฎด้านบน:

void ( *(*f[]) () ) ();        "f is"  
          ^  

void ( *(*f[]) () ) ();        "f is an array"  
           ^^ 

void ( *(*f[]) () ) ();        "f is an array of pointers" 
         ^    

void ( *(*f[]) () ) ();        "f is an array of pointers to function"   
               ^^     

void ( *(*f[]) () ) ();        "f is an array of pointers to function returning pointer"
       ^   

void ( *(*f[]) () ) ();        "f is an array of pointers to function returning pointer to function" 
                    ^^    

void ( *(*f[]) () ) ();        "f is an array of pointers to function returning pointer to function returning `void`"  
^^^^

นี่คือ GIF แสดงให้เห็นว่าคุณไปอย่างไร (คลิกที่ภาพเพื่อดูขนาดใหญ่):

ป้อนคำอธิบายรูปภาพที่นี่


กฎระเบียบที่กล่าวถึงที่นี่จะนำมาจากหนังสือC Programming วิธีการสมัยใหม่โดย KN KING


นี่เป็นเหมือนวิธีการมาตรฐานเช่น "การประกาศการใช้กระจก" ในตอนนี้ฉันต้องการถามเรื่องอื่น: คุณแนะนำหนังสือของ KN King หรือไม่? ฉันเห็นบทวิจารณ์ที่ดีมากมายเกี่ยวกับหนังสือเล่มนี้
Motun

1
ใช่. ฉันแนะนำหนังสือเล่มนั้น ฉันเริ่มเขียนโปรแกรมจากหนังสือเล่มนั้น ข้อความและปัญหาที่ดีในนั้น
haccks

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

@FabioTurati; ฟังก์ชันไม่สามารถส่งคืนอาร์เรย์หรือฟังก์ชัน char (x())[5]จะส่งผลให้เกิดข้อผิดพลาดทางไวยากรณ์ แต่ Cdecl แยกเป็น: ประกาศxcharเป็นฟังก์ชั่นการกลับมาของอาร์เรย์ 5
haccks

12

มันเป็นเพียง "เกลียว" เท่านั้นเนื่องจากในการประกาศนี้มีเพียงหนึ่งตัวดำเนินการในแต่ละด้านภายในวงเล็บแต่ละระดับ การอ้างว่าคุณดำเนินการ "เป็นเกลียว" โดยทั่วไปจะแนะนำให้คุณสลับระหว่างอาร์เรย์และพอยน์เตอร์ในการประกาศint ***foo[][][]เมื่อในความเป็นจริงระดับอาร์เรย์ทั้งหมดมาก่อนระดับตัวชี้ใด ๆ


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

7

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


3
อย่างไรก็ตามมันเป็นสิ่งสำคัญที่จะรู้วิธีการแยกมันแม้ว่าจะรู้วิธีการแยกวิเคราะห์ typedef!
inetknght

1
@inetknght วิธีที่คุณใช้กับ typedefs คือการทำให้มันง่ายพอที่จะไม่ต้องแยกวิเคราะห์
SergeyA

2
ผู้ที่ถามคำถามประเภทนี้ในระหว่างการสัมภาษณ์ทำได้เพียงทำจังหวะให้กับ Egos ของพวกเขาเท่านั้น
Casey

1
@ JohnBode และคุณจะทำตัวเองชอบโดยพิมพ์ค่าตอบแทนของฟังก์ชั่น
SergeyA

1
@ JohnBode ฉันพบว่าเป็นเรื่องของการเลือกส่วนบุคคลไม่คุ้มค่าที่จะโต้วาที ฉันเห็นการตั้งค่าของคุณฉันยังมีของฉัน
SergeyA

7

คุณอาจพบว่ามันเป็นเรื่องสนุกที่รู้ว่ามีคำภาษาอังกฤษที่ใช้อธิบายการอ่านการประกาศของ C: Boustrophedonicallyนั่นคือสลับจากขวาไปซ้ายกับซ้ายไปขวา

อ้างอิง: Van der Linden, 1994 - หน้า 76


1
คำนั้นไม่ได้ระบุว่าอยู่ภายในเหมือนซ้อนโดย parens หรือในบรรทัดเดียว มันอธิบายรูปแบบ "งู" พร้อมกับบรรทัด LTR ตามด้วยบรรทัด RTL
Potatoswatter

5

เกี่ยวกับประโยชน์ของสิ่งนี้เมื่อทำงานกับ shellcode คุณจะเห็นสิ่งนี้สร้างขึ้นมากมาย:

int (*ret)() = (int(*)())code;
ret();

แม้ว่าจะไม่ซับซ้อนเหมือนกัน แต่รูปแบบเฉพาะนี้เกิดขึ้นมากมาย

ตัวอย่างที่สมบูรณ์มากขึ้นในการนี้คำถาม SO

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


5

การประกาศ

void (*(*f[])())()

เป็นเพียงวิธีคลุมเครือในการพูด

Function f[]

กับ

typedef void (*ResultFunction)();

typedef ResultFunction (*Function)();

ในทางปฏิบัติชื่ออธิบายเพิ่มเติมจะต้องแทนResultFunctionและฟังก์ชั่น voidถ้าเป็นไปได้ฉันก็จะระบุรายการพารามิเตอร์


4

ฉันพบวิธีที่อธิบายโดย Bruce Eckel ว่ามีประโยชน์และง่ายต่อการปฏิบัติตาม:

การกำหนดตัวชี้ฟังก์ชั่น

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

void (*funcPtr)();

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

หากต้องการตรวจสอบ“ เริ่มตรงกลาง” (“ funcPtr คือ ... ”) ไปทางขวา (ไม่มีสิ่งใดอยู่ - คุณหยุดโดยวงเล็บขวา) ไปทางซ้ายแล้วค้นหา '*' (“ ... ตัวชี้ไปยัง ... ”) ไปทางขวาและค้นหารายการอาร์กิวเมนต์ที่ว่างเปล่า (“ ... ฟังก์ชันที่ไม่มีอาร์กิวเมนต์ ... ”) ไปทางซ้ายและค้นหาช่องว่าง (“ funcPtr คือ ตัวชี้ไปยังฟังก์ชันที่ไม่มีอาร์กิวเมนต์และส่งกลับค่าเป็นโมฆะ”)

คุณอาจสงสัยว่าทำไม * funcPtr จึงต้องใช้วงเล็บ หากคุณไม่ได้ใช้คอมไพเลอร์จะเห็นว่า:

void *funcPtr();

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

การประกาศและคำจำกัดความที่ซับซ้อน

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

//: C03:ComplicatedDefinitions.cpp

/* 1. */     void * (*(*fp1)(int))[10];

/* 2. */     float (*(*fp2)(int,int,float))(int);

/* 3. */     typedef double (*(*(*fp3)())[10])();
             fp3 a;

/* 4. */     int (*(*f4())[10])();


int main() {} ///:~ 

เดินผ่านแต่ละคนและใช้แนวทางด้านซ้ายขวาเพื่อคิดออก Number 1กล่าวว่า“ fp1 เป็นตัวชี้ไปยังฟังก์ชันที่รับอาร์กิวเมนต์จำนวนเต็มและส่งกลับตัวชี้ไปยังอาร์เรย์ที่เป็นโมฆะ 10 ตัวชี้”

Number 2กล่าวว่า“ fp2 เป็นตัวชี้ไปยังฟังก์ชันที่รับอาร์กิวเมนต์สามตัว (int, int และ float) และส่งกลับตัวชี้ไปยังฟังก์ชันที่รับอาร์กิวเมนต์จำนวนเต็มและส่งคืน float”

หากคุณกำลังสร้างคำจำกัดความที่ซับซ้อนจำนวนมากคุณอาจต้องการใช้ typedef หมายเลข 3แสดงวิธีที่ typedef บันทึกการพิมพ์คำอธิบายที่ซับซ้อนทุกครั้ง มันบอกว่า“ fp3 เป็นตัวชี้ไปยังฟังก์ชันที่ไม่ขัดแย้งและส่งกลับตัวชี้ไปยังอาร์เรย์ 10 พอยน์เตอร์ไปยังฟังก์ชันที่ไม่มีอาร์กิวเมนต์และส่งกลับเป็นสองเท่า” จากนั้นก็ระบุว่า“ a เป็นหนึ่งในประเภท fp3 เหล่านี้” typedef โดยทั่วไปมีประโยชน์สำหรับการสร้างคำอธิบายที่ซับซ้อนจากคำง่าย ๆ

หมายเลข 4คือการประกาศฟังก์ชันแทนการกำหนดตัวแปร มันบอกว่า "f4 เป็นฟังก์ชั่นที่ส่งกลับตัวชี้ไปยังอาร์เรย์ 10 ตัวชี้ไปยังฟังก์ชั่นที่คืนค่าจำนวนเต็ม"

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

นำมาจาก: การคิดใน C ++ เล่ม 1 ฉบับที่สองตอนที่ 3 หัวข้อ "Function Addresses" โดย Bruce Eckel


4

จำกฎเหล่านี้สำหรับการประกาศ C
และความสำคัญจะไม่มีข้อสงสัย:
เริ่มต้นด้วยคำต่อท้ายดำเนินการด้วยคำนำหน้า
และอ่านทั้งสองชุดจากภายในสู่ภายนอก
- ฉันกลางปี ​​1980

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

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

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

มันอาจจะเลวร้ายยิ่งคุณรู้ มีคำสั่ง PL / I ทางกฎหมายที่เริ่มต้นด้วยบางสิ่งเช่น:

if if if = then then then = else else else = if then ...

2
คำสั่ง PL / ผมเป็นและมีการแยกวิเคราะห์เป็นIF IF = THEN THEN THEN = ELSE ELSE ELSE = ENDIF ENDIF if (IF == THEN) then (THEN = ELSE) else (ELSE = ENDIF)
โคลจอห์นสัน

ฉันคิดว่ามีรุ่นที่ก้าวไปอีกขั้นหนึ่งโดยใช้เงื่อนไข IF / THEN / ELSE แบบมีเงื่อนไข (เทียบเท่ากับ C's? :) ซึ่งได้ชุดที่สามในการผสม ... แต่มันไม่กี่สิบปีและอาจมี ขึ้นอยู่กับภาษาเฉพาะของภาษา ประเด็นยังคงอยู่ว่าภาษาใดมีรูปแบบทางพยาธิวิทยาอย่างน้อยหนึ่ง
keshlam

4

ฉันเคยเป็นนักเขียนดั้งเดิมของกฎเกลียวที่ฉันเขียนมาหลายปีแล้ว (เมื่อฉันมีผมเยอะมาก :) และได้รับเกียรติเมื่อมันถูกเพิ่มเข้าไปใน cfaq

ฉันเขียนกฎหมุนวนเป็นวิธีที่ทำให้นักเรียนและเพื่อนร่วมงานของฉันอ่านประกาศ C ในหัวได้ง่ายขึ้น เช่นโดยไม่ต้องใช้เครื่องมือซอฟต์แวร์เช่น cdecl.org เป็นต้นฉันไม่เคยตั้งใจที่จะประกาศว่ากฎหมุนวนเป็นวิธีที่เป็นที่ยอมรับในการแยกวิเคราะห์ C นิพจน์ ฉันดีใจที่ได้เห็นว่ากฎนี้ช่วยให้นักเรียนซีและนักเรียนฝึกหัดเขียนโปรแกรม C หลายพันคนตลอดระยะเวลาหลายปี!

สำหรับบันทึก,

มีการระบุอย่างถูกต้องหลายครั้งในหลาย ๆ ไซต์รวมทั้ง Linus Torvalds (คนที่ฉันเคารพอย่างมาก) ว่ามีสถานการณ์ที่กฎเกลียวของฉัน "หยุด" สิ่งมีชีวิตที่พบมากที่สุด:

char *ar[10][10];

ตามที่คนอื่น ๆ ชี้ไว้ในเธรดนี้กฎอาจได้รับการอัปเดตเพื่อบอกว่าเมื่อคุณพบอาร์เรย์ให้ใช้ดัชนีทั้งหมดเหมือนเขียนว่า:

char *(ar[10][10]);

ตอนนี้ตามกฎเกลียวฉันจะได้รับ:

"ar เป็นพอยน์เตอร์สองมิติขนาด 10 x 10 เพื่อให้เป็นถ่าน"

ฉันหวังว่ากฎหมุนวนจะมีประโยชน์ในการเรียนรู้ C!

PS:

ฉันชอบภาพ "C ไม่ยาก" :)


3
  • เป็นโมฆะ (*(*f[]) ()) ()

การแก้ไขvoid>>

  • (*(*f[]) ()) () = เป็นโมฆะ

การรีโหลด()>>

  • (* (*f[]) ()) = ฟังก์ชันส่งคืน (เป็นโมฆะ)

การแก้ไข*>>

  • (*f[]) () = ตัวชี้ไปยัง (ฟังก์ชั่นกลับมา (เป็นโมฆะ))

การแก้ไข()>>

  • (* f[]) = ฟังก์ชั่นกลับมา (ตัวชี้ไปที่ (ฟังก์ชั่นกลับมา (เป็นโมฆะ)))

การแก้ไข*>>

  • f[] = ตัวชี้ไปยัง (ฟังก์ชั่นกลับมา (ตัวชี้ไปยัง (ฟังก์ชั่นกลับมา (เป็นโมฆะ))))

การแก้ไข[ ]>>

  • f = อาร์เรย์ของ (ตัวชี้ไปยัง (ฟังก์ชั่นกลับมา (ตัวชี้ไปยัง (ฟังก์ชั่นกลับมา (เป็นโมฆะ))))
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.