C++ 继承 文字+图片+代码 超详细解刨

什么是继承?

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。

继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。

3437c5f7cf254eefa6dc75be82cd8ac6.png

我们称被继承的类为基类(父类),称继承的类叫做派生类(子类)。

继承

继承方式

继承有三种方式,分为public继承,protected继承,private继承,每种继承都有不同之处,我们用一张图来很好的总结它们的区别

1f24d85be919497eb55f0533333e4668.png

public继承

public继承是我们最常用的继承方式,而大部分情况下我们也只会用到这种继承方式。

class Person {                     //基类(父类)

public:
	string _name = "张三";
protected:
	string _tel;
private:
	string _sex;
};


class Student : public Person{    // 派生类(子类)
};

public继承对于基类的public成员继承给派生类仍然是public成员,

对于基类的protected成员继承给派生类仍然是protected成员,

但是对于基类的private成员,继承给是派生类是不可见状态,你无法在派生类访问到基类的private成员,就像是在类外一样。

继承对于基类的private成员是特殊的,无论是什么继承,基类的private成员在派生类中都是不可见的!

5a10fb834d8c4fe5823c62078ed9d86c.png

 protected继承

class Person {                     //基类(父类)

public:
	string _name = "张三";
protected:
	string _tel;
private:
	string _sex;
};


class Student : protected Person{    // 派生类(子类)
};

protected继承对于基类的public成员继承给派生类会变为protected成员,

对于基类的protected成员继承给派生类仍然是protected成员,

对于基类的private成员,继承给是派生类是不可见状态。

 1a24526e5bd84b43a7147071da4f9f43.png

private继承

class Person {                     //基类(父类)

public:
	string _name = "张三";
protected:
	string _tel;
private:
	string _sex;
};


class Student : private Person{    // 派生类(子类)
};

了解了上面两种继承,是否掌握了规律?

private继承对于基类的public成员继承给派生类会变为private成员,

对于基类的protected成员继承给派生类会变为private成员,

对于基类的private成员,继承给是派生类是不可见状态。

24751a4879d74e4c87b4a157b4a95e93.png

 默认继承

class和struct有各自的默认继承方式,既然是默认继承,那么就说明可以不指定继承方式

class Person {                     //基类(父类)
public:
	string _name = "未知";
};

class Student : Person {    // class的默认继承方式是private继承
protected:
	int _id;
};

class Teacher : Person {    // struct的默认继承方式是public
protected:
	int _workid;
};

即使有默认继承方式,我们也推荐显式写上它的继承方式。

总结

1f24d85be919497eb55f0533333e4668.png

1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私
有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面
都不能去访问它。


2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在
派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。


3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他
成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected
> private。


4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过
最好显示的写出继承方式。


5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡
使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里
面使用,实际中扩展维护性不强

基类的隐藏

当基类和派生类存在同名成员变量或者同名成员函数会发生什么?

会使得基类的同名成员构成隐藏,使得基类的同名成员无法直接访问,需要指定基类的类域才能访问!

class Person {                     //基类(父类)

public:
	void func()
	{
		cout << "Person" << endl;
	}
	string _name = "未知";
};


class Student : private Person {    // 派生类(子类)
public:
	void func()
	{
		cout << "Student" << endl;
	}
	string _name = "张三";
};

如果我们不指定类域直接访问

9cd2155f0db944cead27d97d61681a4a.png

会发现都是访问的派生类的同名成员。

那么有没有办法访问到基类的同名成员呢?  指定类域!

5a862894ac1b4c08a44137bdabf74402.png

  从这里也可以发现,派生类是会储存基类的数据的。

派生类对基类的赋值转换

派生类对象可以赋值给 基类的对象 / 基类的指针 / 基类的引用。 而我们称这种现象叫做“切片”,为什么叫切片呢?   我们看下面这张图
 

5b5e1e23278a4f4cbf1ff64f7f23d3ca.png

 这种赋值对于基类来讲,就相当于派生类把自己特有的那一部分切掉变成了基类。

而这种赋值,并不是我们之前遇到的赋值,会发生隐式类型转换。

这种赋值更适合叫它为一种特殊的转换。

我们也可以借着切片来了解派生类的结构

