4、指向父类名字
当在继承类中重载一个成员函数时,只要与其它代码相关就要有效替换掉原有的代码。然而,成员函数的父版本仍然存在,你可能会想使用它。例如,一个重载的成员函数会保持基类实现的行为,加上其它的一些。看一下WeatherPrediction类的getTemperature()成员函数返回一个string表示当前温度:
cpp
export class WeatherPrediction
{
public:
virtual std::string getTemperature() const;
// Remainder omitted for brevity.
};
可以在MyWeatherPrediction类中重载这个成员函数如下:
cpp
export class MyWeatherPrediction : public WeatherPrediction
{
public:
std::string getTemperature() const override;
// Remainder omitted for brevity.
};
假定继承类想要通过首先调用基类的getTemperature()成员函数给字符串添加°F,然后就给它加了°F。代码可能如下:
cpp
string MyWeatherPrediction::getTemperature() const
{
// Note: \u00B0 is the ISO/IEC 10646 representation of the degree symbol.
return getTemperature() + "\u00B0F"; // BUG
}
然而,这样是不灵的,因为,在c++命名解析规则之下,它首先解析的是本地范围,然后解析类范围,以调用MyWeatherPrediction::getTemperature()结束。这会导致无限递归,直到栈空间耗尽(有些编译器会检测到这种错误在编译时报错)。
真的想要这么做,需要使用范围解析符如下:
cpp
string MyWeatherPrediction::getTemperature() const
{
// Note: \u00B0 is the ISO/IEC 10646 representation of the degree symbol.
return WeatherPrediction::getTemperature() + "\u00B0F";
}
注意:微软Visual c++支持非标准的__super关键字(带有两个下划线)。允许写出如下代码:
cpp
return __super::getTemperature() + "\u00B0F";
调用当前成员函数的父类版本在c++中广泛使用。如果有一个继承类链,每个可能想要执行早已在基类中定义但是还要加一些另外的功能的操作。
我们看另外一个例子,设想一个书类型的类层次结构,如下图所示:
因为每个在层次结构中低层次的类指出书的类型,获得书描述的成员函数确实需要考虑层次结构的所有层次。可以通过将父类成员函数分链条来完成。下面的代码展示了这种模式:
cpp
import std;
using namespace std;
class Book
{
public:
virtual ~Book() = default;
virtual string getDescription() const { return "Book"; }
virtual int getHeight() const { return 120; }
};
class Paperback : public Book
{
public:
string getDescription() const override {
return "Paperback " + Book::getDescription();
}
};
class Romance : public Paperback
{
public:
string getDescription() const override {
return "Romance " + Paperback::getDescription();
}
int getHeight() const override { return Paperback::getHeight() / 2; }
};
class Technical : public Book
{
public:
string getDescription() const override {
return "Technical " + Book::getDescription();
}
};
int main()
{
Romance novel;
Book book;
println("{}", novel.getDescription()); // Outputs "Romance Paperback Book"
println("{}", book.getDescription()); // Outputs "Book"
println("{}", novel.getHeight()); // Outputs "60"
println("{}", book.getHeight()); // Outputs "120"
}
Book基类有两个virtual成员函数:getDescription()与getHeight()。所有的继承类都重载了getDescription(),但是只有Romance类通过在父类(Paperback)上调用getHeight()并将其除以2重载了getHeight()。Paperback()没有重载getHeight(),但是c++沿着类层次结构往上找,发现实现了getHeight()的类。在这个例子中,Paperback::getHeight()解析到了Book::getHeight()。
5、向上转化与向下转化
你早已看到,对象可以被转化或赋值给它的父类。下面为示例:
cpp
Derived myDerived;
Base myBase { myDerived }; // Slicing!
分片会在像这种情况下出现,因为结果是一个Base对象,Base对象缺少在Derived类中定义的另外的功能。然而,如果继承类被赋值给指向它的基类的指针或引用的话就不会出现分片:
cpp
Base& myBase { myDerived }; // No slicing!
对于有关基类的指向继承类,这是通常的正确的方式,也叫做向上转化。这也是为什么对于函数来说用用类的引用而不是直接使用类的对象总是一个好主意的原因。通过使用引用,继承类被传递而没有分片。
警告:当向上转化时,使用基类的指针或引用避免分片。
从基类转化为继承类,也叫做向下转化,常令专业c++程序员皱眉,原因是无法保证对象确实属于继承类,还因为向下了范围内是糟糕设计的标志。例如,考虑如下代码:
cpp
void presumptuous(Base* base)
{
Derived* myDerived { static_cast<Derived*>(base) };
// Proceed to access Derived member functions on myDerived.
}
如果presumptuous()的作者也去写调用presumptuous()的代码,可能一切都没有问题,虽然很丑,因为作者知道函数椟要Derived*类型的参数。然而,如果其它程序员调用presumptuous(),他们可能会传进来一个Base*。编译时是不检查参数类型的,函数盲目地就会认为base就是一个指向Derived对象的指针。
向下转化有时是必要的,在控制环境下使用它是有效的。然而,如果你想要向下转化,应该使用dynamic_cast(),它使用对象内建的类型知识来拒绝没道理的转化。这种内建知识典型地存在于vtable中,意味着dynamic_cast()只在对象拥有vtable时才有效,也就是说,对象至少要有一个virtual成员。如果dynamic_cast在指针上失败了,结果就是nullptr而不是指向无效的数据。如果dynamic_cast在对象引用上失败了,就会抛出std::bad_cast例外。本章的最后一节会详细讨论转化的不同选项。
上面的例子可以写成如下代码:
cpp
void lessPresumptuous(Base* base)
{
Derived* myDerived { dynamic_cast<Derived*>(base) };
if (myDerived != nullptr) {
// Proceed to access Derived member functions on myDerived.
}
}
然而,要记住使用向下转化是糟糕设计的标志。要重新思考并且 修改设计以避免向下转化。例如,lessPresumptuous()函数只在Derived对象上能好好干活,所以浊接受Base指针,它应该只是接受Derived指针。这减少了向下转化的需要。如果函数应该在不同的继承类上工作,所有从Based继承的,会找一个使用多态的解决方案,这个我们后面再讨论。
警告:只在确实需要的时候使用向下转化,确保使用了dynamic_cast()。