第十五章——虚函数

在c++语言中,当我们使用基类引用或指针调用一个虚成员函数时会执行动态绑定。因为我们直到运行时才知道到底要调用哪个版本的虚函数,所以所有的虚函数都必须定义,我们必须为每一个虚函数提供定义无论它是否会被用到,因为编译器也无法确定要使用哪个虚函数。

对虚函数的调用可能在运行时才被解析

当虚函数通过指针或者引用调用时,编译器产生的代码直到运行时才能确定应该调用哪个版本的函数

例子,print_total函数,该函数通过其名为item的参数来进一步调用net_price,item类型是Quote&,因为item是引用并且net_price()是虚函数,所以调用net_price的版本依赖于运行时绑定到item的实参的实际(动态)类型

double print_total(ostream &os,const Quote& item,size_t n){
    //根据item对象类型来决定使用Quote::price()
    //还是Bulk_quote::net_price()
    double ret = item.price();
    ...;
}

再次强调:动态绑定只有当我们通过指针或引用调用虚函数才会发生!

当我们通过一个普通类型表达式来调用虚函数,在编译时就会将调用的版本确定下来。例如,通过base来调用net_price()。调用哪个版本的net_price() 就显而易见,虽然我们可以改变base的内容,但是不会改变对象的类型。因此在编译时该调用就会被解析成Quote::net_price();

Quote base;
base.net_price(10);

关键概念:C++的多态性

       面向对象(OOP)的核心思想是多态性(polymorphism)。多态这个词源于希腊语,含义为“多种形式”,我们把具有继承关系的多个类型成为多态类型。因为我们能使用这些类型的“多种形式”而无需在意它们的差异。引用或指针的静态类型与动态类型不同这一事实正是C++语言支持多态性的根本所在。当我们使用基类的引用或指针调用基类中定义的一个函数时,我们并不知道该函数真正作用的对象是什么类型,因为它可能是一个基类的对象也有可能是一个派生类的对象。如果该函数是虚函数,则直到运行时才会决定到底执行哪个版本,判定的依据是引用或指针所绑定的对象的真实类型。

       另一方面,对非虚函数的调用是在编译时进行绑定。类似的,通过对象进行的函数调用也在编译时绑定。对象的类型是确定不变的,我们无论如何都不可能令对象的动态类型与静态类型不一致,因此,通过对象进行的函数调用将在编译时绑定到该对象所属类的函数版本上。
NOTE:当且仅当通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同。

派生类中的虚函数

在派生类覆盖某个虚函数时,可以调用virtual关键字指出该函数的性质。但是并非必须,因为一旦某个函数被声明为虚函数,则在所有派生类中它都是虚函数。

一个派生类函数如果覆盖基类的虚函数,则形参类型必须和基类一致,并且返回类型也必须和基类函数匹配。此规则的一个例外,当虚函数返回类型是类本身的指针或引用,上述规则无效。如果D由B派生得到,则基类的虚函数可以发挥B*,而派生类对应的函数可以返回D*,只不过这样的返回类型要求从D到B的类型转换是可以访问的。

NOTE:基类的虚函数在派生类中隐含的也是一个虚函数。当派生类覆盖某个虚函数时,该函数在基类中的形参必须和派生类中的形参严格匹配。

final和override说明符

派生类如果定义一个函数与基类虚函数名字相同但是形参列表不同,这仍然是合法的行为。编译器将认为这个新定义的函数和基类中原有的函数是相互独立的。要调试这样的错误非常困难,C++11新标准我们可以用override关键字来说明派生类中的虚函数。如果使用override标记了某个函数,但是该函数没有覆盖已存在的虚函数,此时编译器将报错:

struct B{
    virtual void f1(int) const;
    virtual void f2();
    void f3();
};
struct D1:B{
    void f1(int) const override;    //正确:f1与基类的f1匹配
    void f2(int) override;          //错误:参数类型错误
    void f3() override;             //错误:f3并非虚函数
    void f4() override;             //错误:B没有名为f4的函数
}

还可以把某个函数指定为final,如果我们已经把函数定义成final了,任何尝试覆盖该函数的操作都将引发错误:

struct D2:B{
    void f1(int) const final; //后续其它类不能再覆盖f1(int)
}
struct D3:D2{
    void f2();                //正确覆盖从B中继承来的f2() 
                              //可以不再显示的指出virtual,但是参数列表一定要正确
    void f1(int) const;       //错误:D2已经把f2声明成final
}

注意:override关键字表示我们想要覆盖基类的虚函数,final和override说明符出现在形参列表(包括任何const或者引用修饰符)以及尾置返回类型之后。

虚函数与默认实参

如果我们通过基类的引用或指针调用函数,则使用基类定义的默认实参,即使实际运行的是派生类中的函数版本也是一样。传入派生类函数的是基类函数定义的默认实参。

Best Practic:如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。并且切记是通过引用或者指针调用,如果是直接派生类对象调用时根据自己接口中的默认实参。

回避虚函数的机制

希望对虚函数调用不要进行动态绑定,而是强制执行虚函数的某个特定版本。使用作用域运算符即可实现:

double undiscounted = baseP->Quote::net_prive(42);

NOTE:通常情况下,只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数的机制

什么时候需要回避虚函数默认机制?通常是派生类虚函数调用它覆盖的基类的虚函数版本时。

WARNING:如果没有使用作用域运算符,则在运行时将调用被解析为对自身版本的调用,从而导致无限制的递归。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值