栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

c++函数重载原理

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

c++函数重载原理

函数重载的概念

在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格式下是能正常运行的。

a = 2
b = 1
c = 2.222000
d = 1.111000

名字修饰(name Mangling)

为什么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++能够实现就依托于它的函数修饰规则,只要参数不同,修饰出来的名字就不一样。

  • 同时我们也理解了,为什么函数重载要求参数不同,而跟返回值没关系。

extern “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;
}
  1. 如果直接运行会不会有问题?

  2. 如果只把func.cpp改为func.c呢?

  3. 那如果只把test.cpp改为test.c呢?

  4. 如果都改呢?

1、4一定可以正常运行,我们主要看2、3.

c++调用c

error 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”的具体功能。

  1. 从c++调用c可以看得出extern “C”可以改变调用步骤的函数名,调用没有修饰过的函数名
  2. 从c语言调用c++可以看的出,extern “C”可以改变符号表中的函数名

最后:总结不易,大家三连支持以下啦,文章中不严谨测地方欢迎大家多多指出。


  1. 定义函数时后面括号里的东西,如(int* a, int* b) ↩︎

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/862494.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号