动态联编和继承的教程已经很多了,这里只记录几个容易混淆的误区。
同名成员变量
-
派生类默认隐藏基类同名成员变量,但可通过作用域解析运算符访问基类同名成员。
(这视作一种重新定义) -
基类指针指向派生对象时,函数所用成员变量取决于函数本身所在的类。
即:派生类若重写了基类虚函数,动态联编导致进入派生类方法,则默认访问派生类内同名成员变量。否则访问基类成员。 -
用基类指针(引用)直接访问公有成员变量(不良风格),即使指向的是派生对象,仍然访问基类同名成员。
同名方法
-
派生类默认隐藏基类同名方法,但可以通过作用域解析运算符访问基类同名方法。
-
派生类无法对直接对基类的虚函数完成重载,需要先在基类中重载出不同版本,而后在派生类中分别重写。注意:如果试图仅在派生类内重载,基类指针无法调用仅在派生类中重载的方法。
这是一个试图直接重载的例子:class A{ public: virtual int Func(int x){cout<<"FUNC A int\n";} }; class B:public A{ public: int Func(int x){cout<<"FUNC B int\n";}//Override double Func(double x){cout<<"FUNC B double\n";}//Overload }; A *Pa =new B; Pa->Func(1) //动态联编,调用派生方法 Pa->Func(1.0)//类型转换int后,能够匹配基类方法的特征标,调用派生方法int
动态联编的本质是不同类的虚函数表中,同一方法(严格同名同参数符)指向了不同地址。若基类中不存在能够匹配的函数,则无从谈起派生类的方法覆盖(重写)。
-
重写不同于重定义,前者要求严格的特征标匹配,否则将会变成某种重定义。(重定义不会触发重写的动态联编,且总会隐藏同名方法)
class A public: virtual int Func(int x){cout<<"FUNC A int\n";} }; class B:public A{ public: // int Func(int x){cout<<"FUNC B int\n";} double Func(double x){cout<<"FUNC B double\n";}//redefine!!! }; A *Pa =new B; Pa->Func(1) //未被重写,调用基类方法 Pa->Func(1.0)//类型转换int后,调用基类方法
返回协变:允许重写将返回类型从基类引用/指针改为派生引用/指针。这并不视作一种重定义,是唯一的一种特征标例外。
管理继承:在c++11后,使用关键字override
、final
显式指出继承关系,可以尽早在编译阶段察觉错误。
重载是一种类内关系,无法跨类完成;反之,重写和重定义是一种继承关系。
虚函数继承链
- 一个函数从它被声明为虚的类开始,之后的整条派生链中都是虚的(即使后续没有显式声明
virtual
)。动态联编总会将对此函数的调用指向最接近对象的最后被重写的位置。 - 类将继承的某个非虚函数声明为虚,则视作一种重定义。从被重定义的位置开始,向后的表现同1.
final
关键字将禁止后续派生类对此方法的重写和重定义。