文章目录
- 总结
- 1、重载
- 示例代码
- 特点
- 1. 模板函数和非模板函数重载
- 2. 重载示例与调用规则
- 示例代码
- 调用规则解释
- 3. 特殊情况与注意事项
- 二义性问题
- 函数特化与重载的交互
- 2. 函数隐藏(Function Hiding)
- 概念
- 示例代码
- 特点
- 3. 函数覆盖(重写,Function Overriding)
- 概念
- 示例代码
- 特点
总结
1、重载:在同一个作用域,函数名相同,参数列表不同,与返回值无关
2、隐藏:在基类和派生类之间发生的关系,函数名相同,派生类的函数把基类的函数给隐藏了,只关注函数数名,不管返回值和参数
3、覆盖:覆盖是隐藏的一种特殊情况,派生类和基类的函数,1 函数名相同;2 返回值相同;3 参数列表相同(不包括this指针在内);4 基类函数为虚函数;即为覆盖。
1、重载
- 函数重载是指在同一个作用域(通常是在同一个类中,也可以是在全局作用域下)内,存在多个同名函数,但它们的参数列表(参数的个数、类型或者顺序)不同。编译器会根据调用时传递的实际参数来决定调用哪个具体的重载函数,函数重载实现了用同一个函数名来执行相似但参数有所不同的操作,提高了代码的可读性和易用性。
总结来说就是:在同一个作用域,函数名相同,参数列表不同,与返回值无关
示例代码
#include <iostream>
// 重载函数,参数个数不同
int add(int num1, int num2) {
return num1 + num2;
}
int add(int num1, int num2, int num3) {
return num1 + num2 + num3;
}
// 重载函数,参数类型不同
double add(double num1, double num2) {
return num1 + num2;
}
int main() {
std::cout << add(2, 3) << std::endl;
std::cout << add(2, 3, 4) << std::endl;
std::cout << add(2.5, 3.5) << std::endl;
return 0;
}
在上述代码中,add
函数有多个重载形式,有的重载函数参数个数不同(如两个 int
参数和三个 int
参数的版本),有的重载函数参数类型不同(如 int
参数和 double
参数的版本)。在 main
函数中调用 add
函数时,编译器根据传入的实际参数情况来选择匹配的重载函数进行调用。
特点
- 作用域相同:重载的函数必须在同一个作用域内定义,比如都在某个类的内部或者都在全局作用域中。
- 函数名相同:这是重载的关键特征之一,多个函数共享同一个函数名,方便代码的调用和理解,从使用者角度看好像是同一个函数根据不同情况执行不同逻辑。
- 参数列表有差异:参数个数、类型或者顺序至少有一项不同,而返回类型不同不能作为函数重载的依据(因为仅返回类型不同时,编译器无法仅根据调用情况准确判断该调用哪个函数)。
除了以上常规的重载,还有一些同学认为泛型编程也存在重载,即模板函数和非模板函数的重载,
在C++中,模板函数与非模板函数之间可以存在重载关系,以下是关于它们重载的详细介绍:
1. 模板函数和非模板函数重载
- 重载机制:重载允许在同一作用域内存在多个同名函数(对于函数重载而言,这些函数的参数列表有所不同,而返回类型不能作为区分重载的唯一依据),编译器会根据调用时实际传入的参数情况来决定调用哪一个具体的函数。模板函数和非模板函数的重载就是利用了这一机制,在合适的场景下,编译器会依据调用参数去选择调用模板函数版本还是非模板函数版本。
2. 重载示例与调用规则
示例代码
#include <iostream>
// 非模板函数
int add(int num1, int num2)
{
return num1 + num2;
}
// 模板函数
template<typename T>
T add(T num1, T num2)
{
return num1 + num2;
}
int main()
{
int result1 = add(3, 5); // 调用非模板函数 add(int, int)
double result2 = add(3.5, 2.5); // 调用模板函数 add<double>(double, double)
std::cout << "整数相加结果: " << result1 << std::endl;
std::cout << "浮点数相加结果: " << result2 << std::endl;
return 0;
}
调用规则解释
- 精确匹配优先:当进行函数调用时,编译器首先会寻找参数类型与函数参数列表能精确匹配的非模板函数。在上述代码的
add(3, 5)
调用中,非模板函数add(int, int)
的参数类型刚好和传入的两个整数参数完全匹配,所以编译器优先选择调用这个非模板函数,而不会去考虑模板函数版本,即使模板函数也能够通过实例化(将T
实例化为int
)来处理这两个整数参数。 - 模板函数实例化匹配:如果没有找到精确匹配的非模板函数,编译器会尝试对模板函数进行实例化,看能否通过实例化后的模板函数来匹配调用参数。例如在
add(3.5, 2.5)
调用中,不存在参数类型为两个double
的非模板函数add
,此时编译器会查看模板函数,将模板参数T
实例化为double
,生成add(double, double)
这样一个实例化后的函数版本,它能很好地匹配传入的两个double
类型参数,所以就调用这个实例化后的模板函数版本。
3. 特殊情况与注意事项
二义性问题
- 当模板函数和非模板函数的参数匹配存在模糊情况时,可能会导致编译错误,出现二义性问题。例如:
#include <iostream>
// 非模板函数
void func(int num)
{
std::cout << "非模板函数func(int)" << std::endl;
}
// 模板函数
template<typename T>
void func(T num)
{
std::cout << "模板函数func(T)" << std::endl;
}
int main()
{
func(5); // 编译错误,存在二义性,不知道该调用模板函数还是非模板函数
return 0;
}
在这个例子中,调用 func(5)
时,传入的整数 5
既可以匹配非模板函数 func(int)
,也可以通过将模板函数的 T
实例化为 int
来匹配模板函数 func(T)
,编译器无法确定到底该调用哪一个函数,就会报二义性的编译错误。要解决这类问题,可以通过显式指定模板参数(如 func<int>(5)
就会明确调用模板函数版本)或者调整函数的参数类型等方式,使得调用具有明确的匹配对象。这个问题并不是所有的编译器都存在,在VS2022就不存在该问题。
函数特化与重载的交互
- 函数模板可以进行特化,即针对特定的类型提供专门的模板函数实现。在存在函数特化的情况下,特化版本、模板函数的通用版本以及非模板函数之间的重载关系也需要遵循上述的调用规则。例如:
#include <iostream>
// 非模板函数
void printData(int num)
{
std::cout << "非模板函数打印整数: " << num << std::endl;
}
// 模板函数
template<typename T>
void printData(T data)
{
std::cout << "模板函数通用版本打印数据: " << data << std::endl;
}
// 模板函数特化,针对char类型
template<>
void printData<char>(char data)
{
std::cout << "模板函数特化版本打印字符: " << data << std::endl;
}
int main()
{
int num = 10;
char ch = 'A';
printData(num); // 调用非模板函数printData(int)
printData(ch); // 调用模板函数特化版本printData<char>(char)
return 0;
}
在这里,对于 printData
函数,有非模板函数、模板函数通用版本以及针对 char
类型的特化版本。调用 printData(num)
时,根据精确匹配优先原则,会调用非模板函数 printData(int)
;而调用 printData(ch)
时,由于存在针对 char
类型的特化版本,会优先调用这个特化版本,而不是模板函数的通用版本,同样体现了编译器在选择调用函数时遵循的优先匹配规则。
2. 函数隐藏(Function Hiding)
概念
- 函数隐藏同样出现在类的继承关系中,是指在派生类中定义了与基类同名的函数(不管参数列表是否相同),此时派生类的函数会隐藏基类中同名的所有函数(包括重载函数),在派生类的作用域内,如果不使用作用域限定符显式指定,就无法访问到基类中被隐藏的同名函数。
隐藏:在基类和派生类之间发生的关系,函数名相同,派生类的函数把基类的函数给隐藏了,只关注函数数名,不管返回值和参数
示例代码
#include <iostream>
class Base {
public:
void func() {
std::cout << "Base类的func函数" << std::endl;
}
void func(int num) {
std::cout << "Base类的func(int)函数" << std::endl;
}
};
class Derived : public Base {
public:
void func(double num) {
std::cout << "Derived类的func(double)函数" << std::endl;
}
};
int main() {
Derived derived_obj;
derived_obj.func(3.0); // 调用Derived类的func(double)函数
// 以下代码编译错误,因为Derived类的func函数隐藏了Base类的func函数,
// 不能直接在Derived类作用域内调用Base类的func函数
// derived_obj.func();
// 使用作用域限定符可以访问Base类的func函数
derived_obj.Base::func();
return 0;
}
在上述代码中,Derived
类中定义了 func(double num)
函数,它隐藏了 Base
类中的 func
函数和 func(int num)
函数,所以在 Derived
类的作用域内直接调用 func
函数时,编译器会认为是调用 Derived
类自身定义的函数,如果要访问基类中被隐藏的同名函数,需要通过 Base::func()
这样的作用域限定符来明确指定。
特点
- 存在继承关系:也是基于类的继承场景出现的情况,派生类定义了与基类同名的函数。
- 同名即隐藏:只要函数名相同就会发生隐藏,与参数列表是否相同无关,而且是隐藏基类中所有同名函数,这一点和重载、覆盖都不同,重载是通过参数差异来区分不同函数,覆盖是严格按照函数签名一致且有虚函数特性来实现的。
- 作用域相关访问问题:隐藏导致在派生类作用域内,默认情况下无法直接访问基类中被隐藏的同名函数,需要使用作用域限定符来打破这种隐藏效果,才能调用基类的同名函数。
3. 函数覆盖(重写,Function Overriding)
概念
- 函数覆盖发生在类的继承关系中,是指在派生类中重新定义了基类中的虚函数,并且要求函数签名(函数名、参数列表、返回类型,返回类型如果是指针或引用类型时允许协变)完全一致(除了
const
修饰符,派生类重写函数可以比基类函数多const
修饰),当通过基类指针或引用调用该函数时,会根据对象的实际类型(是基类对象还是派生类对象)来决定调用基类的函数还是派生类重写后的函数,这是实现多态性的重要机制。
覆盖:覆盖是隐藏的一种特殊情况,派生类和基类的函数,1 函数名相同;2 返回值相同;3 参数列表相同(不包括this指针在内);4 基类函数为虚函数;即为覆盖。
其原理是子类虚函数表的函数地址覆盖了父类虚函数表里函数的指针。
示例代码
#include <iostream>
class Base {
public:
virtual void func() {
std::cout << "Base类的func函数" << std::endl;
}
};
class Derived : public Base {
public:
void func() override {
std::cout << "Derived类的func函数" << std::endl;
}
};
int main() {
Base* ptr = new Derived();
ptr->func(); // 调用Derived类重写后的func函数
Base base_obj;
base_obj.func(); // 调用Base类的func函数
Derived derived_obj;
derived_obj.func(); // 调用Derived类的func函数
return 0;
}
在这个例子中,Derived
类重写了 Base
类中的虚函数 func
,通过基类指针 ptr
指向派生类对象时,调用 func
函数会执行 Derived
类中重写后的版本,体现了多态性。而直接使用基类对象或者派生类对象调用 func
函数时,则分别调用各自类中定义的函数。
特点
- 存在继承关系:必须在派生类和基类之间发生,基类中声明了虚函数,派生类对其进行重写。
- 函数签名要求严格一致:函数名、参数列表、返回类型(遵循协变规则等情况除外)要相同,比如基类函数是
void func(int num)
,派生类重写的函数也得是void func(int num)
,目的是让编译器能准确识别这是重写关系,以便在多态调用时正确执行相应版本的函数。 - 虚函数特性:基类中的函数必须是虚函数(通过
virtual
关键字修饰),这样编译器才会在运行时根据对象的实际类型来动态决定调用哪个类的函数,实现多态行为。