Kết hợp C ++ và C - #ifdef __cplusplus hoạt động như thế nào?


319

Tôi đang làm việc trên một dự án có nhiều mã C kế thừa . Chúng tôi đã bắt đầu viết bằng C ++, với mục đích cuối cùng là chuyển đổi mã kế thừa. Tôi hơi bối rối về cách C và C ++ tương tác. Tôi hiểu rằng bằng cách gói mã C bằng extern "C"trình biên dịch C ++ sẽ không xáo trộn tên của mã C , nhưng tôi không hoàn toàn chắc chắn làm thế nào để thực hiện điều này.

Vì vậy, ở đầu mỗi tệp tiêu đề C (sau các bộ bảo vệ bao gồm), chúng tôi có

#ifdef __cplusplus
extern "C" {
#endif

và ở phía dưới, chúng tôi viết

#ifdef __cplusplus
}
#endif

Ở giữa hai, chúng ta có tất cả các nguyên mẫu bao gồm, typedefs và hàm. Tôi có một vài câu hỏi, để xem liệu tôi có hiểu đúng về điều này không:

  1. Nếu tôi có tệp C ++ A.hh bao gồm tệp tiêu đề C Bh, bao gồm một tệp tiêu đề C khác Ch, thì nó hoạt động như thế nào? Tôi nghĩ rằng khi trình biên dịch bước vào Bh, __cplusplussẽ được xác định, vì vậy nó sẽ bọc mã bằng extern "C" (và __cplusplussẽ không được xác định bên trong khối này). Vì vậy, khi nó bước vào Ch, __cplusplussẽ không được xác định và mã sẽ không được bọc extern "C". Điều này có đúng không?

  2. Có điều gì sai với việc gói một đoạn mã với extern "C" { extern "C" { .. } }? Người thứ hai extern "C" sẽ làm gì?

  3. Chúng tôi không đặt trình bao bọc này xung quanh các tệp .c, chỉ các tệp .h. Vậy, điều gì xảy ra nếu một chức năng không có nguyên mẫu? Trình biên dịch có nghĩ rằng đó là một hàm C ++ không?

  4. Chúng tôi cũng đang sử dụng một số mã của bên thứ ba được viết bằng C và không có loại trình bao bọc này. Bất cứ khi nào tôi bao gồm một tiêu đề từ thư viện đó, tôi đã đặt extern "C"xung quanh #incoide. Đây có phải là cách đúng đắn để đối phó với điều đó?

  5. Cuối cùng, đây có phải là một ý tưởng tốt? Có điều gì khác chúng ta nên làm không? Chúng tôi sẽ kết hợp C và C ++ trong tương lai gần và tôi muốn chắc chắn rằng chúng tôi sẽ đảm bảo tất cả các cơ sở của chúng tôi.



2
Chính xác, đây là lời giải thích tốt nhất: To ensure that the names declared in that portion of code have C linkage, and thus C++ name mangling is not performed. (Tôi đã nhận được từ liên kết )
anhldbk

Bạn không cần phải đặt tên của ngôn ngữ C in đậm
Edward Karak

Câu trả lời:


290

extern "C"không thực sự thay đổi cách trình biên dịch đọc mã. Nếu mã của bạn nằm trong tệp .c, nó sẽ được biên dịch thành C, nếu là trong tệp .cpp, nó sẽ được biên dịch thành C ++ (trừ khi bạn làm gì đó lạ với cấu hình của mình).

Những gì extern "C"không ảnh hưởng đến liên kết. Các hàm C ++, khi được biên dịch, có tên của chúng được đọc sai - đây là điều làm cho quá tải có thể xảy ra. Tên hàm được sửa đổi dựa trên các loại và số lượng tham số, do đó hai hàm có cùng tên sẽ có tên ký hiệu khác nhau.

Mã bên trong an extern "C"vẫn là mã C ++. Có những hạn chế về những gì bạn có thể làm trong khối "C" bên ngoài, nhưng tất cả đều liên quan đến liên kết. Bạn không thể xác định bất kỳ biểu tượng mới nào không thể được xây dựng bằng liên kết C. Điều đó có nghĩa là không có lớp hoặc mẫu, ví dụ.

