C++继承详解

废话不多说直接上代码

class 派生类名:[继承方式] 基类名{
    派生类新增加的成员
};

继承方式限定了基类成员在派生类中的访问权限,包括 public(公有的)、private(私有的)和 protected(受保护的)。此项是可选项,如果不写,默认为 private(成员变量和成员函数默认也是 private)。

现在我们知道,public、protected、private 三个关键字除了可以修饰类的成员,还可以指定继承方式。

继承方式

不同的继承方式会影响基类成员在派生类中的访问权限。

1) public继承方式

  • 基类中所有 public 成员在派生类中为 public 属性;
  • 基类中所有 protected 成员在派生类中为 protected 属性;
  • 基类中所有 private 成员在派生类中不能使用。

2) protected继承方式

  • 基类中的所有 public 成员在派生类中为 protected 属性;
  • 基类中的所有 protected 成员在派生类中为 protected 属性;
  • 基类中的所有 private 成员在派生类中不能使用。

3) private继承方式

  • 基类中的所有 public 成员在派生类中均为 private 属性;
  • 基类中的所有 protected 成员在派生类中均为 private 属性;
  • 基类中的所有 private 成员在派生类中不能使用。


通过上面的分析可以发现:
1) 基类成员在派生类中的访问权限不得高于继承方式中指定的权限。例如,当继承方式为 protected 时,那么基类成员在派生类中的访问权限最高也为 protected,高于 protected 的会降级为 protected
2) 基类中的 private 成员在派生类中始终不能使用(不能在派生类的成员函数中访问或调用)。
3) 如果希望基类的成员能够被派生类继承并且毫无障碍地使用,那么这些成员只能声明为 public 或 protected;只有那些不希望在派生类中使用的成员才声明为 private。
4) 如果希望基类的成员既不向外暴露(不能通过对象访问),还能在派生类中使用,那么只能声明为 protected。
注意,我们这里说的是基类的 private 成员不能在派生类中使用,并没有说基类的 private 成员不能被继承。实际上,基类的 private 成员是能够被继承的,并且(成员变量)会占用派生类对象的内存,它只是在派生类中不可见,导致无法使用罢了。

改变访问权限

使用 using 关键字可以改变基类成员在派生类中的访问权限,例如将 public 改为 private、将 protected 改为 public。

注意:using 只能改变基类中 public 和 protected 成员的访问权限,不能改变 private 成员的访问权限,因为基类中 private 成员在派生类中是不可见的,根本不能使用,所以基类中的 private 成员在派生类中无论如何都不能访问。

#include<iostream>
using namespace std;//基类People
class People {
public:void show();
protected:char *m_name;int m_age;
};
void People::show() {cout << m_name << "的年龄是" << m_age << endl;
}//派生类Student
class Student : public People {
public:void learning();
public:using People::m_name;  //将protected改为publicusing People::m_age;  //将protected改为publicfloat m_score;
private:using People::show;  //将public改为private
};
void Student::learning() {cout << "我是" << m_name << ",今年" << m_age << "岁,这次考了" << m_score << "分!" << endl;
}int main() {Student stu;stu.m_name = "小明";stu.m_age = 16;stu.m_score = 99.5f;stu.show();  //compile errorstu.learning();return 0;
}

代码中首先定义了基类 People,它包含两个 protected 属性的成员变量和一个 public 属性的成员函数。定义 Student 类时采用 public 继承方式,People 类中的成员在 Student 类中的访问权限默认是不变的。

不过,我们使用 using 改变了它们的默认访问权限,如代码第 21~25 行所示,将 show() 函数修改为 private 属性的,是降低访问权限,将 name、age 变量修改为 public 属性的,是提高访问权限。

因为 show() 函数是 private 属性的,所以代码第 36 行会报错。把该行注释掉,程序输出结果为:
我是小明,今年16岁,这次考了99.5分!

继承时的名字遮蔽问题

如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,那么就会遮蔽从基类继承过来的成员。所谓遮蔽,就是在派生类中使用该成员(包括在定义派生类时使用,也包括通过派生类对象访问该成员)时,实际上使用的是派生类新增的成员,而不是从基类继承来的。
 

基类和派生类的构造函数

类的构造函数不能被继承。因为即使继承了,它的名字和派生类的名字也不一样,不能成为派生类的构造函数。

在设计派生类时,对继承过来的成员变量的初始化工作也要由派生类的构造函数完成,但是大部分基类都有 private 属性的成员变量,它们在派生类中无法访问,更不能使用派生类的构造函数来初始化。

这种矛盾在C++继承中是普遍存在的,解决这个问题的思路是:在派生类的构造函数中调用基类的构造函数。
 

Student::Student(char *name, int age, float score): People(name, age), m_score(score){ }