bb7b719400b94eaeb410884362c8d648.png

 我们调用内存监控,就可以发现我们这里的被转化的基类Person ps1在内存中只占了4个字节,相对于派生类的8个字节被切掉了派生类本身自有的4个字节。

而对于基类指针和基类引用的转换

c907dcdc0e5543bfb14db1565c46c4fa.png

 可以发现 基类指针 和 基类引用  的地址与派生类的地址是一致的。说明它们会访问同一块空间,只不过Person* 和 Person& 访问的空间有限,只能访问自己(基类)的成员。

那么,基类对象能否转化为派生类对象呢?

答案是不能的!  不过基类指针可以通过强制类型转换 变为派生类指针 。(这会存在隐患,会有非法访问的问题)

 8223ac90f7e849f094b1850cef1ae0b2.png

派生类的默认成员函数

继承的派生类的默认成员函数相对于没有继承的类的默认成员函数是有不同的,为什么呢?

首先要明白派生类的地址结构

15cdac0a66c44540a7d5dd56beedfee4.png

 既然一个派生类要基类和派生类的成员,那么对于基类的成员,派生类怎么操作呢?

默认构造函数

class Person {                     //基类(父类)

public:
	Person(string name)
		:_name(name)
	{}

public:
	string _name = "未知";
};


class Student : public Person {    // 派生类(子类)
public:
	Student(string name,int id = 2023, int age = 18)
		:Person(name)       //是通过这样的格式来给基类成员初始化的
		,_id(id)
		, _age(age)
	{}
	void Print()
	{
		cout << "姓名:" << _name << "   学号:" << _id << "   年龄:" << _age << endl;
	}
//protected:
public:
	int _id;
	int _age;
};


int main()
{
	Student st("张三");
	st.Print();
	return 0;
}

通过构造函数的初始化列表来给基类成员进行初始化!

注意:我们这里模拟的是编译器默认生成的默认构造函数。对于派生类的构造函数,我们可以调用基类的构造函数来完成对基类成员的初始化!

拷贝构造函数

class Person {                     //基类(父类)

public:
	Person(const Person& st)
		:_name(st._name)
	{}
public:
	string _name = "未知";
};


class Student : public Person {    // 派生类(子类)
public:
	Student(const Student& st)
		:Person(st)         //这里就用了基类的赋值转换知识
		,_id(st._id)
		,_age(st._age)
	{}

	void Print()
	{
		cout << "姓名:" << _name << "  学号:" << _id << "  年龄:" << _age << endl;
	}
//protected:
public:
	int _id;
	int _age;
};

注意:我们这里模拟的是编译器默认生成的默认拷贝构造函数。对于派生类的拷贝构造函数,我们可以调用基类的拷贝构造函数来完成对基类成员的初始化!

赋值重载函数

class Person {                     //基类(父类)

public:
	Person& operator=(const Person& ps)
	{
		if (this != &ps)
		{
			_name = ps._name;
		}
		return *this;
	}
public:
	string _name = "未知";
};


class Student : public Person {    // 派生类(子类)
public:
	Student& operator=(const Student& st)
	{
		if (this != &st)
		{
			Person::operator=(st);
			_id = st._id;
			_age = st._age;
		}
		return *this;
	}
	void Print()
	{
		cout << "姓名:" << _name << "   学号:" << _id << "   年龄:" << _age << endl;
	}
//protected:
public:
	int _id;
	int _age;
};

这里的赋值重载就与 刚刚写的构造函数和拷贝构造函数不一样了,他需要在派生类中指定基类的类域来访问基类的赋值重载函数,并且这里也运用了切片的知识!

efaa31557f13485d9de587174772851b.png

析构函数

class Person {                     //基类(父类)

public:
	~Person()
	{}

public:
	string _name = "未知";
};


class Student : public Person {    // 派生类(子类)
public:
	~Student()
	{
		//Person::~Person();   //这里我们需要手动去调基类的析构函数吗?
                       //-> 不需要,因为派生类的析构函数结束时会自动调用基类的析构函数!
	}

//protected:
public:
	int _id;
	int _age;
};

派生类的析构函数需要特别注意,在派生类析构函数结束的时候,会自动去调用基类的析构函数,所以我们就不需要手动去调用基类的析构函数,不然的话会调用两次析构。

而如果你想手动去调用 则跟赋值重载一样指定类域即可。(一般不会这么做)

