C++ 虚函数讲解

本文详细介绍了C++中的虚函数和多态,包括静态多态和动态多态的概念,以及如何通过`override`和`final`关键字来控制函数重写。此外,还讨论了虚函数与默认实参的交互以及如何回避虚函数的动态绑定。

虚函数表示使用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
评论 3
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值