文章来源:c++从入门到精通(第四版 清华大学出版社)

目录

一.单继承

1.类的继承

2.继承后可访问性

3.构造函数访问顺序

4.子类显式调用父类构造函数

5.子类隐藏父类的父类函数

二.多重继承

1.多重继承定义

2.二义性

3.多重继承构造顺序


继承(inheritance)是面向对象的主要特征(此外还有封装和多态)之一,它使得一个类可以从现有的类中派生,而不必重新定义一个新的类。继承的实质是用已有的数据类型创建新的数据类型,并保留已有数据类型的特点,以旧类为基础创建新类,新类包含了旧类的数据成员和成员函数,并且可以在新类中添加新的数据成员和成员函数。旧类被称为基类或父类,新类被称为派生类或子类。

一.单继承

1.类的继承

class 派生类名标识符:[继承方式] 基类名标识符
{[访问控制修饰符:][成员声明列表]} 

继承方式有三种派生类型,分别为公有(public)型、保护(protected)型和私有(private)型

,访问控制修饰符也是public、protected、private 3种类型。成员声明函数列表中包含类的成员变量和成员函数,是派生类新增的成员。“:”是一个运算符,表示基类和派生类之间的继承关系。

例如,定义一个继承员工类的操作成员类。

定义一个员工类,它包含员工id,员工姓名,所属部门等信息。

class CEmployee{//定义员工类 public:int m_ID;//id char m_Name[128];//姓名 char m_Depart[128];//部门
};

定义一个操作员类。通常操作员属于公司的员工,它包含员工ID,员工姓名,所属部门等信息,此外还包含密码信息、登陆方法等。

class COperator:public CEmployee//定义一个操作员类,从CEmployee类派生而来
{public:char m_password[128];//定义密码 bool Login();
}

操作员类是从员工类派生的一个新类,新类中增加密码信息、登陆方法等信息,员工id、员工姓名、所属部门等信息直接从员工类中继承得到。

实例1 以公有方式继承

