C++学习笔记----9、发现继承的技巧(三)---- 尊重父类(2)

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()。

相关推荐
立志成为大牛的小牛1 天前
数据结构——三十七、关键路径(王道408)
数据结构·笔记·程序人生·考研·算法
froginwe111 天前
HTML5 测验
开发语言
User_芊芊君子1 天前
【成长纪实】我的鸿蒙成长之路:从“小白”到独立开发,带你走进鸿蒙的世界
学习·华为·harmonyos·鸿蒙开发
oe10191 天前
好文与笔记分享 A Survey of Context Engineering for Large Language Models(下)
人工智能·笔记·语言模型·agent
冷雨夜中漫步1 天前
高级系统架构师笔记——系统质量属性与架构评估(1)软件系统质量属性
笔记·架构·系统架构
野生技术架构师1 天前
牛客网Java 高频面试题总结(2025最新版)
java·开发语言·面试
一只鹿鹿鹿1 天前
系统安全设计方案书(Word)
开发语言·人工智能·web安全·需求分析·软件系统
oe10191 天前
好文与笔记分享 A Survey of Context Engineering for Large Language Models(中)
人工智能·笔记·语言模型·agent开发
持梦远方1 天前
【C++日志库】启程者团队开源:轻量级高性能VoyLog日志库完全指南
开发语言·c++·visual studio
聪明努力的积极向上1 天前
【C#】HTTP中URL编码方式解析
开发语言·http·c#