以下大部分内容均来源于《C++ Primer》
文章是以读书笔记性质来巩固所学的内容。
虚函数
使用基类的指针或引用调用一个虚成员函数时会执行动态绑定。因为直到运行时才知道调用哪个版本的虚函数,所以所有的虚函数都必须有定义。虽然在通常情况下,如果我们不使用某个函数则无需对它进行定义,但是我们必须对每个虚函数都提供定义,因为编译器无法确定会使用哪个虚函数。
1、 对虚函数的调用可能在运行时才会被解析
当某个虚函数通过指针或引用调用时,编译器产生的代码直到运行时才能确定应该调用哪个版本的函数,被调用的函数是与绑定到指针或引用上的对象的动态类型相匹配的那一个。
double print_total(ostream &os,const Quote &item, size_t n)
{
/*根据传入的item形参的对象类型调用
Quote::net_price
或者Bulk_quote::net_price*/
double ret = item.net_price(n);
os<<"ISBN: "<<item.isbn()
<<" # sold: " <<n<<"total due: " << ret << endl;
return ret;
}
代码引自C++ primer 527页
因为item是Quote类型的引用且net_price 是虚函数,所以会发生动态绑定。
当我们通过一个具有普通类型(非指针引用)的表达式调用虚函数,编译时就确定要调用的版本。
Base base;
Derive derive;
base = derive;
base.f(); //f是虚函数,且derive 重写了f()
//这里调用的是Base::f()
C++的多态
当且仅当对通过指针或引用来调用虚函数时,才会在运行时解析该调用,也只有这种情况下对象的动态类型才有可能与静态类型不同。
2、派生类中的虚函数
在派生类中覆盖了某个虚函数时,可以使用virtual关键字指出该函数的性质,这种操作不是必须的,因为一旦某个函数被声明为虚函数时,在所有派生类中它都是虚函数。
一个派生类中的函数如果对某个继承来的虚函数进行override,那它的形参类型必须与被它覆盖的基类函数完全一致。同样,派生类的虚函数返回类型也必须与基类函数相匹配。但有一个例外
当基类的虚函数可以返回B的指针类型而派生类的对应函数可以返回D的指针类型,不过完成上述操作要求返回类型从D到B的类型转换是可访问的。
class Quote{
public:
//该虚函数返回当前对象的一份动态分配的拷贝
virtual Quote* clone() const & {return new Quote (*this) ; }
virtual Quote* clone() && {return new Quote (std::move(*this)) ; }
}
class Bulk_quote : public Quote {
public:
//该虚函数返回当前对象的一份动态分配的拷贝
Bulk_quote* clone() const & {return new Bulk_quote (*this) ; }
Bulk_quote* clone() && {return new Bulk_quote (std::move(*this)) ; }
//其他成员与之前的版本一致
}
3、final 和 override 的说明符
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;
//错误:B没有形如f2(int)的函数
void f3() override;
//错误:f3不是虚函数
void f4() override;
//错误:B没有名为f4的函数
};
struct D2:B{
//从B继承f2()和f3(),覆盖f1(int)
void f1(int) const final;
//不允许后续的其他类覆盖f1(int)
};
struct D3 : D2{
void f2(); //正确,覆盖从间接基类B继承而来的f2
void f1(int) const;
//错误,D2已经将f1声明为final
};
final和override说明符出现在形参列表(包含任何const或引用修饰符)以及尾置返回类型之后
4、虚函数与默认实参
虚函数调用默认实参的时候,实参值由本次调用的静态类型决定。
换句话说,如果我们通过基类的指针或引用来调用函数时,则使用基类定义的默认实参。即使实际运行的是派生类中的函数版本,此时,传入派生类函数的将是基类函数定义的默认实参。如果派生类依赖不同的实参,则程序结果与我们预期的不符。
struct b{
virtual void f(int x=1,int y=1) {cout<<"BX: "<<x<<" BY: "<<y<<endl;}
};
struct d : public b {
virtual void f(int x=2,int y=3) {cout<<"DX: "<<x<<" DY: "<<y<<endl;}
};
int main(void){
d derive;
b *pb = &derive;
pb->f();
return 0;
}
//输出 DX: 1 DY: 1,调用的是d::f(),使用的却是b::f()的参数
如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。
5、回避虚函数的机制
我们希望对虚函数的调用不要进行动态绑定,而是强迫其执行虚函数的特定版本,可以使用域运算符来解决这个问题。
例如:
//强行调用基类中定义的函数版本而不管basep的动态类型是什么
double number = basep->Base::price();
//该调用在编译时就完成解析,强行调用基类的price函数。
通常情况下,只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数的机制。
使用场景:当派生类的虚函数要调用它覆盖的基类虚函数版本时。如果没有使用域作用符,则在运行时将该调用将被解析为对派生类版本自身的调用,从而导致无限递归。
无限递归的例子
class base{
public:
string name() { return basename; }
virtual void print(ostream &os){ os << basename; }
private:
string basename;
};
class derived : public base{
public:
void print(ostream &os) { print(os); os<<" "<<i; }
//注意,这里的print调用的是派生类自身的版本,解决方法就是在把print(os)改为base::print(os);
private:
int i;
};