1 为什么要出现函数重载?
我们平时在写代码中,有可能会遇到要用到几个函数,但是他们的实现功能相同,但是有些细节却不同。例如:交换两个数的值,其中包括(int,float,char,double)这些个类型。在 C语言中我们是利用不同的函数名来加以区分。
void Swap1(int* a, int* b);
void Swap2(float* a, float* b);
void Swap3(char* a, char* b);
void Swap4(double* a, double* b);
我们可以看出这样的代码不美观,而且给程序员也带来了很多的不便。于是在 C++ 中,人们提出了用一个函数名定义多个函数,也就是所谓的函数重载。
2 函数重载的定义
在 C++ 中,函数重载指的是定义多个具有相同函数名,但是参数列表不同的函数。这些函数可以具有不同的参数类型、参数个数或参数顺序,但是他们的函数名必须相同。
注意:重载函数的参数个数、参数类型或参数顺序,这三者中必须用一个不同。
#include<Windows.h>
#include<iostream>
using namespace std;
int Add(int a, int b)
{
return a + b;
}
double Add(double a, double b)
{
return a + b;
}
float Add(float a, float b)
{
return a + b;
}
int main()
{
cout<<Add(1,2)<<endl;
cout<<Add(3.5, 4.5)<<endl;
cout << Add(2.22, 3.33) << endl;
system("pause");
return 0;
}
函数重载是一种多态性的表现形式,它允许在同一作用域定义多个同名函数,以便于程序员更加方便地调用不同的函数,从而提高程序的可读性和可维护性。
在调用函数时,编译器会根据函数调用的实参类型和个数来选择相应的函数进行调用。如果没有找到完全匹配的函数,则编译器会尝试进行隐式类型转换或者参数个数的匹配来寻找最合适的函数进行调用。如果仍然无法找到匹配的函数,则会出现编译错误。
需要注意的是,函数重载是在编译时进行决策的,而不是在运行时进行决策的。因此,函数重载对程序的性能影响非常小。
函数重载的规则:
● 函数名必须相同
● 参数列表必须不同(个数不同、类型不同、参数排序顺序不同等)
● 函数的返回值可以相同也可以不同
● 仅仅返回类型不同不足以成为函数重载
3 为什么C++中可以出现函数重载
C++ 支持函数重载是因为其面向对象的特性,允许程序员在同一作用域内定义多个同名函数,从而实现多态性的特性。
而 C 语言不支持函数重载是因为其设计初衷是为了实现简洁的低级别编程,没有面向对象的特性。在 C 语言中,所有的函数都是全局函数,通过函数名来进行调用,没有重载的概念。
在 C++中,编译器编译函数的过程中,会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名,这就能实现根据输入的参数类型不同,进而选择调用相应的函数名相同的函数;而 C 语言并不支持函数重载,在编译器编译函数的过程中,不会带上函数的参数类型,一般只包括函数名。所以,C 语言中函数名是唯一的标识,所以不能在同一个作用域内定义多个同名函数。
4 函数重载的底层实现原理
我们先来回顾以下程序编译的过程:
编译器如何去查找调用函数?
在 filename.o 文件里会生成一个符号表,符号表里存着函数的修饰名,修饰名记录保存着函数的地址。
在调用函数时,编译器会通过符号表里查看修饰名来得到函数的地址实现调用。
在 main 函数的指令中,有两句指令 call,call 后面所跟的就是函数的地址,函数的名字和函数的地址。然后我们来看一下这个过程,真正的函数地址就是第一句指令的地址。函数的名字也会有自己的命名规则。
4.1 函数名修饰
函数名字修饰是一种将函数名和参数类型等信息组合起来构成一个新的名称的技术。它通常用于支持函数重载、命名空间和链接库等特性。
1、C语言中函数名字修饰规则
在 C 语言中,没有函数重载和命名空间等特性,因此函数名字修饰的规则相对简单。一般为在函数名前加一个下划线,例如变成 _fun()。所以如果 C 程序里有两个相同名字的函数,那么在编译的时候,它们生成的修饰名一样,调用时编译器找的时候就不知道用哪一个,也就解释了为什么 C 不支持函数重载。
2、C++中函数名字修饰规则
C++的函数名字修饰规则有些复杂,但是信息更加充分,通过分析修饰名,可以知道函数的调用方式、返回值类型、参数个数甚至参数类型。
命名规则: _Z+函数名长度+函数名+类型首字符
但是在不同的编译器下规则会有所不同。
所以 C++ 的重载函数名字相同,但是编译出的修饰函数名不同,就可以对应参数 不同调用到相应的函数了。
我们假设有下面这个 C++ 程序,其中定义了两个同名但参数类型不同的函数 foo:
#include <iostream>
void foo(int x) {
std::cout << "foo(int): " << x << std::endl;
}
void foo(double x) {
std::cout << "foo(double): " << x << std::endl;
}
int main() {
foo(42);
foo(3.14);
return 0;
}
当我们使用编译器编译这个程序时,它会为这两个 foo 函数生成不同的名称,以便在链接时区分它们。我们可以使用一个工具来查看编译器生成的名称,这个工具就是 nm 命令,它可以显示一个可执行文件或目标文件中的符号表,其中包含了函数、变量等的名称和地址信息。
我们可以在终端中使用 nm 命令查看编译器生成的符号表,如下所示:
$ g++ -c example.cpp
$ nm example.o
0000000000000000 T _Z3fooi
0000000000000010 T _Z3food
可以看到,编译器为 foo(int) 生成的名称是 _Z3fooi,为 foo(double) 生成的名称是 _Z3food。这些名称包含了函数名字 foo 和参数类型信息,以确保不同的 foo 函数生成不同的名称。
在程序中调用 foo 函数时,编译器会根据函数参数的类型来选择正确的函数。例如,当我们调用 foo(42) 时,编译器会选择 foo(int),输出的结果是 foo(int): 42;当我们调用 foo(3.14) 时,编译器会选择 foo(double),输出的结果是 foo(double): 3.14。
这就是 C++ 中函数名字修饰的原理和实现方式。需要注意的是,不同的编译器可能会生成不同的函数名称,因此在链接不同编译器生成的目标文件时可能会出现问题。为了避免这种问题,C++ 标准库提供了 extern "C" 的语法,用于告诉编译器使用 C 语言的函数名字修饰方式,从而使得函数能够在不同编译器之间正确链接。文章来源:https://www.uudwc.com/A/W1n8R/
文章来源地址https://www.uudwc.com/A/W1n8R/