การรวม C ++ และ C - #ifdef __cplusplus ทำงานอย่างไร


319

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

ดังนั้นที่ด้านบนของไฟล์ส่วนหัวCแต่ละไฟล์ (หลังจากที่มียามรวม) เรามี

#ifdef __cplusplus
extern "C" {
#endif

และที่ด้านล่างเราเขียน

#ifdef __cplusplus
}
#endif

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

  1. ถ้าฉันมีไฟล์ C ++ A.hh ซึ่งรวมถึงไฟล์ส่วนหัวC Bh รวมถึงไฟล์ส่วนหัวCอีกCh วิธีนี้ทำงานอย่างไร ฉันคิดว่าเมื่อคอมไพเลอร์เข้าสู่ Bh __cplusplusจะถูกกำหนดดังนั้นมันจะล้อมโค้ดด้วยextern "C" (และ__cplusplusจะไม่ถูกกำหนดภายในบล็อกนี้) ดังนั้นเมื่อเข้าสู่ Ch แล้ว __cplusplusจะไม่ถูกกำหนดและรหัสจะไม่ถูกรวมเข้าด้วย extern "C"กัน ถูกต้องหรือไม่

  2. มีอะไรผิดปกติในการห่อโค้ดด้วย extern "C" { extern "C" { .. } }หรือไม่? สิ่งที่สองextern "C" จะทำอย่างไร

  3. เราไม่ได้ใส่เสื้อคลุมนี้ไว้รอบ ๆ ไฟล์. c เพียงแค่ไฟล์. h แล้วจะเกิดอะไรขึ้นถ้าฟังก์ชั่นไม่มีต้นแบบ? คอมไพเลอร์คิดว่าเป็นฟังก์ชั่น C ++ หรือไม่?

  4. นอกจากนี้เรายังใช้รหัสของบุคคลที่สามซึ่งเขียนด้วยภาษา Cและไม่มีเสื้อคลุมแบบนี้อยู่รอบ ๆ เมื่อใดก็ตามที่ฉันรวมส่วนหัวจากห้องสมุดนั้นฉันได้ใส่extern "C"#include นี่เป็นวิธีที่ถูกต้องในการจัดการกับเรื่องนี้หรือไม่?

  5. ในที่สุดการตั้งค่านี้เป็นความคิดที่ดีหรือไม่? มีอะไรอีกบ้างที่เราควรทำ? เรากำลังจะผสมCและ C ++ สำหรับอนาคตอันใกล้และฉันต้องการให้แน่ใจว่าเราครอบคลุมฐานทั้งหมดของเรา



2
รัดกุมนี้เป็นคำอธิบายที่ดีที่สุด: To ensure that the names declared in that portion of code have C linkage, and thus C++ name mangling is not performed. (ผมเคยได้รับมันจากการเชื่อมโยง )
anhldbk

คุณไม่ต้องใส่ชื่อภาษา C เป็นตัวหนา
Edward Karak

คำตอบ:


290

extern "C"ไม่ได้เปลี่ยนวิธีที่คอมไพเลอร์อ่านรหัสจริงๆ หากรหัสของคุณอยู่ในไฟล์. c มันจะถูกคอมไพล์เป็น C หากอยู่ในไฟล์. cpp มันจะถูกคอมไพล์เป็น C ++ (เว้นแต่คุณจะทำสิ่งที่แปลกกับการตั้งค่าของคุณ)

สิ่งที่extern "C"มีผลต่อการเชื่อมโยง ฟังก์ชั่น C ++ เมื่อรวบรวมมีชื่อของพวกเขา mangled - นี่คือสิ่งที่ทำให้การบรรทุกเกินพิกัดเป็นไปได้ ชื่อฟังก์ชั่นได้รับการแก้ไขตามประเภทและจำนวนพารามิเตอร์ดังนั้นสองฟังก์ชั่นที่มีชื่อเดียวกันจะมีชื่อสัญลักษณ์ที่แตกต่างกัน

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

