Tại sao chúng ta cần extern C C {{incoide <foo.h>} trong C ++?


136

Tại sao chúng ta cần sử dụng:

extern "C" {
#include <foo.h>
}

Đặc biệt:

  • Khi nào chúng ta nên sử dụng nó?

  • Điều gì đang xảy ra ở cấp trình biên dịch / trình liên kết yêu cầu chúng ta sử dụng nó?

  • Làm thế nào về mặt biên dịch / liên kết, điều này giải quyết các vấn đề đòi hỏi chúng ta phải sử dụng nó?

Câu trả lời:


122

C và C ++ tương tự bề ngoài, nhưng mỗi phần biên dịch thành một bộ mã rất khác nhau. Khi bạn bao gồm một tệp tiêu đề với trình biên dịch C ++, trình biên dịch sẽ mong đợi mã C ++. Tuy nhiên, nếu đó là một tiêu đề C, thì trình biên dịch dự kiến ​​dữ liệu chứa trong tệp tiêu đề sẽ được biên dịch theo một định dạng nhất định C ++ 'ABI', hoặc 'Giao diện nhị phân ứng dụng', vì vậy trình liên kết sẽ bị kẹt. Điều này là tốt hơn để truyền dữ liệu C ++ đến một chức năng mong đợi dữ liệu C.

(Để đi sâu vào thực tế, ABI của C ++ nói chung là 'mangles' tên của các hàm / phương thức của chúng, vì vậy gọi printf()mà không gắn cờ nguyên mẫu là hàm C, C ++ thực sự sẽ tạo ra cuộc gọi mã _Zprintf, cộng với crap thêm vào cuối. )

Vì vậy: sử dụng extern "C" {...}khi bao gồm tiêu đề ac, nó thật đơn giản. Nếu không, bạn sẽ có một mã không khớp trong mã được biên dịch và trình liên kết sẽ bị sặc. Tuy nhiên, đối với hầu hết các tiêu đề, bạn thậm chí sẽ không cần externbởi vì hầu hết các tiêu đề hệ thống C sẽ tính đến thực tế là chúng có thể được bao gồm bởi mã C ++ và đã là externmã của chúng.


1
Bạn có thể vui lòng giải thích thêm về "hầu hết các tiêu đề hệ thống C sẽ giải thích cho thực tế rằng chúng có thể được bao gồm bởi mã C ++ và đã thoát ra khỏi mã của chúng." ?
Bulat M.

7
@BulatM. Chúng chứa một cái gì đó như thế này: #ifdef __cplusplus extern "C" { #endif Vì vậy, khi được bao gồm từ tệp C ++, chúng vẫn được coi là tiêu đề C.
Calmarius

111

extern "C" xác định cách đặt tên các ký hiệu trong tệp đối tượng được tạo. Nếu một hàm được khai báo không có "C" bên ngoài, tên biểu tượng trong tệp đối tượng sẽ sử dụng xáo trộn tên C ++. Đây là một ví dụ.

Đã cho test.C như vậy:

void foo() { }

Biên dịch và liệt kê các ký hiệu trong tệp đối tượng cho:

$ g++ -c test.C
$ nm test.o
0000000000000000 T _Z3foov
                 U __gxx_personality_v0

Hàm foo thực sự được gọi là "_Z3foov". Chuỗi này chứa thông tin loại cho kiểu trả về và tham số, trong số những thứ khác. Nếu bạn thay vì viết test.C như thế này:

extern "C" {
    void foo() { }
}

Sau đó biên dịch và nhìn vào các biểu tượng:

$ g++ -c test.C
$ nm test.o
                 U __gxx_personality_v0
0000000000000000 T foo

Bạn nhận được liên kết C. Tên của hàm "foo" trong tệp đối tượng chỉ là "foo" và nó không có tất cả các thông tin loại ưa thích xuất phát từ việc xáo trộn tên.

Bạn thường bao gồm một tiêu đề trong extern "C" {} nếu mã đi kèm với nó được biên dịch bằng trình biên dịch C nhưng bạn đang cố gắng gọi nó từ C ++. Khi bạn làm điều này, bạn đang nói với trình biên dịch rằng tất cả các khai báo trong tiêu đề sẽ sử dụng liên kết C. Khi bạn liên kết mã của mình, các tệp .o của bạn sẽ chứa các tham chiếu đến "foo", chứ không phải "_Z3fooblah", hy vọng phù hợp với bất cứ điều gì trong thư viện mà bạn liên kết.

Hầu hết các thư viện hiện đại sẽ đặt các vệ sĩ xung quanh các tiêu đề như vậy để các biểu tượng được khai báo với mối liên kết phù hợp. ví dụ: trong rất nhiều tiêu đề bạn sẽ tìm thấy:

#ifdef __cplusplus
extern "C" {
#endif

... declarations ...

#ifdef __cplusplus
}
#endif

Điều này đảm bảo rằng khi mã C ++ bao gồm tiêu đề, các ký hiệu trong tệp đối tượng của bạn khớp với những gì trong thư viện C. Bạn chỉ nên đặt "C" {} bên ngoài xung quanh tiêu đề C của mình nếu nó đã cũ và chưa có những người bảo vệ này.


22

Trong C ++, bạn có thể có các thực thể khác nhau có chung tên. Ví dụ ở đây là danh sách các hàm có tên foo :

  • A::foo()
  • B::foo()
  • C::foo(int)
  • C::foo(std::string)

Để phân biệt giữa tất cả chúng, trình biên dịch C ++ sẽ tạo các tên duy nhất cho mỗi cái trong một quy trình gọi là xáo trộn tên hoặc trang trí. Trình biên dịch C không làm điều này. Hơn nữa, mỗi trình biên dịch C ++ có thể làm điều này là một cách khác nhau.

extern "C" yêu cầu trình biên dịch C ++ không thực hiện bất kỳ việc xáo trộn tên nào trên mã trong dấu ngoặc nhọn. Điều này cho phép bạn gọi các hàm C từ trong C ++.


14

Nó có liên quan đến cách các trình biên dịch khác nhau thực hiện xáo trộn tên. Trình biên dịch C ++ sẽ xử lý tên của biểu tượng được xuất từ ​​tệp tiêu đề theo cách hoàn toàn khác với trình biên dịch C, vì vậy khi bạn cố gắng liên kết, bạn sẽ gặp lỗi liên kết cho biết có thiếu biểu tượng.

Để giải quyết vấn đề này, chúng tôi yêu cầu trình biên dịch C ++ chạy ở chế độ "C", do đó, nó thực hiện việc xáo trộn tên giống như trình biên dịch C sẽ làm. Làm như vậy, các lỗi liên kết được sửa chữa.


11

C và C ++ có các quy tắc khác nhau về tên của các biểu tượng. Biểu tượng là cách trình liên kết biết rằng lệnh gọi hàm "openBankAccount" trong một tệp đối tượng do trình biên dịch tạo ra là một tham chiếu đến hàm mà bạn gọi là "openBankAccount" trong một tệp đối tượng khác được tạo từ một tệp nguồn khác bởi cùng một tệp (hoặc tương thích) trình biên dịch. Điều này cho phép bạn tạo một chương trình từ nhiều hơn một tệp nguồn, đây là một cứu cánh khi làm việc trên một dự án lớn.

Trong C quy tắc rất đơn giản, các ký hiệu đều nằm trong một không gian tên duy nhất. Vì vậy, số nguyên "vớ" được lưu trữ dưới dạng "vớ" và hàm Count_socks được lưu trữ dưới dạng "Count_socks".

Các trình liên kết được xây dựng cho C và các ngôn ngữ khác như C với quy tắc đặt tên biểu tượng đơn giản này. Vì vậy, các biểu tượng trong trình liên kết chỉ là các chuỗi đơn giản.

Nhưng trong C ++, ngôn ngữ cho phép bạn có không gian tên, và tính đa hình và nhiều thứ khác mâu thuẫn với quy tắc đơn giản như vậy. Tất cả sáu hàm đa hình của bạn được gọi là "thêm" cần phải có các ký hiệu khác nhau, hoặc hàm sai sẽ được sử dụng bởi các tệp đối tượng khác. Điều này được thực hiện bằng cách "xáo trộn" (đó là một thuật ngữ kỹ thuật) tên của các biểu tượng.

Khi liên kết mã C ++ với thư viện C hoặc mã, bạn cần bất kỳ "C" bên ngoài nào được viết bằng C, chẳng hạn như tệp tiêu đề cho thư viện C, để báo cho trình biên dịch C ++ của bạn rằng các tên biểu tượng này không bị xáo trộn, trong khi phần còn lại của Tất nhiên mã C ++ của bạn phải được đọc sai hoặc nó sẽ không hoạt động.


11

Khi nào chúng ta nên sử dụng nó?

Khi bạn đang liên kết C libaries vào các tệp đối tượng C ++

Điều gì đang xảy ra ở cấp trình biên dịch / trình liên kết yêu cầu chúng ta sử dụng nó?

C và C ++ sử dụng các sơ đồ khác nhau để đặt tên biểu tượng. Điều này báo cho trình liên kết sử dụng lược đồ của C khi liên kết trong thư viện đã cho.

Làm thế nào về mặt biên dịch / liên kết, điều này giải quyết các vấn đề đòi hỏi chúng ta phải sử dụng nó?

Sử dụng sơ đồ đặt tên C cho phép bạn tham chiếu các ký hiệu kiểu C. Nếu không, trình liên kết sẽ thử các biểu tượng kiểu C ++ sẽ không hoạt động.


7

Bạn nên sử dụng extern "C" bất cứ lúc nào bạn bao gồm các hàm xác định tiêu đề nằm trong tệp được biên dịch bởi trình biên dịch C, được sử dụng trong tệp C ++. (Nhiều thư viện C tiêu chuẩn có thể bao gồm kiểm tra này trong tiêu đề của họ để làm cho nhà phát triển đơn giản hơn)

Ví dụ: nếu bạn có một dự án với 3 tệp, produc.c, produc.h và main.cpp và cả các tệp .c và .cpp được biên dịch bằng trình biên dịch C ++ (g ++, cc, v.v.) thì đó không phải là ' t thực sự cần thiết, và thậm chí có thể gây ra lỗi liên kết. Nếu quá trình xây dựng của bạn sử dụng trình biên dịch C thông thường cho produc.c, thì bạn sẽ cần sử dụng "C" bên ngoài khi bao gồm cả produc.h.

Điều đang xảy ra là C ++ mã hóa các tham số của hàm trong tên của nó. Đây là cách chức năng quá tải hoạt động. Tất cả những gì có xu hướng xảy ra với hàm C là việc thêm dấu gạch dưới ("_") vào đầu tên. Không sử dụng extern "C", trình liên kết sẽ tìm kiếm một hàm có tên DoS Something @@ int @ float () khi tên thực của hàm là _DoSthing () hoặc chỉ DoS Something ().

Sử dụng extern "C" giải quyết vấn đề trên bằng cách nói với trình biên dịch C ++ rằng nó sẽ tìm một hàm tuân theo quy ước đặt tên C thay vì C ++.


7

Trình biên dịch C ++ tạo các tên biểu tượng khác với trình biên dịch C. Vì vậy, nếu bạn đang cố gắng thực hiện cuộc gọi đến một hàm nằm trong tệp C, được biên dịch dưới dạng mã C, bạn cần thông báo cho trình biên dịch C ++ rằng các ký hiệu mà nó đang cố gắng giải quyết trông khác với mặc định; nếu không thì bước liên kết sẽ thất bại.


6

Cấu extern "C" {}trúc này hướng dẫn trình biên dịch không thực hiện xáo trộn các tên được khai báo trong dấu ngoặc nhọn. Thông thường, trình biên dịch C ++ "tăng cường" tên hàm để chúng mã hóa thông tin loại về các đối số và giá trị trả về; này được gọi là tên đọc sai . Cấu extern "C"trúc ngăn chặn việc xáo trộn.

Nó thường được sử dụng khi mã C ++ cần gọi thư viện ngôn ngữ C. Nó cũng có thể được sử dụng khi hiển thị hàm C ++ (ví dụ từ DLL) cho các máy khách C.


5

Điều này được sử dụng để giải quyết vấn đề xáo trộn tên. extern C có nghĩa là các chức năng nằm trong API kiểu C "phẳng".


0

Biên dịch một g++nhị phân được tạo để xem những gì đang xảy ra

Để hiểu tại sao externcần thiết, điều tốt nhất cần làm là hiểu chi tiết những gì đang diễn ra trong các tệp đối tượng với một ví dụ:

main.cpp

void f() {}
void g();

extern "C" {
    void ef() {}
    void eg();
}

/* Prevent g and eg from being optimized away. */
void h() { g(); eg(); }

Biên dịch với đầu ra ELF GCC 4.8 Linux :

g++ -c main.cpp

Dịch ngược bảng ký hiệu:

readelf -s main.o

Đầu ra chứa:

Num:    Value          Size Type    Bind   Vis      Ndx Name
  8: 0000000000000000     6 FUNC    GLOBAL DEFAULT    1 _Z1fv
  9: 0000000000000006     6 FUNC    GLOBAL DEFAULT    1 ef
 10: 000000000000000c    16 FUNC    GLOBAL DEFAULT    1 _Z1hv
 11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _Z1gv
 12: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND eg

Diễn dịch

Chúng ta thấy rằng:

  • efegđược lưu trữ trong các ký hiệu có cùng tên như trong mã

  • các biểu tượng khác được đọc sai Chúng ta hãy tháo gỡ chúng:

    $ c++filt _Z1fv
    f()
    $ c++filt _Z1hv
    h()
    $ c++filt _Z1gv
    g()

Kết luận: cả hai loại ký hiệu sau không được đọc sai:

  • xác định
  • khai báo nhưng không xác định ( Ndx = UND), được cung cấp tại liên kết hoặc thời gian chạy từ tệp đối tượng khác

Vì vậy, bạn sẽ cần extern "C"cả hai khi gọi:

  • C từ C ++: nói g++để mong đợi các biểu tượng không bị thay đổi được tạo bởigcc
  • C ++ từ C: yêu g++cầu tạo các ký hiệu không thay đổi gccđể sử dụng

Những thứ không hoạt động ở bên ngoài C

Rõ ràng là bất kỳ tính năng C ++ nào yêu cầu xáo trộn tên sẽ không hoạt động bên trong extern C:

extern "C" {
    // Overloading.
    // error: declaration of C function ‘void f(int)’ conflicts with
    void f();
    void f(int i);

    // Templates.
    // error: template with C linkage
    template <class C> void f(C i) { }
}

C runnable tối thiểu từ ví dụ C ++

Để hoàn thiện và cho các newbs ngoài kia, hãy xem thêm: Làm thế nào để sử dụng các tệp nguồn C trong một dự án C ++?

Gọi C từ C ++ khá dễ dàng: mỗi hàm C chỉ có một biểu tượng không bị sai lệch, do đó không cần phải làm thêm.

main.cpp

#include <cassert>

#include "c.h"

int main() {
    assert(f() == 1);
}

ch

#ifndef C_H
#define C_H

/* This ifdef allows the header to be used from both C and C++. */
#ifdef __cplusplus
extern "C" {
#endif
int f();
#ifdef __cplusplus
}
#endif

#endif

cc

#include "c.h"

int f(void) { return 1; }

Chạy:

g++ -c -o main.o -std=c++98 main.cpp
gcc -c -o c.o -std=c89 c.c
g++ -o main.out main.o c.o
./main.out

Không có extern "C"liên kết thất bại với:

main.cpp:6: undefined reference to `f()'

bởi vì g++hy vọng sẽ tìm thấy một máng xối f, mà gcckhông sản xuất.

Ví dụ trên GitHub .

C ++ tối thiểu có thể chạy được từ ví dụ C

Gọi C ++ từ khó hơn một chút: chúng ta phải tự tạo các phiên bản không bị xáo trộn của từng chức năng mà chúng ta muốn phơi bày.

Ở đây chúng tôi minh họa cách phơi bày quá tải chức năng C ++ cho C.

C chính

#include <assert.h>

#include "cpp.h"

int main(void) {
    assert(f_int(1) == 2);
    assert(f_float(1.0) == 3);
    return 0;
}

cpp.h

#ifndef CPP_H
#define CPP_H

#ifdef __cplusplus
// C cannot see these overloaded prototypes, or else it would get confused.
int f(int i);
int f(float i);
extern "C" {
#endif
int f_int(int i);
int f_float(float i);
#ifdef __cplusplus
}
#endif

#endif

cpp.cpp

#include "cpp.h"

int f(int i) {
    return i + 1;
}

int f(float i) {
    return i + 2;
}

int f_int(int i) {
    return f(i);
}

int f_float(float i) {
    return f(i);
}

Chạy:

gcc -c -o main.o -std=c89 -Wextra main.c
g++ -c -o cpp.o -std=c++98 cpp.cpp
g++ -o main.out main.o cpp.o
./main.out

Không có extern "C"nó thất bại với:

main.c:6: undefined reference to `f_int'
main.c:7: undefined reference to `f_float'

bởi vì g++tạo ra các biểu tượng mangled mà gcckhông thể tìm thấy.

Ví dụ trên GitHub .

Đã thử nghiệm trong Ubuntu 18.04.


1
Cảm ơn đã giải thích downvote, tất cả bây giờ có ý nghĩa.
Ciro Santilli 郝海东 冠状 病 事件
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.