函数重载
与C语言不同,c++的函数名是可以相同的,只要参数不同就可以。可以是类型,个数,顺序,只要一样不同就可以。顺序是指类型顺序。函数重载和返回值没有关系。因为调用的时候无法识别返回值。
c++函数名可以相同的关键就在于函数重载。这个和之前命名空间防止命名冲突又不一样。
下面讲一下c++是如何支持函数重载的。
首先是一个问题:为什么C语言不支持重载,c++支持重载。
这里就又涉及到一些底层的东西,方便起见,再次使用Linux来查看细节。
首先创建三个文件:f.h,f.cpp,test.cpp。如何创建不管,以后再学,重点是函数重载。
test.cpp中需要用到的函数没有在test.cpp中定义,而是在f.cpp中定义。
可以看到,这里使用的是C语言来写的,后面将展示用C语言编译器gcc和c++编译器g++编辑的区别。
vs中编译,调试,编辑等功能是集成的,都在vs这个编译器中;而Linux是命令行操作,是分散的。编辑要一个工具,编译要另外的工具。
由图得:进行-o操作的时候,要给定目标,即输出的文件名;或者不用-o操作,直接默认输出,输出a.out文件。
图示命令即为执行文件(a.out是可执行文件)
通过gccC语言编译器生成tc可执行文件;通过g++C++编译器生成tcpp可执行文件。执行tc和tcpp发现结果是一样的。
与windows不同,Linux是不区分后缀名的,调用c++编译器就用c++编译,调用C语言编译器就用C语言编译。
下面写一个函数重载:
用c++的编译器可以正常运行:
按理来说用C语言编译器gcc应该是编译不过的,但可能出于某种升级(应该是增加了识别后缀的功能),Linux下用gcc来编译也能正常编译。vs就肯定不会编译通过了,用Linux只是能看见底层的情况。所以复制一份,用.c的后缀名。
再使用C语言的编译器编译就会报错:
一些Linux操作的知识不理解不重要,重要的是为什么C语言不支持函数重载。
由之前的知识可知:编译链接f.h f.cpp test.cpp的过程有:1.预处理2.编译3.汇编4.链接
c++编译器在预处理上和C语言差不多是一样的,会有语法细节上的差异。
预处理:头文件展开,去注释,条件编译,宏替换。最终生成f.i和test.i文件(头文件已经被展开包含了)
编译:检查语法,生成汇编代码(汇编代码是一个指令级的语言,vs进入调试后,右击反汇编就可以看到汇编代码)最终生成test.s和f.s文件。缺省参数就会在这里被检查。在头文件展开的情况下,声明中如果没有缺省,调用时缺省了就会报错。
汇编:虽然汇编代码已经是指令了,但CPU只认0和1,所以要再翻译成二进制代码。最终生成test.o和f.o文件,也就是目标文件。
链接:找调用函数的地址,链接目标文件和库,生成可执行文件。
test.cpp中有函数的声明和调用,f.cpp中有声明和定义。在编译和汇编的过程中,还生成了符号表。符号表中有函数名和函数地址的映射(就是函数名和虚拟地址的映射,这里还没有物理地址)。比如f.cpp中有两个f函数的地址,test.cpp中有main函数的地址。在main函数的汇编指令中,因为函数调用会出现两个call f()指令,但是在编译阶段不知道f()的地址。但是在转换成指令的时候一定是call一个地址。在当前项目中有定义就f()括号中就是定义位置的地址;没有定义有声明,f()括号中一般是NULL,代表声明过。在链接的时候,发现是NULL,就拿函数名到其他的文件符号表中查找。找到了就填上对应地址。如果没有找到,就会报链接错误。
前面都是铺垫,下面才是C语言不支持函数重载的原因。
tcpp是c++编译器产生的,用图示命令可以看到指令集中符号的标识,代表对应的函数。
上图为两个不同的f函数的汇编指令,前面的数字是地址。由图可知他们的符号标识不是f,而是_Z1fdi和_Z1fid。
符号标识的命名规则:_Z + 函数名长度+函数名+参数类型的首字母(参数是指针的话要加上大写的p)
所以只要参数的类型不同,顺序不同,个数不同就可以区分符号标识,也是函数重载可以实现的原因。链接的时候不是拿f去符号表中查找,而是拿_Z1fdi和_Z1fid到符号表中查找,符号表中的函数名是以符号标识的形式存在的。
下面来看看C语言是如何命名的(要把函数重载去除)
gcc编译器生成tc可执行文件,使用objdump -S tc命令行查看。注意文件后缀名(gcc升级了),如果文件是test.cpp和f.cpp,gcc编译器还是会用c++的语法生成符号标识。
这是按照C语言的方式来编译的。
由图可知,C语言的编译方式,函数的符号标识就是函数名。这就是C语言不支持重载的原因。
看起来似乎铺垫了很久,答案又很简单,因为函数重载也是铺垫。
我们常说的库是指静态库和动态库,库可以看做常用的函数集合。C语言的库用C语言写,c++的库用c++写。但是可以C语言调用c++的库吗?或者c++调用C语言的库?答案是:可以
库遵循.elf文件格式,有符号表。也就是说,库中函数的符号标识是已经确定的。
当然,自己创建的静态库直接调用是不可以的。C语言是用直接的函数名作符号标识来找,c++函数的符号标识是被修饰过的。
由此引出extern "C",extern"C"可以改变编译和链接方式,帮助我们c++调c和c调c++。
先自己创建一个静态库:
静态库创建过程:vs中创建一个空项目。静态库文件名以.lib为后缀,会在静态库完成建立后自动加上。选择一个项目作为静态库中的内容,比如选择stack.c和stack.h作为内容。这是如果编译是会报错的,因为没有main函数,也就是被当做可执行文件了。但是静态库不是可执行程序。
如何设置静态库:(这里使用vs2013示范,vs2019大体相同)
建立空项目,命名为Stack_C。源文件——右击——添加——添加现有项,然后会出现以下界面:
找到要添加的文件,复制,粘贴,添加
然后步骤如下:
然后点击应用,确定。生成——生成解决方案,可以看到生成Stack_C.lib
正常编译会生成.exe可执行文件。
找到项目所在位置,找到debug文件夹,打开后会找到Stack_C.lib。就是静态库。这个静态库是用C语言写的,其中函数的符号标识就是函数名,如果现在有一个cpp程序要用这个静态库应该如何用?
先找一个需要用到栈的代码。拷贝过来。
要包含静态库中的stack.h要使用静态库中的相对路径。默认在当前路径下,也就是创建工程的路径下。
#include"../Stack_C.lib/Stack_C.lib/Stack.h"
//其中..的意思是当前所在目录向上跳一层,最终目的是找到Stack.h所在路径
这是运行结果,编译的时候通过,链接时链接不上。(?静态库涉及物理地址吗)
链接不上的原因是还有再配置一下。(这里使用vs2019示范,vs2013大体相同)
附加库目录(附加库目录是添加非标准的库的地方,也就是自己写的库或者大佬写的库在这里添加,所以也叫第三方库),选择静态库文件夹中的debug(静态库所在文件夹,虽然选择时看不到静态库,因为生成静态库是在debug版本下生成的),然后转到输入,在附加依赖项中添加静态库的文件名。附加依赖项中都是需要用到的库。
然后点击应用,确定。
这时再运行,还是会报错;但是如果将静态库中stack.c改为stack.cpp然后编译。再运行当前程序就能正常通过。
stack.c链接不上原因在于,静态库中函数的符号标识就是函数名,而c++程序在进行链接时,是用被修饰过的函数名作为符号标识去静态库中找的。
如果将头文件用extern "C"包含就能解决这个问题。
extern "C"
{
#include"../Stack_C.lib/Stack_C.lib/Stack.h"
}
extern "C"就是在告诉编译器,声明的函数是C库,要用c的方式去链接调用。
注意:extern "C"是c++中的语法,C语言中是没有extern "C"的。
这就是c++调c的方法。
下面来看看c调c++
首先是正常的创建c++的静态库,过程同前面的一样,就是在项目中stack.c要改为stack.cpp,保证以c++的方式编译成静态库。
然后是找到stack.h所在路径,包含。编译,报错。还是在链接部分(obj文件是链接时需要的)出错。
注意:在重定义栈时的类型名,cpp中stack就是一个类型,在cpp中可以直接使用stack。但是如果重定义时,没有定义成stack,而且项目中的栈的类型名为stack,那么要对test.c的代码进行修改,stack修改为重定义的类型名,或者struct stack。
再配置静态库,属性页……具体见上面。
编译还是不通过,原因和上面类似:C语言项目中,函数的符号标识为函数名,c++库中函数的符号标识为被修饰过的函数名。用函数名符号标识来找被修饰的函数名符号标识,肯定是找不到的。
这时用extern "C"包含#include"../Stack_C.lib/Stack_C.lib/Stack.h"就行不通了,extern "C"是告诉编译器用c的方式找这里面的函数,但C语言的项目,本来就是用c的方式来找的,c的方式是找不到c++的符号标识的。
所以要在c++的部分进行操作,还是extern "C",这次放在静态库头文件声明函数的前面,每个函数的前面都放一个。意为该函数的符号标识C语言的方式,即函数名。
编译一下还是没有通过,原因在于extern "C"是c++的语法,在头文件包含的时候extern "C"被包含了,编译的时候会报语法错误。这时就需要条件编译了。
#ifdef __cplusplus
//__cplusplus是C++的宏标识,是c++自己就有的
#define EXTERN_C extern "C"
#else
#define EXTERN_C
#endif
EXTERN_C void StackInit(Stack * ps);
EXTERN_C void StackDestroy(Stack* ps);
EXTERN_C void StackPush(Stack* ps, StackDateType x);
EXTERN_C void StackPop(Stack* ps);
EXTERN_C StackDateType StackTop(Stack* ps);
EXTERN_C bool StackEmpty(Stack* ps);
EXTERN_C int StackSize(Stack* ps);
还有一种方法,小小的优化一下:
#ifdef __cplusplus
extern "C"
{
#endif
void StackInit(Stack * ps);
void StackDestroy(Stack* ps);
void StackPush(Stack* ps, StackDateType x);
void
StackPop(
Stack
*
ps
);
StackDateType StackTop(Stack* ps);
bool StackEmpty(Stack* ps);
int StackSize(Stack* ps);
#ifdef __cplusplus
}
#endif
在c++的库中,EXTERN_C被替换成extern "C",函数的符号标识是C语言的形式。被包含到头文件中后,执行#define EXTERN_C,函数声明还是void StackInit(Stack * ps);这种形式,编译时声明是C语言形式的符号标识,静态库中也是C语言类型的符号标识。extern "C"的使用,可以让静态库被C语言调用,也可以被c++调用。原因在于extern "C"的修饰的声明,声明的函数即使在c++中被使用,编译的时候还是按照c的规则确定函数标识符。而且在链接的时候,源代码已经转换成二进制代码了,也就不用担心c++和c的语法冲突问题。
无论是c调c++,还是c++调c都有对应的场景。
这是我们自己写库的场景。在标准库中情况会有一些不同,而且不同情况下,有不同情况的标准库。
已知的一些信息:
C语言和C++的内容是一样的。
程序在编译时,不会涉及物理地址,所有的地址都是虚拟地址。物理地址只有在真正使用的时候,把它加载到内存中的时候,才会开辟物理内存,才能知道物理内存在哪。