C、C++混合調用


在項目中,C和C++代碼相互調用是很常見的,但在調用時,究竟應該如何編寫代碼和頭文件,有一些講究,不然就可能出現編譯時鏈接不通過的問題,典型的編譯錯誤日志是:

undefined reference to `xxx'

要編寫出C或C++都能正常調用的代碼,需要明白編譯器在編譯時,究竟做了什么。下面就以幾段簡單的代碼為例,來說明一下GCC系列編譯器在編譯C、C++代碼時,分別做了什么,我們該如何編寫自己的函數庫以供C和C++代碼調用。

本文驗證的環境是:Ubuntu Server 18.04 LTS,gcc/g++ 7.3.0,nm 2.30

C函數庫如何被C和C++代碼調用

sum.c是一個使用C代碼編寫的對整數求和函數,代碼非常簡單:

1 #include "sum.h"
2 
3 int sum(int a, int b)
4 {
5     return a + b;
6 }

在不考慮C++的調用時,頭文件sum.h通常會按照如下寫法:

1 #ifndef __SUM_H__
2 #define __SUM_H__
3 
4 int sum(int a, int b);
5 
6 #endif /* __SUM_H__ */

我們編寫下面的main.cpp代碼來調用它:

 1 #include <iostream>
 2 
 3 #include "sum.h"
 4 
 5 int main(void)
 6 {
 7     std::cout << sum(1, 1) << std::endl;
 8     
 9     return 0;
10 }

編譯並運行一下看看:

$ g++ -o main main.cpp sum.c
$ ./main 
2

從結果來看,沒有任何問題,程序正常編譯通過和執行。

但是,如果sum.c是要做成一個庫文件,可供C或C++代碼調用時,又該如何呢?以靜態庫為例,sum.c是先編譯成.o文件,再和其它的同類文件一起打包到.a中,由於只有一個文件,這里就不把它打包到.a文件了,僅把它生成一個.o文件作為函數庫看一下:

$ gcc -c sum.c
$ g++ -o main main.cpp sum.o
/tmp/ccNnKecX.o: In function `main':
main.cpp:(.text+0xf): undefined reference to `sum(int, int)'
collect2: error: ld returned 1 exit status

從上述輸出可以看到,鏈接是不能通過的。編譯器告訴我們,這個引用沒有定義。但我們都知道,sum函數是真實存在的。出現這種問題的原因,在於C++是支持面向對象的,函數可以重載,為了支持重載,編譯時生成的.o文件中,函數名稱不會像源文件那樣。用nm列出目標文件中的符號,就可以看到真實的情況:

$ nm sum.o
0000000000000000 T sum
$ g++ -c main.cpp
$ nm main.o 
                 U __cxa_atexit
                 U __dso_handle
                 U _GLOBAL_OFFSET_TABLE_
0000000000000086 t _GLOBAL__sub_I_main
0000000000000000 T main
                 U _Z3sumii
000000000000003d t _Z41__static_initialization_and_destruction_0ii
                 U _ZNSolsEi
                 U _ZNSolsEPFRSoS_E
                 U _ZNSt8ios_base4InitC1Ev
                 U _ZNSt8ios_base4InitD1Ev
                 U _ZSt4cout
                 U _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
0000000000000000 r _ZStL19piecewise_construct
0000000000000000 b _ZStL8__ioinit

從上面的輸出中,我們可以看到,用gcc命令編譯生成的sum.o,包含有一個符號sum。但是用g++編譯出來的main.o,它里面引用到sum函數時,用的名字其實是_Z3sumii,這個就是g++編譯器對C++代碼做的處理。由於sum.o中並沒有_Z3sumii函數,鏈接當然要失敗。

那么,為什么一開始的命令g++ -o main main.cpp sum.c能正常生成可執行文件呢,因為在這條命令執行時,對sum.c也當作是C++代碼來處理的,我們可以對sum.c調用g++命令來驗證一下:

$ g++ -c sum.c
$ nm sum.o
0000000000000000 T _Z3sumii

可見,對於C源文件,調用g++命令時,是把C代碼視作C++來處理的。對於C++文件去調用gcc命令,又當如何呢?有興趣的可以自行嘗試一下,這里就不再展開了,僅給出一個規則:

  • 對於C代碼,使用gcc命令去編譯,讓編譯器按C代碼的規則來處理,這樣便於其它C代碼調用,如果按照C++代碼處理了,雖然C++調用是方便了,但其它C代碼調用就麻煩了
  • 對於C++代碼,使用g++命令編譯,按C++的規則處理

知道了編譯器會做什么后,我們看一下如何讓C++調用C函數庫。查看系統庫的標准頭文件,我們會看到很多這樣的代碼:

1 #ifdef __cplusplus
2 extern "C" {
3 #endif
4 ...
5 #ifdef __cplusplus
6 }
7 #endif

其實,extern "C"就是告訴編譯器,使用extern "C"修飾的代碼是用C的目標文件格式來編譯的,這樣符號名稱就是按照C的命令規則去查找和生成。extern "C"有兩種形式,一種是修飾單行語句的,例如:

extern "C" int sum(int a, int b);

這種形式可以單獨寫在某個源代碼文件中,但不常見。另一種是對整塊代碼做修飾,例如:

extern "C" {
    int sum(int a, int b);
    ...
}

為什么系統頭文件要加#ifdef __cplusplus呢,因為C編譯器不認識extern "C",如果插入這個,C編譯器就要報錯,所以,只應該對C++代碼這么定義,由於在編譯C++代碼時,編譯器會自動定義宏__cplusplus,因此,就可以利用這個宏來做條件編譯。現在,我們把sum.h改造成:

 1 #ifndef __SUM_H__
 2 #define __SUM_H__
 3 
 4 #ifdef __cplusplus
 5 extern "C" {
 6 #endif
 7 
 8     int sum(int a, int b);
 9 
10 #ifdef __cplusplus
11 }
12 #endif
13 
14 #endif /* __SUM_H__ */

然后,重新使用原來報錯的命令試試:

$ gcc -c sum.c
$ g++ -o main main.cpp sum.o
$ ./main 
2

這樣就成功了。總結下來,就是C頭文件中,加上extern "C" {}這樣的聲明,並對C代碼仍是按C語言的方式編譯,這樣做,C或C++代碼調用都沒有問題。
extern "C"還有另一個用法,有些已經存在的C函數庫及其頭文件,並沒有做這樣的處理,那C++代碼又當如何引用呢?答案是在C++代碼中,按照如下方式編寫代碼:

extern {
#include "C_header.h"
}

C代碼如何調用C++的函數

這里,仍舊使用相同的示例來說明,只是反過來,sum.cpp如下:

#include "sum.h"

int sum(int a, int b)
{
    return a + b;
}

sum.h如下:

#ifndef __SUM_H__
#define __SUM_H__

#ifdef __cplusplus
extern "C" {
#endif

    int sum(int a, int b);

#ifdef __cplusplus
}
#endif

#endif /* __SUM_H__ */

main.c如下:

 1 #include <stdio.h>
 2 
 3 #include "sum.h"
 4 
 5 int main(void)
 6 {
 7     printf("%d\n", sum(1, 1));
 8 
 9     return 0;
10 }

編譯運行結果如下:

$ g++ -c sum.cpp
$ nm sum.o
0000000000000000 T sum
$ gcc -o main main.c sum.o
$ ./main
2

從這里仍可以看到,extern "C"在起作用,如果把extern "C"的部分去掉,再編譯時,就會看到鏈接不過的提示:

$ g++ -c sum.cpp
$ nm sum.o
0000000000000000 T _Z3sumii
$ gcc -o main main.c sum.o
/tmp/ccJkz0dn.o: In function `main':
main.c:(.text+0xf): undefined reference to `sum'
collect2: error: ld returned 1 exit status

解決此問題的最簡單辦法,就是對main.c調用g++命令,由於C++對C的兼容性,這樣做完全不成問題:

$ g++ -o main main.c sum.o
$ ./main
2

當然,這只適用於非成員函數,如果想在C代碼中調用成員函數,由於涉及到類,需要額外的包裝,這里,我們把這個函數放在一個類Math里,作為一個靜態成員函數來演示,非靜態成員函數的情況更麻煩,建議直接使用C++代碼來處理后,再包裝成靜態成員的方式做轉換,示例的math.h:

 1 class Math {
 2 public:
 3     static int sum(int a, int b);
 4 };

示例的math.cpp:

1 #include "math.h"
2 
3 int Math::sum(int a, int b)
4 {
5     return a + b;
6 }

編寫的包裝器頭文件math_wrapper.h:

 1 #ifndef __MATH_WRAPPER_H__
 2 #define __MATH_WRAPPER_H__
 3 
 4 #ifdef __cplusplus
 5 extern "C" {
 6 #endif
 7 
 8     int sum(int a, int b);
 9 
10 #ifdef __cplusplus
11 }
12 #endif
13 
14 #endif /* __MATH_WRAPPER_H__ */

包裝器代碼math_wrapper.cpp:

1 #include "math.h"
2 #include "math_wrapper.h"
3 
4 int sum(int a, int b)
5 {
6     return Math::sum(a, b);
7 }

編譯上述代碼並查看符號:

$ g++ -c math.cpp
$ nm math.o
$ g++ -c math_wrapper.cpp
$ nm math_wrapper.o
                 U _GLOBAL_OFFSET_TABLE_
0000000000000000 T sum
                 U _ZN4Math3sumEii

我們可以看到,math_wrapper.o把符號成功地轉換了,寫個main.c調用一下:

#include <stdio.h>

#include "math_wrapper.h"

int main(void)
{
    printf("%d\n", sum(1, 1));

    return 0;
}

編譯運行毫無問題:

$ gcc -o main main.c math_wrapper.o math.o
$ ./main
2

基本思路就是:對已經按照C++的方式生成的C++庫,用C++寫個包裝器來引用它的函數,但命名規則使用C的方式處理,這樣就把函數轉換成C代碼可以調用的了,如果函數重載了,就寫多個函數來轉換。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM