类的继承
1类的继承
"is a kind of"关系
自然界存在一种关系: A 是一种 B
x其中, A是一种类型, B是一种类型。
例如,
苹果树AppleTree 是一种 树Tree
燕子Swallow 是一种 鸟Bird
小麦Wheat 是一种 农作物Crops
"is a kind of"关系
由C++构成的抽象世界也存在is a kind of的关系
例如,
视频文件VideoFile 是一种 文件File
MP4文件Mp4File 是一种 视频文件VideoFile
视频教程VideoTutorial 是一种 教程Tutorial
继承
在C++里,有继承的语法来表示is kind of的关系
class Tutorial
{
};
class VideoTutorial : public Tutorial
{
};
语法:class B : public A {}
表示类B继承于类A,把A称为父类(基类),把B称为子类(派生类)
继承
当B继承于A时,则自动地将父类中的所有public成员继承。
例如,
class Tutorial
{
public:
char name[32];
char author[16];
public:
void ShowInfo();
};
则在VideoTutorial类中也具有了这些成员,而不必显式写出。
继承
VideoTutorial cpp_guide;
strcpy(cpp_guide.name, “C/C++学习指南”);
strcpy(cpp_guide.author, “邵发”);
cpp_guide.ShowInfo();
注:可以直接在VC下查看该变量的成员
继承
子类只需要把自己的独有的那部分特性写出来
例如,
class VideoTutorial : public Tutorial
{
public:
void Play(); // 播放
public:
char url[128]; // 在线观看的URL地址
int visits; // 播放量
};
访问修饰符 protected
在描述继承关系时,新增一种访问修饰符 protected(受保 护的)
当一个类成员被修饰为protected的时候,有以下规则成立:
(1)该成员不能被外部访问,同private
(2)该成员可以被子类继承,同public
所以,public和protected的成员都能够被子类继承
访问修饰符 protected
例如,将父类的成员变量声明为protected
class Tutorial
{
protected:
char name[32];
char author[16];
public:
void ShowInfo();
};
问题
在内存上描述父类和子类的关系: 子类对象的前半部分就是父类 对象。
class Parent
{
public:
int a;
};
class Child : public Parent
{
public:
int b;
};
(1)用sizeof验证
(2)在内存窗口中直接观测
问题
问题:父类的private成员变量也会出现在内存中吗?
是的,父类的所有成员变量都在子类对象中,只是编译器限 制了访问。
小结
- 用class B : public A {}表示B继承于A
- 当B继承于A后,父类的所有protected/public成员都被 继承。
- 什么叫被继承?就是这些父类的成员就如同直接写在子 类里一般。
- 代码上可以简化
2虚拟继承, 虚函数virtual
函数的重写
子类可以重写从父类继承而来的函数 (overwriting)
class Parent
{
public:
void Test();
};
class Child : public Parent
{
public:
void Test();
};
函数的重写
则
Child ch;
ch.Test(); // 调用的是子类的Test()函数
函数的重写
如果重写的时候,还是要嵌入调用一下父类的函数,怎么办?
void Child::Test()
{
Parent::Test(); // 显式地调用父类的函数
}
父类指针指向子类对象
可以将父类指针指向一个子类的对象,这是完全允许的。
例如,
// 左侧为Tree*,右侧为AppleTree*
Tree* p = new AppleTree();
从普通的逻辑来讲,苹果树是一种树,因而可以把 AppleTree视为一种Tree
从语法本质上讲,子类对象的前半部分就是父类,因而可以 将子类对象的指针直接转化为父类。
父类指针指向子类对象
有父类和子类:
class Parent
{
public:
int a;
};
class Child : public Parent
{
public:
int b;
};
父类指针指向子类对象
int main()
{
Child ch;
ch.a = 0x11111111;
ch.b = 0x22222222;
Parent* p = &ch; // p指向的对象是Child*
printf(“Parent::a = %d \n”, p->a);
return 0;
}
所以,从直观感觉到内在机理都允许这么做
父类指针指向子类对象
问题:考虑以下情况,
Parent* p = new Child();
p->Test();
那么,此时调用的Test()是父类的、还是子类的?
(1)指针p的类型是Parent*
(2)指针p指向的对象是Child*
调用者的初衷:因为p指向的是对象是子类对象,所以应该 调用子类的Test()。
虚拟继承: virtual
当一个成员函数需要子类重写,那么在父类应该将其声明为 virtual。
(有时将声明为virtual的函数为“虚函数”)
例如
class Parent
{
public:
virtual void Test();
};
virtual本身表明该函数即将被子类重写。
虚拟继承: virtual
加virtual关键字是必要的。
考虑以下情况,
Parent* obj = new Child(); // 语法允许,合乎情理
obj->Test();
此时,如果Test()在父类中被声明为virtual,是调用的是子类 的Test()。
这解释了virtual的作用:根据对象的实际类型,调用相应类型 的函数。
虚拟继承: virtual
注意:
(1)只需要在父类中将函数声明为virtual,子类自动地就是 virtual了。
(2)即将被重写的函数添加virtual,是一条应该遵守的编码 习惯。
小结
- 介绍继承关系中,对函数重写后的结果
- 介绍virtual关键字的作用和必要性(父类指针指向子类 对象)
3再说构造与析构,virtual析构函数
继承:构造与析构
有Child类继承于 Parent类
class Child : public Parent {}
那么,当创建一个子类对象时:(编译器默认动作)
子类对象构造时,先调用父类的构造函数,再调用子类的构 造函数。
子类对象析构时,先调用子类的析构函数,再调用父类的构 造函数。
继承:构造与析构
在VC中演示:
子类的构造
子类的析构
继承:构造与析构
当父类有多个构造函数,可以显式的调用其中的一个构造函 数。
如果没有显式调用,则调用了父类的“默认构造函数”
记住调用方法: Parent(1,1)
virtual 析构函数
当一个类被继承时,应该将父类的析构函数声明为virtual, 否则会有潜在的问题。
class Parent
{
virtual ~Parent(){} // 声明为virtual
};
virtual 析构函数
考虑以下场景:
Parent* p = new Child();
delete p; // 此时,调用的是谁的析构函数?
如果析构函数没有标识为virtual,则有潜在的隐患,并有 可能直接导致程序崩溃。(资源没有被释放,并引申一系列 问题)
类的大小,与 virtual关键字的影响
(1) 类的大小由成员变量决定。(这struct的原理相同)
类的大小成员函数的个数无关,即使一个类有10000个成员 函数,对它所占的内存空间是没有影响的。
(2) 但是,如果有一个成员函数被声明为virtual,那类的 大小会有些微的变化。(这个变化由编译器决定,一般是增 加了4个字节)
小结
- 介绍继承关系中,父类的构造函数和析构函数将被调用。
- 当一个类被别的类继承时,应该将父类的析构函数声明 为virtual。(注:如果这个类在设计的时候,已经明确它不会被继承, 则不需要声明为virtual)
- 构造函数不能加 virtual
4多重继承
多重继承
(注:初学者可以跳过这一集,或者听一下有个印象就行)
定义这个语法的本意:一个孩子有父有母,可以从父母处各 自继承一些特点。
多重继承
语法:
用Father, Mother表示二个类
class Child : public Father, public Mother
{
};
表示Child继承于Father,Mother
在写法上,以冒号引导,每个父类用逗号隔开
多重继承
多重继承的结果:从所有父类中,继承他们所有可以被继承 的成员(public/protected)
多重继承的问题
多重继承的问题:很明显,当多个父类有相同的成员时,会 影响冲突。
所以,C++的抽象世界和现实世界是不一样的。
实际上,多重继承的理念一般是不会用到的。问题颇多。
(多重继承的有一个有用的地方,在下一集“纯虚函数”中 介绍)
小结
- 可以多重继承,但多重继承一般是不使用的。(只有一 种常见应用场景,在下一章)
- 我们要记住什么:只需要记住它是怎么写的 class Child : public Parent1, public Parent2 { };
5纯虚函数,抽象类
什么是纯虚函数
这次课的地位:很重要,设计模式中的概念:接口
但初学者第一次学习时只需要有个印象,等学完了全书再回 头专门学习。
什么是纯虚函数
纯虚函数的语法:
(1)将成员函数声明为virtual
(2)后面加上 = 0
(3)该函数没有函数体
例如, class CmdHandler
{
public:
virtual void OnCommand(char* cmdline) = 0;
};
什么是纯虚函数
含有纯虚函数的类,称为抽象类(Abstract Class) (或称纯虚类)。
例如,CmdHandler中有一个纯虚函数OnCommand(),因此, 它是纯虚类。
抽象类不能够被实例化
,即无法创建该对象。
CmdHandler ch; // 编译错误!!
CmdHandler* p = new CmdHandler(); // 编译错误!
问题:不能被实例化,还定义这个类做什么用???
抽象类的实际作用
抽象类/纯虚函数的实际用途:充当的“接口规范”
(相当于Java中的interface语法)
(用于替代C中的回调函数的用法)
接口规范:凡是遵循此规范的类,都必须实现指定的函数接口。通常是一 系列接口。
比如,
class CmdHandler
{
public:
virtual void OnCommand(const char* cmdline) = 0;
};
可以理解为:凡是遵循CmdHandler规范的类,都必须实现指定的函数接口: OnCommand()
实例演示
项目需求:用户输入一行命令,按回车完成输入。要求解析 命令输入,并且处理。
设计:
CmdInput:用于接收用户输入
CmdHandler: 规定一系列函数接口
MyParser: 接口的实现,实际用于解析处理的类
实例演示
// main.cpp //
#include “CmdInput.h”
#include “MyParser.h”
int main()
{
CmdInput input;
MyParser parser;
input.SetHandler(&parser);
input.Run();
return 0;
}
小结
- 如何定义一个纯虚函数
- 抽象类的实质作用: 接口规范 因为它只代表了一个规范,并没有具体实现,所以它不能被实例 化。
- 抽象类通常被多重继承 比如,一个普通的类,实现了多套接口规范,又继承于原有的父 类。
- 抽象类的析构函数应该声明为virtual,因为它是被设计用于 继承的。