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,因为它是被设计用于继承的。
时间: 2024-10-18 02:44:59

C++发哥笔记(4):类的继承的相关文章

C#学习笔记(类的继承、重写)

1 隐藏基类的方法 当基类继承一个(非抽象的)成员时,也就继承了其实现代码.如果继承的成员是虚拟的,就可以用override关键字重写这段实现代码.无论继承的成员是否为虚拟,都可以隐藏这些代码. 使用下面的代码就可以隐藏: public class MyBaseClass { public void DoSomething() { //Base implementation. } public class MyDerivedClass : MyClass { public void DoSome

C++发哥笔记(1):class的分离式写法

把class的成员函数的定义写在class之外,即class的大括号的外面. 例如, /////////// main.cpp //////////// class Object { public: int x; void Test();   // (1) 成员函数的声明 }; void Object::Test() //(2) 成员函数写在外边,加上类名限定 { } 总结一下这种写法: 1:成员变量:还是写在类里面 2:成员函数:在类里保留其函数声明,而函数的定义写在类体之外. 3:写在外面的

C++ Primer 学习笔记_72_面向对象编程 --句柄类与继承[续]

面向对象编程 --句柄类与继承[续] 三.句柄的使用 使用Sales_item对象能够更easy地编写书店应用程序.代码将不必管理Item_base对象的指针,但仍然能够获得通过Sales_item对象进行的调用的虚行为. 1.比較两个Sales_item对象 在编写函数计算销售总数之前,须要定义比較Sales_item对象的方法.要用Sales_item作为关联容器的keyword,必须能够比較它们.关联容器默认使用keyword类型的小于操作符,可是假设给Sales_item定义小于操作符,

C++ Primer 学习笔记_69_面向对象编程 --继承情况下的类作用域

面向对象编程 --继承情况下的类作用域 引言: 在继承情况下,派生类的作用域嵌套在基类作用域中:如果不能在派生类作用域中确定名字,就在外围基类作用域中查找该名字的定义. 正是这种类作用域的层次嵌套使我们能够直接访问基类的成员,就好像这些成员是派生类成员一样: Bulk_item bulk; cout << bulk.book() << endl; 名字book的使用将这样确定[先派生->后基类]: 1)bulk是Bulk_item类对象,在Bulk_item类中查找,找不到名

mootools1.5.1使用笔记:类的创建,继承,为现有类增加新方法

1 window.addEvent('domready',function(){ 2 /* 3 新建一个Person的类,类上有 name属性和sayHello方法: 4 */ 5 var Person= new Class({ 6 initialize: function(name){ 7 this.name = name; 8 }, 9 sayHello:function(){ 10 console.log('hello,my name is '+this.name); 11 } 12 13

【代码笔记】Java基础:类的继承(构造器)

在Java中,创建对象的格式为: 类名 对象名 = new 类名(): 如: 1 JFrame jf = new JFrame(); 一个对象被创建出来时,经常要先做一些事这个对象才能正常使用,也可以说是准备工作,比如给这个对象赋些初始值,或者先运行一些别的方法.这时候就需要一个“构造器”用于: 构造对象 初始化属性这样,把那些在创建时要做的事写在构造方法里,每次创建就会被执行一遍. 我们常见的一种Java格式: public 返回类型 方法名(参数){ } 如: 1 public void s

Python 笔记 : 类和继承

# -*- coding=  utf-8 -*- # 文件编码定义的语法规则是: coding[:=]/s*([-/w.]+) # 未指定编码将默认为 : ASCII # 同时要注意物理文件的编码也要符合本语言指定的编码 # 更多可参考: http://python.org/dev/peps/pep-0263/ class CBase: '''''资源相关类'''     # 通过CBase.__doc__ 或者类实例.__doc__ 可以输出此说明 counter = 0;    # 这是属于

C++ Primer 学习笔记_71_面向对象编程 --句柄类与继承

面向对象编程 --句柄类与继承 引言: C++中面向对象编程的一个颇具讽刺意味的地方是:不能使用对象支持面向对象编程,相反,必须使用指针或引用. void get_prices(Item_base object, Item_base *pointer, Item_base &reference){ //需要根据指针或引用实际所绑定的类型进行调用 cout<< pointer ->net_price(1)<< endl; cout<< reference.n

C++ Primer 学习笔记_69_面向对象编程 -继承景况下的类作用域

面向对象编程 --继承情况下的类作用域 引言: 在继承情况下,派生类的作用域嵌套在基类作用域中:如果不能在派生类作用域中确定名字,就在外围基类作用域中查找该名字的定义. 正是这种类作用域的层次嵌套使我们能够直接访问基类的成员,就好像这些成员是派生类成员一样: Bulk_item bulk; cout << bulk.book() << endl; 名字book的使用将这样确定[先派生->后基类]: 1)bulk是Bulk_item类对象,在Bulk_item类中查找,找不到名