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


136

Mengapa kita perlu menggunakan:

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

Secara khusus:

  • Kapan kita harus menggunakannya?

  • Apa yang terjadi pada level compiler / linker yang mengharuskan kita untuk menggunakannya?

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

Jawaban:


123

C dan C ++ mirip secara dangkal, tetapi masing-masing dikompilasi menjadi sekumpulan kode yang sangat berbeda. Saat Anda menyertakan file header dengan kompilator C ++, kompilator mengharapkan kode C ++. Namun, jika ini adalah header C, maka compiler mengharapkan data yang terdapat dalam file header akan dikompilasi ke format tertentu — C ++ 'ABI', atau 'Application Binary Interface', sehingga linker tersendat. Ini lebih disukai daripada meneruskan data C ++ ke fungsi yang mengharapkan data C.

(Untuk masuk ke seluk-beluknya, ABI C ++ umumnya 'mengacaukan' nama fungsi / metode mereka, jadi memanggil printf()tanpa menandai prototipe sebagai fungsi C, C ++ sebenarnya akan menghasilkan pemanggilan kode _Zprintf, ditambah omong kosong tambahan di akhir. )

Jadi: gunakan extern "C" {...}saat menyertakan header ac — sesederhana itu. Jika tidak, Anda akan mengalami ketidakcocokan dalam kode yang dikompilasi, dan linker akan tersedak. Namun, untuk sebagian besar header, Anda bahkan tidak memerlukannya externkarena sebagian besar header sistem C sudah memperhitungkan fakta bahwa mereka mungkin disertakan oleh kode C ++ dan sudah externkodenya.


1
Bisakah Anda menjelaskan lebih lanjut tentang "sebagian besar header sistem C sudah memperhitungkan fakta bahwa mereka mungkin disertakan oleh kode C ++ dan sudah mengeluarkan kode mereka." ?
Bulat M.

7
@Bulat. Mereka berisi sesuatu seperti ini: #ifdef __cplusplus extern "C" { #endif Jadi ketika dimasukkan dari file C ++ mereka masih diperlakukan sebagai header C.
Calmarius

112

extern "C" menentukan bagaimana simbol dalam file objek yang dihasilkan harus diberi nama. Jika suatu fungsi dideklarasikan tanpa extern "C", maka nama simbol pada file objek akan menggunakan nama C ++ mangling. Berikut contohnya.

Diberikan test. C seperti ini:

void foo() { }

Mengompilasi dan membuat daftar simbol dalam file objek memberikan:

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

Fungsi foo sebenarnya disebut "_Z3foov". String ini berisi antara lain informasi tipe untuk tipe kembalian dan parameter. Jika Anda malah 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 hubungan C. Nama fungsi "foo" di file objek hanyalah "foo", dan tidak memiliki semua info tipe mewah yang berasal dari nama mangling.

Anda biasanya menyertakan tajuk dalam extern "C" {} jika kode yang menyertainya dikompilasi dengan compiler C tetapi Anda mencoba memanggilnya dari C ++. Saat 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 pustaka yang Anda tautkan.

Sebagian besar perpustakaan modern akan menempatkan pelindung di sekitar tajuk tersebut sehingga simbol dinyatakan dengan tautan yang tepat. misalnya di banyak tajuk standar, Anda akan menemukan:

#ifdef __cplusplus
extern "C" {
#endif

... declarations ...

#ifdef __cplusplus
}
#endif

Ini memastikan bahwa ketika kode C ++ menyertakan header, simbol di file objek Anda cocok dengan yang ada di pustaka C. Anda hanya perlu meletakkan "C" {} eksternal di sekitar header C Anda jika sudah tua dan belum memiliki pelindung ini.


22

Di C ++, Anda dapat memiliki entitas berbeda yang memiliki nama yang sama. Misalnya di sini adalah daftar fungsi yang semuanya bernama foo :

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

Untuk membedakan semuanya, compiler C ++ akan membuat nama unik untuk masing-masing dalam proses yang disebut name-mangling atau decorating. Kompiler C tidak melakukan ini. Selain itu, setiap kompiler C ++ dapat melakukan ini dengan cara yang berbeda.

extern "C" memberitahu compiler C ++ untuk tidak melakukan pengubahan nama pada kode di dalam tanda kurung. Ini memungkinkan Anda memanggil fungsi C dari dalam C ++.


14

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

Untuk mengatasi ini, kami memberi tahu compiler C ++ untuk dijalankan dalam mode "C", sehingga compiler tersebut menjalankan name mangling dengan cara yang sama seperti compiler C. Setelah melakukannya, kesalahan penaut diperbaiki.


11

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

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

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

Tapi di C ++ bahasanya memungkinkan Anda memiliki namespace, dan polimorfisme dan berbagai hal lain yang bertentangan dengan aturan sederhana tersebut. Keenam fungsi polimorfik Anda yang disebut "tambah" harus memiliki simbol yang berbeda, atau simbol yang salah akan digunakan oleh file objek lain. Ini dilakukan dengan "merusak" (itu istilah teknis) nama-nama simbol.

