这篇博客文字会比较多目的就是把命名倾轧说清楚,因为在之后的开发中很重要,读者读者可以多次阅读进行理解
函数重载(Function Overload)
函数重载和运算符重载我们在之前已经提到,现在专门说明函数重载。
函数重载会出现重名的函数,重名的函数会根据语境来决定调用,运算符重载也是一种函数重载,之后我们在运算符重载中详细说明。
我们通过代码进行函数重载演示:
#include
using namespace std;
//函数重载会出现同名函数,崇明函数会根据语境来决定调用
//运算符重载,也是一种函数重载
void func(int a)
{
cout << "void func(int a)" <<a<< endl;
}
void func(float a)
{
cout << "void func(float a)"<<a<< endl;
}
void func(char a)
{
cout << "void func(float a)" <<a<< endl;
}
int main()
{
int a = 1;
func(a);
float b = 1.23;
func(b);
char c = 'a';
func(c);
return 0;
}
执行结果为:
我们可以看到各自各自的函数参数进行匹配调用。
函数重载的好处:
一种简洁的需要
例如我们进行求绝对值:
如果不适用函数重载:
#include <iostream>
using namespace std;
int absOfint(int a)
{
return a > 0 ? a : -a;
}
float absOffloat(float a)
{
return a > 0 ? a : -a;
}
int main()
{
int a = -5;
cout << absOfint(a) << endl;
float b = -1.23;
cout << absOffloat(b) << endl;
return 0;
}
那么就是定义两个函数,执行结果为:
两个函数根据各自的函数名进行函数调用。
我们使用函数重载:
#include <iostream>
using namespace std;
int absOfData(int a)
{
return a > 0 ? a : -a;
}
float absOfData(float a)
{
return a > 0 ? a : -a;
}
int main()
{
int a = -5;
cout << absOfData(a) << endl;
float b = -1.23;
cout << absOfData(b) << endl;
return 0;
}
执行结果为:
函数重载规则
1 函数名相同
2参数个数不同,参数的类型不同,参数顺序不同均可以构成重载
3返回值类型,不作为重载的标准
void foo(int a)
{
}
void foo(float b)
{
}
void foo(int a,float b)
{
}
void foo(float b,int a)
{
}
上面的函数形式都构成函数重载。
匹配测试
我们先给出以下代码:
void foo(double a)
{
printf("double\n");
}
void foo(long b)
{
printf("long\n");
}
int main()
{
long a = 5;
foo(a);
double b = 5;
foo(b);
return 0;
}
执行结果为:
上面根据类型匹配函数重载调用没有问题。
但是如果我们定义的只是 int类型就会出现问题。
void foo(double a)
{
printf("double\n");
}
void foo(long b)
{
printf("long\n");
}
int main()
{
int a = 5;
foo(a);
}
上面的代码编译是不能通过的,会显示错误:有多个重载函数与参数对应。
C++ 允许:
int 到 long 和 double, double 到 int 和 float, int 到 short 和 char
等隐式类型转换。遇到这种情型,则会引起二义性。
所以我们引入匹配规则:
1.严格匹配,找到则调用
2.如果严格匹配不满足,寻求是否有隐式转化。找到则调用。
解决办法就是把匹配的类型都提前提供然后严格匹配调用。
重载实现机制
C++利用 Name Mangling(命名倾轧)技术,来改变函数名,区分参数不同的同名函数。
实现原理:用 v-c- i-f- l- d 表示 void char int float long double 及其引用。具体平台,实现有差异。
例如我们上面给出的:
void func(int a)//底层可能是func_i
{
cout << "void func(int a)" <<a<< endl;
}
void func(float a)//底层可能是func_i
{
cout << "void func(float a)"<<a<< endl;
}
void func(char a) //底层可能是func_c
{
cout << "void func(float a)" <<a<< endl;
}
int main()
{
int a = 1;
func(a);
float b = 1.23;
func(b);
char c = 'a';
func(c);
return 0;
}
每一个函数的底层函数名都是不一样的,我们所看到的函数名是相同的,但是在编译器看来都是不同的。
对生成的可执行文件 a.out,采用命令 nm a.out 查看符号表,显示两个重载的函数在编译后是如何区分的。
我们在linux平台进行演示:
通过vim创建aa.cpp文件代码如下:
#include <iostream>
using namespace std;
void foo(double d)
{}
void foo(int a)
{}
int main()
{
int a = 10;
foo(a);
return 0;
}
然后在linux平台进行编译
会生成a.out文件
然后我们使用nm a.out 命令进行查看
我们可以在里面找到我们定义的两个foo函数名,我们可以看见,在底层编译之后的函数名是不一样的。
命名倾轧extern “C”
Name Mangling(命名倾轧) 依据函数声明来进行倾轧的。若声明被倾轧,则调用为倾轧版本,若声明为非倾轧版本,则调用也为非倾轧版本。C/C++的编译都是以文件为单位进行编译的。
C++ 默认所有函数倾轧。若有特殊函数不参与倾轧,则需要使用 extercn “C” 来进行声明。
例如我们先给出以前在C语言里我们通过文件编译:
main.cpp
内容为:
#include "mystr.h"
#include <iostream>
using namespace std;
int main()
{
const char* p = "china";
int len = myStrlen(p);
cout << "n = " << len << endl;
}
mystr.h
内容为:
#pragma once
int myStrlen(const char*);
mystr.cpp
内容为:
int myStrlen(const char* s)
{
int len = 0;
while (*s)
{
len++;
s++;
}
return len;
}
我们通过编译运行结果为:
我们可以看到没有问题,接下来我们就这个例子进行倾轧说明。
extern “C” 的意思避免明明倾轧。
C++默认所有函数倾轧
我们知道函数的使用先看到声明,当把头文件包含使用之后,主函数中
int len = myStrlen§; 在调用函数的时候会根据.h里面的文件声明就会把.h里面的文件进行倾轧,在使用的时候自然就是前面在mystr.h里面倾轧之后的内容。这个时候在调用函数的时候就能够对应起来。
接下来 如果我们在
mystr.h里面使用extern “C” 避免倾轧
把mystr.h内容改为:
#pragma once
extern "C" int myStrlen(const char*);
那么由于C++对于所有函数都倾轧,所以在main函数中
#include "mystr.h"
#include <iostream>
using namespace std;
int main()
{
const char* p = "china";
int len = myStrlen(p);
cout << "n = " << len << endl;
}
#include "mystr.h"
是不倾轧的,其他所有内容倾轧,因为我们在mystr.h里面使用了extern “C”
那么main函数中
int len = myStrlen(p);
在进行函数调用的时候,由于声明的版本是未倾轧的,所以在调用的时候也是不倾轧版本,但是在mystr.cpp里面的内容也是进行倾轧的。
那么就会出现函数声明的时候没有倾轧,但是函数在实现的时候是发生倾轧,所以就会在链接的时候出现错误,无法连接,因为我们知道命名倾轧会在底层编译改变函数名,那么要解决这个问题就要在函数实现的地方也进行倾轧。
把mystr.cpp里面的内容改为:
extern "C" {
int myStrlen(const char* s)
{
int len = 0;
while (*s)
{
len++;
s++;
}
return len;
}
}
通过以上代码把mystr.cpp里面函数实现也改为不倾轧,那么这个时候,声明是不倾轧的,函数定义实现也是不倾轧的,在调用的时候就不会有问题。
前面我们运行不成功实例的是函数声明倾轧,函数在定义实现的时候不倾轧,那么如果我们把函数声明不倾轧,把函数定义倾轧也不能链接成功。所以对于一个函数来说要么在声明的和定义都倾轧,要么声明和定义都不倾轧,否则会出现连接错误。
C++完全兼容C语言,如果C语言是源代码的方式给出,放在C++里面重新编译的时候会全部倾轧,没有问题的。但是像C标准库一样,给出的.so 只能在链接环节使用,C标准库里面多提供的都是未倾轧的,但是我们在C++里面如果使用头文件进行包含的时候就会把头文件里面的所有声明进行倾轧,那么就会出现问题,那么在C++里面使用函数调用的时候,使用在头文件里面倾轧的函数声明,调用库文件里面未倾轧的函数实现在链接的时候是会链接失败。但是我们在正常使用的过程中没有发生连接失败,是因为我们在头文件里面加上了ertern “C” 那么凡是C的头文件里声明面都不会参与倾轧,函数实现是通过C库函数进行实现也就没有参与倾轧。那么非倾轧的函数声明链接非倾轧的库就没有问题。所以在所有的C头文件里面都会有
意思就是所如果这个头文件被C++引用的话,头文件里面在出现上面代码之后的所有内容都不参与命名倾轧。
那么如果出现一种情况,函数实现就是给C语言的,也就是说函数在实现的时候就是未倾轧版本,但是在声明的时候包含到头文件还是要参与倾轧,但是我们最好不要进入头文件该的话就可以使用下面的方式进行修改:
extern “C”
{
#include “mystr.h”
}
那么C++里面不可避免的要使用C库,所以命名倾轧的理解还是很重要的。
小结
C++ 完全兼容 C 语言,那么就面临着,完全兼容 C 的类库。由.c 文件的生成的库文件 中函数名,并没有发生 name mangling 行为,而我们在包含.c 文件所对应的.h 文件时,.h 文件要发生 name manling 行为,因而会在链接的时候发生的错误。 C++为了避免上述错误的发生,重载了关键字 extern。只需要在避免 name manling 的函数前,加 extern “C”,如有多个,则 extern “C”{ } 将函数的声明放入{}中即可。