extern "C"khối tổ độc đáo. Cũng cóextern "C++" nếu bạn thấy mình bị mắc kẹt trong extern "C"khu vực, nhưng đó không phải là một ý tưởng hay từ góc độ sạch sẽ.

Bây giờ, cụ thể liên quan đến câu hỏi được đánh số của bạn:

Về # 1: __cplusplus sẽ được xác định bên trong extern "C" khối. Điều này không quan trọng, mặc dù, vì các khối nên lồng gọn gàng.

Về # 2: __cplusplus sẽ được xác định cho bất kỳ đơn vị biên dịch nào đang được chạy qua trình biên dịch C ++. Nói chung, điều đó có nghĩa là tệp .cpp và bất kỳ tệp nào được bao gồm bởi tệp .cpp đó. Cùng một .h (hoặc .hh hoặc .hpp hoặc what-have-you) có thể được hiểu là C hoặc C ++ tại các thời điểm khác nhau, nếu các đơn vị biên dịch khác nhau bao gồm chúng. Nếu bạn muốn các nguyên mẫu trong tệp .h tham chiếu đến tên ký hiệu C, thì chúng phải có extern "C"khi được hiểu là C ++ và chúng không nên có extern "C"khi được hiểu là C - do đó#ifdef __cplusplus kiểm tra.

Để trả lời câu hỏi số 3 của bạn: các hàm không có nguyên mẫu sẽ có liên kết C ++ nếu chúng nằm trong tệp .cpp chứ không phải bên trong một extern "C" khối. Mặc dù vậy, điều này cũng tốt, vì nếu nó không có nguyên mẫu, nó chỉ có thể được gọi bởi các chức năng khác trong cùng một tệp, và sau đó bạn thường không quan tâm liên kết trông như thế nào, bởi vì bạn không có kế hoạch có chức năng đó được gọi bởi bất cứ điều gì bên ngoài cùng một đơn vị biên dịch.

Đối với # 4, bạn đã có nó chính xác. Nếu bạn đang bao gồm một tiêu đề cho mã có liên kết C (chẳng hạn như mã được biên dịch bởi trình biên dịch C), thì bạn phải extern "C"tiêu đề - theo cách đó bạn sẽ có thể liên kết với thư viện. (Nếu không, trình liên kết của bạn sẽ tìm kiếm các hàm có tên như _Z1hickhi bạn đang tìm kiếmvoid h(int, char)

5: Kiểu trộn này là một lý do phổ biến để sử dụng extern "C"và tôi không thấy có gì sai khi thực hiện theo cách này - chỉ cần đảm bảo bạn hiểu những gì bạn đang làm.


10
Tốt để đề cập extern "C++"khi tiêu đề / mã C ++ của bạn bị mắc kẹt sâu bên trong một số mã C
depdebme

1
Tôi đã viết một chương trình C đơn giản. Trong đó, tôi đã thêm khối #ifdef __cplusplus và thêm một printf ("__ cplusplus được xác định \ n"); trong đó. Nếu tôi biên dịch nó bằng gcc, "__cplusplus được xác định" sẽ không được in, nhưng nếu tôi biên dịch nó bằng g ++, nó sẽ được in. Vì vậy, tôi hình __cplusplus có nghĩa là trình biên dịch là trình biên dịch C ++ (bạn đã nói nó). Có đúng không? (bởi vì tôi thấy bạn nói '__cplusplus nên được xác định bên trong các khối "C" bên ngoài. Chúng ta có thể định nghĩa rõ ràng __cplusplus không?
Chan Kim

1
Mặc dù bạn có thể xác định (gần như) bất cứ điều gì bạn muốn, toàn bộ vấn đề __cpluspluslà xác định xem C++có đang được sử dụng so với hay không C, do đó, việc xác định nó theo cách thủ công / rõ ràng thách thức mục đích của nó ...
nicksi

"C" bên ngoài thực sự không phải là về cách trình biên dịch xem tệp nguồn, tất cả là về cách nó xem tệp tiêu đề. cấu trúc có thể có các kích cỡ khác nhau khi được biên dịch thành C vs C ++, dĩ nhiên có tên xáo trộn và cũng có khả năng khác biệt.
Nick