Saat menautkan kode C ++ ke pustaka atau kode C, Anda memerlukan "C" eksternal apa pun yang ditulis dalam C, seperti file header untuk pustaka C, untuk memberi tahu compiler C ++ Anda bahwa nama-nama simbol ini tidak boleh dihancurkan, sementara yang lainnya kode C ++ Anda tentu saja harus rusak atau tidak akan berfungsi.


11

Kapan kita harus menggunakannya?

Saat Anda menautkan pustaka C ke dalam file objek C ++

Apa yang terjadi pada level compiler / linker yang mengharuskan kita untuk menggunakannya?

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

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

Menggunakan skema penamaan C memungkinkan Anda mereferensikan simbol gaya C. Jika tidak, linker akan mencoba simbol gaya C ++ yang tidak akan berfungsi.


7

Anda harus menggunakan "C" eksternal kapan pun Anda menyertakan fungsi pendefinisian header yang berada dalam file yang dikompilasi oleh kompilator C, yang digunakan dalam file C ++. (Banyak pustaka C standar mungkin menyertakan pemeriksaan ini di header mereka untuk membuatnya lebih sederhana 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 compiler C ++ (g ++, cc, dll) maka isn ' t benar-benar diperlukan, dan bahkan dapat menyebabkan kesalahan tautan. Jika proses build Anda menggunakan compiler C biasa untuk util.c, Anda perlu menggunakan "C" extern saat menyertakan util.h.

Apa yang terjadi adalah C ++ mengkodekan parameter fungsi dalam namanya. Beginilah cara kerja kelebihan beban fungsi. Semua yang cenderung terjadi pada fungsi C adalah penambahan garis bawah ("_") di awal nama. Tanpa menggunakan "C" eksternal, linker akan mencari fungsi bernama DoSomething @@ int @ float () saat nama sebenarnya dari fungsi tersebut adalah _DoSomething () atau hanya DoSomething ().

Menggunakan extern "C" memecahkan masalah di atas dengan memberi tahu compiler C ++ bahwa ia harus mencari fungsi yang mengikuti konvensi penamaan C, bukan C ++.


7

Kompilator C ++ membuat nama simbol yang berbeda dari kompilator C. Jadi, jika Anda mencoba membuat panggilan ke fungsi yang berada dalam file C, yang dikompilasi sebagai kode C, Anda perlu memberi tahu compiler C ++ bahwa nama simbol yang coba diatasi terlihat berbeda dari yang semula; jika tidak, langkah tautan akan gagal.


6

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

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


5

Ini digunakan untuk menyelesaikan masalah kerusakan nama. extern C berarti bahwa fungsinya berada dalam API gaya-C "datar".


0

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

Untuk memahami mengapa externperlu, hal terbaik yang harus dilakukan adalah memahami apa yang terjadi secara detail di file objek dengan contoh:

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 keluaran ELF Linux GCC 4.8 :

g++ -c main.cpp

Dekompilasi tabel simbol:

readelf -s main.o

Outputnya 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:

  • efdan egdisimpan dalam simbol dengan nama yang sama seperti di kode

  • simbol-simbol lainnya hancur. Mari kita uraikan:

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

Kesimpulan: kedua tipe simbol berikut tidak rusak:

  • ditentukan
  • dideklarasikan tetapi tidak ditentukan ( Ndx = UND), untuk diberikan pada tautan atau waktu proses dari file objek lain

Jadi, Anda memerlukan extern "C"keduanya saat menelepon:

  • C dari C ++: kirim g++untuk mengharapkan simbol tak berubah yang diproduksi olehgcc
  • C ++ dari C: kirim g++untuk menghasilkan simbol tak beraturan untuk gccdigunakan

Hal-hal yang tidak bekerja di luar C

Jelas bahwa fitur C ++ apa pun yang membutuhkan name 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 newbs di luar sana, lihat juga: Bagaimana menggunakan file sumber C dalam proyek C ++?

Memanggil C dari C ++ cukup mudah: setiap fungsi C hanya memiliki satu kemungkinan simbol yang tidak rusak, jadi tidak diperlukan pekerjaan tambahan.

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

Lari:

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 menemukan yang hancur f, yang gcctidak menghasilkan.

Contoh di GitHub .

Minimal C ++ yang dapat dijalankan dari contoh C.

Memanggil C ++ dari agak lebih sulit: kita harus membuat versi non-rusak secara manual dari setiap fungsi yang ingin kita tampilkan.

Di sini kami mengilustrasikan cara mengekspos overload 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);
}

Lari:

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

Tanpanya extern "C"gagal dengan:

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

karena g++menghasilkan simbol rusak yang gcctidak dapat ditemukan.

Contoh di GitHub .

Diuji di Ubuntu 18.04.


1
Terima kasih telah menjelaskan downvote tersebut, semuanya masuk akal sekarang.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.