People(name, age)就是调用基类的构造函数,并将 name 和 age 作为实参传递给它,m_score(score)是派生类的参数初始化表,它们之间以逗号,隔开。

也可以将基类构造函数的调用放在参数初始化表后面:

 

Student::Student(char *name, int age, float score): m_score(score), People(name, age){ }

但是不管它们的顺序如何,派生类构造函数总是先调用基类构造函数再执行其他代码(包括参数初始化表以及函数体中的代码),总体上看和下面的形式类似:

Student::Student(char *name, int age, float score){

    People(name, age);

    m_score = score;

}

当然这段代码只是为了方便大家理解,实际上这样写是错误的,因为基类构造函数不会被继承,不能当做普通的成员函数来调用。换句话说,只能将基类构造函数的调用放在函数头部,不能放在函数体中。

另外,函数头部是对基类构造函数的调用,而不是声明,所以括号里的参数是实参,它们不但可以是派生类构造函数参数列表中的参数,还可以是局部变量、常量等,例如:

Student::Student(char *name, int age, float score): People("小明", 16), m_score(score){ }

构造函数的调用顺序

从上面的分析中可以看出,基类构造函数总是被优先调用,这说明创建派生类对象时,会先调用基类构造函数,再调用派生类构造函数,如果继承关系有好几层的话,例如:

A --> B --> C

那么创建 C 类对象时构造函数的执行顺序为:

A类构造函数 --> B类构造函数 --> C类构造函数

构造函数的调用顺序是按照继承的层次自顶向下、从基类再到派生类的。

还有一点要注意,派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的。以上面的 A、B、C 类为例,C 是最终的派生类,B 就是 C 的直接基类,A 就是 C 的间接基类。

C++ 这样规定是有道理的,因为我们在 C 中调用了 B 的构造函数,B 又调用了 A 的构造函数,相当于 C 间接地(或者说隐式地)调用了 A 的构造函数,如果再在 C 中显式地调用 A 的构造函数,那么 A 的构造函数就被调用了两次,相应地,初始化工作也做了两次,这不仅是多余的,还会浪费CPU时间以及内存,毫无益处,所以 C++ 禁止在 C 中显式地调用 A 的构造函数。 

调用规则:事实上,通过派生类创建对象时必须要调用基类的构造函数,这是语法规定。换句话说,定义派生类构造函数时最好指明基类构造函数;如果不指明,就调用基类的默认构造函数;如果没有默认构造函数,那么编译失败。

 

基类和派生类的析构函数

和构造函数类似,析构函数也不能被继承。与构造函数不同的是,在派生类的析构函数中不用显式地调用基类的析构函数,因为每个类只有一个析构函数,编译器知道如何选择,无需程序员干涉。

另外析构函数的执行顺序和构造函数的执行顺序也刚好相反:

  • 创建派生类对象时,构造函数的执行顺序和继承顺序相同,即先执行基类构造函数,再执行派生类构造函数。
  • 而销毁派生类对象时,析构函数的执行顺序和继承顺序相反,即先执行派生类析构函数,再执行基类析构函数。

 

多继承

容易让代码逻辑复杂、思路混乱,一直备受争议,中小型项目中较少使用,后来的 Java、C#、PHP 等干脆取消了多继承。
多继承的语法也很简单,将多个基类用逗号隔开即可。例如已声明了类A、类B和类C,那么可以这样来声明派生类D:
class D: public A, private B, protected C{
    //类D新增加的成员
}

D 是多继承形式的派生类,它以公有的方式继承 A 类,以私有的方式继承 B 类,以保护的方式继承 C 类。D 根据不同的继承方式获取 A、B、C 中的成员,确定它们在派生类中的访问权限。

多继承下的构造函数

多继承形式下的构造函数和单继承形式基本相同,只是要在派生类的构造函数中调用多个基类的构造函数。以上面的 A、B、C、D 类为例,D 类构造函数的写法为:

D(形参列表): A(实参列表), B(实参列表), C(实参列表){
    //其他操作
}

基类构造函数的调用顺序和和它们在派生类构造函数中出现的顺序无关,而是和声明派生类时基类出现的顺序相同。仍然以上面的 A、B、C、D 类为例,即使将 D 类构造函数写作下面的形式:

D(形参列表): B(实参列表), C(实参列表), A(实参列表){
    //其他操作
}

那么也是先调用 A 类的构造函数,再调用 B 类构造函数,最后调用 C 类构造函数。

命名冲突:当两个或多个基类中有同名的成员时,如果直接访问该成员,就会产生命名冲突,编译器不知道使用哪个基类的成员。这个时候需要在成员名字前面加上类名和域解析符::,以显式地指明到底使用哪个类的成员,消除二义性。

