Extern “C” 链接指示 & 函数重载
用于在 cpp 代码中调用 c 语言代码
1. 测试代码,cpp 文件中调用 c 代码
test.h
文件
#pragma once
#include<stdio.h>
void show();
test.c
文件
#include "test.h"
void show()
{
printf("hello world CCCC\n");
}
ex01.cpp
文件
#include<iostream>
#include "test.h"
using namespace std;
int main()
{
show();
return 0;
}
2. 编译测试
2.1 报错
windows环境
:vs2019
编译通过,但是无法执行,
LNK1120 1 个无法解析的外部命令
LNK2019 无法解析的外部符号 “void __cdecl show(void)” (? show@@YAXXZ),函数 _main 中引用了该符号 test.obj
linux环境
:gcc version 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.1
$ g++ -c ex01.cpp # 按照 c++ 编译 通过 生成 ex01.o 文件
$ gcc -c test.c # 按照 c 编译 通过 生成 test.o 文件
$ g++ test.o ex01.o -o exec01 # 报错 test.o 和 ex01.o 顺序可换
/usr/bin/ld: ex01.o: in function `main':
ex01.cpp:(.text+0x9): undefined reference to `show()'
collect2: error: ld returned 1 exit status
按照 vs2019
的 报错信息,无法解析,大概是链接时找不到方法或者链接不到函数
vs2019
和 gcc
都将错误指向了 show() 方法,但是两者都能正常编译,应该是链接出现了问题。
原因:C++ 为实现函数重载(c 语言不支持函数重载),使用了和 C 不同的函数命名,同样的 show() 函数,在 ex01.cpp 和 test.c 中使用了不同的命名方式,所以链接时,ex01.cpp 按照 cpp 的命名结果去找 show() ,而 test.c 的 show() 不符合 cpp 的要求 (两者名字是不一样的), 所以 出现找不到 show() 方法的情况
具体看 4 汇编
2.2 使用 Extern “C” 解决
在 ex01.cpp 中对 show() 方法使用 extern “C” 修饰,告诉编译器 show() 不再按照 cpp 方式处理,而是按照 c 语言方式处理
2.2.1 方法一:修改 ex01.cpp 文件
#include<iostream>
// #include "test.h"
using namespace std;
//C++ 中调用 C语言方法
extern "C" void show(); // 使用 C语言方式链接 show() 方法
int main()
{
show();
return 0;
}
注意要注释 #include “test.h” , 否则会重复声明 show
vs2019 下直接运行
linux 下要重新编译 ex01.cpp
$ g++ -c ex01.cpp
$ g++ test.o ex01.o -o exec01
$ ./exec01
# 输出
hello world CCCC
2.2.2 方法二:修改 test.h 文件, 使用条件编译
#pragma once
#include<stdio.h>
#ifdef __cplusplus //使用条件编译,__cplusplus是cpp中的自定义宏,那么定义了这个宏的话表示这是一段cpp的代码
extern "C"
#endif
void show(); // show() 受到 extern "C" 的影响
void show2() // show2() 不受 extern "C" 的影响
注意上述的条件编译,只能作用 #endif 后面的第一个 函数,
ex01.cpp 保持不变
#include<iostream>
#include "test.h"
using namespace std;
//C++ 中调用 C语言方法
// extern "C" void show(); // 使用 C语言方式链接 show() 方法
int main()
{
show();
return 0;
}
linux gcc 下需要重新编译 test.c
3. CPP 中引用多个 C 函数
定义多个 show() 函数
test.h
#pragma once
#include<stdio.h>
void show();
void show2(int a);
void show3(int a, double b);
void show4(int a, int b, double c);
test.c
#include "test.h"
void show()
{
printf("hello world CCCC\n");
}
void show2(int a)
{
printf("hello world CCCC %d\n", a);
}
void show3(int a, double b)
{
printf("hello world CCCC %f\n", a + b);
}
void show4(int a, int b, double c)
{
printf("hello world CCCC %f\n", a + b + c);
}
3.1 方法一:ex01.cpp 中对每一个 C 函数 使用 extern “C”
ex01.cpp
#include<iostream>
//#include "test.h"
using namespace std;
//C++ 中调用 C语言方法
extern "C" void show(); // 使用 C语言方式链接 show() 方法
extern "C" void show2(int a);
extern "C" void show3(int a, double b);
extern "C" void show4(int a, int b, double c);
int main()
{
show();
show2(1);
show3(1, 2.0);
show4(1, 2, 3.0);
return 0;
}
注意要注释 #include “test.h” , 否则会重复声明 show
这里给出 linux 下的结果
$ gcc -c test.c
$ g++ -c ex01.cpp
$ g++ test.o ex01.o -o exec01
$ ./exec01
hello world CCCC
hello world CCCC 1
hello world CCCC 3.000000
hello world CCCC 6.000000
ex01.cpp
也可以使用 复合 的方式,一个 extern “C” 作用多个函数
#include<iostream>
//#include "test.h"
using namespace std;
extern "C"
{
void show();
void show2(int a);
void show3(int a,double b);
void show4(int a,int b,double c);
}
int main()
{
show();
show2(1);
show3(1, 2.0);
show4(1, 2, 3.0);
return 0;
}
3.2 方法二: 在 test.h 中使用条件编译
注意,类似 2.2.2, 一个 extern “C” 只能作用一个函数,所以也要用 复合 的方式进行处理
test.h
#pragma once
#include<stdio.h>
#ifdef __cplusplus //条件编译 __cplusplus是cpp中的自定义宏
extern "C"{
#endif
void show();
void show2(int a);
void show3(int a, double b);
void show4(int a, int b, double c);
#ifdef __cplusplus
}
#endif
ex01.cpp
#include<iostream>
#include "test.h"
using namespace std;
int main()
{
show();
show2(1);
show3(1, 2.0);
show4(1, 2, 3.0);
return 0;
}
4. linux 查看汇编结果
用 vs2019 查看也可以,比较麻烦
使用以下测试代码
test.h
#pragma once
#include<stdio.h>
void show();
void show2(int a);
void show3(int a, double b);
void show4(int a, int b, double c);
test.c
#include "test.h"
void show()
{
printf("hello world CCCC\n");
}
void show2(int a)
{
printf("hello world CCCC %d\n", a);
}
void show3(int a, double b)
{
printf("hello world CCCC %f\n", a + b);
}
void show4(int a, int b, double c)
{
printf("hello world CCCC %f\n", a + b + c);
}
ex01.cpp
#include<iostream>
#include "test.h"
using namespace std;
int main()
{
show();
show2(1);
show3(1, 2.0);
show4(1, 2, 3.0);
return 0;
}
linux下分别编译 ex01.cpp
和 test.c
, 可以知道上面的代码可以编译,但是无法链接得到可以执行文件 (没有使用 extern “C”)
$ gcc -c test.c
$ g++ -c ex01.cpp
$ g++ test.o ex01.o -o exec01
/usr/bin/ld: ex01.o: in function `main':
ex01.cpp:(.text+0x9): undefined reference to `show()'
/usr/bin/ld: ex01.cpp:(.text+0x13): undefined reference to `show2(int)'
/usr/bin/ld: ex01.cpp:(.text+0x29): undefined reference to `show3(int, double)'
/usr/bin/ld: ex01.cpp:(.text+0x44): undefined reference to `show4(int, int, double)'
collect2: error: ld returned 1 exit status
4.1 查看汇编结果
4.1.1 ex01.cpp 汇编结果
$ g++ -S -fverbose-asm -g ex01.cpp -o ex01.s
以下是 test.s
的部分汇编结果
main:
.LFB1522:
.file 1 "ex01.cpp"
.loc 1 8 1
.cfi_startproc
endbr64
pushq %rbp #
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp #,
.cfi_def_cfa_register 6
# ex01.cpp:9: show();
.loc 1 9 6
call _Z4showv@PLT #
# ex01.cpp:10: show2(1);
.loc 1 10 7
movl $1, %edi #,
call _Z5show2i@PLT #
# ex01.cpp:11: show3(1, 2.0);
.loc 1 11 7
movq .LC0(%rip), %rax #, tmp84
movq %rax, %xmm0 # tmp84,
movl $1, %edi #,
call _Z5show3id@PLT #
# ex01.cpp:12: show4(1, 2, 3.0);
.loc 1 12 7
movq .LC1(%rip), %rax #, tmp85
movq %rax, %xmm0 # tmp85,
movl $2, %esi #,
movl $1, %edi #,
call _Z5show4iid@PLT #
# ex01.cpp:13: return 0;
可以看到 4 个 show 函数在 cpp 中汇编后, 分别对应 如下 的函数名
show() _Z4showv@PLT
show2(int a) _Z5show2i@PLT
show3(int a, double b) _Z5show3id@PLT
show4(int a, int b, double c) _Z5show4iid@PLT
@PLT不知道是什么…
4.1.2 test.c 汇编结果
为了方便查看,对 test.c
增加一个 main 函数, 看看调用 show 函数时,用的名字是什么
不加 main 函数, 直接看 函数 的汇编也一样
#include "test.h"
void show()
{
printf("hello world CCCC\n");
}
void show2(int a)
{
printf("hello world CCCC %d\n", a);
}
void show3(int a, double b)
{
printf("hello world CCCC %f\n", a + b);
}
void show4(int a, int b, double c)
{
printf("hello world CCCC %f\n", a + b + c);
}
int main() // 增加 main 函数
{
show();
show2(1);
show3(1, 2.0);
show4(1, 2, 3.0);
return 0;
}
得到 test.c
的汇编结果, 注意要用 gcc
$ gcc -S -fverbose-asm -g test.c -o test.s
截取的部分汇编结果
main:
.LFB4:
.loc 1 26 1
.cfi_startproc
endbr64
pushq %rbp #
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp #,
.cfi_def_cfa_register 6
# test.c:27: show();
.loc 1 27 2
movl $0, %eax #,
call show #
# test.c:28: show2(1);
.loc 1 28 2
movl $1, %edi #,
call show2 #
# test.c:29: show3(1, 2.0);
.loc 1 29 2
movq .LC3(%rip), %rax #, tmp84
movq %rax, %xmm0 # tmp84,
movl $1, %edi #,
call show3 #
# test.c:30: show4(1, 2, 3.0);
.loc 1 30 2
movq .LC4(%rip), %rax #, tmp85
movq %rax, %xmm0 # tmp85,
movl $2, %esi #,
movl $1, %edi #,
call show4 #
# test.c:32: return 0;
可以看到 4 个 show 函数在 C 语言中 汇编后, 分别对应 如下 的函数名
show() show
show2(int a) show2
show3(int a, double b) show3
show4(int a, int b, double c) show4
4.1.3 对比
函数名 | C 语言编译结果 (test.c ) | 无extern “C” C++ 编译结果 ( ex01.cpp ) |
---|---|---|
show() | show | _Z4showv@PLT |
show(int a) | show2 | _Z5show2i@PLT |
show(int a, double b) | show3 | _Z5show3id@PLT |
show(int a, int b, double c) | show4 | _Z5show4iid@PLT |
所以,如果 ex01.cpp 去调用 test.c 中的 show 函数,在链接时就会由于 函数名称 对不上而报错,无法解析 show(void), show(int), show(int ,double), show(int, int, double)
函数 show 在C++文件中声明 (调用),在 C 文件中定义
C++ 文件中 show 函数名按照 函数+形参 处理,C 中直接只用原始函数名,两者不一致
4.1.4 在 ex01.cpp 中增加 extern “C” 后查看汇编结果
ex01.cpp
这里使用 3.1 中的 复合 方式
#include<iostream>
//#include "test.h"
using namespace std;
//C++ 中调用 C语言方法 使用 C语言方式链接 show() 方法
extern "C"
{
void show();
void show2(int a);
void show3(int a, double b);
void show4(int a, int b, double c);
}
int main()
{
show();
show2(1);
show3(1, 2.0);
show4(1, 2, 3.0);
return 0;
}
重新得到 ex01.cpp 的汇编结果
$ g++ -S -fverbose-asm -g ex01.cpp -o ex01.s
main:
.LFB1522:
.file 1 "ex01.cpp"
.loc 1 15 1
.cfi_startproc
endbr64
pushq %rbp #
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp #,
.cfi_def_cfa_register 6
# ex01.cpp:16: show();
.loc 1 16 6
call show@PLT #
# ex01.cpp:17: show2(1);
.loc 1 17 7
movl $1, %edi #,
call show2@PLT #
# ex01.cpp:18: show3(1, 2.0);
.loc 1 18 7
movq .LC0(%rip), %rax #, tmp84
movq %rax, %xmm0 # tmp84,
movl $1, %edi #,
call show3@PLT #
# ex01.cpp:19: show4(1, 2, 3.0);
.loc 1 19 7
movq .LC1(%rip), %rax #, tmp85
movq %rax, %xmm0 # tmp85,
movl $2, %esi #,
movl $1, %edi #,
call show4@PLT #
# ex01.cpp:20: return 0;
函数名 | C 语言编译结果 (test.c ) | extern "C"下 C++ 编译结果 ( ex01.cpp ) |
---|---|---|
show() | show | show@PLT |
show(int a) | show2 | show2@PLT |
show(int a, double b) | show3 | show3@PLT |
show(int a, int b, double c) | show4 | show4@PLT |
可以看到现在 test.c
和 ex01.cpp
汇编下 的 函数名已经一致了
这样就可以成功链接了(记得去掉4.1.2 在 test.c
中添加的 mian 函数)
4.1.5 在 test.h 中使用条件编译的方式增加 extern “C”
不用 4.1.4 的方式,现在 在 test.h
中添加 extern “C”
也就是使用如下的测试代码
test.h
#pragma once
#include<stdio.h>
#ifdef __cplusplus
extern "C" {
#endif
void show();
void show2(int a);
void show3(int a, double b);
void show4(int a, int b, double c);
#ifdef __cplusplus
}
#endif
test.c
#include "test.h"
void show()
{
printf("hello world CCCC\n");
}
void show2(int a)
{
printf("hello world CCCC %d\n", a);
}
void show3(int a, double b)
{
printf("hello world CCCC %f\n", a + b);
}
void show4(int a, int b, double c)
{
printf("hello world CCCC %f\n", a + b + c);
}
// int main()
// {
// show();
// show2(1);
// show3(1, 2.0);
// show4(1, 2, 3.0);
// return 0;
// }
ex01.cpp
#include<iostream>
#include "test.h"
using namespace std;
int main()
{
show();
show2(1);
show3(1, 2.0);
show4(1, 2, 3.0);
return 0;
}
首先查看 ex01.cpp
的汇编结果
$ g++ -S -fverbose-asm -g ex01.cpp -o ex01.s
main:
.LFB1522:
.file 1 "ex01.cpp"
.loc 1 8 1
.cfi_startproc
endbr64
pushq %rbp #
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp #,
.cfi_def_cfa_register 6
# ex01.cpp:9: show();
.loc 1 9 6
call show@PLT #
# ex01.cpp:10: show2(1);
.loc 1 10 7
movl $1, %edi #,
call show2@PLT #
# ex01.cpp:11: show3(1, 2.0);
.loc 1 11 7
movq .LC0(%rip), %rax #, tmp84
movq %rax, %xmm0 # tmp84,
movl $1, %edi #,
call show3@PLT #
# ex01.cpp:12: show4(1, 2, 3.0);
.loc 1 12 7
movq .LC1(%rip), %rax #, tmp85
movq %rax, %xmm0 # tmp85,
movl $2, %esi #,
movl $1, %edi #,
call show4@PLT #
# ex01.cpp:13: return 0;
然后查看 test.c
的汇编结果 , 同样加上 main 函数
$ gcc -S -fverbose-asm -g test.c -o test.s
main:
.LFB4:
.loc 1 26 1
.cfi_startproc
endbr64
pushq %rbp #
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp #,
.cfi_def_cfa_register 6
# test.c:27: show();
.loc 1 27 2
movl $0, %eax #,
call show #
# test.c:28: show2(1);
.loc 1 28 2
movl $1, %edi #,
call show2 #
# test.c:29: show3(1, 2.0);
.loc 1 29 2
movq .LC3(%rip), %rax #, tmp84
movq %rax, %xmm0 # tmp84,
movl $1, %edi #,
call show3 #
# test.c:30: show4(1, 2, 3.0);
.loc 1 30 2
movq .LC4(%rip), %rax #, tmp85
movq %rax, %xmm0 # tmp85,
movl $2, %esi #,
movl $1, %edi #,
call show4 #
# test.c:32: return 0;
对比汇编结果
函数名 | C 语言编译结果 (test.c ) | 条件编译 extern “C” C++ 编译结果 ( ex01.cpp ) |
---|---|---|
show() | show | show@PLT |
show(int a) | show2 | show2@PLT |
show(int a, double b) | show3 | show3@PLT |
show(int a, int b, double c) | show4 | show4@PLT |
C++ 和 C 中的函数名保持一致
5. 小结
函数名 | C 语言编译结果 (test.c ) | 无extern “C” C++ 编译结果 ( ex01.cpp ) |
---|---|---|
show() | show | _Z4showv@PLT |
show(int a) | show2 | _Z5show2i@PLT |
show(int a, double b) | show3 | _Z5show3id@PLT |
show(int a, int b, double c) | show4 | _Z5show4iid@PLT |
C 语言和 C++ 语言在函数命名上使用了不同的规则
-
C语言:不支持函数重载,编译后直接使用原始函数名
-
C++:支持函数重载,编译后使用 函数名 + 形参 重新定义了函数名字
extern “C” 告诉 C++ 编译器 使用 C语言 的方式 来对函数进行 编译
6. 函数重载
由此也可以对 C++ 函数重载的原理有一定的认识
C++ 使用 函数名 + 形参 重新定义了函数名字,所以相同名字的函数,只要有不同形参列表,编译后就能拥有不同的名字,就能加以区分
也可以看到编译后没有把 返回值类型 加入到函数名中,所以 不能 依靠 返回值类型 来实现函数重载。
在linux下 查看 C++ 函数重载的汇编结果
overload.cpp
#include<iostream>
using namespace std;
void func(int a)
{
cout<<a<<endl;
}
void func(int a,int b)
{
cout<<a+b<<endl;
}
void func(int a,int b,double c)
{
cout<<a+b+c<<endl;
}
extern "C" void func(int a,int b,int c,double d){cout<<a+b+c+d<<endl;}
/*
extern "C" 的方式只能定义一次 func, 之后就重名了
linux gcc下报错 overload.cpp:20:17: error: conflicting declaration of C function ‘void func(int, int, int, int, double)’
windows vs2019下报错 “func”: 无法重载具有外部 "C" 链接的函数
*/
//extern "C" void func(int a,int b,int c,int d, double e){cout<<a+b+c+d+e<<endl;}
int main()
{
func(1);
func(1,2);
func(1,2,3.0);
func(1,2,3,4.0);
return 0;
}
获得汇编结果
$ g++ -S -fverbose-asm -g overload.cpp -o overload.s
main:
.LFB1526:
.loc 1 22 5
.cfi_startproc
endbr64
pushq %rbp #
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp #,
.cfi_def_cfa_register 6
# overload.cpp:23: func(1);
.loc 1 23 13
movl $1, %edi #,
call _Z4funci #
# overload.cpp:24: func(1,2);
.loc 1 24 13
movl $2, %esi #,
movl $1, %edi #,
call _Z4funcii #
# overload.cpp:25: func(1,2,3.0);
.loc 1 25 13
movq .LC0(%rip), %rax #, tmp84
movq %rax, %xmm0 # tmp84,
movl $2, %esi #,
movl $1, %edi #,
call _Z4funciid #
# overload.cpp:26: func(1,2,3,4.0);
.loc 1 26 13
movq .LC1(%rip), %rax #, tmp85
movq %rax, %xmm0 # tmp85,
movl $3, %edx #,
movl $2, %esi #,
movl $1, %edi #,
call func #
# overload.cpp:27: return 0;
函数名 | C ++编译结果 (overload.cpp ) |
---|---|
func(int a) | _Z4funci |
func(int a, int b) | _Z4funcii |
func(int a, int b, double c) | _Z4funciid |
extern “C” func(int a, int b, int c, double) | func |
可以看到,前 3 个 func 使用了 函数名 + 形参 重新定义了名字,而 最后 1 个 func 使用了 extern “C”,告诉 C++ 编译器使用 C 语言的方式进行编译命名,且 使用 extern “C” 的方式只能定义一次 func, 因为之后就重名了,全都是func。
所以 C++ 可以函数重载,C 不行
从汇编的角度看 C++ 中 extern “C” 的作用 及 C++ 函数重载的实现的基本原理