C++超详细分析函数重载的使用
一、函数重载分析(上)
1.1 重载的定义
定义:同一个标识符在不同的上下文有不同的意义
1.2 函数重载的定义
- 用同一个函数名定义不同的函数
- 当函数名和不同的参数搭配时函数的含义不同
如下:
下面看一段代码,感受一下:
#include <stdio.h> #include <string.h> int func(int x) { return x; } int func(int a, int b) { return a + b; } int func(const char* s) { return strlen(s); } int main(int argc, char *argv[]) { printf("%d\n", func(3)); printf("%d\n", func(4, 5)); printf("%d\n", func("D.T.Software")); return 0; }
下面为输出结果:
1.3 函数重载需要满足的条件
函数重载至少满足下面的一个条件:
- 参数个数不同
- 参数类型不同
- 参数顺序不同
下图所示就是参数的顺序不同:
下面看一个函数默认参数遇上函数重载的实例程序:
#include <stdio.h> int func(int a, int b, int c = 0) { return a * b * c; } int func(int a, int b) { return a + b; } int main(int argc, char *argv[]) { int c = func(1, 2); return 0; }
下面为输出结果:
编译报错,因为模棱两可。如果说调用第一个函数说的过去,因为符合函数默认参数规则,c 的值已经确定;调用第二个函数也符合常理,所以编译不会通过。
1.4 编译器调用重载函数的准则
将所有同名函数作为候选者
尝试寻找可行的候选函数
- 精确匹配实参
- 通过默认参数能够匹配实参
- 通过默认类型转换匹配实参
匹配失败
- 最终寻找到的候选函数不唯一,则出现二义性,编译失败。
- 无法匹配所有候选者,函数未定义,编译失败。
1.5 函数重载的注意事项
- 重载函数在本质上是相互独立的不同函数
- 重载函数的函数类型不同
- 函数返回值不能作为函数重载的依据
函数重载是由函数名和参数列表决定的!!!
函数重载的本质是什么?下面通过一段代码深入分析,编译环境为VS2012。
#include "stdafx.h" #include <stdio.h> int add(int a, int b) // int(int, int) { return a + b; } int add(int a, int b, int c) // int(int, int, int) { return a + b + c; } int main() { printf("%p\n", (int(*)(int, int))add); printf("%p\n", (int(*)(int, int, int))add); return 0; }
由C语言的知识可以知道,函数名就是函数的入口地址,所以输出结果如下:
可以看到,两个 add() 函数的入口地址不一样,所以这两个 add 是两个不同的函数。
编译器是如何看待这两个 add() 函数的呢?下面来深入分析。先看一下编译器产生的中间结果,在Test -> Debug -> Test.obj 文件中。
然后使用VS2012里面自带的命令行工具查看 Test.obj 里面有什么东西。
上图示为VS2012 命令行所在位置
输入 dumpbin,如下:
这里只需要关系 SYMBOLS(符号表),符号表就是编译器在编译过程中根据源代码所生成的一张表,这张表有程序的函数名变量等等。
输入以下命令,其中 /symbols 后面为 Test.obj 所在的位置。
找到下面的地方,可以看到编译器编译 (int __cdecl add(int,int)) 时标识符为?add@@YAHHH@Z;而编译器编译(int __cdecl add(int,int,int)) 时标识符为?add@@YAHHHH@Z ,也就是说编译器在编译这两个函数时已经把这两个函数分别对待,尽管它们名字一样,所以两个 add() 函数的入口地址不一样,这就很好理解了。
1.6 小结
- 函数重载是 C++ 中引入的概念
- 函数重载用于模拟自然语言中的词汇搭配
- 函数重载使得 C++ 具有更丰富的语义表达能力
- 函数重载的本质为相互独立的不同函数
- C++ 中通过函数名和函数参数确定函数调用
二、函数重载分析(下)
2.1 函数重载遇上函数指针
将重载函数名赋值给函数指针时
- 根据重载规则挑选与函数指针参数列表一致的候选者
- 严格匹配候选者的函数类型与函数指针的函数类型
下面看一段代码:
#include <stdio.h> #include <string.h> int func(int x) { return x; } int func(int a, int b) { return a + b; } int func(const char* s) { return strlen(s); } typedef int(*PFUNC)(int a); int main(int argc, char *argv[]) { int c = 0; PFUNC p = func; c = p(1); printf("c = %d\n", c); return 0; }
下面为输出结果:
这也就是前面说的通过函数指针所指向的函数类型参数列表来进行选择。
注意事项
- 函数重载必然发生在同一个作用域中
- 编译器需要用参数列表或函数类型进行函数选择
- 无法直接通过函数名得到重载函数的入口地址(可以通过指针来获取)
如下,这段代码想通过函数名获取重载函数的入口地址:
#include <stdio.h> int add(int a, int b) // int(int, int) { return a + b; } int add(int a, int b, int c) // int(int, int, int) { return a + b + c; } int main() { printf("%p\n", add); printf("%p\n", add); return 0; }
编译的时候会报错,无法确定是哪个函数。
2.2 C++和C的相互调用
- 实际工程中C++和C代码相互调用是不可避免的
- C++编译器能够兼容C语言的编译方式
- C++编译器会优先使用C++编译的方式
- extern 关键字能强制让C++编译器进行C方式的编译
如下:
在 Linux环境下新建一个 9-2 文件夹,先在文件夹下新建 add.c 和 add.h 文件,如下:
add.c :
#include "add.h" int add(int a, int b) { return a + b; }
add.h :
int add(int a, int b);
通过 linux 命令 cd 进入 9-2 文件夹,再将 add.c 转换成 add.o 文件,如下所示:
然后在 9-2 文件夹下建一个 main.cpp 文件,如下:
mian.cpp :
#include <stdio.h> #include "add.h" int main() { int c = add(1,2); printf("c = %d\n", c); return 0; }
对程序进行编译,发现程序报错,没有定义 add() 函数,但是函数确实已经定义了,可以使用 linux 中的 nm 指令查看 add.o 里面的信息,打印出来的就是符号表信息,可以看到确实有 add 。
这个时候就需要使用 extern关键字强制让C++编译器进行C方式的编译,所以 main.cpp就要修改成这样:
#include <stdio.h> extern "C" { #include "add.h" } int main() { int c = add(1,2); printf("c = %d\n", c); return 0; }
这样编译就能通过了:
如果在 9-2 文件中新建一个 main.c 文件,main.c 里面的代码 与 main.cpp 中的相同。
进行编译,发现会报错误,因为 extern 关键词写法是 C++ 中的, C语言不支持该写法。那有没有一种写法既能被 C语言编译通过,又能让 C++编译通过呢?且看下面。
2.3 使得C代码只会以C的方式被编译的解决方案
- _cplusplus 是C++编译器内置的标准宏定义
- _cplusplus 的意义是确保C代码以统一的C方式被编译成目标文件
如下:
所以上述代码可以写作,main.c和 main.cpp 均为:
#include <stdio.h> #ifdef __cplusplus extern "C" { #endif #include "add.h" #ifdef __cplusplus } #endif int main() { int c = add(1, 2); printf("c = %d\n", c); return 0; }
这样程序在 C语言和 C++ 的编译环境下均能通过,如下:
注意事项
C++编译器不能以C的方式编译重载函数
编译方式决定函数名被编译后的目标名
- C++编译方式将函数名和参数列表编译成目标名
- C 编译方式只将函数名作为目标名进行编译
下面通过一个例子说明一下:
int add(int a, int b) // int(int, int) { return a + b; } int add(int a, int b, int c) // int(int, int, int) { return a + b + c; }
将该代码编译成目标文件,取名为 test.oo,然后通过 linux 中的 nm 命令查看 test.oo 中的东西,可以看到 test 符号表里面有两个东西 T _Z3addii 和T _Z3addiii,这就是 add 函数被编译过后的目标函数名,ii 表示两个参数, iii 表示三个参数。
如果采用 C 方式编译重载函数,代码如下:
extern "C" { int add(int a, int b) // ==>add { return a + b; } int add(int a, int b, int c) // ==>add { return a + b + c; } }
下面为编译结果,可以看到编译报错,说两个 add() 函数冲突了。
2.4 小结
- 函数重载是 C++ 对 C 的一个重要升级
- 函数重载通过函数参数列表区分不同的同名函数
- extern 关键字能够实现 C 和 C++的相互调用
- 编译方式决定符号表中的函数名的最终目标名