注意:调用析构的话,析构顺序是先析构派生类的成员,然后再析构基类的成员。

继承与友元

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
 

class Person {                     //基类(父类)

public:
	friend void func();
protected:
	string _name;
};


class Student : public Person {    // 派生类(子类)
public:
	void Print()
	{
		cout << "姓名:" << _name << "   学号:" << _id << "   年龄:" << _age << endl;
	}
protected:
	int _id;
	int _age;
};

void func()
{
	Person ps;
	ps._name = "张三";      //因为是友元,所以可以直接进行访问
	cout << ps._name << endl;

	Student st;
	st._id = 2023;           //友元关系无法被继承
	cout << st._id << endl;

}

继承与静态成员

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。
 

class Person {                     //基类(父类)

public:
	string _name;
	static int _count;
};

int Person::_count = 0;

class Student : public Person {    // 派生类(子类)
public:
protected:
	int _id;
	int _age;
};

13149796400f4e8781446eb8aa0ac21d.png

 继承体系中的静态成员是共享的。

多继承

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

class Animal {
public:
	string _species = "human";
};

class Person {                     //基类(父类)

public:
	string _name;
};

class Student : public Person ,public Animal{    // 派生类(子类)
public:
protected:
	int _id;
	int _age;
};

菱形继承

菱形继承是基于多继承的一种复杂且特殊的情况

8b6c14d2310a43808dae1869bc36b664.png

 菱形继承就会出现这样一个问题,继承的B类和C类同时拥有A类的结构,如果B、C再多继承给D类,就会出现D类拥有两个A类的结构,这会导致什么问题?   二义性和数据冗余

class A {
public:
	int _num;
};


class B : public A{
public:
	int _Bid;
};

class C : public A {
public:
	int _Cid;
};

class D : public C, public B {
public:

};

二义性问题

d711c02679be4321932fbc445a25abc3.png

 如果不指明类域,就会有二义性问题,所以指定类域可以解决这种问题

2069f821127c4e43ae221b0a47f027d6.png

 从这张图可以看出来,C空间的数据是先存储的,其次是B空间的数据,最后才是D空间的数据,而导致这个顺序的原因就是我们是先继承的C,再继承的B。 

31af7031f135496ba7bba7f248880607.png

虽然说指定类域可以解决二义性的问题,但是实际我们真的需要两个A类的数据吗?

一般是不需要的!  既然如此,这是不是就属于浪费了空间!

为了解决这个问题,C++专门有一个关键字来处理这个问题————virtual(虚拟继承)

虚拟继承(virtual)

使用方法: 在菱形继承体系的腰部加入virtual关键字

class A {
public:
	int _num;
};


class B : virtual public A{
public:
	int _Bid;
};

class C : virtual public A{
public:
	int _Cid;
};

class D : public C, public B {
public:
	int _Did;
};

这个时候我们称A类为虚基类

菱形虚拟继承对被多继承的类的影响

如果使用了虚拟进程,那么D的模型结构就会发生变化 

b101455aecf54e39861c60977119831f.png

