首页 > 代码库 > C++发哥笔记(4):类的继承
C++发哥笔记(4):类的继承
继承 在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 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成员都被继承。 什么叫被继承?就是这些父类的成员就如同直接写在子类里一般。 代码上可以简化 虚拟继承,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()是父类的、还是子类的? 指针p的类型是Parent* 指针p指向的对象是Child* 调用者的初衷:因为p指向的是对象是子类对象,所以应该调用子类的Test()。 虚拟继承: virtual 当一个成员函数需要子类重写,那么在父类应该将其声明为virtual。 (有时将声明为virtual的函数为“虚函数”) 例如 class Parent { public: virtual void Test(); }; virtual本身表明该函数即将被子类重写。 加virtual关键字是必要的。 考虑以下情况, Parent* obj = new Child(); // 语法允许,合乎情理 obj->Test(); 此时,如果Test()在父类中被声明为virtual,是调用的是子类的Test()。 这解释了virtual的作用:根据对象的实际类型,调用相应类型的函数。 注意: (1)只需要在父类中将函数声明为virtual,子类自动地就是virtual了。 (2)即将被重写的函数添加virtual,是一条应该遵守的编码习惯。 小结 介绍继承关系中,对函数重写后的结果 介绍virtual关键字的作用和必要性(父类指针指向子类对象) 继承:构造与析构 有Child类继承于 Parent类 class Child : public Parent {} 那么,当创建一个子类对象时:(编译器默认动作) 子类对象构造时,先调用父类的构造函数,再调用子类的构造函数。 子类对象析构时,先调用子类的析构函数,再调用父类的构造函数。 在VC中演示: 子类的构造 子类的析构 当父类有多个构造函数,可以显式的调用其中的一个构造函数。 如果没有显式调用,则调用了父类的“默认构造函数” 记住调用方法: Parent(1,1) virtual 析构函数 当一个类被继承时,应该将父类的析构函数声明为virtual, 否则会有潜在的问题。 class Parent { virtual ~Parent(){} // 声明为virtual }; 考虑以下场景: Parent* p = new Child(); delete p; // 此时,调用的是谁的析构函数? 如果析构函数没有标识为virtual,则有潜在的隐患,并有可能直接导致程序崩溃。(资源没有被释放,并引申一系列问题) 类的大小,与 virtual关键字的影响 (1) 类的大小由成员变量决定。(这struct的原理相同) 类的大小成员函数的个数无关,即使一个类有10000个成员函数,对它所占的内存空间是没有影响的。 (2) 但是,如果有一个成员函数被声明为virtual,那类的大小会有些微的变化。(这个变化由编译器决定,一般是增加了4个字节) 小结 1.介绍继承关系中,父类的构造函数和析构函数将被调用。 2.当一个类被别的类继承时,应该将父类的析构函数声明为virtual。 (注:如果这个类在设计的时候,已经明确它不会被继承,则不需要声明为virtual) 3. 构造函数不能加 virtual 多重继承 (注:初学者可以跳过这一集,或者听一下有个印象就行) 定义这个语法的本意:一个孩子有父有母,可以从父母处各自继承一些特点。 语法: 用Father, Mother表示二个类 class Child : public Father, public Mother { }; 表示Child继承于Father,Mother 在写法上,以冒号引导,每个父类用逗号隔开 多重继承的结果:从所有父类中,继承他们所有可以被继承的成员(public/protected) 在VC中展示 多重继承的问题 多重继承的问题:很明显,当多个父类有相同的成员时,会影响冲突。 所以,C++的抽象世界和现实世界是不一样的。 实际上,多重继承的理念一般是不会用到的。问题颇多。 (多重继承的有一个有用的地方,在下一集“纯虚函数”中介绍) 小结 1.可以多重继承,但多重继承一般是不使用的。(只有一种常见应用场景,在下一章) 2.我们要记住什么:只需要记住它是怎么写的 class Child : public Parent1, public Parent2 { }; 纯虚函数,抽象类 什么是纯虚函数 这次课的地位:很重要,设计模式中的概念:接口 但初学者第一次学习时只需要有个印象,等学完了全书再回头专门学习。 纯虚函数的语法: 将成员函数声明为virtual 后面加上 = 0 该函数没有函数体 例如, 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; } 小结 1.如何定义一个纯虚函数 2抽象类的实质作用: 接口规范 因为它只代表了一个规范,并没有具体实现,所以它不能被实例化。 3. 抽象类通常被多重继承 比如,一个普通的类,实现了多套接口规范,又继承于原有的父类。 4. 抽象类的析构函数应该声明为virtual,因为它是被设计用于继承的。
C++发哥笔记(4):类的继承
声明:以上内容来自用户投稿及互联网公开渠道收集整理发布,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任,若内容有误或涉及侵权可进行投诉: 投诉/举报 工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。