类的继承
- 类的继承
-
is a kind of关系
自然界存在一种关系,A是一种B
其中,A是一种类型,B是一种类型
-
由C++构成的抽象世界也存在 is a 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() { printf("Tutorial: %s, %s \n"name,aithor) } }; // 则在VideoTutorial类中也具有了这些成员,而不必显式写出。
-
Main
int main() { VideoTutorial_cpp_guide; strcpy(cpp.guide.name,"C/C++学习"); strcpy(cpp.guide.author,"Jiang Weibin"); cpp.guide.ShowInfo(); return 0; }
- 继承
-
子类只需要把自己的独有的那部分特性写出来
class VideoTurtorial : public Turtorial { public: void Play() // 播放 { printf("Playing..."); } public: char url[128]; // 在线观看的URL地址 int visits; // 播放量 };
-
访问修饰符protected
在描述继承关系时,新增一种访问修饰符protected(受保护的)
当一个类成员被修饰为protected的时候,有以下规则成立:
(1)该成员不能被外部访问,同private
(2)该成员可以被子类继承,同public
所以,public和protected的成员都能够被子类继承
-
举例:将父类的成员变量声明为protected
class Turtorial { protected: char name[32]; char author[16]; public: void ShowInfo(); }
-
问题1
在内存上描述父类和子类的关系:子类对象的前半部分就是父类对象。
class Parent { public: int a; private: int ddd; }; class Child : public Parent { public: int b; }; int main() { Child c; c.a = 0x11111111; c.b = 0x22222222; return 0; } // (1) 用sizeof验证 // (2) 在内存窗口中直接观测 // 结论,内存上面从父类继承的成员变量放在前面,自己的成员变量放在后面
-
问题2:父类的private成员变量也会出现在内存中么?
回答:是的,父类的所有成员变量都在子类对象中,只是编译器限制了访问。
-
小结:
- 用class B : class A {}表示B继承于A
- 当B继承于A后,父类的所有protected/public成员都被继承
- 什么叫被继承?就是这些父类的成员就如同直接写在子类里一般。
- 继承可以使代码简化
- 虚拟继承:虚函数virtual
-
子类可以重写从父类继承而来的函数(overwriting)
class Parent { public: void Test() { printf("Parent:...\n"); } }; class Child : public Parent { public: void Test() { printf("Child:...\n"); } };
-
如果重写的时候,还是要嵌入调用一下父类的函数,怎么办?
void Child::Test() { // 解决方法:显式地调用父类的函数 Parent::Test(); printf("Child:...\n"); }
-
父类指针指向子类对象
可以将父类指针指向子类的对象,这是完全允许的。
// 左侧为Tree*,右侧为AppleTree* Tree* p = new AppleTree;
从普通的逻辑来讲,苹果树是一种树,因而可以吧AppleTree*视为一种Tree*
从语法本质上讲,子类对象的前半部分就是父类,因而可以将子类对象的指针直接转化为父类
-
问题:考虑以下情况
Parent* p = new Child(); p->Test();
那么,此时调用的Test()是父类的、还是子类的?以下两种观点:
(1)指针p的类型是Parent* ->父类Test
(2)指针p指向的对象是Child*->子类Test
调用者的初衷:因为p指向的对象是子类对象,所以应该调用子类的Test()。
结果:观点(1)被实现,调用了父类的函数Test()
-
虚拟继承:virtual
当一个成员函数需要子类重写,那么在父类应该将其声明为virtual(子类virtual自动继承,可写可不写)
virtual本身表明该函数即将被子类重写
-
加virtual关键字是必要的
考虑以下情况
// 语法允许,合乎情理 Patent* obj = new Child(); p->Test();
此时,如果Test()在父类中被声明为virtual,是调用的是子类的Test()。
这解释了virtual的作用,根据对象的实际类型,调用相应类型的函数
-
注意:
- 只需要在父类中将函数声明为vitual,子类自动地就是 virtual了
- 即将被重写的函数添加virtual,是一条应该遵守的编码习惯
-
小结:
- 介绍继承关系中,对函数重写后的结果
- 介绍virtual关键字的作用和必要性(父类指针指向子类对象)
- 继承:构造与析构
-
有Child类继承于Parent类
class Child : public Parent {}
那么,当创建一个子类对象时:(编译器默认动作)
子类对象构造时,先调用父类的构造函数,再调用子类的构造函数。
子类对象析构时,先调用子类的析构函数,再调用父类的构造函数。
-
当父类有多个构造函数时,可以显式地调用其中的一个构造函数
如果没有显示调用,则调用父类的"默认构造函数"
记住调用方法:Parent(1,1)
-
virtual析构函数
当一个类被继承时,应该将父类的析构函数声明为virtual,否则会有潜在的问题
class Parent { // 声明为virtual virtual ~Parent() {} }; int main() { Parent* p = new Child(); delete p; //此时,调用的是谁的析构函数 return 0; } // 如果析构函数没有标志为virtual,则有潜在的隐患,并有可能直接导致程序崩溃(资源没有被释放,并引申一系列问题)
-
类的大小,与virtual关键字的影响
-
类的大小由成员变量决定。(与struct的原理相同)
类的大小与成员函数的个数无关,即使一个类有10000个成员函数,对它所占的内存空间是没有影响的。
-
但是,如果有一个成员函数被声明为virtual,那类的大小会有些微的变化。(这个变化由编译器决定,一般是增加了4个字节,多加了一个指针)
-
-
小结:
-
继承关系中,父类的构造函数和析构函数将被调用。
-
当一个类被别的类继承时,应该将父类的析构函数声明为virtual。
(注:如果这个类在设计的时候,已经明确它不会被继承,则不需要声明为virtual)
-
- 多重继承
-
定义这个语法的本意:一个孩子有父有母,可以从父母处各自继承一些特点
-
语法:
// 用Father, Mother表示两个类 class Child : public Father, public Mother {};
表示Child继承于Father,Mother
在写法上,以冒号引导,每个父类用逗号隔开
-
多重继承的结果:从所有父类中,继承他们所有可以被继承的成员(public/protected)
-
多重继承的问题:当多个父类有相同的成员时,会影响冲突。多重继承一般不会被用到,多重继承在纯虚函数中可以发挥作用。
-
小结:
-
多重继承的常见应用场景:纯虚函数
-
语法:
-
class Child : public Parent1, public Parent2 {};
```
5. **纯虚函数与纯虚类**
+ 与设计模式中的接口概念有关
+ 纯虚函数的语法
1. 将成员函数声明为virtual
2. 后面加上 = 0 ("纯")
3. 该函数没有函数体
```cpp
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,实现联调 input.SetHandler(&parser); input.Run(); return 0; } //CmdInput.h/ #ifndef _CMD_INPUT_H #define _CMD_INPUT_H #include "CmdHandler.h" class CmdInput { public: CmdInput(); void SetHandler(CmdHandler* h); // 开始运行 int Run(); private: CmdHandler* m_handler; } //CmdInput.cpp// void CmdInput::SetHandler(CmdHandler* h) { m_handler = h; } int CmdInput::Run() { char cmdline[256]; while(1) { // 输入 printf(">"); gets(cmdline); // 退出 if(strcmp(cmdline,"exit") == 0) { break; } // 解析与执行 if(m_handler) { m_handler->OnCommand(cmdline); } } return 0; } //CmdHandler.h// #ifndef _CMD_HANDLER_H #define _CMD_HANDLER_H /* CmdHandler: 接口类 */ class CmdHandler { public: ~CmdHandler() {} // 析构函数声明为 virtual virtual void OnCommand(char* cmdline) = 0; // 纯虚函数 }; #endif ///MyParser.h/// #ifndef _MY_PARSER_H #define _MY_PARSER_H #include "CmdHandler.h" /* MyParser: 一个遵循了CmdHandler接口的类 */ class MyParser : public CmdHandler { public: MyParser(); public: // 函数接口集 virtual void OnCommand(char* cmdline); private: // 解析命令 int Split(char text[],char* parts[]) }; ///MyParser.cpp/ (补全) void MyParser::OnCommand(char* cmdline) { char* argv[128]; int argc = Split(cmdline, argv); if(argv > 0) { printf("命令:%s \n", argv[0]); printf("参数:"); for(int i=1; i<argc; i++) { printf("%s",argv[i]); } printf("\n\n"); } }
-
小结:
-
如何定义一个纯虚函数
-
抽象类的实质作用:接口规范
因为它只代表了一个规范,并没有具体实现,所以它不能被实例化
-
抽象类通常被多重继承
比如,一个普通的类,实现了多套接口规范,又继承于原有的父类
-
抽象类的析构函数应该声明为virtual,因为它是被设计用于继承的
-