15-3 虚函数
调用时解析
使用基类的引用或指针调用虚函数时,会发生动态绑定
所以,知道运行时才会知道到底调用了哪个版本的虚函数
因此:
- 虚函数是在运行时才被解析
- 虚函数必须被定义
注意:
- 普通类型(非指针和引用类型)调用虚函数时不会发生动态绑定
class A{
public :
//基类的print是虚函数
virtual void print() const {cout<<"class A"<<endl;};
};
class B : public A{
public:
//B覆盖了A的虚函数
void print() const {cout<<"class B"<<endl;}
};
void Test_Print(const A a){ //传入基类的拷贝而不是引用
a.print();
}
int main(){
A a;
B b;
Test_Print(a); //打印 class A
Test_Print(b); //打印 class A
a.print(); //打印 class A
b.print(); //打印 class B
return 0;
}
Test_Print(b)
没有发生动态绑定,因为Test_Print传入的不是引用
- 调用一般的重载函数而不是虚函数时不会发生动态绑定
class A{
public :
//基类的print不是虚函数
void print() const {cout<<"class A"<<endl;};
};
class B : public A{
public:
//B重载了A的print函数
void print() const {cout<<"class B"<<endl;}
};
void Test_Print(const A &a){ //传入基类的引用
a.print();
}
int main(){
A a;
B b;
Test_Print(a); //打印 class A
Test_Print(b); //打印 class A
a.print(); //打印 class A
b.print(); //打印 class B
return 0;
}
Test_Print(b)
没有发生动态绑定,因为print不是虚函数
派生类中的虚函数
基类中的虚函数在派生类中隐含地也是一个虚函数。
当派生类覆盖了某个虚函数时,该函数在基类中的形参必须与派生类中的形参严格匹配。
final和override说明符
当派生类想要覆盖基类的虚函数时,由于需要严格的参数匹配,如果派生类中的参数列表不小心写错了,那么派生类中与基类虚函数同名的函数,并没有覆盖掉基类的虚函数,这样的写法是合法的,编译器不会报错,错误非常难以察觉。
当我们明确地想要让派生类的某个函数覆盖掉基类的虚函数时,可以加上override
关键字,这样,如果没有成功覆盖,程序就会报错
另外,当把一个函数指定为final
的时候,任何尝试覆盖该函数的行为都会报错
虚函数与默认实参
-
虚函数可以拥有默认实参
-
实参的值由本次调用的静态类型决定
动态类型与静态类型的概念详见15.2.3
换句话说,如果我们通过基类的引用或指针调用函数,则使用基类中定义的默认实参,即使实际运行的是派生类中的函数版本也是如此。
class A{
public :
virtual void print(string s = "class A") const
{cout<<s<<endl;};
};
class B : public A{
public:
void print(string s = "class B") const
{cout<<s<<endl;}
};
void Test_Print_A(const A &a){//参数的静态类型是A
a.print(); //调用A的默认参数,即使A绑定的是派生类B
}
int main(){
A a;
B b;
Test_Print_A(a); //打印 class A
Test_Print_A(b); //打印 class A
return 0;
}
如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致
回避虚函数机制
在某些情况下,我们希望对虚函数的调用不要进行动态绑定,而是强迫其执行虚函数的某个特定版本。使用作用域运算符可以实现这一目的,例如下面的代码:
//强行调用基类中定义的函数版本而不管baseP的动态类型到底是什么
double undiscounted = baseP->Quote::net_price(42);
通常情况下,只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数的机制。
什么时候我们需要回避虚函数的默认机制呢?
通常是当一个派生类的虚函数调用它覆盖的基类的虚函数版本时。在此情况下,基类的版本通常完成继承层次中所有类型都要做的共同任务,而派生类中定义的版本需要执行一些与派生类本身密切相关的操作。
如果一个派生类虚函数需要调用它的基类版本,但是没有使用作用域运符,则在运行时该调用将被解析为对派生类版本自身的调用,从而导致无F递归