C++的重载、覆盖、隐藏、虚函数、纯虚函数等概念,都体现于多态的体系中。
本文提及的 父类 即 基类,子类 即 派生类。
重载:
先看demo:
#include <stdio.h>
class OverLoadClass{
public:
void run(int i) {printf("int: %d\n", i);}
void run(float i) {printf("float: %f\n", i);}
// void run(float j) {printf("float: %f\n", j);} // error
void run(double i) {printf("double: %f\n", i);}
// double run(double i) {return i;} // error
void run(int i, double j) {printf("int: %d, double: %f\n", i, j);}
void run(double j, int i) {printf("double: %f, int: %d\n", j, i);}
};
void TestOverLoad() {
OverLoadClass a;
int var1 = 1;
float var2 = 3.14;
double var3 = 3.14;
a.run(var1); // int: 1
a.run(var2); // float: 3.140000
a.run(var3); // double: 3.140000
a.run(3.14); // double: 3.140000
a.run(var1, var3); // int: 1, double: 3.140000
a.run(var3, var1); // double: 3.140000, int: 1
}
int main() {
TestOverLoad(); return 0;
}
以上是重载的应用场景:在同一个作用域内(所有的run
函数都在OverLoadClass
类),相同的函数名,若有不同的参数类型、顺序,则在调用时会选择最适合的函数来调用。
注意: 重载只对形式参数的类型和类型的顺序敏感,与形式参数的参数名(如demo里定义的6、7行)和函数的返回类型(如demo里定义的8、9行)是无关的。
另外从demo也能看出,float
和double
类型默认输出小数点后6位。
重载基本上over了,再看demo:
class BaseClass{
public:
void run1() {printf("BaseClass's run1\n");}
virtual void run2() {printf("BaseClass's run2\n");}
};
class SubClass: public BaseClass{
public:
void run1() {printf("SubClass's run1\n");}
void run2() {printf("SubClass's run2\n");}
};
void Test() {
BaseClass *a;
SubClass sub;
a = ⊂
// part 1 静态链接
a->run1(); // BaseClass's run1
// part 2 覆盖
a->run2(); // SubClass's run2
// part 3 隐藏
sub.run1(); // SubClass's run1
// part 4 覆盖
sub.run2(); // SubClass's run2
// part 5
SubClass *subp = ⊂
subp->run1(); // SubClass's run1
subp->run2(); // SubClass's run2
}
int main() {
Test(); return 0;
}
这里的5个part分别展示了不同的知识点,Let’s go!
// part 1 静态链接
a->run1(); // BaseClass's run1
// part 5
SubClass *subp = ⊂
subp->run1(); // SubClass's run1
subp->run2(); // SubClass's run2
首先part 1和part 5作对比,a
是父类类型的指针,subp
是子类类型的指针,并都指向了子类实例sub
的内存地址&sub
。通过->
操作符调用所指内存的run1
函数,发现两者的结果并不相同。
原因是a
调用的run1
被编译器设置为父类的版本,也就是静态链接,函数调用在程序执行之前就准备好了。而subp
指针通过地址调用run1
其实相当于sub
变量直接调用run1
,就会隐藏父类的同名函数(父类函数还存在),而直接调用子类的函数体(没关系,隐藏的概念稍后会详细解释)。
如果不能让父类的指针调用子类实现的函数体,继承、多态等编程设计将会大打折扣。那么有没有办法解决呢?
请看part 2。
// part 2 覆盖
a->run2(); // SubClass's run2
part 2中,父类类型的指针a
指向子类sub
的内存地址&sub
,并调用函数run2
,发现调用的是子类实现的函数体内容。
因为在父类定义时,在run2
函数上加了virtual
修饰符,这个函数就会被定义为虚函数。加上这个修饰符,编译器就不会静态链接到父类定义的函数体,而是在程序执行过程中调用指针指向的函数(子类实例sub
的run2
),相对地,这种方式叫动态链接。
同时,也称作子类函数覆盖了父类函数(父类函数被覆盖,不存在了)。
覆盖的条件如下:
- 父类、子类的函数名、参数都相同;
- 父类函数带有
virtual
修饰符。
// part 1 静态链接
a->run1(); // BaseClass's run1
// part 3 隐藏
sub.run1(); // SubClass's run1
对比part 1和part 3,通过子类实例sub
直接调用run1
函数,实现的是子类的函数体。原因是子类的run1
隐藏了父类的同名函数run1
(父类函数还存在)。
隐藏的情况如下:
- 父类、子类的函数名相同,但参数不同,则父类函数被隐藏;
- 父类、子类的函数名、参数都相同,但父类函数无
virtual
修饰符,则被隐藏。
综上,带virtual
,且函数名、参数类型相同、返回类型相同的是覆盖,否则是隐藏。
顾名思义,覆盖会让程序无法再调用父类的虚函数,但隐藏只是借助了名称的查找方式让程序调用了子类的函数,父类的函数依然存在的。那么该如何调用父类的函数呢?
调用被隐藏的函数:
void func1() {printf("external func1\n");}
class ClassA{
public:
void func1() {printf("ClassA's func1\n");}
};
class ClassB: public ClassA{
public:
void func1() {
printf("ClassB's func1\n");
ClassA::func1();
::func1();
}
};
void TestHiding() {
ClassA a;
ClassB b;
func1(); // external func1
printf("-----\n");
a.func1(); // ClassA's func1
printf("-----\n");
b.func1(); // ClassB's func1
// ClassA's func1
// external func1
printf("-----\n");
}
int main() {
TestHiding(); return 0;
}
案例中外部函数func1
被ClassA隐藏了,ClassA的func1
也被ClassB隐藏了。但原函数依然存在,调用方式就是通过作用域进行调用。
// part 3 隐藏
sub.run1(); // SubClass's run1
// part 4 覆盖
sub.run2(); // SubClass's run2
// part 5
SubClass *subp = ⊂
subp->run1(); // SubClass's run1
subp->run2(); // SubClass's run2
最后看part 3、part 4和part 5,通过子类类型的指针调用、以及通过子类变量直接调用,结果完全一致。
至于纯虚函数在demo中没有体现,但其实是虚函数的特殊情况。
虚函数可以在父类中定义函数体,可以被子类直接继承(不覆盖),也可以被子类覆盖。
而纯虚函数则在父类中不进行任何函数体的定义,只声明了函数的名称、参数类型、返回类型,实现方式如下:
virtual void func(int, int) = 0;
把原本虚函数的函数体替换为= 0
,就意味着在父类中声明了纯虚函数。
继承了含纯虚函数的子类,必须覆盖所有父类的纯虚函数,才能实例化。否则编译器会报错。
总结
作用域 | 函数名 | virtual | 形参类型/顺序 | 返回类型 | |
---|---|---|---|---|---|
重载 | 相同作用域 | 相同 | 可有可无 | 不同 | 可同可不同 |
隐藏 | 不同作用域 | 相同 | 可有可无 | 可同可不同 | 可同可不同 |
覆盖 | 不同作用域 | 相同 | 有 | 相同 | 相同 |