C++的三大特性之继承

目录

一 继承的概念

代码:

总结:

二 继承中的关系

 三 继承中的作用域问题

什么是域?

隐藏:

隐藏的场景:

总结

四 赋值兼容原则

什么是赋值兼容原则?

与平时强制类型转换的区别

 这一个赋值兼容原则的底层实现是怎么样的?

五 关于继承关系下的六个默认成员函数

什么是默认成员函数?

 对于初始化

1 构造函数

2 拷贝构造函数 

3 赋值运算符的重载

对于析构

对于取地址

六 继承和友元

七 继承和静态成员

八 多继承

多继承导致的问题:

题外话:如何定义一个不能被继承的类?

如何解决菱形继承带来的问题?

虚继承的原理

九 继承和组合


一 继承的概念

C++有三大特性,分别是封装,继承和多态。

继承主要体现了类设计层次的一个复用关系。打个比方,比如:

如图所示,Person派生生成了Student和Teacher类。Student类和Teacher继承了Person类。那么对于Person类中的成员变量和成员函数都是可以复用的,也就是说_agem_name,_sex在子类中各自都有一份。不管访问限定符是什么,都被复用到对应的类中了

如上图,这样去复用了别人的成员的是子类(基类),提供成员给别人复用的是父类(基类)

代码:

class Person
{
public:
	int _age;
	string _name;
	string _sex;
};
class Stduent:public Person
{
public:
	string _sid;
};
class Teacher :public Person
{
public:
	string _tid;
};

总结:

①复用:将共有的成员函数或者成员变量提取出来,作为父类,子类通过继承父类,就可以使用了。

②关系(一对对象,两种关系):父类和子类,基类和派生类。派生或者继承。

二 继承中的关系

根据基类的访问限定符和子类的继承方式,一共有如下的几种关系:

 

 

 注意:这里最后的关系都是在派生类中的体现

我们最后通过排列组合可以发现,一共有九种关系。非常的复杂和繁琐。我这里归纳了一下

1 如果父类是private的成员,不管是什么方式继承,都是不可见的。

所谓不可见就是无论是在类内还是在类外都是不能访问的。区别于派生类中的protect关系,在子类中可见,在类外不可见。
2  除了第一条的,继承方式和访问限定符中取较小权限的

我们发现,基类中的私有成员,无论如何继承下去,在派生类中都是不可见的。那么如果我们只想在父类中访问的话,不想被外界继承和使用,可以定义成私有的。但是说实话,如果这样定义的话,继承就没啥意义了,因为继承就是想使用父类的东西。

另外,如果类class不写继承方式的话,默认的是私有的,struct也是可以定义成类的,但是是私有的。不算语法错误

class Teacher: Person

但是,我们日常生活中,最常用的也就只有public相关的继承

 

 

 三 继承中的作用域问题

什么是域?

由{}括起来的,属于各自特定的域。可能会影响生命周期和访问。

比如:C语言中的域有全局域和局部域这样的概念,有些是会对生命周期造成影响的:局部域

但是c++涉及到的类域,只影响访问

基于此,在同一个域中,我们不能定义同名的函数,如果我们非要定义同名函数就会引发对应的问题。c++中也对此做了特定的处理。

隐藏:

隐藏的场景:

从c++的继承关系出发,如果父类有_name了的基础上,子类中也定义了_name的成员变量,这样子的话,由于子类继承父类的时候,会去复用父类的成员,在子类中,就会存在两个_name.

那么我们在子类中访问_name的时候,访问的是父类的还是子类的_name呢?

不妨自己验证一下

#include<iostream>
#include<string>
using namespace std;
class Person
{
public:
	int _age;
	string _name;
	string _sex;
};
class Student:public Person
{
public:
	string _sid;
	string _name;
};
class Teacher: public Person
{
public:
	string _tid;
};
int main()
{
	Student s;
	s._name = "张三";
 	Person p;

	return 0;
}

 发现确实,如果父类子类都有_name的话,访问的时候优先访问的是子类中的,因为局部优先原则。如果我们想通过子类对象去访问父类中的_name的话,我们需要限定域

举一个用子类访问父类同名成员的例子

总结

什么是隐藏:

