【C++】继承

我们在实际中可能遇到这种情况,比如说两个类有相同的成员变量 ,当然也有不同的(比如说老师和学生都有姓名和年龄等,但是学生有学号,老师有工号等)。在这种情况下,难道我们要重复的去定义吗?对于大佬来说,当然是不允许的。所以,就有了继承 这个概念,就是说子类要去继承父类 中的东西,子类也叫派生类,父类也叫基类。

那么我先写一个简单的父类和子类

cpp 复制代码
class person {
public:
	void func() {
		cout <<"_name "<< _name << endl;
		cout << "_age " << _age << endl;
	}
protected:
	string _name="abc";
	int _age=0;
};
class student :public person {//public就是继承方式

protected:
	int _s_id;
};
class teacher :public person {

protected:
	int _t_id;
};
int main() {
	student s1;
	s1.func();
	return 0;
}

父类的成员函数也是会继承给子类的,那么,不知道你有没有发现,我们这里给成员变量的访问限定符是protected ,之前都没有用过这个。如果要讨论这个,那就不得不提及基类成员在派生类中的访问方式

父类成员在子类中的访问方式跟父类成员的访问限定符和继承方式有关,它们有这样的关系

|----------------|-----------------|-----------------|---------------|
| 类成员/继承方式 | public继承 | protected继承 | private继承 |
| 基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
| 基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
| 基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |

首先我们认为public>protected>private,这是它们之间的能够访问的权限的大小的比较。在前两行都是取权限小的那个。

最后一行的不可见是什么意思呢?不可见并不是不存在,而是存在却不可在子类中 直接用,更不要说在外边了,但是可以间接用,比如说调用个父类的函数是可以访问的到的。
如果不写继承方式的话,class默认是private,struct 默认是public,当然我们肯定最好写上

我们大多数用到的继承方式是public,其他两个用到的很少
我们之前学的函数模板是函数层面上的代码复用,而这里的继承是类层次上的复用
我们知道赋值时如果有类型转换会产生临时变量,比如说

就是因为产生的临时变量具有常属性,所以要用const引用

但是在我们继承这里,子类对象可以赋值给父类对象,当引用或者用指针时父类对象其实就用的是子类的一部分 ,把这一部分切割出来给父类 ,所以我们叫切割或者是赋值兼容转换

我们可以看到,它们的地址确实是一样的,它们指的确实是同一块地方

基类和派生类都有各自独立的作用域 ,这就说明它们可以有同名的成员,我们直接用的话是用的子类的,父类的是隐藏的,要想用的话要指明类域 。并且对于成员函数来说,只要父类子类中的函数名相同 则构成隐藏或者叫重定义,这就意味着返回值和参数可以不同。并且这不叫函数重载,因为函数重载是要定义在同一作用域的。
我们正常的类有六个默认成员函数,基类就和正常的类是一样的,派生类不同,因为他有一部分是继承的父类的。对于派生类来说,我们要自己实现的就四个:构造,析构,拷贝构造,赋值重载,至于取地址重载我们一般不用自己实现
下面我们先来看一下构造函数,其实对于父类的成员就去调用父类的构造函数,我们写子类时不用处理,就算要处理,也是通过父类的构造函数取处理,对于自己的成员你就需要去写上。并且最后初始化的顺序是先父类成员,后子类成员,因为初始化顺序跟声明顺序有关。大概就是这样的

cpp 复制代码
class person {
public:
	person(const char* s="Tony", int n=10)
		:_name(s)
		,_age(n)
	{}
protected:
	string _name;
	int _age;
};

class student :public person {
public:
	student(const char* s = "Tony", int age = 10, int id = 1)
		:_s_id(id)
		, person(s, age)
	{}
protected:
	int _s_id;
};

下面是拷贝构造,它的大致形式就是长这样的

第二个为什么一个student的对象可以传给person呢?因为它们在进行类型转换的时候叫做切割, 这就是我们上边说的,下边看一下赋值重载

