it-swarm-vi.tech

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

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

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

Cụ thể :

  • 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 để giải quyết các vấn đề đòi hỏi chúng ta sử dụng nó?

133
Landon

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 kiểu lạ mắt 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 đúng. 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.

108
Todd Gamblin

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ừ bên trong C++.

21
Trent

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 một 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 các ký hiệu.

Để 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.

14
1800 INFORMATION

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 để giải quyết các vấn đề đòi hỏi chúng ta 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.

11
Tony M

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 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.

10
tialaramex

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 nói với 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.

7
mbyrne215

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ả ing.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
HitScan

Cấu trúc extern "C" {} 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ề; cái này được gọi là tên đọc sai. Cấu trúc extern "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.

6
Paul Lalonde

Đ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".

5
Eric Z Beard

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

Tôi đang chuyển câu trả lời này từ: Hiệu ứng của "C" bên ngoài trong C++ là gì? vì câu hỏi đó được coi là một bản sao của câu hỏi này.

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 GCC 4.8 Linux ELF đầu ra:

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

Giải thích

Chúng ta thấy rằng:

  • efeg được lưu trữ trong các biểu tượng 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 là không được đọc:

  • 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ả khi gọi:

  • C từ C++: nói với g++ để mong đợi các biểu tượng không bị thay đổi được tạo bởi gcc
  • C++ từ C: nói với g++ để tạo các biểu tượng không bị 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 có thể chạy 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: Cách sử dụng tệp nguồn C trong 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 có thể, do đó không cần phải làm thêm.

main.cpp

#include <cassert>

#include "c.h"

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

c.h

#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

c.c

#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 không thành công với:

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

bởi vì g++ mong muốn tìm thấy một f được đọc sai, mà gcc không tạo ra.

Ví dụ trên GitHub .

C++ có thể chạy tối thiểu 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" thì 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 thu thập mà gcc không thể tìm thấy.

Ví dụ trên GitHub .

Đã thử nghiệm trong Ubuntu 18.04.