c++(6)

  C++中的函数重载大家并不陌生,也经常使用。这篇博文首先简单介绍一下重载的规则与调用匹配,然后介绍一下重载的底层原理,最后再介绍一下 extern “C”的使用。

1. 重载规则与调用匹配

1.1 重载规则

首先来看下函数的重载规则,这个比较简单,如下:

  1. 函数名要相同。
  2. 参数个数不同,参数的类型不同,参数顺序不同,均可构成重载。
  3. 返回值类型不同则不可以构成重载。

如下:

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
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

  为什么返回值类型不同不可以构成重载呢?因为有的函数虽然有返回值类型,但不与参数表达式运算,而作一条单独的语句。

1.2 调用匹配

匹配的原则也很简单:

1,严格匹配,找到则调用。 
2,通过隐式转换寻求一个匹配,找到则调用。

举个简单的例子:

#include 
using namespace std;

void print(double a)
{
    cout<void print(int a)
{
    cout<int main()
{
    print(1);   // print(int)
    print(1.1); // print(double)
    print('a'); // print(int)
    print(1.11f); // print(double)
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

  但是要注意的一点是:在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
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

  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 
#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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

  现在这样是没有问题的,声明阶段和编译阶段都会对myStrlen函数进行倾轧,但是如上文所述,.c文件的类库文件中的函数名不会发生倾轧,也就是说现在我们要让mystring.c文件中的myStrlen函数不发生倾轧,这里使用extern “C”可以做到,如下:

//mystring.c文件
extern "C"
{
    int myStrlen(char *str)
    {
        int len = 0;
        while(*str++)
            len++;
        return len;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

  这样的话,再次运行main.cpp文件就会发生错误。因为声明阶段发生了倾轧,而在编译阶段我们没有让它发生倾轧,这就导致了无法找到这个函数。这就模拟了上文提到的这个问题。那该如何解决这个问题呢? 
  C++为了避免上述错误的发生,重载了关键字extern。只需要在要避免name manling的函数前,加extern “C” 即可,如有多个,则extern “C”{}。那么现在既然.c文件的类库文件中的函数名不会发生倾轧,我们在include其对应的.h文件时,在.h文件中对应的函数上加上extern “C”,也不让其在声明阶段发生倾轧即可。我们看看常见的string.h中是如何处理的,在main.cpp中include一下”string.h”,然后跟踪进去,我们可以发现它的处理方式: 
c++(6)_第1张图片 
  它先使用#ifdef __cplusplus,即在C++中不使用倾轧,因为C中没有重载这个东西,在C++中,类库的函数在编译时是不会进行倾轧的,所以要在这声明处加上extern “C”来防止声明的时候发生倾轧。所以我们平时只要#include了”string.h”就可以正常使用该类库中的函数,原因在于开发者们早已将它们在声明阶段取消了倾轧行为。

你可能感兴趣的:(c++(6))