pengembangan-web-mp-pd.com

Mengapa kita membutuhkan extern "C" {#include <foo.h>} di C ++?

Mengapa kita perlu menggunakan:

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

Khususnya:

  • Kapan kita harus menggunakannya?

  • Apa yang terjadi di tingkat kompiler/tautan yang mengharuskan kita untuk menggunakannya?

  • Bagaimana dalam hal kompilasi/penautan hal ini menyelesaikan masalah yang mengharuskan kita untuk menggunakannya?

133
Landon

C dan C++ serupa secara dangkal, tetapi masing-masing mengkompilasi ke dalam serangkaian kode yang sangat berbeda. Saat Anda menyertakan file header dengan kompiler C++, kompiler mengharapkan kode C++. Namun, jika itu adalah header C, maka kompiler mengharapkan data yang terkandung dalam file header dikompilasi ke format tertentu — C++ 'ABI', atau 'Application Binary Interface', sehingga penghubung tersedak. Ini lebih disukai untuk meneruskan data C++ ke fungsi yang mengharapkan data C.

(Untuk masuk ke dalam benar-benar seluk-beluk, ABI C++ umumnya 'mangles' nama fungsi/metode mereka, sehingga memanggil printf() tanpa menandai prototipe sebagai fungsi C, C++ sebenarnya akan menghasilkan panggilan kode _Zprintf, ditambah omong kosong tambahan di akhir.)

Jadi: gunakan extern "C" {...} saat menyertakan header c — sesederhana itu. Jika tidak, Anda akan memiliki ketidakcocokan dalam kode yang dikompilasi, dan tautan akan tersedak. Namun, untuk sebagian besar tajuk, Anda bahkan tidak akan memerlukan extern karena sebagian besar tajuk sistem C sudah memperhitungkan fakta bahwa mereka mungkin dimasukkan oleh kode C++ dan sudah extern kode mereka.

121
duane

extern "C" menentukan bagaimana simbol dalam file objek yang dihasilkan harus dinamai. Jika suatu fungsi dideklarasikan tanpa extern "C", nama simbol dalam file objek akan menggunakan mangling nama C++. Ini sebuah contoh.

Diberikan test.C seperti:

void foo() { }

Kompilasi dan daftar simbol dalam file objek memberi:

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

Fungsi foo sebenarnya disebut "_Z3foov". String ini berisi informasi tipe untuk tipe dan parameter pengembalian, antara lain. Jika Anda sebaliknya menulis test.C seperti ini:

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

Kemudian kompilasi dan lihat simbol:

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

Anda mendapatkan tautan C. Nama fungsi "foo" dalam file objek hanya "foo", dan tidak memiliki semua info tipe mewah yang berasal dari nama mangling.

Anda biasanya menyertakan header dalam extern "C" {} jika kode yang menyertainya dikompilasi dengan kompiler C tetapi Anda mencoba untuk memanggilnya dari C++. Ketika Anda melakukan ini, Anda memberi tahu kompiler bahwa semua deklarasi di header akan menggunakan tautan C. Saat Anda menautkan kode, file .o Anda akan berisi referensi ke "foo", bukan "_Z3fooblah", yang diharapkan cocok dengan apa pun yang ada di perpustakaan yang Anda tautkan.

Sebagian besar perpustakaan modern akan menempatkan penjaga di sekitar header tersebut sehingga simbol dideklarasikan dengan tautan yang tepat. misalnya di banyak header standar, Anda akan menemukan:

#ifdef __cplusplus
extern "C" {
#endif

... declarations ...

#ifdef __cplusplus
}
#endif

Ini memastikan bahwa ketika kode C++ menyertakan header, simbol dalam file objek Anda cocok dengan apa yang ada di perpustakaan C. Anda hanya harus meletakkan extern "C" {} di sekitar header C Anda jika sudah lama dan belum memiliki penjaga ini.

108
Todd Gamblin

Di C++, Anda bisa memiliki entitas berbeda yang berbagi nama. Sebagai contoh di sini adalah daftar fungsi yang semuanya bernama foo:

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

Untuk membedakan di antara mereka semua, kompiler C++ akan membuat nama-nama unik untuk masing-masing dalam proses yang disebut pengubahan nama atau dekorasi. C compiler tidak melakukan ini. Selain itu, setiap kompiler C++ dapat melakukan ini dengan cara yang berbeda.

extern "C" memberi tahu kompiler C++ untuk tidak melakukan pengubahan nama pada kode di dalam kurung kurawal. Ini memungkinkan Anda untuk memanggil fungsi C dari dalam C++.

21
Trent

Ini berkaitan dengan cara kompiler yang berbeda melakukan pengubahan nama. Kompiler C++ akan memotong nama simbol yang diekspor dari file header dengan cara yang sama sekali berbeda dari kompiler C, jadi ketika Anda mencoba untuk menautkan, Anda akan mendapatkan kesalahan tautan yang mengatakan ada simbol yang hilang.

Untuk mengatasi ini, kami memberi tahu kompiler C++ untuk dijalankan dalam mode "C", sehingga ia melakukan pengubahan nama dengan cara yang sama seperti yang dilakukan oleh kompiler C. Setelah melakukannya, kesalahan tautan diperbaiki.

14
1800 INFORMATION

Kapan kita harus menggunakannya?

Saat Anda menautkan perpustakaan C ke file objek C++

Apa yang terjadi di tingkat kompiler/tautan yang mengharuskan kita untuk menggunakannya?

C dan C++ menggunakan skema berbeda untuk penamaan simbol. Ini memberi tahu tautan untuk menggunakan skema C saat menautkan di perpustakaan yang diberikan.

Bagaimana dalam hal kompilasi/penautan hal ini menyelesaikan masalah yang mengharuskan kita untuk menggunakannya?

Menggunakan skema penamaan C memungkinkan Anda untuk referensi simbol gaya-C. Kalau tidak, penghubung akan mencoba simbol gaya C++ yang tidak akan berfungsi.

11
Tony M

C dan C++ memiliki aturan berbeda tentang nama simbol. Simbol adalah bagaimana penghubung mengetahui bahwa panggilan untuk berfungsi "openBankAccount" dalam satu file objek yang dihasilkan oleh kompiler adalah referensi ke fungsi yang Anda sebut "openBankAccount" di file objek lain yang dihasilkan dari file sumber berbeda dengan cara yang sama (atau kompatibel) penyusun. Ini memungkinkan Anda membuat program dari lebih dari satu file sumber, yang melegakan ketika mengerjakan proyek besar.

Dalam C aturannya sangat sederhana, simbol semua dalam satu ruang nama saja. Jadi integer "socks" disimpan sebagai "socks" dan fungsi count_socks disimpan sebagai "count_socks".

Linker dibuat untuk C dan bahasa lain seperti C dengan aturan penamaan simbol sederhana ini. Jadi simbol dalam linker hanyalah string sederhana.

Namun dalam bahasa C++ Anda dapat menggunakan ruang nama, polimorfisme, dan berbagai hal lain yang bertentangan dengan aturan sederhana tersebut. Keenam fungsi polimorfik Anda yang disebut "tambah" harus memiliki simbol yang berbeda, atau yang salah akan digunakan oleh file objek lainnya. Ini dilakukan dengan "mangling" (itu istilah teknis) nama-nama simbol.

Saat menautkan kode C++ ke pustaka C atau kode, Anda memerlukan extern "C" apa pun yang ditulis dalam C, seperti file header untuk pustaka C, untuk memberi tahu kompiler C++ Anda bahwa nama-nama simbol ini tidak boleh dicacah, sedangkan sisanya dari kode C++ Anda tentu saja harus rusak atau tidak akan berfungsi.

10
tialaramex

Kompiler C++ membuat nama simbol berbeda dari kompiler C. Jadi, jika Anda mencoba untuk membuat panggilan ke fungsi yang berada di file C, dikompilasi sebagai kode C, Anda perlu memberi tahu kompiler C++ bahwa nama simbol yang sedang mencoba untuk diselesaikannya terlihat berbeda dari yang standarnya; jika tidak, langkah tautan akan gagal.

7
mbyrne215

Anda harus menggunakan extern "C" kapan saja Anda menyertakan header yang mendefinisikan fungsi yang berada dalam file yang dikompilasi oleh kompiler C, yang digunakan dalam file C++. (Banyak pustaka C standar mungkin menyertakan pemeriksaan ini di tajuknya agar lebih mudah bagi pengembang)

Misalnya, jika Anda memiliki proyek dengan 3 file, util.c, util.h, dan main.cpp dan kedua file .c dan .cpp dikompilasi dengan kompiler C++ (g ++, cc, dll) maka itu bukan t benar-benar diperlukan, dan bahkan dapat menyebabkan kesalahan tautan. Jika proses build Anda menggunakan compiler C reguler untuk util.c, maka Anda perlu menggunakan extern "C" saat menyertakan util.h.

Apa yang terjadi adalah bahwa C++ mengkodekan parameter fungsi dalam namanya. Ini adalah cara kerja overloading fungsi. Semua yang cenderung terjadi pada fungsi C adalah penambahan garis bawah ("_") ke awal nama. Tanpa menggunakan extern "C" linker akan mencari fungsi bernama DoSomething @@ int @ float () ketika nama sebenarnya fungsi tersebut adalah _DoSomething () atau hanya DoSomething ().

Menggunakan extern "C" memecahkan masalah di atas dengan memberi tahu kompiler C++ bahwa ia harus mencari fungsi yang mengikuti konvensi penamaan C alih-alih C++.

7
HitScan

extern "C" {} construct menginstruksikan kompiler untuk tidak melakukan mangling pada nama yang dideklarasikan dalam kurung kurawal. Biasanya, kompiler C++ "meningkatkan" nama fungsi sehingga mereka menyandikan informasi jenis tentang argumen dan nilai kembali; ini disebut nama hancur. extern "C" construct mencegah mangling.

Ini biasanya digunakan ketika kode C++ perlu memanggil perpustakaan bahasa-C. Ini juga dapat digunakan ketika mengekspos fungsi C++ (dari DLL, misalnya) ke klien C.

6
Paul Lalonde

Ini digunakan untuk menyelesaikan masalah pengelolaan nama. extern C berarti bahwa fungsinya ada dalam API gaya C "datar".

5
Eric Z Beard

Dekompilasi a g++ dihasilkan biner untuk melihat apa yang sedang terjadi

Saya bergerak dalam jawaban ini dari: Apa efek extern "C" di C++? karena pertanyaan itu dianggap duplikat dari yang ini.

main.cpp

void f() {}
void g();

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

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

Kompilasi dengan GCC 4.8 Linux ELF output:

g++ -c main.cpp

Dekompilasi tabel simbol:

readelf -s main.o

Output berisi:

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

Penafsiran

Kami melihat bahwa:

  • ef dan eg disimpan dalam simbol dengan nama yang sama seperti dalam kode

  • simbol-simbol lainnya hancur. Mari kita pisahkan mereka:

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

Kesimpulan: kedua jenis simbol berikut adalah tidak hancur:

  • didefinisikan
  • dideklarasikan tetapi tidak terdefinisi (Ndx = UND), disediakan pada tautan atau waktu berjalan dari file objek lain

Jadi Anda perlu extern "C" keduanya saat menelepon:

  • C dari C++: tell g++ untuk mengharapkan simbol tidak cacat yang dihasilkan oleh gcc
  • C++ dari C: tell g++ untuk menghasilkan simbol tanpa cacat untuk gcc untuk digunakan

Hal-hal yang tidak berfungsi di extern C

Menjadi jelas bahwa setiap fitur C++ yang membutuhkan nama mangling tidak akan berfungsi di dalam 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) { }
}

Minimal runnable C dari contoh C++

Demi kelengkapan dan untuk newb di luar sana, lihat juga: Bagaimana cara menggunakan file sumber C dalam proyek C++?

Memanggil C dari C++ cukup mudah: setiap fungsi C hanya memiliki satu simbol yang tidak cacat, sehingga tidak diperlukan kerja ekstra.

main.cpp

#include <cassert>

#include "c.h"

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

c

#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

#include "c.h"

int f(void) { return 1; }

Menjalankan:

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

Tanpa extern "C" tautan gagal dengan:

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

karena g++ berharap dapat menemukan f yang hancur, yang gcc tidak hasilkan.

Contoh pada GitHub .

Minimal runnable C++ dari contoh C

Memanggil C++ dari sedikit lebih sulit: kita harus secara manual membuat versi yang tidak cacat dari setiap fungsi yang ingin kita paparkan.

Di sini kita menggambarkan bagaimana mengekspos kelebihan fungsi C++ ke C.

main.c

#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);
}

Menjalankan:

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

Tanpa extern "C" gagal dengan:

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

karena g++ menghasilkan simbol-simbol yang rusak yang gcc tidak dapat temukan.

Contoh pada GitHub .

Diuji di Ubuntu 18.04.