虚继承

多继承是指从多个直接基类中产生派生类的能力,多继承的派生类继承了所有父类的成员。尽管概念上非常简单,但是多个基类的相互交织可能会带来错综复杂的设计问题,命名冲突就是不可回避的一个。

类 A 派生出类 B 和类 C,类 D 继承自类 B 和类 C,这个时候类 A 中的成员变量和成员函数继承到类 D 中变成了两份,一份来自 A-->B-->D 这条路径,另一份来自 A-->C-->D 这条路径。

在一个派生类中保留间接基类的多份同名成员,虽然可以在不同的成员变量中分别存放不同的数据,但大多数情况下这是多余的

//间接基类A
class A{
protected:int m_a;
};//直接基类B
class B: public A{
protected:int m_b;
};//直接基类C
class C: public A{
protected:int m_c;
};//派生类D
class D: public B, public C{
public:void seta(int a){ m_a = a; }  //命名冲突
private:int m_d;
};

为了消除歧义,我们可以在 m_a 的前面指明它具体来自哪个类:

void seta(int a){ B::m_a = a; }

虚继承

虚继承使得在派生类中只保留一份间接基类的成员。
在继承方式前面加上 virtual 关键字就是虚继承。

//间接基类A
class A{
protected:int m_a;
};//直接基类B
class B: virtual public A{  //虚继承
protected:int m_b;
};//直接基类C
class C: virtual public A{  //虚继承
protected:int m_c;
};//派生类D
class D: public B, public C{
public:void seta(int a){ m_a = a; }  //正确void setb(int b){ m_b = b; }  //正确void setc(int c){ m_c = c; }  //正确void setd(int d){ m_d = d; }  //正确
private:int m_d;
};int main(){D d;return 0;
}

虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的 A 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。

我们会发现虚继承的一个不太直观的特征:必须在虚派生的真实需求出现前就已经完成虚派生的操作。在上图中,当定义 D 类时才出现了对虚派生的需求,但是如果 B 类和 C 类不是从 A 类虚派生得到的,那么 D 类还是会保留 A 类的两份成员。

换个角度讲,虚派生只影响从指定了虚基类的派生类中进一步派生出来的类,它不会影响派生类本身。

在实际开发中,位于中间层次的基类将其继承声明为虚继承一般不会带来什么问题。通常情况下,使用虚继承的类层次是由一个人或者一个项目组一次性设计完成的。对于一个独立开发的类来说,很少需要基类中的某一个类是虚基类,况且新类的开发者也无法改变已经存在的类体系。

使用多继承经常会出现二义性问题,必须十分小心。上面的例子是简单的,如果继承的层次再多一些,关系更复杂一些,程序员就很容易陷人迷魂阵,程序的编写、调试和维护工作都会变得更加困难,因此不提倡在程序中使用多继承

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/444070.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

串【数据结构F】

先来讲解一下串结构的概念性质的东西&#xff0c;以及我们需要注意的一些问题 串结构简单的ADT以及一些基本的操作 最小操作函数&#xff1a;就是功能已经达到了最小的功能实现了&#xff0c;不能继续执行更大的功能&#xff0c;类似于我们在家盖房子一样&#xff0c;水泥的…

C++ STL与迭代器

将容器类模板实例化时&#xff0c;会指明容器中存放的元素是什么类型的&#xff1a;可以存放基本类型的变量&#xff0c;也可以存放对象。 对象或基本类型的变量被插入容器中时&#xff0c;实际插入的是对象或变量的一个复制品。 STL 中的许多算法&#xff08;即函数模板&…

git/github使用完整教程(1)基础

安装git 在Linux上安装Git 首先输入git&#xff0c;看看系统有没有安装Git&#xff1a; $ git The program git is currently not installed. You can install it by typing: sudo apt-get install git像上面的命令&#xff0c;有很多Linux会友好地告诉你Git没有安装&#x…

git/github使用完整教程(2)分支

分支 首先&#xff0c;我们创建dev分支&#xff0c;然后切换到dev分支&#xff1a; $ git checkout -b dev Switched to a new branch devgit checkout命令加上-b参数表示创建并切换&#xff0c;相当于以下两条命令&#xff1a; $ git branch dev $ git checkout dev Switch…

数组【数据结构】

前提 数组的定义以及数组的延伸 这种不好进行理解&#xff0c;那么我们下面以二维数组进行解释 多维数组的数据特点 存储数组结构的两种方式 问题抽象总结

Kafka深度解析

原创文章&#xff0c;转载请务必将下面这段话置于文章开头处&#xff08;保留超链接&#xff09;。 本文转发自技术世界&#xff0c;原文链接 http://www.jasongj.com/2015/01/02/Kafka深度解析 背景介绍 Kafka简介 Kafka是一种分布式的&#xff0c;基于发布/订阅的消息系统。…