39
  1. extern "C"không thay đổi sự hiện diện hay vắng mặt của __cplusplusmacro. Nó chỉ thay đổi liên kết và xáo trộn tên của các khai báo được gói.

  2. Bạn có thể làm tổ extern "C"khá vui vẻ.

  3. Nếu bạn biên dịch các .ctệp của mình thành C ++ thì mọi thứ không nằm trong một extern "C"khối và không có extern "C"nguyên mẫu sẽ được coi là hàm C ++. Nếu bạn biên dịch chúng thành C thì tất nhiên mọi thứ sẽ là hàm C.

  4. Đúng

  5. Bạn có thể kết hợp C và C ++ một cách an toàn theo cách này.


Nếu bạn biên dịch .ccác tệp dưới dạng C ++, thì mọi thứ sẽ được biên dịch dưới dạng mã C ++, ngay cả khi nó nằm trong một extern "C"khối. Các extern "C"mã không có thể sử dụng tính năng mà phụ thuộc vào C ++ ước gọi (ví dụ như điều hành quá tải) nhưng cơ quan chức năng vẫn đang biên soạn như C ++, với tất cả những đòi hỏi.
David C.

21

Một vài câu hỏi thường gặp về câu trả lời xuất sắc của Andrew Shelansky và không đồng ý một chút với việc không thực sự thay đổi cách trình biên dịch đọc mã

Vì các nguyên mẫu hàm của bạn được biên dịch thành C, nên bạn không thể nạp chồng các tên hàm giống nhau với các tham số khác nhau - đó là một trong những tính năng chính của việc xáo trộn tên của trình biên dịch. Nó được mô tả như là một vấn đề liên kết nhưng điều đó không hoàn toàn đúng - bạn sẽ gặp lỗi từ cả trình biên dịch và trình liên kết.

Các lỗi biên dịch sẽ là nếu bạn cố gắng sử dụng các tính năng C ++ của khai báo nguyên mẫu, chẳng hạn như quá tải.

Các lỗi liên kết sẽ xảy ra sau đó vì chức năng của bạn sẽ không được tìm thấy, nếu bạn không có trình bao bọc "C" bên ngoài xung quanh các khai báo và tiêu đề được bao gồm trong hỗn hợp nguồn C và C ++.

Một lý do để không khuyến khích mọi người sử dụng cài đặt biên dịch C dưới dạng C ++ là vì điều này có nghĩa là mã nguồn của họ không còn khả dụng. Cài đặt đó là cài đặt dự án và vì vậy nếu tệp .c được thả vào dự án khác, nó sẽ không được biên dịch thành c ++. Tôi muốn mọi người dành thời gian để đổi tên hậu tố tập tin thành .cpp.


1
Đây là nguyên nhân khó hiểu, kéo tóc tôi ra. Thực sự cần phải được đăng ở đâu đó.
Mitchell Currie

3

Đó là về ABI, để cho phép cả ứng dụng C và C ++ sử dụng giao diện C mà không gặp vấn đề gì.

Vì ngôn ngữ C rất dễ dàng, việc tạo mã đã ổn định trong nhiều năm đối với các trình biên dịch khác nhau, chẳng hạn như GCC, Borland C \ C ++, MSVC, v.v.

Trong khi C ++ ngày càng trở nên phổ biến, rất nhiều thứ phải được thêm vào miền C ++ mới (ví dụ cuối cùng, Cfront đã bị bỏ rơi tại AT & T vì C không thể bao gồm tất cả các tính năng cần thiết). Chẳng hạn như tính năng mẫu và tạo mã thời gian biên dịch, từ xưa, các nhà cung cấp trình biên dịch khác nhau thực sự đã thực hiện riêng trình biên dịch và trình liên kết C ++, ABI thực tế hoàn toàn không tương thích với chương trình C ++ ở các nền tảng khác nhau.

Mọi người vẫn có thể muốn triển khai chương trình thực tế trong C ++ nhưng vẫn giữ giao diện C và ABI cũ như bình thường, tệp tiêu đề phải khai báo extern "C" {} , nó báo cho trình biên dịch tạo C ABI tương thích / cũ / đơn giản / dễ dàng cho các chức năng giao diện nếu trình biên dịch là trình biên dịch C chứ không phải trình biên dịch C ++.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.