在继承关系中,如果子类中有父类同名的成员(函数或者变量),访问子类的对应成员,优先去匹配子类域中的,除非特别指定父类的,否则就访问不到父类的。

对于函数:不在乎参数,只要函数名相同,就构成隐藏。

也就是说,如果构成隐藏,除非特定指定访问父类的,否则就默认访问子类的,访问不到父类的

隐藏是由于子类和父类都有独立的域导致的

区别隐藏(重定义),重载,不可见(隐身)

隐藏(重定义):在继承关系中,由于父类和子类中有相同名字的成员变量,访问的时候默认访问子类中的

重载:在同一个域中,两个函数同名但是参数列表不同(包括个数 顺序),并且不要求返回值。这样子编译器编译的时候,优先匹配最合适的

不可见(隐身):是因为基类的private成员导致的派生类中继承关系是不可见的,那么在类内或者类外都无法访问

四 赋值兼容原则

什么是赋值兼容原则?

子类的对象指针或者引用可以直接赋值给父类

Student s;
	Person p = s;
	Person* p = &s;
	Person& p = s;

与平时强制类型转换的区别

我们平时写代码的时候,如果两个数据的数据类型不统一的话,要么编译器自动转换,要么就是手动写上强制类型转换才可以规避语法错误

但是,对于子类和父类的关系。为什么可以直接赋值?

这一种行为是完全不同于上述的行为的,因为他支持引用的直接赋值

正常情况下编译器转换的话,引用是不可以的。因为两个变量进行赋值的时候,中间会产生一个临时变量,是把这个临时变量赋值给另外的变量的但是临时变量具有常性。因此是属于权限的放大, 是会产生语法错误的

 这一个赋值兼容原则的底层实现是怎么样的?

发生了一个切片行为。可以这样理解:子类中除了与父类共有的那一部分,还有自己特有的。所以可以用子类给父类赋值,只用取出父类中的就可以了。

 

 同理,指针也是取出父类特有的。将子类的指针给父类,那么改变了指针+1的位置。

 引用也是同理

五 关于继承关系下的六个默认成员函数

按照默认成员函数的功能分类,可以分成以上这三大类。

什么是默认成员函数?

默认成员函数就是即使当自己什么都不写的时候,编译器默认生成的成员函数。一旦自己针对自己的需求写了对应的成员函数,编译器就不会生成了

那么在继承体系下,这几类默认成员函数函数是怎么使用的呢?

总体的大规则就是:继承体系下,派生类中的默认成员函数如果自己去定义,需要遵循“合成”的规则。把父类和子类分别当做一个整体,来进行操作 

 对于初始化

1 构造函数

在一般的情况下(这里的一般情况是指没有继承),默认构造函数对内置类型不做处理,对自定义类型会去调用类中自定义类型的构造函数完成初始化。

继承体系下,在子类中自己特有的也遵循这样的规则。但是对于父类的需要去调用父类的构造函数完成初始化。

这时候有两种情况1.父类没有显示写出,那么编译器生成一个默认的,子类直接去调用。2 父类如果定义了 子类如果通过初始化列表来写,需要创建一个匿名对象写出

class Person
{
public:
	int _age;
	string _name="李老师";
	string _sex;
	/*void show()
	{
		cout << _name << endl;
	}*/
	Person(int age,const char*name,const char*sex)
		:_age(age)
		,_name(name)
		,_sex(sex)
	{

	}
};
class Student:public Person
{
public:
	string _sid;
	Student(int age, const char* name, const char* sex, const char* sid)
		:Person(age,name,sex)
		, _sid(sid)
	{

	}
};

2 拷贝构造函数 

一般情况下,对于内置类型进行值拷贝。对于自定义类型,调用自定义类型的拷贝构造函数(内置类型进行值拷贝是没有问题的。但是对于在栈上等地方开辟了空间的,如果进行值拷贝的话,不行,会两个内容指向同一空间,造成析构两次或者改变的时候错误改变的问题。所以自定义类型是要去调用自定义类型的拷贝构造函数的:比如用两个栈实现一个队列,此时初始化这个队列调用的就是栈的拷贝构造函数)

继承体系下,子类自己特有的遵循上述的一般规则。但是对于父类的,需要调用父类的拷贝构造函数完成初始化

 对于父类的,如何在子类中取出父类的一部分呢?

