在C语言阶段,我们交换两个数,如果用函数是实现,那么这时一定要为不同的参数类型构造不同的函数。
在C语言中如下的构造方式一定是不可以的:
test.cpp
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void Swap(double* a, double* b)
{
double tmp = *a;
*a = *b;
*b = tmp;
}
error C2084: 函数“void Swap(int *,int *)”已有主体
编译器一定会报个这样的错给你。
因为你在同一作用域写了两个相同名称的函数,你只能做些标记让它不同名,这大大增加了用户定义函数的成本,更增加了使用函数的成本。
c++为了改进c这个缺点,引入了函数重载的概念:
允许在同一作用域中声明几个功能类似的同名函数,这些函数的参数列表1(参数个数或类型或顺序)必须不同,常用来处理实现功能类似,类似数据类型不同的问题。
注意:不可以通过返回类型区别重载
test.cpp
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void Swap(double* a, double* b)
{
double tmp = *a;
*a = *b;
*b = tmp;
}
int main()
{
int a = 1, b = 2;
double c = 1.111, d = 2.222;
Swap(&a, &b);
Swap(&c, &d);
printf("a = %dnb = %dnc = %lfnd = %lf", a, b, c, d);
}
这段程序在cpp格式下是能正常运行的。
名字修饰(name Mangling)a = 2
b = 1
c = 2.222000
d = 1.111000
为什么c++支持,但c语言不支持呢?
编译链接在解释重载的问题前要有一些对函数编译链接的了解:点我
形成可执行文件要四个阶段:预处理、编译、汇编、链接。
我们这里要了解,但只要了解一点点
-
预编译阶段:把头文件中函数的声明拷贝到源文件,免得编译的时候语法分析,说你没定义;
-
编译阶段:除了刚刚说的语法分析,还要把当前文件的所有函数名汇总给起来,称之为符号汇总;
-
汇编:给刚刚的函数名加一个地址(函数定义处的地址),也就是之后只要有函数名就能链到函数定义的位置,直接执行函数内的操作
-
链接:之前所有的操作都是各个文件自己走自己的,这一步就要合起来了,把各个文件的符号表合在一起,声明处的符号与定义处的符号合并。
那么问题来了,C语言定义多个同名函数而报错可以理解,因为编译器也不知道符号表里的几个Swap到底是谁是谁。
c++是怎么解决的呢?
c++会对函数名进行修饰,而每家的c++编译器都会有不同的修饰规则,由于windows的VS命名规则过于复杂,而Linux下gcc的修饰规则简单易懂,下面我们就用gcc演示这个修饰后的名字。
Linux环境下test.c
void f(int a,double b)
{
printf("%d, %lfn", a, b);
}
编译后:
test.cpp
void f(int a,double b)
{
printf("%d, %lfn", a, b);
}
void f(double b, int a)
{
printf("%lf, %ldn", a, b);
}
编译后:
可以看到,c语言下,汇编之后函数名依然是
但c++下,汇编之后两个函数名就变成了<_Z1fid>_和<Zfdi>
可以看出**c++**的函数修饰后变成【_Z+函数长度+函数名+类型首字母】。
再来几个例子演示一下:
结论:在linux下,采用**g++编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中。
Windows下名字修饰规则对比Linux会发现,windows下C++编译器对函数名字修饰非常诡异,但道理都是一样的。
总结:-
到这里,我们就理解了c语言没办法实现重载是因为同名函数没办法区分。而c++能够实现就依托于它的函数修饰规则,只要参数不同,修饰出来的名字就不一样。
-
同时我们也理解了,为什么函数重载要求参数不同,而跟返回值没关系。
在语言发展的过程中,一门新的语言往往要兼容之前的语法,c++作为C语言的一个升级无疑要遵循此法。在数C语言出现的数十年中,已经留下了太多痕迹,c++如果不对其进行兼容,那需要付出的代价太大,数十年的积淀难道要重新来过?
而且,目前看来,C语言的效率是最接近汇编语言的,某些非常追求效率的特殊场景,C语言是有其得天独厚的优势的。
而extern “C”可以说就是用来支持这两种语言互相调用的。
extern “C”解释再回到Windows环境,先写两个文件作为案例:
func.cpp
void func()
{
int a = 0;
}
test.cpp
void func();
int main()
{
func();
return 0;
}
-
如果直接运行会不会有问题?
-
如果只把func.cpp改为func.c呢?
-
那如果只把test.cpp改为test.c呢?
-
如果都改呢?
1、4一定可以正常运行,我们主要看2、3.
c++调用cerror LNK2019: 无法解析的外部符号void __cdecl f(int,int)" (?f@@YAXHH@Z),函数 _
main 中引用了该符号已定义且可能匹配的符号上的提示: _f
fatal error LNK1120: 1 个无法解析的外部命令
用c++中的main函数调用c中的func函数,会有以上报错。
分析:
-
test.cpp和func.c经过编译链接,生成test.o文件和func.o文件
-
test.o中的mian()函数中调用(汇编代码中的call)的是一个修饰过的函数,但是func.o给过来的的表中的函数是没有被修饰过的,也就是说你给过来的我不认识。
解决方法:
在test.cpp中的f()声明的时候套一层extern "C"
如下:
extern "C" void func();
int main()
{
func();
return 0;
}
或
extern "C"
{
void func(int a, int b);
}
int main()
{
func();
return 0;
}
这里extern "C"就是告诉编译器,这里要用c语言的函数命名规则去调用(汇编代码中的call),不用修饰,同时自己形成符号表时也不修饰,这时两个文件都是c的规则,自然不会出问题。
c调用c++test.obj : error LNK2019: 无法解析的外部符号 _func,函数 _main 中引用了该符号
“void __cdecl func(int,int)” (?f@@YAXHH@Z)
fatal error LNK1120: 1 个无法解析的外部命令
用c中的main函数调用c++中的func函数,会有以上报错。
分析:
- 同样先形成各自的.o文件
- test.o文件中函数没有进行过修饰,也就是说调用的时候会直接调func();
- func.o文件中函数进行过修饰,也就是说,我给出的表是修饰过的func(如:_Z4func)
又对不上了。
解决方法:
由于c早于c++出现,c不可能为了适应c++有一个语法产生。
所以还是在c++上做手脚,还是extern “C”的语法。
在函数定义的上面摆一个extern的声明
extern "C"
{
void f(int a, int b);
}
//或
//extern "C" void f(int a, int b);
void func()
{
int c = 0;
}
这样,在func文件生成表的时候,就不会对func函数进行修饰,这样test中的函数就能查表查到func,从而链接过去。
但一般情况都是在头文件进行extern “C”的,如下:
func.h
extern "C"
{
void func();
}
func.c
#include "func.h"
void func()
{
int c = 0;
}
test.c
#include "func.h"
int main()
{
func();
return 0;
}
静态库
实际情况下,如上的调用方式很少见。
我们手里有的可能只是一个静态库和一个它的.h文件,那么我们看看
.cpp调用.c文件产生的静态库或动态库
或.c文件调用.cpp文件产生的静态库或动态库的情况
建议大家看一下如何建一个静态库,里边讲解了静态库创建、静态库使用、相对路径包头文件等知识,便于大家理解和实操测试。
以下我们用一个只有加法函数的静态库作为案例讲解。
c++调用c首先创建一个项目Add_lib,在其中创建Add.c文件,我们用Add.c文件生成一个.lib文件:
并为该项目写.h文件
Add.h
#pragma once int Add(int a, int b);
此时就可以模拟一个只有静态库和一个它的.h文件的情况。
然后新建项目,在新项目中创建.cpp文件,调用Add()函数。
test.cpp
#include//Add.h(相对路径包含) #include"../../Add_C_lib/Add_C_lib/Add.h" int main() { int add = Add(1, 2); printf("%d", add); }
此时又形成了 cpp 调 c 的情况,
运行以下,果然出错辣
error LNK2019: 无法解析的外部符号 “int __cdecl Add(int,int)” (?Add@@YAHHH@Z),函数 _main 中引用了该符号
解决方法很简单
用一个extern“C”将头文件包住,如下:
#include//Add.h(相对路径包含) extern "C" { #include"../../Add_C_lib/Add_C_lib/Add.h" } int main() { int add = Add(1, 2); printf("%d", add); }
这时,CPP的编译器就会用C语言的函数名修饰规则调用Add,从而不会发生命名冲突。
c语言调用c++静态库和头文件:
Add.cpp文件生成一个.lib文件:
Add.h
#pragma once int Add(int a, int b);
调用文件:
test.c
#include//Add.h(相对路径包含) #include"../../Add_C_lib/Add_C_lib/Add.h" int main() { int add = Add(1, 2); printf("%d", add); }
分析:
这里要试其函数名修饰方式形同,就要在.lib文件生成过程中就使用c语言的修饰方式。
这里可操作的方式就是在头文件Add.h中,对Add的声明加extern“C”:
Add.h
#pragma once extern "C" int Add(int a, int b);
然后重新生成解决方案,此时产生的.lib文件就是c语言的修饰方式
分析:
在被翻译成.lib文件的**.cpp文件**一定是包含Add.h的,也就是说,预处理之后
extern "C" int Add(int a, int b);
这句话会被替换到.cpp中,此时用它生成的.lib文件的过程中,编译器就不会对函数名进行修饰,也就是说用C语言调也可以过
万事具备,我们试一下
error C2059: 语法错误:“字符串”
又报错辣
分析一下:
哦哦哦哦哦,test.c中也包了Add.h这个头文件,extern "C" int Add(int a, int b);这句话被包到了C语言文件中了。
C语言肯定不认识extern "C"啊。
那怎么解决呢?
C++生成解决方案的时候需要extern "C",C语言调用的时候不要extern "C",大家想到了什么方法?
诶,条件编译。
Add.h
#pragma once #ifdef __cplusplus #define EXTERN_C extern "C" #else #define EXTERN_C #endif EXTERN_C int Add(int a, int b);
这是用条件改过的头文件
注:__cplusplus是c++默认定义过的宏,而C语言没有这个宏
翻译:
- 如果是c++编译器,最下面的EXTERN_C会被换成extern "C"
- 如果是C语言编译器,EXTERN_C替换为空。
如果需要包的函数很多,一句一句加EXTERN_C很麻烦,也可以这样
#ifdef __cplusplus
extern "C"
{
#endif
int Add(int a, int b);
//其它函数声明
//.......
//.......
//.......
#ifdef __cplusplus
}
#endif
总结
结合c++调用c、c调用c++,我们可以总结一下extern “C”的具体功能。
- 从c++调用c可以看得出extern “C”可以改变调用步骤的函数名,调用没有修饰过的函数名
- 从c语言调用c++可以看的出,extern “C”可以改变符号表中的函数名
最后:总结不易,大家三连支持以下啦,文章中不严谨测地方欢迎大家多多指出。
定义函数时后面括号里的东西,如(int* a, int* b) ↩︎