#include<bits/stdc++.h>
using namespace std;class CEmployee{//定义员工类 public:int m_ID;//id char m_name[128];//姓名 char m_Depart[128];//部门 CEmployee(){memset(m_name,0,128);//初始化m_namememset(m_Depart,0,128); } void OutputName(){cout<<"员工姓名:"<<m_name<<endl;}
};
class COperator:public CEmployee//定义一个操作员类,从CEmployee类派生而来
{public:char m_password[128];//定义密码 bool Login(){if(strcmp(m_name,"Phoebe")==0&&strcmp(m_password,"phoebe12345")==0){cout<<"登陆成功!"<<endl;return true; }else{cout<<"登录失败!"<<endl;return false; }}};
int main()
{COperator optr;strcpy(optr.m_name,"Phoebe");strcpy(optr.m_password,"phoebe12345");optr.Login();optr.OutputName();return 0;
}

程序中CEmployee类是COperator类中的基类,也就是父类,COperator类将继承CEmployee类的非私有成员(private类型成员不能被继承)。optr对象初始化m_name和m_password成员后,调用了Login成员函数。

用户在父类中派生子类时,可能存在一种状况,即在子类中定义了一个与父类名相同的成员函数。例如重新定义了一个COperator类,添加一个OutputName成员函数。

2.继承后可访问性

继承方式有public、private、protected 3种,它们的说明如下:

1.公有类派生

公有型派生表示对于基类中的public数据成员和成员函数,在派生类中依然是public;对于基类中的private数据成员和成员函数,在派生类中依然是private。

例如

class CEmployee{public:void Output(){cout<<m_ID<<endl;cout<<m_Name<<endl;cout<<m_Depart<<endl;}private:int m_ID;char m_Name[128];char m_Depart[128];
};
class COperator:public CEmployee
{public:void Output(){cout<<m_ID<<endl;//引用基类的私有成员 错误 cout<<m_Name<<endl;//引用基类的私有成员 错误 cout<<m_Depart<<endl;//引用基类的私有成员 错误 cout<<m_Password<<endl;//正确 }private:char m_password[128];bool Login();}

COperator类中无法访问后,CEmployee类中的private数据成员m_ID、m_Name和m_Depart,如果将CEmployee类中的所有成员都设置为public后,COperator类才能访问CEmployee类中的所有成员。

例如

class CEmployee{public:void Output(){cout<<m_ID<<endl;cout<<m_Name<<endl;cout<<m_Depart<<endl;}
//  private:int m_ID;char m_Name[128];char m_Depart[128];
};
class COperator:public CEmployee
{public:void Output(){cout<<m_ID<<endl;//正确 cout<<m_Name<<endl;//正确 cout<<m_Depart<<endl;//正确 cout<<m_Password<<endl;//正确 }private:char m_password[128];bool Login();}

2.私有类派生

私有型派生表示对于基类中的public、protected数据成员和成员函数,在派生类中可以访问,基类中的private成员,在派生类中不可以访问。

class CEmployee{public:void Output(){cout<<m_ID<<endl;cout<<m_Name<<endl;cout<<m_Depart<<endl;}int m_ID;char m_Name[128];private: char m_Depart[128];
};
class COperator:private CEmployee
{public:void Output(){cout<<m_ID<<endl;//正确 cout<<m_Name<<endl;//正确 cout<<m_Depart<<endl;//错误 cout<<m_Password<<endl;//正确 }private:char m_password[128];bool Login();}

3.保护类派生

保护类派生表示对于基类中的public、protected数据成员和成员函数,在派生类中均为protected,protected类型在派生类定义时可以访问。用派生类声明的对象不可以访问。也就是说在类体外不可以访问。protected成员可以被基类所有派生类使用,这一性质可以沿继承树无限向下传播。

因为保护类的内部数据不能被随便更改,实例类本身负责维护,这就起到很好的封装作用。把一个类分作两部分,一部分是公共的,另一部分是保护。保护成员对于使用者来说是不可见的,也是不需要了解的,这就减少了类与其他代码的关联程度。类的功能是独立的,它不依赖于应用程序的运行环境。既可以放到这个程序中使用,也可以放到另一个程序中使用。这就非常容易的用一个类来替换另一个类。类访问限制的保护机制使人们编制的应用程序更加可靠和容易维护。

3.构造函数访问顺序

由于父类和子类都有构造函数和析构函数,因此子类对象在创建时是父类先进行构造,还是子类先进行呢?对于其析构函数呢?

答案是:当从父类派生一个子类并声明一个子类的对象时,它将先调用父类的构造函数,然后调用当前类的构造函数来创建对象;

在释放子类对象时,先调用的是当前类的构造函数,然后是父类的析构函数(像搭房子一样,从下往上盖,从上往下拆)。

实例(构造函数的访问顺序):

#include<bits/stdc++.h>
using namespace std;
class CEmployee{public:int m_ID;char m_Name[128];char m_Depart[128];CEmployee(){cout<<"CEmployee类构造函数被调用"<<endl; } ~CEmployee(){cout<<"CEmployee类析构函数被调用"<<endl; }
};
class COperator:private CEmployee
{public:char m_password[128];COperator(){strcpy(m_Name,"phoebe");//设置数据成员 cout<<"COperator类构造函数被调用"<<endl; }~COperator(){cout<<"COperator类析构函数被调用"<<endl; }};
int main(){COperator optr;return 0;
}

在分析完对象的创建、释放过程后,会考虑这样一种情况:

定义一个基类类型的指针,调用子类的构造函数为其创建对象,当对象释放时,需要调用父类的析构函数还是先调用子类的析构函数再调用父类的析构函数呢?

答案是:如果析构函数是虚函数,则先调用子类的析构函数,再调用父类的析构函数。如果析构函数不是虚函数则只调用父类的析构函数。可以想象如果在子类中为某个数据成员在堆中分配了空间,父类中的析构函数不是虚成员函数,将使子类中的析构函数不再被调用,其结果是对象不能正确的被释放,导致内存泄漏的发生。因此,在编写类的成员函数时,析构函数通常是虚函数。构造函数的调用顺序不受基类在成员初始化列表中是否存在以及被列出列表顺序的影响。

4.子类显式调用父类构造函数

当父类含有带参数的构造函数时,子类创建的时候会调用它吗?

答案是通过显示调用才可以调用。

无论创建子类对象时,调用的是哪种子类构造函数,都会自动调用父类默认构造函数。若想调用父类带参数的构造函数则需要显示的方式。

#include<bits/stdc++.h>
using namespace std;
class CEmployee{public:int m_ID;char m_name[128];char m_Depart[128];CEmployee(char name[]){strcpy(m_name,name);cout<<m_name<<"调用了带参的构造函数"<<endl; }CEmployee()//相当于函数重载{strcpy(m_name,"Phoebe");cout<<m_name<<"CEmployee类无参构造函数被调用"<<endl;//输出信息 } ~CEmployee(){cout<<"CEmployee类构造函数被调用"<<endl; }
};
class COperator:public CEmployee
{public:char m_password[128];COperator(char name[]):CEmployee(name)//显示调用父类带参数的构造函数 {//设置数据成员 cout<<"COperator类构造函数被调用"<<endl; }COperator():CEmployee("Phoebe")//显示调用父类带参数构造函数 {//设置数据成员 cout<<"COperator类构造函数被调用"<<endl; }~ COperator(){cout<<"COperator类析构函数被调用"<<endl; }
};
int main(){COperator optr1;//定义一个COperator对象 调用自身无参的构造函数 COperator optr2("Lilith");//定义一个COperator对象  调用自身带参的构造函数 return 0;
}

在父类无参构造函数中初始化成员字符串数组m_name为Phoebe,从运行结果上看,子类对象创建时没有调用父类无参构造函数,调用的是带参数的构造函数。

注意:当父类只有带参数的构造函数时,子类必须以显示方法调用父类带参数的构造函数,否则编译会出现错误。

5.子类隐藏父类的父类函数

如果子类中定义了一个和父类一样的成员

#include<bits/stdc++.h>
using namespace std;
class CEmployee{public:int m_ID;char m_name[128];char m_Depart[128];CEmployee()//相当于函数重载{} ~CEmployee(){} void OutputName(){cout<<"调用CEmloyee类的OutputName成员函数"<<endl; }
};
class COperator:public CEmployee
{public:char m_password[128];void OutputName(){cout<<"调用COperator类的OutputName成员函数"<<endl;//输出操作员姓名 }
};
int main(){COperator optr;//定义一个COperator对象 调用自身无参的构造函数 optr.OutputName(); return 0;
}

而如果用户想要访问父类的OutputName成员函数,需要显示使用父类名。(使用预作用符)

    COperator optr;optr.OutputName(); optr.CEmployee::OutputName();//调用父类成员函数 

如果子类中隐藏了父类的成员函数,则父类中所有同名的成员函数(重载函数)均被隐藏,因此下面标注部分的代码是错误的。

#include<bits/stdc++.h>
using namespace std;
class CEmployee
{public:int m_ID;char m_Name[128];char m_Depart[128];CEmployee(){memset(m_Name,0,128);memset(m_Depart,0,128);cout<<"员工类构造函数被调用"<<endl; }void OutputName(){cout<<"员工姓名:"<<endl; }void OutputName(const char *pchData){if(pchData!=NULL){strcpy(m_Name,pchData);cout<<"设置并输出员工姓名"<<pchData<<endl; }}
};
class COperator:public CEmployee
{public:char m_Password[128];void OutputName(){cout<<"操作员姓名:"<<m_Name<<endl; }bool Login(){if(strcmp(m_Name,"Phoebe")==0&&strcmp(m_Password,"12345")==0){cout<<"登陆成功!"<<endl; return true;}else{cout<<"登陆失败!"<<endl;return false; }}
};
int main(){COperator optr;optr.OutputName("Phoebe");//错误代码,不能访问基类的重载成员函数 要改成optr.CEmployee::OutputName才行 return 0;
}

在派生完一个子类后,可以定义一个父类的类型指针,通过子类的构造函数为其创建对象。例如:

CEmployee *pWorker = new COperator()//定义CEmployee类型指针,调用子类构造函数。

如果使用pWorker对象调用OutputName成员函数,例如执行pWorker->OutputName();语句,那么调用的是CEmployee类的OutputName成员函数还是COperetor类的OutputName成员函数呢?

答案是调用CEmployee类的OutputName成员函数。编译器对OutputName成员函数进行的是静态绑定,即根据对象定义时的类型来确定调用哪个类的成员函数。由于pWorker属于CEmployee类型,因此调用的是CEmployee类的OutputName成员函数。

那么是否有成员函数执行pWorker->OutputName();语句调用COperetor类的OutputName成员函数呢?答案是通过定义虚函数可以实现。虚函数会在后面的章节讲到。

二.多重继承

1.多重继承定义

多重继承是指有多个基类名标识符,其声明形式如下:

class<派生类名>;<继承方式1><基类名>,<继承方式2><基类名2>...
{<派生类类体>
} ;

声明形式中有“:”运算符,基类名标识符之间用“,”运算符分开。

例如:鸟能在天空飞翔,鱼能在水中游泳,而水鸟两者得兼。因此在定义水鸟类时,将鸟和鱼同时作为其基类。

#include<bits/stdc++.h>
using namespace std;
class CBird
{public:void FlyinSky(){cout<<"鸟能在天空飞翔"<<endl;}void Breath(){cout<<"鸟能够呼吸"<<endl; }
};
class CFish
{public:void SwiminWater(){cout<<"鱼能够在水里游"<<endl; }void Breath(){cout<<"鱼能够呼吸"<<endl; }
};
class CWaterBird:public CBird,public CFish{public:void Action(){cout<<"水鸟既能飞又能游"<<endl; }
};
int main(){CWaterBird waterbird;waterbird.FlyinSky();//调用从鸟类继承而来的FlyinSky成员函数  waterbird.SwiminWater();//调用从鱼类继承而来的FlyinSky成员函数  return 0;
}

程序中定义了鸟类CBird,定义了鱼类CFish,然后从鸟类和鱼类派生了一个子类水鸟类CWaterBird.鸟类自然继承了鸟类和鱼类的所有公有和受保护的成员,因此CWaterBird类对象能够调用FlyinSky和SwiminWater 成员函数。在CBird类中提供了一个Breath成员函数,在CFish类中同样提供了Breath成员函数,如果CWaterBird类对象调用Breath成员函数,将会执行哪个类的Breath成员函数呢?

答案是将会出现编译错误,编译器将产生歧义,不知道具体调用哪个类的Breath成员函数。为了让CWaterBird类对象能够访问Breath成员函数,需要在Breath成员函数前具体指定类名,例如:

waterbird.CFish::Breath();
waterbird CBird::Breath();

在多重继承中存在这样一种情况, 假如CBird类和CFish类均派生于同一个父类,如CAnimal类,那么当从CBird类和CFish类派生子类CWaterBird时,在CWaterBird类中将存在两个CAnimal类的复制。能否在派生CWaterBird类时,使其只存在一个CAnimal基类呢?为了解决该问题,C++语言提供了虚继承的机制,虚继承会在后面章节讲到。

2.二义性

派生类在调用成员函数时,先在自身的作用域中寻找,如果找不到,会到基类中寻找。但当派生类继承的基类中有同名成员时,派生类中就会出现来自不同基类的同名成员。例如:

class CBaseA{public:void function();
};
class CBaseB{public:void function();
};
class CDeriveC:public CBaseA,public CBaseB{public:void function();
};

CBaseA和CBaseB都是CDeriveC的父类,并且两个父类中都含有function成员函数,CDeriveC将不知道调用哪个基类function成员函数,这句产生了二义性。

3.多重继承构造顺序

单一继承是先调用基类的构造函数,然后调用派生类的构造函数,但多重继承将如何调用呢?多重继承中的基类构造函数被调用的顺序以类派生表中声明的顺序为准。派生表就是多重继承的定义中继承方式后面的内容,调用顺序就是按照基类名标识符的前后顺序进行的。

#include<bits/stdc++.h>
using namespace std;
class CBicycle
{public:CBicycle(){cout<<"Bicycle Construct"<<endl;}CBicycle(int iWeight){m_iWeight=iWeight;}void Run(){cout<<"Run"<<endl;}protected:int m_iWeight;
};
class CAirplane{public:CAirplane(){cout<<"Airplane Construct"<<endl;}CAirplane(int iWeight){m_iWeight=iWeight;}void Fly(){cout<<"Airplane"<<endl;}protected:int m_iWeight;
};
class CAirBicycle:public CBicycle,public CAirplane
{public:CAirBicycle(){cout<<"CAirBicycle Construct"<<endl;}void RunFly(){cout<<"Run and Fly"<<endl;}
};
int main(void){CAirBicycle ab;ab.RunFly();
}

程序中基类的声明顺序是先CBicycle类,后CAirplane类,所以对象的构造顺序就是先CBicycle类,后CAirplane,最后CAirBicycle类。

一篇文章读懂C++ 继承相关推荐

  1. 一篇文章读懂MySQL的各种联合查询

    一篇文章读懂MySQL的各种联合查询 联合查询是指将两个或两个以上的表的数据根据一定的条件合并在一起! 联合查询主要有以下几种方式: 全连接:将一张表的数据与另外一张表的数据彼此交叉联合查询出来 举例 ...

  2. 一篇文章读懂“天猫无货源店群”,这是一个怎么样的项目?

    这是个什么样的项目?(有经验的人可以自动跳过) 天猫店群,一种通过盗取他人天猫店铺内的产品,来进行盈利的电商操作模式,因为不需要我们自己有货,所以被也被称为无货源模式.在天猫上操作就叫天猫无货源店群, ...

  3. java多线程 模型_一篇文章读懂Java多线程模型

    要真正了解Java的多线程,我们还要从进程和线程的概念说起 进程 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础.在早期 ...

  4. 8问8答,一篇文章读懂空间音效

    近日,第一届网易集团创新奖评选落下帷幕,网易智企"逼近人耳极限-音频通话"项目从众多参赛作品中脱颖而出,荣获"0-1创新奖"三等奖. 此次获奖的项目诞生于网易智 ...

  5. 一篇文章读懂Java类加载器

    Java类加载器算是一个老生常谈的问题,大多Java工程师也都对其中的知识点倒背如流,最近在看源码的时候发现有一些细节的地方理解还是比较模糊,正好写一篇文章梳理一下. 关于Java类加载器的知识,网上 ...

  6. WebSocket - 一篇文章读懂websocket

    一篇文章了解WebSocket WebSocket 产生背景 在我们开发过程中使用最多的就是 HTTP协议,当我们想要获取某些数据时由客户端发起请求,服务端接受请求并返回相对应的数据. 但是这种单项请 ...

  7. 一篇文章读懂JSON

    什么是json? W3C JSON定义修改版: JSON 指的是 JavaScript 对象表示法(JavaScript Object Notation) JSON 是轻量级的文本数据交换格式,并不是 ...

  8. 一篇文章 读懂产品需求文档PRD

    转自:松勤软件学院公众号 互联网公司人员组织架构 按职责分类 有产品经理 前端开发 后端开发 软件测试 运营 UI设计 视觉设计师 运维工程师 销售 客服 等 谁来写需求文档呢? 答案是产品经理 谁来 ...

  9. c 多线程运行混乱_一篇文章读懂 Python 多线程

    本文作者为 Michael Driscoll,是其新书 Python 201 的一节.本文译者为 linkcheng,由EarlGrey@编程派校对. 译者简介:linkcheng,专业电子信息工程. ...

  10. 【Java基础】一篇文章读懂多线程

    1.多线程 1.1并发和并行 并发:两个或多个事件在同一时间段发生 并行:两个或多个事件在同一时刻发生 1.2线程和进程 进程是程序的一次执行过程,进程是系统运行应用程序的基本单位,一个应用程序可以同 ...

最新文章

  1. sdwan解决方案的分类—Vecloud
  2. 文巾解题 面试题 03.06. 动物收容所
  3. ue4集合类型_UE4粒子系统渲染管线概述
  4. windows10双系统安装ubuntu18.04
  5. Vercel反向代理做CDN,免费给网站加速隐藏源站,可绑定域名
  6. Spring消息之STOMP
  7. Android第三十八天
  8. html yy直播,网页YY直播间进入方法 网页YY迷你版怎么用
  9. 设计模式在项目中的应用案例_案例|P6软件在水电项目施工管理中的应用
  10. 用眼学计算机,专家提醒:电脑使用者要学会科学用眼
  11. python中plot函数参数_Python的 plot函数和绘图参数设置
  12. 算法竞赛零散知识点记录
  13. ​​​​​​​墨画子卿第三章:初心第2节:回家
  14. 【houdini vop】Block
  15. android模拟器mac版本下载,MaxMac电脑版怎么下载 安卓模拟器电脑版下载地址
  16. IO流-常用的IO流总结
  17. 连连看核心算法与基本思想(附全部项目代码链接与代码详细注释)
  18. 云服务器搭建开发环境
  19. 腾讯,干掉 Redis 项目,正式开源、太牛逼啦
  20. 大话西游猛击源码_我们猛击Return(Enter)键可能会演变的原因

热门文章

  1. 电脑蓝屏,问题:你的电脑未正确启动,按“重启”以重启你的电脑,有时这样可以解决问题,你还可以按“高级选项”,尝试使用其他选项修复你的电脑
  2. 《NLP汉语自然语言处理原理与实践》学习二
  3. OpenCV学习笔记_图像扭曲及旋转操作
  4. Windows下自动云备份思源笔记到Gitee
  5. 基于单片机的通用定时器调度器SmartTimer
  6. php 人物换装原理,装备库解析——换装机制,宝石选择原理
  7. 微服务架构:基于微服务和Docker容器技术的PaaS云平台架构设计
  8. php urldecode 加号,php|urldecode urlencode 的加号问题
  9. 进入显示器工厂模式的方法! 【95种品牌,维修珍藏资料】
  10. dropbox 下载不了问题