extern "C"บล็อกรังอย่างสวยงาม นอกจากนี้ยังมีextern "C++"ถ้าคุณพบว่าตัวเองติดอยู่ในextern "C"ภูมิภาคอย่างสิ้นหวังแต่ไม่ใช่ความคิดที่ดีจากมุมมองด้านความสะอาด

ตอนนี้โดยเฉพาะเกี่ยวกับคำถามหมายเลขของคุณ:

เกี่ยวกับ # 1: __cplusplus จะยังคงอยู่ภายในextern "C"บล็อก แม้ว่ามันจะไม่สำคัญเนื่องจากบล็อกควรซ้อนอย่างเรียบร้อย

เกี่ยวกับ # 2: __cplusplus จะถูกกำหนดไว้สำหรับหน่วยการรวบรวมใด ๆ ที่กำลังเรียกใช้ผ่านคอมไพเลอร์ C ++ โดยทั่วไปนั่นหมายถึงไฟล์. cpp และไฟล์ใด ๆ ที่รวมอยู่ในไฟล์. cpp นั้น .h (หรือ. hh หรือ. hpp หรือ what-have-you) เดียวกันสามารถตีความได้ว่าเป็น C หรือ C ++ ในเวลาที่ต่างกันหากหน่วยการรวบรวมที่แตกต่างกันรวมอยู่ด้วย หากคุณต้องการให้ต้นแบบในไฟล์. h อ้างถึงชื่อสัญลักษณ์ C พวกเขาจะต้องมีextern "C"เมื่อถูกตีความว่าเป็น C ++ และพวกเขาไม่ควรมีextern "C"เมื่อถูกตีความว่าเป็น C - ดังนั้นการ#ifdef __cplusplusตรวจสอบ

ในการตอบคำถาม # 3 ของคุณ: ฟังก์ชั่นที่ไม่มีต้นแบบจะมีลิงค์ C ++ หากอยู่ในไฟล์. cpp และไม่อยู่ในextern "C"บล็อก อย่างไรก็ตามนี่เป็นเรื่องปกติเพราะถ้ามันไม่มีต้นแบบมันสามารถถูกเรียกได้โดยฟังก์ชั่นอื่น ๆ ในไฟล์เดียวกันและโดยทั่วไปคุณไม่สนใจว่าหน้าตาการเชื่อมโยงนั้นเป็นอย่างไรเพราะคุณไม่ได้วางแผนที่จะใช้ฟังก์ชั่นนั้น ถูกเรียกโดยสิ่งใดนอกหน่วยการรวบรวมเดียวกัน

สำหรับ # 4 คุณได้รับมันอย่างแน่นอน หากคุณกำลังรวมส่วนหัวของรหัสที่มี C linkage (เช่นรหัสที่รวบรวมโดยตัวแปลภาษา C) คุณต้องextern "C"มีส่วนหัว - ด้วยวิธีนี้คุณจะสามารถเชื่อมโยงกับไลบรารีได้ (มิฉะนั้นลิงเกอร์ของคุณจะมองหาฟังก์ชั่นที่มีชื่อเหมือน_Z1hicตอนที่คุณกำลังมองหาvoid h(int, char)

5: การผสมแบบนี้เป็นเหตุผลทั่วไปที่จะใช้ extern "C"และฉันไม่เห็นอะไรผิดปกติกับการทำแบบนี้ - แค่ให้แน่ใจว่าคุณเข้าใจสิ่งที่คุณกำลังทำอยู่


10
เหมาะสำหรับการกล่าวขวัญextern "C++"เมื่อ c ++ ส่วนหัวของคุณ / รหัสถูกขังอยู่ข้างในลึก ๆ บางรหัส C
deddebme

1
ฉันเขียนโปรแกรม C อย่างง่าย ข้างในฉันเพิ่ม #ifdef __cplusplus block และเพิ่ม printf ("__ cplusplus ที่กำหนด \ n"); ในนั้น. ถ้าฉันรวบรวมมันด้วย gcc "__cplusplus ที่กำหนด" จะไม่ถูกพิมพ์ แต่ถ้าฉันรวบรวมด้วย g ++ มันจะถูกพิมพ์ ดังนั้นฉันคิดว่า __cplusplus หมายความว่าคอมไพเลอร์คือคอมไพเลอร์ C ++ (คุณบอกว่ามัน) มันไม่ถูกต้องเหรอ? (เพราะฉันเห็นคุณพูดว่า '__cplusplus ควรกำหนดไว้ในบล็อก extern "C"' เราสามารถกำหนด __cplusplus ได้อย่างชัดเจนหรือไม่?
Chan Kim