为什么子类在调用父类的赋值重载时要指明类域呢 ?因为它们两个函数名相同,构成隐藏关系,只能通过指明才能调出来。下面看一下析构

这里的子类为什么还要指明类域呢?因为由于后面的多态的原因,析构函数会被特殊处理,函数名都会被处理成destructor(),所以子类的析构函数和父类的析构函数会构成隐藏关系 ,并且为了保证先子后父,父类的析构会在子类析构后自动调用,所以上面那么写也是不对的,应该不用写第一句。为什么要保证先子后父 呢?这是由于子类会访问父类的成员,如果先析构父类,则会不安全。
下一个要说的就是友元关系不能继承,这个也好理解,父亲的朋友并不是我们的朋友,要想让它成为我们的朋友我们就要自己声明一下。

如果基类定义了static静态成员,那么则整个继承体系中只有一个这样的成员,无论派生出多少个子类,都只有一个static成员实例
下面我们说一下单继承和多继承,单继承的意思是只有一个直接父类,多继承是指有两个或更多直接父类。

我们之前说的都是单继承,没有什么问题,但是对于多继承来说就有问题了,比如B和C都单继承于A,而D多继承于B和C,这就导致一个问题,就是D这个类中有两份A的成员,这就叫做棱形继承 ,它会出现数据的冗余和二义性

cpp 复制代码
class A {
public:
//protected:
	int _a;
};
class B :public A{
public:
//protected:
	int _b;
};
class C:public A {
public:
//protected:
	int _c;
};
class D :public B, public C {
public:
//protected:
	int _d;
};

int main() {
	D dd;
	dd._a = 10;//不明确
	return 0;
}

这时如果我们要访问dd的_a就会显示_a不明确。

当然我们如果指明类域的话可以的,但是并不是每个成员都有两个意义,我们只需要一个,这时就要用到虚继承,就是给B和C加上virtual

这样_a就只有一份了,下面我们从底层,也就是内存的角度来看看它们是怎么存储的

先看不是虚继承的

再看虚继承的

这两个内存分别是dd和ddd的地址处的内存情况,我们可以看到第一行和第三行是一样的,这其实也是一个地址,我们看看其中存着什么

我们可以看到,一个存的是20,一个是12,这是十进制。它指的就是_a存在后面20和12个字节的位置,这个表也叫虚基表 ,存的就是偏移量。总之,应该是当初祖师爷没想到多继承后面会出现这样的问题,等到发现之后已经无法删掉多继承了,所以就想到了这样的办法去弥补。

相关推荐
软件黑马王子3 小时前
C#初级教程(4)——流程控制:从基础到实践
开发语言·c#
闲猫3 小时前
go orm GORM
开发语言·后端·golang
计算机小白一个4 小时前
蓝桥杯 Java B 组之设计 LRU 缓存
java·算法·蓝桥杯
万事可爱^4 小时前
HDBSCAN:密度自适应的层次聚类算法解析与实践
算法·机器学习·数据挖掘·聚类·hdbscan
黑不溜秋的4 小时前
C++ 设计模式 - 策略模式
c++·设计模式·策略模式
李白同学5 小时前
【C语言】结构体内存对齐问题
c语言·开发语言
黑子哥呢?6 小时前
安装Bash completion解决tab不能补全问题
开发语言·bash
青龙小码农6 小时前
yum报错:bash: /usr/bin/yum: /usr/bin/python: 坏的解释器:没有那个文件或目录
开发语言·python·bash·liunx
大数据追光猿6 小时前
Python应用算法之贪心算法理解和实践
大数据·开发语言·人工智能·python·深度学习·算法·贪心算法
Dream it possible!7 小时前
LeetCode 热题 100_在排序数组中查找元素的第一个和最后一个位置(65_34_中等_C++)(二分查找)(一次二分查找+挨个搜索;两次二分查找)
c++·算法·leetcode