虚函数表示使用virtual关键字修饰的函数,一般使用在类中的成员函数上。那么我们为何要使用虚函数呢?针对这个问题,我们可以先跟大家介绍一下多态。
多态
多态是面向对象的三大特征之一,所谓多态,就是指“多种形态”,即对同一接口的不同表现形式。比如动物这个类别,可能表现为狗,也可能表现为猫等等。
多态分为:静态多态和动态多态
静态多态
静态多态也叫早绑定,是编译时多态,在编译的后就已经确定了需要调用的函数,如果不存在,则出现编译错误。
静态多态主要有两种实现方式:函数重载(普通函数的重载和成员函数重载)、函数模板
静态多态示例:成员函数重载
// 父类
class Parent{
public:
Parent(int a) {
this->a = a;
}
void print() {
cout << "Parent print: " << a << endl;
}
private:
int a;
};
// 子类
class Child : public Parent{
public:
Child(int b) : Parent(3){
this->b = b;
}
void print() {
cout << "Child print:" << b << endl;
}
private:
int b;
};
// 打印函数
void howToPrint(Parent &base) {
base.print();
}
int main() {
// 调用
Parent p1(20);
Child c1(10);
howToPrint(p1); // 调用Parent中的print函数,打印20
howToPrint(c1); // 同样是调用Parent中的print函数,打印3
}
从上例中可以看出,无论传入的是Parent对象还是Child对象,最终访问的都是Parent中的print函数。主要原因在于该方式是静态多态,编译器在编译时认为最安全的做法是编译到父类的print函数。因此编译后,Child对象中的print函数来自于父类。
然而这种编译方式并不是我们想要的,我们希望本身是什么类别就调用自身的成员函数。解决方式就是使用virtual关键字修饰成员函数,这也就是动态多态的方式。
动态多态
动态多态是运行时多态,编译器产生的代码知道运行时才能确定应该调用哪个版本的函数(根据引用对象的实际类型调用其对应的方法)。动态多态的实现方式是使用virtual关键字队成员函数进行修饰。
对上面的示例进行修改:
// 父类
class Parent{
public:
Parent(int a) {
this->a = a;
}
void print() {
cout << "Parent print: " << a << endl;
}
private:
int a;
};
// 子类
class Child : public Parent{
public:
Child(int b) : Parent(3){
this->b = b;
}
virtual void print() {
cout << "Child print:" << b << endl;
}
private:
int b;
};
// 打印函数
void howToPrint(Parent &base) {
base.print();
}
int main() {
// 调用
Parent p1(20);
Child c1(10);
howToPrint(p1); // 调用Parent中的print函数,打印20
howToPrint(c1); // 调用Child中的print函数,打印10
}
加上virtual关键字之后,则调用相应类型对象中的成员函数。
注意:
动态绑定只有当我们通过指针或引用调用虚函数是才会发生。
int main() {
Child c1(10);
Parent p1(20);
p1 = c1; // 把Child中Parent的部分拷贝给p1
p1.print(); // 调用的是Parent中的print函数,打印3
return 0;
}
由于未使用指针或引用,编译时紧紧是将Child中包含Parent的那部分拷贝给对象p1,因此调用时,访问的依然是Parent中的函数。
静态类型和动态类型说明:
- 静态类型:对象声明时的类型,编译时确定
- 动态类型:现在所指对象的类型,运行时确定

override关键字作用
有些时候,我们在重写基类中的成员函数时,会不小心写错,比如多加了一个参数,此时编译器并不会认为是错误的,而是认为重载了一个新的成员函数。为了能够让编译器帮助我们识别是否正确重写了基类中的成员函数,可以使用override关键字进行修饰。
struct A {
virtual void f1(int) const;
virtual void f2();
void f3();
};
struct B : A {
void f1(int) const override; // 正确:f1与基类中的虚函数f1匹配
void f2(int) override; // 错误:因为基类中不存在虚函数f2(int)
void f3() override; // 错误:因为基类中不存在虚函数f3()
};
如果基类中不存在对应的虚函数,则在子类中使用override就会报错(只有虚函数才能被覆盖)。
final关键字作用
有些时候,我们希望某些成员函数不能被重写(覆盖),可以使用final进行修饰。使用final的成员函数,则之后任何尝试覆盖该函数的操作都会引发错误。
struct A {
virtual void f1(int) const;
};
struct B : A {
void f1(int) const final; // f1覆盖基类中的虚函数,默认也是虚函数,此处定义为final,不能被其后的子类覆盖
};
struct C : B {
void f1(int) const; // 错误:B中已经将f1声明为final
};
虚函数与默认实参
- 虚函数也可以拥有默认实参
- 基类和派生类版本中默认实参值可以不同,但是默认实参的值由本次调用的静态类型决定。也就是说,通过基类的指针或引用调用函数,默认实参的值使用的是基类中的版本,即使实际运行的是派生类中的版本也是如此。
class A {
public:
virtual void f1(int a, int t = 10) {
cout << a << " " << t << endl;
}
};
class B : public A {
public:
void f1(int b, int t = 5) {
cout << b << " " << t << endl;
}
};
int main() {
A* p1 = new A();
A* p2 = new B();
p1->f1(2); // 输出 2 10
p2->f1(5); // 输出 5 10
return 0;
}
从上面的结果可以看出,虽然p2指向的是派生类,但是调用派生类中的f1()函数时,默认实参t的值依然是10。因此通常情况下,将基类和派生类的默认实参定义成相同的数值。
回避虚函数的机制
在某些情况下,我们希望对虚函数的调用不要进行动态绑定,而是强迫其执行虚函数的某个特定版本(基类或派生类版本)。可以使用域运算符解决这一问题。
用途:通常用在当一个派生类的虚函数调用它覆盖的基类的虚函数版本时
比如对上例中的类成员进行调用
A* p2 = new B();
p2->f1(2); // 调用B中的f1
p2->A::f1(5); // 强制调用A中的f1
本文详细介绍了C++中的虚函数和多态,包括静态多态和动态多态的概念,以及如何通过`override`和`final`关键字来控制函数重写。此外,还讨论了虚函数与默认实参的交互以及如何回避虚函数的动态绑定。
2290

被折叠的 条评论
为什么被折叠?



