当前位置: 首页 > news >正文

Learn:C++ Primer Plus Chapter13

第13章 类继承

前言

Finally, it 到了我曾经没有踏足过的地方。继承多态 可以说是 C++ 最 significant 特性了,函数 多态 在之前几 Chapters 中已经有过一些接触了,一句简单的话就是 一个函数名,解决多种问题。运算符重载就是一个非常 explicit 的例子。

While 继承 则以一种便利的方式提供了可重用代码 in a class, and virtual function 提供的类多态方式也给管理带来的极大便利。

Abstract

  • is-a 关系
  • 虚函数(virtual function)
  • 抽象基类 ABC(Abstract Base Class)

1. An Example

本章的例子由 13.2.h13.2.cpp 两个文件 consist of。从中我们将看到一个基本的类继承实例,即 基类Ninja) 和 派生类Naruto, Sasuke)。

需要说明下 protected 关键字在继承中的作用,它可以使得 基类 数据对于派生类 like public,while,外部 like private。有这样的设计原则:protected 中应包含 函数结构体 等非数据成员,可以让派生类获得外部无法访问的一些接口,而 不应该包含数据,这样就违背了数据私有的原则。

  • 13.2.h
#ifndef CHAPTER_13_2_H
#define CHAPTER_13_2_H
#include <iostream>
#include <Windows.h>
using namespace std;

// below is for debug output
class FWColor {
	WORD wColor;
public:
	FWColor(WORD color) : wColor(color) {}
	friend std::ostream& operator<<(std::ostream&, const FWColor&);
};

std::ostream& operator<<(std::ostream& os, const FWColor& clsColor) {
	SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), clsColor.wColor);
	return os;
}

