关键字 extern C 详解
关键字 extern C 详解
2024年5月14日
摘要
在 C/C++ 的开发中,会出现两种语言互相调用的情况,此时会用到 extern "C"
这样的用法,本文将对其进行详细的讲解。
关于 GCC
在编译 C/C++ 程序时,最常用的工具链是 GCC(GNU Compiler Collection),这是一个编译器集合,可以编译 C,C++,Fortran,Pascal,Objective-C 等语言。而小写的 gcc 则是 GNU C Compiler,和大写的不是一个东西,只是GCC 中的一个组件,g++ 则是 GNU C++ Compiler。
当我们在命令行中调用 gcc 时,调用的其实是大写的 GCC,他会根据输入源代码的后缀名是 .c
还是 .cpp
来选择按照 C 还是 C++ 的规则进行编译。而调用 g++ 时,则一律按照 C++ 的规则进行编译。
当然,GCC 也提供了命令行选项用于指定规则,例如 -xc
是指定按照 C 的规则编译,-xc++
则是按照 C++ 的规则进行编译。
C++ 调用 C
假如存在三个待编译的文件:
cfun.h
:
##### cfun.h
#ifndef _C_FUN_H_
#define _C_FUN_H_
void cfun(int i);
#endif
cfun.c
:
##### cfun.c
#include <stdio.h>
#include "cfun.h"
void cfun(int i)
{
printf("cfun(%d)\n", i);
}
main.cpp
:
##### main.cpp
#include "cfun.h"
int main()
{
cfun(2);
return 0;
}
我们调用:
gcc cfun.c main.cpp
则会出现以下报错:
Undefined symbols for architecture arm64:
"cfun(int)", referenced from:
_main in main-1dbef1.o
NOTE: found '_cfun' in cfun-4451cb.o, declaration possibly missing 'extern "C"'
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
其原因就是 .c
的源文件和 .cpp
的源文件中,同一个函数被采用不同的规则生成了不同的符号,在链接阶段无法被正常匹配链接所导致的。为了验证这个原因,我们可以通过 gcc -S
来生成汇编文件观察编译生成的符号名,但由于汇编代码较长,不便于放入博客,因此我们生成更简短的符号表来观察:
gcc -c cfun.c main.cpp
此时生成了 cfun.o
和 main.o
两个对象文件,我们使用 objdump -t
命令来观察符号表:
objdump -t cfun.o
cfun.o: file format mach-o arm64
SYMBOL TABLE:
0000000000000000 l F __TEXT,__text ltmp0
0000000000000038 l O __TEXT,__cstring l_.str
0000000000000038 l O __TEXT,__cstring ltmp1
0000000000000048 l O __LD,__compact_unwind ltmp2
0000000000000000 g F __TEXT,__text _cfun
0000000000000000 *UND* _printf
objdump -t main.o
main.o: file format mach-o arm64
SYMBOL TABLE:
0000000000000000 l F __TEXT,__text ltmp0
0000000000000030 l O __LD,__compact_unwind ltmp1
0000000000000000 g F __TEXT,__text _main
0000000000000000 *UND* __Z4cfuni
可以看到,C 源文件 cfun.o
对 void cfun(int i);
按照 C 规则生成的符号是 _cfun
,而 C++ 源文件 main.cpp
对 void cfun(int i);
按照 C++ 规则生成的符号是 __Z4cfuni
(根据 #include
进来的函数声明里产生),二者无法对应。
因此,有两个解决办法:修改引用文件和修改被引用文件。
修改引用文件
我们可以在 C++ 源文件 main.cpp
中 #include
的地方添加 extern "C"
关键字来解决:
#ifdef __cplusplus
extern "C" {
#endif
#include "cfun.h"
#ifdef __cplusplus
}
#endif
int main()
{
cfun(2);
return 0;
}
其中 __cplusplus
关键字将在编译器启用 C++ 编译规则时自动定义,此时,extern "C"
关键字将生效,包含在 cfun.h
中的函数声明将自动被此关键字包裹,编译器将在编译 main.cpp
这个 C++ 源文件时,把 extern "C"
内部声明的函数和变量按照 C 的规则生成符号,此时编译 main.cpp
,objdump
的结果如下:
gcc -c main.cpp
objdump -t main.o
main.o: file format mach-o arm64
SYMBOL TABLE:
0000000000000000 l F __TEXT,__text ltmp0
0000000000000030 l O __LD,__compact_unwind ltmp1
0000000000000000 g F __TEXT,__text _main
0000000000000000 *UND* _cfun
此时,则可以完美与 cfun.c
生成的符号对应,再次将两个源文件一起编译也可产生正确结果。
修改被引用文件
另一个更通用也是更推荐的办法则是直接修改头文件,直接将头文件除了防止重复包含的宏以外的部分全部用 extern "C"
包裹,即可让此头文件在 C 和 C++ 中都可直接被引用,且自适应的生成符号。只需按照如下方法修改 cfun.h
:
#ifndef _C_FUN_H_
#define _C_FUN_H_
#ifdef __cplusplus
extern "C" {
#endif
void cfun(int i);
#ifdef __cplusplus
}
#endif
#endif
即可正常按照:
gcc cfun.c main.cpp
完成编译且正常输出。
C 调用 C++
假如存在三个待编译的文件:
cppfun.hpp
(也可以是 .h
,但使用 .hpp
能更清晰表明这是个 C++ 头文件):
##### cppfun.hpp
#ifndef __CPP_FUN_HPP_
#define __CPP_FUN_HPP_
void cppfun(int i);
#endif
cfun.cpp
:
##### cppfun.cpp
#include <stdio.h>
#include "cppfun.hpp"
void cppfun(int i)
{
printf("cppfun: %d\n", i);
}
main.c
:
##### main.c
#include "cppfun.hpp"
int main()
{
cppfun(2);
return 0;
}
我们调用:
gcc cppfun.cpp main.c
则会出现以下报错:
Undefined symbols for architecture arm64:
"_cppfun", referenced from:
_main in main-a23963.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
其原因就是 .cpp
的源文件和 .c
的源文件中,同一个函数被采用不同的规则生成了不同的符号,在链接阶段无法被正常匹配链接所导致的。为了验证这个原因,我们可以通过 gcc -S
来生成汇编文件观察编译生成的符号名,但由于汇编代码较长,不便于放入博客,因此我们生成更简短的符号表来观察:
gcc -c cppfun.cpp main.c
此时生成了 cppfun.o
和 main.o
两个对象文件,我们使用 objdump -t
命令来观察符号表:
objdump -t cppfun.o
cppfun.o: file format mach-o arm64
SYMBOL TABLE:
0000000000000000 l F __TEXT,__text ltmp0
0000000000000038 l O __TEXT,__cstring l_.str
0000000000000038 l O __TEXT,__cstring ltmp1
0000000000000048 l O __LD,__compact_unwind ltmp2
0000000000000000 g F __TEXT,__text __Z6cppfuni
0000000000000000 *UND* _printf
objdump -t main.o
main.o: file format mach-o arm64
SYMBOL TABLE:
0000000000000000 l F __TEXT,__text ltmp0
0000000000000030 l O __LD,__compact_unwind ltmp1
0000000000000000 g F __TEXT,__text _main
0000000000000000 *UND* _cppfun
可以看到,C++ 源文件 cppfun.o
对 void cppfun(int i);
按照 C++ 规则生成的符号是 __Z6cppfuni
,而 C 源文件 main.c
对 void cfun(int i);
按照 C 规则生成的符号是 _cppfun
(根据 #include
进来的函数声明里产生),二者无法对应。
因此,有两个解决办法:修改引用文件和修改被引用文件。
修改引用文件
我们可以在 C++ 源文件 cppfun.cpp
中 #include
的地方添加 extern "C"
关键字来解决:
#include <stdio.h>
#ifdef __cplusplus
extern "C" {
#endif
#include "cppfun.hpp"
#ifdef __cplusplus
}
#endif
void cppfun(int i)
{
printf("cppfun: %d\n", i);
}
其中 __cplusplus
关键字将在编译器启用 C++ 编译规则时自动定义,此时,extern "C"
关键字将生效,包含在 cppfun.hpp
中的函数声明将自动被此关键字包裹,编译器将在编译 cppfun.cpp
这个 C++ 源文件时,把 extern "C"
内部声明的函数和变量按照 C 的规则生成符号,此时编译 cppfun.cpp
,objdump
的结果如下:
gcc -c cppfun.cpp
objdump -t cppfun.o
cppfun.o: file format mach-o arm64
SYMBOL TABLE:
0000000000000000 l F __TEXT,__text ltmp0
0000000000000038 l O __TEXT,__cstring l_.str
0000000000000038 l O __TEXT,__cstring ltmp1
0000000000000048 l O __LD,__compact_unwind ltmp2
0000000000000000 g F __TEXT,__text _cppfun
0000000000000000 *UND* _printf
此时,则可以完美与 main.c
生成的符号对应,再次将两个源文件一起编译也可产生正确结果。
修改被引用文件
另一个更通用也是更推荐的办法则是直接修改头文件,直接将头文件除了防止重复包含的宏以外的部分全部用 extern "C"
包裹,即可让此头文件在 C 和 C++ 中都可直接被引用,且自适应的生成符号。只需按照如下方法修改 cfun.hpp
:
#ifndef __CPP_FUN_HPP_
#define __CPP_FUN_HPP_
#ifdef __cplusplus
extern "C" {
#endif
void cppfun(int i);
#ifdef __cplusplus
}
#endif
#endif
即可正常按照:
gcc cppfun.cpp main.c
完成编译且正常输出。
其他办法
单独声明
我们还可以单独对某一个声明加上 extern "C"
:
extern "C" void func(int num);
条件编译
还可以定义一个宏进行条件编译:
#ifdef __cplusplus
#define EXTERN_C extern "C"
#else
#define EXTERN_C
#endif
EXTERN_C void func(int char);
总结
由于 extern "C"
是一个 C++ 关键词,因此一定是用在 C++ 源文件一侧,让其按照 C 的规则生成符号。当然,C++ 的函数本身还是按照 C++ 的方式进行编译,只是生成的接口符合 C 的规范而已。并不是所有的函数都能 extern "C"
,因为有些类型无法按照 C 的格式表达。
即使 #include
时也可 extern "C"
,但最好的办法还是写在头文件里,方便在 C/C++ 中都可调用。