数据的存储特殊矩阵压缩存储【数据结构F】

以行为主序 以列为主序 矩阵的前提分类 三角矩阵

图的基本概念【数据结构】

序言 1对1的线性结构&#xff0c;一对多的树二叉树以及森林&#xff0c;第3种就是多对多的结构&#xff0c;也就是我们所要讲到的图的结构&#xff0c;图形结构是数据结构当中最复杂的一种结构&#xff0c;图形结构的特点就是在这个图当中任意两点之间都会有关系&#xff0c;这…

图的遍历算法【数据结构F】

图的遍历算法有哪两种&#xff1f; 深度优先调度算法---------将图结构看成是树形结构&#xff0c;树形结构的子图直接是没有交叉的&#xff0c;但是对于图结构的树形结构之间是有交叉的&#xff0c;类比于树形结构的二叉树&#xff0c;左指数和右指数都会相应的经历三次&#…

最小生成树【数据结构】

前提 【1】网的最小生成树&#xff0c;涉及到生成树了那么就会有最小的权值在里面了 【2】对于一个图来说生成树是由多个的&#xff0c;并不是唯一的 【3】&#xff1a;广度优先算法的遍历是可以得到生成树的&#xff0c;深度优先算法也是可以得到生成树的 任意的一个联通网&am…

广义表的基本概念【数据结构】

实名广义表与匿名广义表的区别&#xff1a;对于匿名的广义表的表示方法我们认为一对括号就是一个广义表&#xff0c;里面的数据可以是广义表也可以是 原子&#xff0c;对于有名字的广义表&#xff0c;也就是大写的字母我们可以直接认为大写的就是广义表的表示方法小练习----广义…

树和二叉树【数据结构】

基本概念 ADT的定义 基本操作 对比树形结构和线性结构 基本术语以及注意事项-不能错误简单的我以为 二叉树是度数小于等于2的树&#xff0c;而不是度为2的树&#xff0c;一定要记住这个概念 小知识&#xff1a;二进制转换成为十进制的方法名称叫做位权求和法&#xff0c;用到…

数据库2.1.1mysql的特点

在mysql5.1当中&#xff0c;mysqlab公司引入了新的插件式存储引擎体系结构&#xff0c;也许将存储引擎加载到正在运行的mysql服务器当中&#xff0c;使用mysql插件是存储引擎体系结构允许数据库用户为特定的应用需求选择专门的存储引擎&#xff0c;完全不需要管理任何特殊的应用…

MySQL常见的两种存储引擎:MyISAM与InnoDB的爱恨情仇

一 MyISAM 1.1 MyISAM简介 MyISAM是MySQL的默认数据库引擎&#xff08;5.5版之前&#xff09;&#xff0c;由早期的 ISAM &#xff08;Indexed Sequential Access Method&#xff1a;有索引的顺序访问方法&#xff09;所改良。虽然性能极佳&#xff0c;而且提供了大量的特性&a…

互联网30年,泡沫如梦

人人都说互联网改变世界&#xff0c;这话没错。 但我认为互联网改变的方式&#xff0c;是泡沫。 资金&#xff0c;资源&#xff0c;人才因为一堆概念聚在一起&#xff0c;形成一个又一个的泡沫&#xff0c;然后泡沫破裂&#xff0c;大部分人失败&#xff0c;少数能够留下来的&a…

cpp知识汇总(1) 指针vs引用、static、const

引用和指针的区别&#xff1f; 指针是一个实体&#xff0c;需要分配内存空间。引用只是变量的别名&#xff0c;不需要分配内存空间。引用在定义的时候必须进行初始化&#xff0c;并且不能够改变。指针在定义的时候不一定要初始化&#xff0c;并且指向的空间可变。&#xff08;…

《三天给你聊清楚redis》第1天先唠唠redis是个啥(18629字)

后端需要知道的关于redis的事&#xff0c;基本都在这里了。 此文后续会改为粉丝可见&#xff0c;所以喜欢的请提前关注。 你的点赞和评论是我创作的最大动力&#xff0c;谢谢。 1、入门 Redis是一款基于键值对的NoSQL数据库&#xff0c;它的值支持多种数据结构&#xff1a;…

使用github+jsdelivr作为视频床

感谢JefferyIF大佬提供的神奇方法。 1. 配置FFmpeg 注&#xff1a;IOS因为不支持HLS&#xff0c;所以对IOS上无法正常播放视频&#xff0c;其他端都可以正常播放。 因为脚本要使用到FFmeg对源视频文件切分成m3u8格式&#xff0c;所以在使用脚本之前&#xff0c;请配置好 FFm…