const FWColor CMD_RED(FOREGROUND_RED);
const FWColor CMD_WHITE(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
const FWColor CMD_YELLOW(FOREGROUND_RED | FOREGROUND_GREEN);
const FWColor CMD_GREEN(FOREGROUND_GREEN);
const FWColor CMD_BLUE(FOREGROUND_BLUE);
const FWColor CMD_PINK(FOREGROUND_RED | FOREGROUND_BLUE);
const FWColor CMD_CYAN(FOREGROUND_GREEN | FOREGROUND_BLUE);

class Ninja {
	string strName;
	string strNinjaArt;
	string strSensei;
protected:
	void SetSensei(string str);
	const string& GetSensei();
public:
	Ninja() {
		strName = "Default";
		strNinjaArt = "NinjaArt";
		strSensei = "None";
	}
	Ninja(string name, string art) {
		strName = name;
		strNinjaArt = art;
	}
	void UseArt();
	virtual void Show();
	virtual ~Ninja() {
		cout << "Destruct 0x" << hex << this << endl;
	};
};

class Naruto : public Ninja {
	string strModel;
public:
	Naruto() : Ninja("Naruto", "Rasengan") {
		SetSensei("Jiraiya");
		strModel = "Sage model";
	}
	virtual void Show();
	virtual ~Naruto() {
		cout << "destruct " << CMD_YELLOW << "Naruto" << endl << CMD_WHITE;
	};
};

class Sasuke : public Ninja {
	string strBro;
public:
	Sasuke() : Ninja("Sasuke", "Chidori") {
		SetSensei("Orochimaru");
		strBro = "Itachi";
	}
	virtual void Show();
	virtual ~Sasuke() {
		cout << "destruct " << CMD_YELLOW << "Sasuke" << endl << CMD_WHITE;
	};
};

#endif // !CHAPTER_13_2_H
  • 13.2.cpp
#include "13.2.h"

void Ninja::SetSensei(string str) {
	strSensei = str;
	return;
}

const string& Ninja::GetSensei() {
	return strSensei;
}

void Ninja::Show() {
	cout << CMD_YELLOW << strName << endl << CMD_WHITE;
	cout << "I can use " << CMD_GREEN << strNinjaArt << endl << CMD_WHITE;
	return;
}

void Ninja::UseArt() {
	cout << CMD_YELLOW << strName << CMD_WHITE << " use " << CMD_GREEN << strNinjaArt << endl << CMD_WHITE;
	return;
}

void Naruto::Show() {
	Ninja::Show();
	cout << "My sensei is " << CMD_CYAN << GetSensei() << endl << CMD_WHITE;
	cout << "I am in " << CMD_PINK << strModel << endl << CMD_WHITE;
	return;
}

void Sasuke::Show() {
	Ninja::Show();
	cout << "My sensei is " << CMD_CYAN << GetSensei() << endl << CMD_WHITE;
	cout << "I urge to revenge " << CMD_RED << strBro << endl << CMD_WHITE;
	return;
}

int main() {
	Ninja* pclsNinja[2];
	Naruto* naruto = new Naruto;
	Sasuke* sasuke = new Sasuke;
	pclsNinja[0] = naruto;
	pclsNinja[1] = sasuke;
	pclsNinja[0]->Show();
	pclsNinja[0]->UseArt();
	cout << endl;
	pclsNinja[1]->Show();
	pclsNinja[1]->UseArt();
	cout << endl;
	delete pclsNinja[0];
	delete pclsNinja[1];
	return 0;
}

代码中构建的类继承关系得到的结果如下图所示:

pic1 pic1

2. 公有继承:is-a 关系

C++ 有三种继承方式:publicprotectedprivate 继承。其中 public 继承是最为常用的方式,它建立一种 is-a 关系,what makes the derived class is also a base class and the derived class can use any operate to the base class。

例如:有一个 Ninja 类,可以记录一些 忍者(ninja) 的基本信息,因为 鸣人(Naruto) 和 佐助(Sasuke) 都是忍者,因此可以从 Ninja 类中派生出 NarutoSasuke 类。新类中将继承原始类的所有数据成员,因此新类中包含了 名字忍术老师 数据成员。同时新类中也可以有专门成员,这些是其他 Ninja 类所不具有的,Naruto 类添加新的 模式 成员,while Sasuke 添加了 Brother 成员。

公有继承 不能 建立 has-a 关系。例如:木叶村(Konoha village)包含有 Ninja,但通常 Konoha 不仅仅包含 Ninja。因此不能从 Ninja 派生出 Konoha 来在木叶村中添加忍者,这是一种 has-a 关系,最好的方式是将 Ninja 作为 Konoha 的数据成员。
在这里插入图片描述
公有继承不能 建立 is-like-a 关系,也就是它不采用比喻。例如:人们通常 say lawyer 就像 shark,但 律师 并不是 鲨鱼。鲨鱼可以在水下生活,但律师不能。继承 可以在 基类 的基础上添加属性,但不能删除基类的属性。

公有继承 不能 建立 is-implemented-as 关系,作为…来实现。例如:可以使用数组来实现栈,但从 Array 类派生出 Stack 类是不合适的,因为栈不是数组。数组的索引访问不是栈所具有的属性。正确的做法是让 Stack 包含一个 private Array 对象来隐藏数组实现。

公有继承 不能 建立 uses-a 关系。例如:计算机 可以使用 打印机,但从 Computer 类派生出 Printer 类是没有意义的 vice versa。However,可以使用 friend 函数来处理 ComputerPrinter 对象之间的通信。

All in allC++ 中完全可以使用公有继承来建立 has-ais-implemented-asuses-a 关系,however,这样做通常会导致编程方面的 problems。So is-a 关系更适合 公有继承

3. 虚函数:二进制层面

3.1 析构函数

更改例子中的代码,将 Ninja 中析构函数的 ~Ninja 设置为非虚函数,即将前面的关键字 virtual 删除。可以得到左图所示的结果,与右图添加 virtual 是不同的。

pic1 pic2

通过如下命令可以在 VS 的命令行中生成带有调试信息文件 .pdb 的代码版本,通过 IDA 打开后可以更方便进行二进制查看。

cl /Z7 13.2.cpp
  • 两者在调用 destructor 的时候产生的结果是不一样的。When 析构函数被定义为 虚函数 会通过虚表查找对应的函数进行调用,however,没有被定义为虚函数时则是直接调用 基类的 destructor
    在这里插入图片描述
非虚析构函数

在这里插入图片描述

虚析构函数
  • 带有 virtual 的派生类析构函数首先会将自身虚表置位,接着进行析构函数中的内存清理,最后会调用 基类 的析构函数。
    在这里插入图片描述
派生类析构函数

在这里插入图片描述

基类析构函数
  • 通过 IDA 进行逆向分析时,fishwheel 发现 MSVC 编译器会给析构函数生成一个 `scalar deleting destructor’ which 对于 delete 进行了 encapsulate。通过一个参数进行控制 whether 进行 delete after 析构。
    在这里插入图片描述
scalar encapsulation

3.2 虚表内存结构

由于我们保留了 .pdb 文件,因此打开 IDAStructures 窗口后就能够看到 Ninja 类在内存中布局是什么样的了。

与不包含 virtualclass 相比多出了一个 4 bytes 位置 at 对象开始的位置(如果是 x64 则是 8 bytes),刚好就是一个指针的长度,这里会存放虚表(virtual table),调用虚函数时会查表进行调用。
在这里插入图片描述

类内存布局

同样的 IDA 也从 .pdb 文件中读取到了相应的内容,所以可以在 Structures 窗口中看到虚表中记录的函数指针接口,you can Press Y at 一个成员,then 你将看到该成员的类型信息,并可以进行修改。

虚表结构

基类派生类 的会根据需要生成不同的虚表,虚表中存放的当前类的虚函数的指针,运行时会跳转到对应位置。
在这里插入图片描述

派生类虚表

在这里插入图片描述

基类虚表

下面是调用虚函数 Show 时的汇编代码,基类派生类 在虚表的 相同位置 存储着 接口相同的虚函数指针,这样只需更换 Object虚表指针 就能够实现对于函数调用的更换。
在这里插入图片描述

虚函数调用

当读者阅读至此,就会发现,in binary level,C++ 是如此质朴,类多态的实现是如此简单:虚表指针 + 虚表中的函数指针 实现了类多态这样的功能

  • 对象虚表指针更换实现 基类派生类 不同的函数调用。
  • 虚表中 偏移相同 的函数指针指向的函数 接口相同,保证了参数传递的一致性。

此时之前的疑问:为什么用 基类 指针指向 派生类 而调用的虚函数却是 派生类 的就解答了。因为:初始化时,派生类将基类虚表指向了派生类的虚表;调用时采用 vtable[index] 方式;相同 index 函数接口相同
在这里插入图片描述

在这里插入图片描述

3.3 注意事项

  • 构造函数不能是虚函数。
  • 析构函数应当是虚函数,unless 该类不用作基类。
  • 友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数。
  • 如果派生类中没有重新定义虚函数,将使用该函数的基类版本
  • 重新定义将隐藏方法。重新定义继承方法不是重载,无论参数列表是否相同,该操作将隐藏所有同名基类方法
  1. 如果重新定义继承的方法,应确保与原来的原形完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针。这种特性被称为返回类型协变(covariance of return type)。这种变化只适用于返回值,而不适用于参数
  2. 如果基类被重载了,则应在派生类中重新定义所有的基类版本

上面删除线所示内容是书中过时内容,在 C++14 之后的标准(fishwheel 能够接触到的最 oldest 的标准)中已经没有这样的限制了,下面的代码将进行实际验证。

  • 13.3.h
#pragma once
#ifndef CHAPTER_13_3
#define CHAPTER_13_3
#include <iostream>
#include <Windows.h>
using namespace std;

// below is for debug output
class FWColor {
	WORD wColor;
public:
	FWColor(WORD color) : wColor(color) {}
	friend std::ostream& operator<<(std::ostream&, const FWColor&);
};

static std::ostream& operator<<(std::ostream& os, const FWColor& clsColor) {
	SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), clsColor.wColor);
	return os;
}

const FWColor CMD_RED(FOREGROUND_RED);
const FWColor CMD_WHITE(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
const FWColor CMD_YELLOW(FOREGROUND_RED | FOREGROUND_GREEN);
const FWColor CMD_GREEN(FOREGROUND_GREEN);
const FWColor CMD_BLUE(FOREGROUND_BLUE);
const FWColor CMD_PINK(FOREGROUND_RED | FOREGROUND_BLUE);
const FWColor CMD_CYAN(FOREGROUND_GREEN | FOREGROUND_BLUE);

class TestBase {
public:
	virtual ~TestBase() {};
	virtual void FWTest();
	virtual void FWTest(double a);
	virtual void FWTest(const char* a);
};

class Test : public TestBase {
	int nJunk;
public:
	Test() { nJunk = 0; }
	virtual ~Test(){}
	virtual int FWTest(int a);
	virtual void FWTest(const char* a);
};

#endif // !CHAPTER_13_3

  • 13.3.cpp
#include "13.3.h"

void TestBase::FWTest() {
	cout << CMD_YELLOW << "TestBase" << endl << CMD_WHITE;
}

void TestBase::FWTest(double a) {
	cout << CMD_YELLOW << "TestBase : " << CMD_WHITE << a << endl ;
}

void TestBase::FWTest(const char* a) {
	cout << CMD_YELLOW << "TestBase : " << CMD_WHITE << a << endl;
}

int Test::FWTest(int a) {
	cout << CMD_CYAN << "Test : " << CMD_WHITE << a << endl;
	return 1;
}

void Test::FWTest(const char* a) {
	cout << CMD_CYAN << "Test : " << CMD_WHITE << a << endl;
}

void test1() {
	Test clsTest;
	TestBase& clsTestBase = clsTest;
	// test the override virtual function
	clsTestBase.FWTest();
	clsTestBase.FWTest(int(5));
	clsTestBase.FWTest("bad");
}

int main() {
	test1();
	return 0;
}

上面的代码用来验证注意事项中的一条,关于虚函数包含多个函数重载时的情况:

  • 通过虚表中的内容可以看到,对于基类 TestBase 中进行了重新定义的函数 void FWTest(const char* a) 在派生类 Test 的虚表中进行了替换,而没有重新定义的则未发生变化。
  • 新添加了重载函数 int FWTest(int a) 被 append 到虚表的后面。
pic1 pic2
  • 通过 IDA 查看函数的调用,通过查找虚表的方式进行调用,相应位置的函数我写在了注释中。
    在这里插入图片描述
  • 程序的运行结果符合预期,派生类中重新定义的虚函数会替换基类中的相应函数 in 虚表
    在这里插入图片描述
  • 最后可以得出结论:书中关于同名虚函数的描述存在 错误,即存在多个重载的虚函数可以只重新定义其中的某些,而不需要全部重新定义 in 派生类。(当然这些是在 C++ 14 的标准是可行的,而书中则是以 C++99/03 作为标准这可能就是原因。fishwheel 很想去用 old 标准跑一下来进行进一步确认,但是 /std (Specify Language Standard Version) 中明确说明了最老的版本是 C++14,我也就作罢了。)
  • 这里是 fishwheel 的一点感受:尽管函数重载使用的相同函数名 in source code 层面,但是在 binary code 层面它们的函数名是完全不同的,所以就应该被看做是普通函数。感觉书中的描述在二进制层面就是存在问题的。

4. 抽象基类 ABC

ABC(Abstract Base Class)是一种包含有纯虚函数(pure virtual function)的 基类,which can’t be instantiated。

4.1 纯虚函数

C++ 通过纯虚函数提供未实现的函数,也就是说 pure virtual function 只需要有函数声明,可以没有函数定义。纯虚函数的声明方法很简单,只需要在函数末尾加上 =0 即可。

class ClassA {
	...
public:
	// a pure virtual function
	virtual Output() = 0;
}

4.2 ABC 理念

ABC 可以看做是一种必须实施的接口。ABC要求具体派生类覆盖其纯虚函数——迫使派生类遵循 ABC 设置的接口规则。这种模型在基于组件的编程模式中很常见,in this condition,使用 ABC 使得组件设计人员能够制定 “接口约定”,这样确保了从 ABC 派生的所有组件都至少支持 ABC 指定的功能。

相关文章:

  • ChainLit快速接入DeepSeek实现一个深度推理的网站应用图文教程-附完整代码
  • Swift 并发任务的协作式取消
  • Mysql 安装教程和Workbench的安装教程以及workbench的菜单栏汉化
  • Python 常用内建模块-itertools
  • HTML(超文本标记语言)
  • Python FastApi(2):基础使用
  • 【SpringBoot】MorningBox小程序的完整后端接口文档
  • 第3章 Internet主机与网络枚举(网络安全评估)
  • Python 爬取 1688 详情接口数据返回说明
  • Mysql架构理论部分
  • github代理 | 快速clone项目
  • 简单理解机器学习中top_k、top_p、temperature三个参数的作用
  • 前端开发:Vue以及Vue的路由
  • AsyncHttpClient使用说明书
  • Android Compose 切换按钮深度剖析:从源码到实践(六)
  • SpringBoot @Scheduled注解详解
  • SQL宏-代替UDF
  • JSONPath 的介绍
  • 搭建主从DNS、nfs、nginx
  • 【MySQL】undo日志页结构
  • 城市轨道交通安全、内河港区布局规划、扎实做好防汛工作……今天的上海市政府常务会议研究了这些重要事项
  • 同济大学原常务副校长、著名隧道及地下工程专家李永盛逝世
  • 《尤物公园》连演8场:观众上台,每一场演出都独一无二
  • 韩国前国务总理韩德洙加入国民力量党
  • 上海国际电影节推出三大官方推荐单元,精选十部优秀影片
  • 国家税务总局泰安市税务局:山东泰山啤酒公司欠税超536万元