提示:我是菜狗,请勿借鉴,概率出错,欢迎指正。
目录
前言
我个人认为函数重载是C++中一个挺爽的特性,因为它可以让代码的语言更加明了美观,不至于像C一样,明明是相同功能的函数,因为参数的些许差异,就一定要求函数名加上前后缀加以区分。所以今天有时间就总结一下我对C++中函数重载的认识。
一、函数重载的定义
老规矩,先是AI定义:函数重载(Function Overloading)是C++中的一种特性,它允许在同一个作用域内定义多个同名函数,但它们的参数列表(参数的数量或类型)必须不同。
然后是我个人的感悟,同时也是对上面定义的些许补充(其实AI已经讲的很好了,我已经汗流浃背了):函数重载是C语言所没有,由C++所添加的新型特性,通过函数名修饰规则(后面会提及)在编译阶段区别同名函数,以此来实现编程特定需求(例如美化代码、提高代码可读性)。
纸面的文字实数枯燥,不如移步示例:
#include<iostream>
using namespace std;
//以下四个函数构成func函数重载
void func(int i,double j)
{
cout << "1" << endl;
}
//参数顺序不同
void func(double j,int i)
{
cout << "2" << endl;
}
//参数类型不同
void func(int i,int j)
{
cout << "3" << endl;
}
//参数个数不同
void func(int i)
{
cout << "4" << endl;
}
int main()
{
func(1,1.1);
func(1.1,1);
func(1,1);
func(1);
return 0;
}
运行结果如下
二、函数重载的底层原理
1.函数名修饰规则
C++中的函数重载底层原理涉及到的函数修饰规则,主要涉及到函数名的改编或重命名机制,这在C++中被称为Name Mangling(名称倾轧)技术。这种技术使得C++编译器能够区分同名但参数不同的函数,实现函数重载。
这里要注意,不同编译环境下函数名修饰规则不同,得到的修饰函数名是不同的,想要验证以上结论也不是很难的事。只需要构建重载函数,然后去除定义,保留声明的部分,编译器就会报错。报错的内容中会有一段话“无法解析的外部符号‘****’(XXX)”其中括号内部的内容(XXX)就是修饰过的函数名。
纸面的文字实数枯燥,不如移步示例(我常用的IDE是VS2023,所以函数名修饰规则比较别致):
#include<iostream>
using namespace std;
//以下四个函数构成func函数重载
void func(int i, double j);
//参数顺序不同
void func(double j, int i);
//参数类型不同
void func(int i, int j);
//参数个数不同
void func(int i);
int main()
{
func(1, 1.1);
func(1.1, 1);
func(1, 1);
func(1);
return 0;
}
如图所示,VS环境下的函数名修饰成(?func@@YAXHN@Z),其中func是原本的函数名,H指代int,N指代double,其他的字符也各有含义,在这就不一一赘述了。
讲到这里,我们大抵了解了C++区分同名函数的方法,但这还不足让我们了解函数重载的全貌——因为我们还不知道C++是在哪里区分的,以及区分的意义。
2.编译
众所周知,在C/C++中程序员所编写的代码要经历四个过程——预处理、编译、汇编、链接。其中最值得了解,同时也是最复杂的就是编译阶段。当然,在这我们不会展开来讲(笑死,因为我根本不懂),我们只要知道它的些许原理和函数使用的关系即可,想要详细了解的可以去搜索一下(有点吓人)。
编译(点击查看AI定义)就是把源代码变成目标代码的过程。如果是C/C++作为源代码,汇编语言作为目标代码,在这个过程中,它对函数会有一个处理方式,就是符号表的生成。那为什么要生成符号表呢?这涉及到函数的调用。
编译过程中,所有的函数定义都会转化成一条条指令,存储在某处地址,等待调用。而代码内部调用函数就是通过符号表中函数名找到函数定义存储地址,然后调用。
符号表图形示例(注意:这只是抽象出来,方便理解的示例图,并不代表编译的底层逻辑)
函数名 | 地址 |
?func@@YAXHN@Z | 0601082h |
?func@@YAXNH@Z | 0602092h |
函数名 | 地址 |
func | 0601082h |
func | 0601082h |
在C++中,代码在编译阶段可以对同名函数生成可区分的符号表,从而对应的调用函数,支持函数重载。其本质上就是将同名函数转化成不同名函数,然后按照原来的方法编译,并没有多么高大上,原理意外的简单。
而C语言之所以不支持函数重载就是因为没有Name Mangling(名称倾轧)技术,无法在编译阶段区分同名函数,从而无法生成符号表。
验证也不难,就copy最上面的函数重载的代码,然后打开反汇编,找到函数名所在的位置就可以看到指令“call”,其后面的地址就是函数地址。
三、函数重载的应用场景
-
不同类型的参数:
当需要对不同类型的输入数据进行相同的操作时,可以使用函数重载。例如,你可能需要打印整数和浮点数,可以分别定义两个重载的打印函数。void print(int i) { std::cout << "Integer: " << i << std::endl; } void print(double d) { std::cout << "Double: " << d << std::endl; }
-
不同数量的参数:
有时,一个函数需要处理不同数量的参数。例如,一个计算矩形面积的函数,可以重载以处理只有宽度或既有宽度又有高度的情况。double area(double width) { // 默认高度为1(例如,一维情况) return width * 1.0; } double area(double width, double height) { return width * height; }
-
不同类型的操作:
当同一个操作需要对不同类型的对象进行不同的处理时,可以使用函数重载。例如,对不同类型的对象进行序列化或反序列化。class Base { public: virtual void serialize(const std::string& filename) const = 0; }; class Derived1 : public Base { public: void serialize(const std::string& filename) const override { // Derived1 特有的序列化逻辑 } }; class Derived2 : public Base { public: void serialize(const std::string& filename) const override { // Derived2 特有的序列化逻辑 } };
-
增强代码可读性:
函数重载可以使代码更加简洁和可读。例如,对向量进行各种操作(如加、减、点积等)时,可以使用重载函数来减少函数名的长度和复杂性。Vector operator+(const Vector& v1, const Vector& v2) { return Vector(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z); } double dotProduct(const Vector& v1, const Vector& v2) { return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z; }
-
实现多态性:
虽然函数重载本身不是多态性(多态性通常通过继承和虚函数实现),但重载函数可以与多态性结合使用。例如,在基类中定义虚函数,并在派生类中重载这些函数。class Base { public: virtual void doSomething() const = 0; }; class DerivedA : public Base { public: void doSomething() const override { // DerivedA 特有的实现 } }; class DerivedB : public Base { public: void doSomething() const override { // DerivedB 特有的实现 } };
-
简化接口设计:
在设计库或框架时,函数重载可以用来简化接口。例如,提供默认参数和非默认参数的函数版本,可以让用户根据需要选择调用哪个版本。void connect(const std::string& host, int port, int timeout = 30) { // 连接逻辑 } // 重载版本,不使用默认超时 void connect(const std::string& host, int port) { connect(host, port, 30); // 调用带默认参数的版本 }
通过函数重载,我们可以编写更加灵活和易于维护的代码,提高代码的可读性和可重用性。
总结
函数重载不算C++的重要特性,但是了解一下有利于对C++学习,了解C与C++的不同之处。最后,我有一个问题——根据参数不同可以设计函数重载,那么可以根据返回值不同来设计吗?望诸位道友与我一道探究编程的奥妙,领悟修仙的真谛。
种图
独乐乐,不如众乐乐