C++中的函数重载大家并不陌生,也经常使用。这篇博文首先简单介绍一下重载的规则与调用匹配,然后介绍一下重载的底层原理,最后再介绍一下 extern “C”的使用。
1. 重载规则与调用匹配
1.1 重载规则
首先来看下函数的重载规则,这个比较简单,如下:
- 函数名要相同。
- 参数个数不同,参数的类型不同,参数顺序不同,均可构成重载。
- 返回值类型不同则不可以构成重载。
如下:
void func(int a); // ok
void func(char a); // ok
void func(char a,int b); // ok
void func(int a, char b); // ok
char func(int a); // error 与第一个函数有冲突
为什么返回值类型不同不可以构成重载呢?因为有的函数虽然有返回值类型,但不与参数表达式运算,而作一条单独的语句。
1.2 调用匹配
匹配的原则也很简单:
1,严格匹配,找到则调用。
2,通过隐式转换寻求一个匹配,找到则调用。
举个简单的例子:
#include <iostream>
using namespace std;
void print(double a)
{
cout<<a<<endl;
}
void print(int a)
{
cout<<a<<endl;
}
int main()
{
print(1); // print(int)
print(1.1); // print(double)
print('a'); // print(int)
print(1.11f); // print(double)
return 0;
}
但是要注意的一点是:在C++中允许int到long和double类型的隐式转换,也允许double到int和float类型的隐式类型转换。遇到这种情况,则会引起二义性。比如将上面的函数void print(int a)
改成void print(long a)
,那么运行print(1)
就会报错。同样地,将void print(int a)
改成void print(float a)
,那么运行print(1.1)
就会报错。
2. 重载的底层原理
在C++中,利用name mangling(倾轧)技术来实现重载,通过命名倾轧来改名函数名,区分参数不同的同名函数。正因为这个倾轧技术需要根据参数的顺序和类型,所以参数的顺序和类型不同都可以构成重载。实现原理是:用v-c- i-f- l- d 表示void char int float long double 类型及其引用。举个简单的例子:
void print(int a); // print_i
void print(char a); // print_c
void print(int a, char b); // print_ic
void print(char a, int b); // print_ci
C++会把这些函数名倾轧成注释这种形式的函数名(实际中真实的可能并非就是这个名字,但是原理相同,这里只是为了简单说明问题),由此可见,即使函数名相同,在进行倾轧之后,它们的函数名已经不同了,所以彼此不会造成冲突。
name mangling 发生在两个阶段:.cpp 编译阶段和.h 的声明阶段。只有两个阶段同时进行倾轧,才能匹配调用。这很好理解,在声明阶段倾轧了,但是在编译阶段并没有倾轧,那么就会找不到这个函数,因为函数名已经不同了,所以必须同时进行才可以。
3. extern “C”的使用
由上面分析可知,C++的name mangling 必须在声明阶段和编译阶段都要进行才可,但是我们知道,C++是完全兼容C语言的,那就意味着,完全兼容C的类库。但是由.c文件的类库文件中的函数名,并没有发生name mangling 行为,而我们在包含.c 文件所对应的.h 文件时,.h 文件要发生name manling 行为,因而会发生在链接的时候的错误。举个简单的例子来模拟一下:
//mystring.h文件
int myStrlen(char *str);
//mystring.c文件
int myStrlen(char *str)
{
int len = 0;
while(*str++)
len++;
return len;
}
//main.cpp文件
#include <iostream>
#include "mystring.h"
using namespace std;
int main()
{
char s[] = "china";
char *p = s;
//char *p = "china"; // C++中不允许,无法从const char*转换成char*类型
int len;
len = myStrlen(p);
cout << len << endl;
return 0;
}
现在这样是没有问题的,声明阶段和编译阶段都会对myStrlen函数进行倾轧,但是如上文所述,.c文件的类库文件中的函数名不会发生倾轧,也就是说现在我们要让mystring.c文件中的myStrlen函数不发生倾轧,这里使用extern “C”可以做到,如下:
//mystring.c文件
extern "C"
{
int myStrlen(char *str)
{
int len = 0;
while(*str++)
len++;
return len;
}
}
这样的话,再次运行main.cpp文件就会发生错误。因为声明阶段发生了倾轧,而在编译阶段我们没有让它发生倾轧,这就导致了无法找到这个函数。这就模拟了上文提到的这个问题。那该如何解决这个问题呢?
C++为了避免上述错误的发生,重载了关键字extern。只需要在要避免name manling的函数前,加extern “C” 即可,如有多个,则extern “C”{}。那么现在既然.c文件的类库文件中的函数名不会发生倾轧,我们在include其对应的.h文件时,在.h文件中对应的函数上加上extern “C”,也不让其在声明阶段发生倾轧即可。我们看看常见的string.h中是如何处理的,在main.cpp中include一下”string.h”,然后跟踪进去,我们可以发现它的处理方式:
它先使用#ifdef __cplusplus,即在C++中不使用倾轧,因为C中没有重载这个东西,在C++中,类库的函数在编译时是不会进行倾轧的,所以要在这声明处加上extern “C”来防止声明的时候发生倾轧。所以我们平时只要#include了”string.h”就可以正常使用该类库中的函数,原因在于开发者们早已将它们在声明阶段取消了倾轧行为。
—–乐于分享,共同进步!
—–更多文章请看:http://blog.csdn.net/eson_15