1
ในขณะที่คุณควรจะสามารถกำหนด (เกือบ) สิ่งที่คุณต้องการจุดทั้งหมดของ__cplusplusคือการตรวจสอบว่าC++มีการใช้ vs Cดังนั้นเพื่อกำหนดด้วยตนเอง / ชัดเจนท้าทายวัตถุประสงค์ของมันอย่างชัดเจน ...
nurchi

externter "C" ไม่ได้เกี่ยวกับวิธีการที่คอมไพเลอร์ดูไฟล์ต้นฉบับ แต่ทั้งหมดเกี่ยวกับวิธีการดูไฟล์ส่วนหัว structs อาจมีขนาดแตกต่างกันเมื่อคอมไพล์เป็น C vs C ++, มีชื่อ mangling แน่นอนและความแตกต่างอื่น ๆ ที่อาจเกิดขึ้นเช่นกัน
Nick

39
  1. extern "C"ไม่เปลี่ยนการมีหรือไม่มี__cplusplusมาโคร มันแค่เปลี่ยนการเชื่อมโยงและการเรียงชื่อของการประกาศที่ถูกห่อ

  2. คุณสามารถทำรังextern "C"บล็อคได้อย่างมีความสุข

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

  4. ใช่

  5. คุณสามารถผสม C และ C ++ ได้อย่างปลอดภัยด้วยวิธีนี้


หากคุณรวบรวม.cไฟล์เป็น C ++ ทุกอย่างจะถูกรวบรวมเป็นรหัส C ++ แม้ว่าจะอยู่ในextern "C"บล็อก extern "C"รหัสไม่สามารถใช้คุณสมบัติที่ขึ้นอยู่กับ C ++ เรียกประชุม (เช่นการดำเนินงานมาก) แต่ร่างกายของฟังก์ชั่นที่ยังคงเป็นที่รวบรวมเป็น C ++ กับสิ่งที่สร้างความ
David C.

21

คู่ของ gotchas ที่เป็นคอลเลคชั่นสำหรับคำตอบที่ยอดเยี่ยมของ Andrew Shelansky และไม่เห็นด้วยเล็กน้อย ไม่ได้เปลี่ยนวิธีที่คอมไพเลอร์อ่านรหัส

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

ข้อผิดพลาดของคอมไพเลอร์จะเกิดขึ้นหากคุณพยายามใช้คุณสมบัติ C ++ ของการประกาศต้นแบบเช่นการบรรทุกเกินพิกัด

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

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


1
นี่เป็นสาเหตุที่คลุมเครือดึงผมออกมา ต้องมีการโพสต์ที่ใดที่หนึ่งจริงๆ
Mitchell Currie

3

มันเกี่ยวกับ ABI เพื่อให้ทั้งแอปพลิเคชัน C และ C ++ ใช้อินเตอร์เฟส C โดยไม่มีปัญหาใด ๆ

เนื่องจากภาษา C นั้นง่ายมากการสร้างรหัสจึงมีความเสถียรเป็นเวลาหลายปีสำหรับคอมไพเลอร์ที่แตกต่างกันเช่น GCC, Borland C \ C ++, MSVC เป็นต้น

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

ผู้ใช้อาจยังคงต้องการใช้งานโปรแกรมจริงใน C ++ แต่ยังคงเก็บอินเตอร์เฟส C เก่าและ ABI ไว้ตามปกติไฟล์ส่วนหัวจะต้องประกาศextern "C" {}มันบอกคอมไพเลอร์สร้างคอมไพเลอร์เข้ากันได้ / เก่า / ง่าย / ง่าย C ABI สำหรับฟังก์ชันอินเตอร์เฟสหากคอมไพเลอร์เป็นคอมไพเลอร์ C ไม่ใช่คอมไพเลอร์ C ++

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