赋值兼容原则把子类传给父类,那么父类中接收到的其实也是子类切片之后和父类吻合的那一部分。

3 赋值运算符的重载

一般情况下,是和拷贝构造函数类似的

在继承体系中,子类中如何写?

对于父类的调用去赋值,而对于子类自己的,需要单独写出

代码

由于子类和父类都有operator=,构成了隐藏。但是这里需要使用父类的,因此需要单独写出。

           

对于析构

一般情况下,对于内置类型不做处理,但是对于自定义类型会去调用自定义类型的析构函数

在继承体系下,不需要写出

为什么子类不用自己显示写出?

这里涉及到两个知识点:隐藏和多态

由于后续多态的需要,析构函数会被统一处理成函数名是destructor的函数。因此父子类函数中构成了隐藏。如果要写的话,对于父类的那一部分需要指定域。但是自己写的时候,可能会有顺序的不一致:构造函数是先初始化父类的再初始化子类的,但是析构函数反之。如果写出反而可能会搞错顺序。因此不用写。

对于取地址

取地址就是取出自己的对应的地址,不需要分成子类和父类两个部分去考虑了。也不需要自己显示的写出。

六 继承和友元

友元关系不能继承。

七 继承和静态成员

被static修饰的成员存储在栈上,父类和子类访问到的是同一个,只有一份的。可以在父类中的构造函数中定义一个static count,就可以统计一共构造了几个父类和子类。

 

八 多继承

一个子类有两个及以上的直接父类就是多继承关系

 

多继承导致的问题:

代码冗余(D中由两份A,分别来自B和C)

二义性,由于是这么存储的,因此在D的对象中,调用A中成员,不指定域的话,就无法分别到底是B的还是C的。(但是即使指定了也无法解决代码冗余这个问题)

题外话:如何定义一个不能被继承的类?

1 父类构造函数私有化:因为后续子类都需要用到父类的 因此私有化之后 就不能被继承了

2 使用c++新增的关键字final最终类,标识该类不能被继承

如何解决菱形继承带来的问题?

使用虚继承:virtual关键字(注意与多态中virtual的区分,属于一个关键词两用)

如何使用?

在腰部的位置虚拟继承。从A继承的地方都要使用虚继承。将共同基类设置为虚基类。

代码:

class B : virtual public A

 

虚继承的原理

如果是普通继承是这样存储的

 

如果是虚拟继承

(由于我的编译器版本有点高 观察结果不太一样 去网络上找了一个比较标准的)

 

 

原博客链接:C++之继承相关问题——菱形继承&继承 - 腾讯云开发者社区-腾讯云 

将共有的提取到最下面的位置。

虚拟继承的话,B和C中有一个虚表指针,指向虚表(本质上是一个函数指针数组),虚表再通过索引指向对应的函数。这里面记录了偏移量和距离,因此就可以通过这个方式找到对应的成员变量。

更复杂的场景:如果对于一个函数在A中,BC继承了,D再继承BC继承了该函数。D中的函数参数传参的时候传指针或者地址,如何找到对应的数据?

首先这个行为是被支持的,因为赋值兼容原理。那么如何找到呢?在汇编时期,汇编代码需要被确定下来,BC中都有一个虚基表,但是他们是不相同的。

因为需要支持多态行为,并且因此重写了函数,并把对应的函数地址放在虚函数表中。(虚函数仍然是存储在公共的代码段的)

那么我们就可以通过虚表指针找到对应的虚函数表,进而找到对应的成员变量和成员函数

注意,这种情况也是属于菱形继承。要解决相关的问题,需要在BC位置带上虚函数virtual标识

                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   

 

九 继承和组合

继承:是一种is a的关系,可以通过“是”来判断。比如学生和人:“学生是人”

组合:has a “有没有” 耦合度比较低。b类有a ,b中的成员除了a,还有其他的组成。

有一些既是组合又是继承。比如stack和queue/vector/deque

如果在这样的一个情况下的话,优先使用组合-》高内聚,低耦合,减少模块与模块之间的耦合度,便于后期的维护。否则一个模块修改了,后期如果复用了这个模块,需要修改的地方就很多

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值