C++解析(7):函数重载分析
0.目录
1.重载的概念
2.C++中的函数重载
3.函数默认参数遇上函数重载
4.编译器调用重载函数的准则
5.重载与指针
6.C++和C相互调用
7.小结
1.重载的概念
自然语言中的上下文——你知道下面词汇中“洗”字的含义吗?
结论:
- 能和“洗”字搭配的词汇有很多
- “洗”字和不同的词汇搭配有不同的含义
重载的概念:
重载(Overload)——同一个标识符在不同的上下文有不同的意义
如:
重载在自然语言中是随处可见的。那么程序设计中是否也有重载呢?
2.C++中的函数重载
函数重载(Function Overload):
- 用同一个函数名定义不同的函数
- 当函数名和不同的参数搭配时函数的含义不同
函数重载至少满足下面的一个条件:
- 参数个数不同
- 参数类型不同
- 参数顺序不同
下面的两个函数可以构成重载函数吗?
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()
{
return 0;
}
#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 c = func(1, 2);
return 0;
}
第一个程序能编译通过,第二个程序不能编译通过,报错信息如下:
[root@bogon Desktop]# g++ test.cpp
test.cpp: In function ‘int main()’:
test.cpp:16: error: call of overloaded ‘func(int, int)’ is ambiguous
test.cpp:4: note: candidates are: int func(int, int, int)
test.cpp:9: note: int func(int, int)
大概意思是编译器说我也不知道调用哪个,所以直接报错了。(造成了二义性,因为确实可能连你自己也不知道想要调用哪一个。)
C++引入了太多的特性进入这门语言,然而一些特性之间又是相冲突的。这就是我们遇到的第一个相冲突的地方。在Java中就会发现,默认值是不允许的。
4.编译器调用重载函数的准则
编译器调用重载函数的准则:
- 将所有同名函数作为候选者
- 尝试寻找可行的候选函数
- 精确匹配实参
- 通过默认参数能够匹配实参
- 通过默认类型转换匹配实参
- 匹配失败
- 最终寻找到的候选函数不唯一,则出现二义性,编译失败。
- 无法匹配所有候选者,函数未定义,编译失败。
函数重载的注意事项:
- 重载函数在本质上是相互独立的不同函数
- 重载函数的函数类型不同
- 函数返回值不能作为函数重载的依据
函数重载是由函数名和参数列表决定的!
函数重载的本质就是定义了相互独立不相同的函数。(下面将证明(重头戏!!!))
(如何证明两个add函数不同呢?)
见证奇迹的时刻——运行结果为:
00DF100A
00DF100F
(地址不同,证明函数入口地址不同,反向证明两个add是不同的函数。)
(编译器是怎么看待这两个add函数的呢?)
编译器产生的中间结果:Test.obj
(使用的编译器是vs2010,进入对应的工程文件夹,有一个Debug文件夹,打开后找到Test.obj,这就是编译Test.cpp后产生的中间文件)
(打开vs自带的命令行工具,输入dumpbin)
dumpbin这个命令是VC所提供的。
这里有很多选项,我们只需要关注符号表。
符号表就是编译器在编译的过程当中根据源代码所生成的一张表,这张表里面有程序里面的各个函数名变量名等等。
输入dumpbin /symbols (Test.obj的路径):
仔细观察发现,标记的那一行就是我们第一个有两个参数的add函数被编译过后生成的符号表当中的一项。
仔细观察这里还有一个有三个参数的add函数编译过后产生的符号表里的一项。
这就相当于注释的东西,编译器编译这两个函数之后所得到的标识符就是?add@@YAHHH@Z和?add@@YAHHHH@Z,在我们看来的add是函数名,但是编译器编译过后它就不认为add是函数名了。对比发现,两个标识符不相同,这就意味着编译器在编译这两个函数的时候已经分开对待了。编译器看到的东西和我们看到的不一样,我们所看到的这两个函数的名字是一样的,然而编译器看到的这两个函数的名字是不同的。因为编译器觉得这是两个不同的函数,所以编译出的最终结果也就是可执行程序当中,这两个add入口地址不同也就很容易理解了。
5.重载与指针
下面的函数指针将保存哪个函数的地址?
函数重载遇上函数指针——将重载函数名赋值给函数指针时:
- 根据重载规则挑选与函数指针参数列表一致的候选者
- 严格匹配候选者的函数类型与函数指针的函数类型
观察下面代码:
#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 c = 0;
PFUNC p = func;
c = p(1);
printf("c = %d\n", c);
return 0;
}
运行结果为:
[root@bogon Desktop]# g++ test.cpp
[root@bogon Desktop]# ./a.out
c = 1
(将typedef int…处换成typedef void…或者是typedef double…都会报错,证明了是严格匹配。)
注意:
- 函数重载必然发生在同一个作用域中
- 编译器需要用参数列表或函数类型进行函数选择
- 无法直接通过函数名得到重载函数的入口地址
只靠函数名的话因为存在二义性所以无法直接通过函数名得到重载函数的入口地址!此时可以依靠函数指针来获取函数入口地址:
#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;
}
运行结果为:
[root@bogon Desktop]# g++ test.cpp
[root@bogon Desktop]# ./a.out
0x4005a4
0x4005b9
6.C++和C相互调用
- 实际工程中C++和C代码相互调用是不可避免的
- C++编译器能够兼容C语言的编译方式
- C++编译器会优先使用C++编译的方式
- extern关键字能强制让C++编译器进行C方式的编译
(C++编译C程序不是直接可以通过吗?确实,直接编译源码可以通过。但是,在工程中,有一些C代码已经不是源码了,已经被编译成了目标文件。相当于我们在C工程里面会用到一些第三方的库,然而这些第三方的库是用C语言编写并编译的,而且这些第三方的库还是要钱的。既然花钱买了,那我们选择了C++后就不用了吗?肯定不可能,所以必须要让C++代码也能够去使用C语言编写的库,那这会不会有问题呢?)
对于以下两段代码:
// add.c
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
// add.h
int add(int a, int b);
命令行执行代码(生成add.o):
[root@bogon Desktop]# gcc -c add.c -o add.o
此时:
// main.cpp
#include <stdio.h>
#include "add.h"
int main()
{
int c = add(1, 2);
printf("c = %d\n", c);
return 0;
}
此程序用C++编译器编译出错:
[root@bogon Desktop]# g++ main.cpp
/tmp/ccwHtgIL.o: In function `main':
main.cpp:(.text+0x13): undefined reference to `add(int, int)'
collect2: ld returned 1 exit status
编译器说add函数未定义。真的没定义吗?
命令行中输入
[root@bogon Desktop]# nm add.o
0000000000000000 T add
发现:符号表中确实有add函数。
那为什么编译说没有呢?因为,这里的add.o是C编译的,C++和C它们的编译方式是不同的。然而C++编译器肯定能兼容C语言的编译方式,但是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;
}
这样就告诉了C++编译器,所包含的这个头文件是C代码,必须用C的方式来编译它。
运行结果为:
[root@bogon Desktop]# g++ main.cpp add.o
[root@bogon Desktop]# ./a.out
c = 3
问题:如何保证一段C代码只会以C的方式被编译?
extern “C”是C++中的代码,C语言中没有。
所以没有办法保证这段程序既能被C++编译器编译通过,也能被C编译器编译通过。
解决方案:
__cplusplus
是C++编译器内置的标准宏定义__cplusplus
的意义——确保C代码以统一的C方式被编译成目标文件
这个程序就可以被C和C++编译器都编译通过:
// main.c
#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;
}
运行结果为:
[root@bogon Desktop]# gcc main.c add.o
[root@bogon Desktop]# ./a.out
c = 3
[root@bogon Desktop]# g++ main.c add.o
[root@bogon Desktop]# ./a.out
c = 3
#ifdef __cplusplus
用来判断是不是C++编译器
注意事项:
-
C++编译器不能以C的方式编译重载函数
-
编译方式决定函数名被编译后的目标名
- C++编译方式将函数名和参数列表编译成目标名
- C编译方式只将函数名作为目标名进行编译
对于这个程序:
// test.cpp
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;
}
命令行输入:
[root@bogon Desktop]# g++ -c test.cpp -o test.oo
[root@bogon Desktop]# nm test.oo
0000000000000000 T _Z3addii
0000000000000015 T _Z3addiii
U __gxx_personality_v0
大家看到addii和addiii比较一下。ii和iii代表了参数。i就是int。
所以:
// test.cpp
extern "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;
}
}
命令行输入:
[root@bogon Desktop]# g++ -c test.cpp -o test.oo
test.cpp: In function ‘int add(int, int, int)’:
test.cpp:10: error: declaration of C function ‘int add(int, int, int)’ conflicts with
test.cpp:5: error: previous declaration ‘int add(int, int)’ here
编译器报错!!!
编译报错说这两个函数通过C方式编译过后得到的目标名相冲突!!!
7.小结
- 函数重载是C++中引入的概念
- 函数重载用于模拟自然语言中的词汇搭配
- 函数重载使得C++具有更丰富的语义表达能力
- 函数重载的本质为相互独立的不同函数
- C++中通过函数名和函数参数确定函数调用
- 函数重载是C++对C的一个重要升级
- 函数重载通过函数参数列表区分不同的同名函数
- extern关键字能够实现C和C++的相互调用
- 编译方式决定符号表中的函数名的最终目标名