先来看看C空间的数据,本该存放虚基类的成员数据区域存放了一个像是指针的数据。(其实就是一个指针,而这个指针叫做虚基表指针

B空间的数据与C一样,本该存放它的虚基类的成员数据区域也存放了一个虚基表指针。

再看虚基类的成员数据的数据竟然是存放在最后面

如此看来,虚拟继承的数据结构就与正常继承的模型结构有了很大的区别。

首先就引出一个问题 ,这个时候如果我们要去访问_num,它怎么去找到存放真正的_num的地址?现在B,C空间本该存放_num的地址存放了虚基表指针。

我们这就探讨虚基表指针到底存放了什么东西

b45cd0f49b4a4ff389c33bf11a8a911d.png

而这段数据,称为虚基表 

这时候发现了什么,第二行数据竟然存放着与存放着真正的_num地址的偏移量!

2e3e71621f724ddda12204a2b750a19b.png

菱形虚拟继承对于腰部类的影响

再来看看B,C的模型

虚拟继承对于B和C的模型结构也产生了类似的变化。

冗余问题的解决

我们看上面的例子,虚拟继承貌似并没有解决代码冗余的问题,它不是仍然存在着数据的浪费吗?

不仅浪费了,甚至还多开了一部分空间来存放数据。

其实不然,这里举例是A只有了一个int,但是如果不仅仅只是一个int数据,或者是一个数组,那么就很好的节约了空间。

class A {
public:
	int _num;
	int _num1;
	int _num2;
	int arr[10];
};


class B : virtual public A {
public:
	int _Bid;
};

class C : virtual public A {
public:
	int _Cid;
};

class D : public C, public B {
public:
	int _Did;
};

fa914e266daf4a2da92226798691871f.png

 这种情况就节约了一大部分空间

继承的总结

C++的继承是比较复杂的,因为它有多继承,从而衍生出了十分复杂的菱形继承,所以可以理解多继承就是C++的一个缺陷。

而相对于java的继承来讲,java并没有那么多种继承方式,java只有一种public继承,并且也没有多继承的概念,也就更没有了菱形继承。

但是,C++作为走在最前沿的语言,是肯定要踩一些坑的,这些无可厚非。

  • 17
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 17
    评论
第一部分 了COM 第1章 COM概述 何谓CoM COM术语 COM利与弊 COM的好处 COM的局限性 COM组件与接口 何谓接口 接口特征 接口类型 接口规则 接口设计 COM组件的实现规则 实现IUnknown规则 内存管理规则 引用计数规则 COM激活 COM类型 COM客户机 COM服务器 ActiveX控件 COM与面向对象技术 包装 抽象 多态 继承 COMTrader应用程序 小结 第2章 由VC++建立并使用COM服务器 IDL文件 建立第一个COM服务器 定义自定义接口 实现IUnknown和自定义接口 完成COM服务器 生成测试客户机 用ATL建立COM服务器 关于ATL 用ATL建立进程内COM服务器 用ATL建立进程外COM服务器 线程与COM服务器 Win32多线程应用 线程COM组件 自动化与IDispatch 用VC++实现IDispatch ATL与自动化 Automation数据类型 再谈类型库 C++自动化客户机 VB自动化客户机 小结 第3章 用VB建立并使用COM服务器 选择COM项目 设计接口 描述接口 浏览接口 生成对象 使用ClassBuilder 增加属性 增加方法 增加事件与枚举 使用ActiveXDataObject(ADO) 在服务器组件中使用Recordset对象 在客户机组件中使用ADOR 生成断开的Recodset 生成自己的RecodsctS 使用用户定义类型 错误处理 服务器客户机错误处理 使用VBErr.Raise机制 在VB中使用线程模型 设置线程模型 了再入性与公寓 小结 第二部分 COM与Internet 第4章 在VC++中建立并使用ActiveX控件 ACtiveX控件概还 属性与方法 控件与容器通信 事件与连接点 建立第一个控件 生成控件 测试控件 增加方法 增加属性 增加事件 增加属性页 允许属性保持 使用控件 建立复合控件 增加复合控件 增加功能 增加事件 处理复合控件事件 处理错误 使用控件 小结 第5章 在VB中建立并使用ActiveX控件 VB控件简介 约束与无约束控件 控件生成技术 属性类型 方法 属性配置 过程属性 环境属性配置 运行时只读属性 只在运行时有效的属性 扩展属性 容器属性 合成控件属性 可关联属性 持续与属性包 属性包 使用ActiveX控件界面向导 了控件寿命 生成ActiveX控件 生成无约束控件 生成设计时数据约束控件 生成运行数据约束控件 小结 第6章 用VC++建立InternetCOM组件 IEActiveX控件 轻量级控件 安全控件 持续属性 文档对象模型编程 活动服务器组件 活动服务器页面 ASP页面的COM组件 小结 第7章 用VB建立InternetCOM组件 无窗口控件 ActiveX控件容器的线程模型 ActiveX控件的安全性 Web页面访问 VBDHTML项目 DHTML项目基础 DHTML应用程序样本 VBIIS应用程序 WebClass 一个IIS应用程序样本 设计控件 设计控件与HTML文件 样本设计控件 小结 第三部分 了DCOM 第8章 DCOM概述 何谓DCOM 为什么使用DCOM DCOM操作 DCOM组件位置 进程内或进程外组件 代理 RPC(RemoteProcedureCall,远程过程调用) 调动 数据传递 DCOM配置实用程序 DCOM应用程序的安全机制 验证 授权 加密 整性检查 小结 第9章 用VC++建立DCOM服务器 标准与自定义调动 标准调动 自定又调动 网络通伯 远程激活 AppID注册表项 可配置AppID注册表项参数 IUknown优化 DCOM与NT服务 NT服务解剖 基于NT服务的COM服务器 小结 第10章 用VB建立DCOM服务器 应用程序对象模型 何谓对象模型 如何生成对象模型 DCOM设计准则与技术 再论调动 按数值与按引用 DCOM进程外服务器 建立DCOM组件 增加测试客户机 IIS应用程序 增加WebClasses 使用模板 增加自定义Webltems 远程错误处理 小结 第四部分 了COM++ 第11章 COM++概述 COM与WindowsDNA 用户界面层技术 中间层技术 数据库层技术 组件服务配置 事务处理 排队组件(QC) 实时结构的限制 事务性消息排队 排队组件结构 排队组件故障恢复 QC安全性 动态负荷平衡 对象地 小结 第12章 用VC++建立COM++组件 ADO编程 ADO与OLEDB VC++中的ADO VC++的ADO扩展 建立COM++应用程序 温习IObjectContext接口 用ATL建立COM++组件 编制基于角色的安全性 处理COM+事务 控制事务结果 指定事务属性 确定事务情境 传递接口指针 共享状态 建立事务性COM+组件 小结 第13章 用VB建立COM+组件 了事务 事务与多层应用程序 COM+与事务 事务属性:ACID COM+系统简介 COM+运行环境 COM+ComponentServices COM+接口 资源分配器 应用程序组件 探索COM+编程模型 COM+组件作为COMDLL 基本COM+编程规则 COM+API 用VB编程COM+ 对象描述表 COM+组件的生命周期 ObjectControl接口 MTS活动 COM+中生成对象 安全引用 组件之间的参数传递 数据类型 使用分布式事务 分布式事务协调器(MSDTC) COM+事务的工作 事务与有状态对象 使用共享属性管理器(SPMSharedProperyManager) 小结 第14章 了MSMQ 何谓MSMQ MSMQ的好处 MSMQ组件 队列 消息 MSMQ对象模型 MSMQ设置 MSMQ基础 消息发送 消息接收 MSMQ事件 MSMQ事务 小结 第五部分 高级COM与COM+ 第15章 VC++与VB中的COM+服务 了COM+激活 描述表包装器 激活顺序 使用即时(JIT)激活 使用对象构造 中性公寓简介 了同步域 表示事务状态 取得对象信息 使用对象地 对象池的好处 对象地要求 对象地配置 使用排队组件 QC限制 QC配置 QC调用 QC播放控件 使用负荷平衡 负荷平衡要求 负荷平衡配置 小结 第16章 COM与COM+安全性 何谓安全性 WindowsNT安全简介 NT验证 NT扮演 NT访问控制 COM安全结构 验证 访问控制 启动权限 标_ 扮演与掩盖 安全总括 COM+安全 COM+说明性安全 COM+角色 编程COM与COM+安全 整个进程安全 接口级安全 激活安全 服务器方安全 调用描述表安全信息 SecuntyProperty信息 安全性与数据库访问 小结 第17章 Windows2000中的新COM特性 同步机制 COM同步API COM同步接口 异步COM 异步接口构造 异步接口调用 关于异步服务器与客户机 让服务器进行异步处理 调用序列化与自动完成 COM管道 COM管道接口 异步管道与提前读取 调用对象与调用取消 调用取消请求 调用取消处理 轻量级处理器 标准LWH 自定义LWH 小结 第六部分 调试与部署COM和COM+应用程序 第18章 调试与剖析COM和COM+应用程序 调试VB组件 调试MTS组件 调试COM+组件 使用条件编译 调试VC++组件 用VisualStUdioAnalyzer剖析 小结 第19章 部署COM与COM+应用程序 DCOM应用程序部署 配置DCOM服务器 配置DCOM客户机 在Internet上部署 Internet上部署与包装 签名CAB文件 许可ActiveX控件 自动化COM+配置 使用COMAdmin接口与集合 配置COM+应用程序 配置组件 配置角色 部署COM+应用程序 小结

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

风君子吖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值