继承&&多态&&多态的对象模型

概念:继承是一种复用手段

#)private/protected限定符都是限定直接访问,那他们之间有什么区别?

&&&派生类可以访问基类的public成员和protected成员,但是不能访问基本的private成员;

一:

关于继承的相关知识:

1.子类拥有父类的所有成员变量和成员函数

2.子类就是一种特殊的父类

3.子类对象可以当作父类对象使用

4.子类可以有自己的成员函数和成员变量。

二:

继承的访问控制

图1

三:

继承的赋值兼容规则(以public为例)

继承与转换--赋值兼容规则--public继承

1. 子类对象可以赋值给父类对象(切割/切片)

2. 父类对象不能赋值给子类对象

3. 父类的指针/引用可以指向子类对象

4. 子类的指针/引用不能指向父类对象(可以通过强制类型转换完成)

总结:

1. 基类的私有成员在派生类中是不能被访问的,如果一些基类成员不想被基类对象直接访问,但需要在派生类中能访问,就定义为保

护成员。可以看出保护成员限定符是因继承才出现的。

2. public继承是一个接口继承,保持is-a原则,每个父类可用的成员对子类也可用,因为每个子类对象也都是一个父类对象。

3. protetced/private继承是一个实现继承,基类的部分成员并未完全成为子类接口的一部分,是 has-a 的关系原则,所以非特殊情况下不会使用这两种继承关系,在绝大多数的场景下使用的都是公有继承。

4. 不管是哪种继承方式,在派生类内部都可以访问基类的公有成员和保护成员,但是基类的私有成员存在但是在子类中不可见(不能访问)。

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

6. 在实际运用中一般使用都是public继承,极少场景下才会使用protetced/private继承.

四:

继承中构造和析构的调用原则:

①子类对象在创建时首先调用父类的构造函数。

在初始化列表里面调用基类的构造函数,没写系统会自己调用(可能是编译器的优化),如果基类没有合适的默认构造函数,需要自己在初始化列表里面显示调用。

②父类构造函数执行结束后,执行子类构造函数

④析构函数调用的顺序与构造函数相反(栈的性质:后进先出)

五:

函数的重载、重写和重定义

图2

协变是父类返回父类的指针或引用 子类返回子类的指针或引用。这样可以构成协变。

六:

多继承单继承的对象模型(虚函数)

图3

需要注意的是,多继承的派生类对象,子类和(第一个父亲)父类共用一张虚表,所以子类独有的虚函数的地址存放在第一个父亲的虚表当中。

图4

七:

虚继承||菱形继承

虚继承会引入一个偏移量表概念,虚表里面存储的是相对偏移量,第一行存储的是子类相对于子类自己的偏移量,(为零)接下来存储的是子类相对于父类的偏移量。

图5

如上图所示,普通的虚继承子类的对象模型就是这样,第一个是偏移量表地址,然后是自己的成员变量,接下来是父类的成员变量。

静态成员变量是全局的,是所有对象共享的,每一个对象都能访问到,但是在单个对象内存当中不存在。他属于全局变量,也就是说从内存的角度来讲,静态成员变量不属于单个对象。

这种继承模型不是菱形虚拟继承,是错误的模型。本质就是虚继承||菱形继承,还是存在数据的二义性和数据冗余问题,并没有解决菱形继承的问题。

图7

普通的虚继承就是,头四个字节是一个偏移量表地址,然后紧跟着就是自己的成员变量,接下来是父类的成员变量(按着继承的顺序左至右依次从上往下排列)

菱形继承的对象模型:

图8

带虚函数的菱形继承就是往Base1和Base2里面的a里面的多加了一个的虚表地址。

八:

菱形虚拟继承

图9

这种模型就是菱形虚拟继承的模型。

菱形虚继承的内存分布规则:把父类排在前面,父类完了之后是自己的成员,最后是祖父的成员;

九:

菱形虚拟继承(带虚表)

 

 

因为不是菱形虚继承嘛,菱形虚继承就是先把两个父类排在前面 然后自己的排在第三个然后把祖父的排在最后 两个父亲和祖父的头四个字节加上一个虚表地址就OK了,自己的和第一个父亲共用一个虚表。

从内存中可以看出,当在继承体系的某个派生类中显式给出构造或者析构函数时,最低层的派生类实例的大小会多四个字节—存放空指针(猜想是:因为空指针下面是祖父类的虚表地址和成员变量,而它们是继承体系中类所共享的,在最低层派生类中只保存一份,空指针上面是两个父类的虚表地址和偏移量表地址以及成员变量,所以空指针应该起到一个标志的作用,说明下面是所有类共享的)。

纯虚函数和抽象类的应用

图15

class Figure//抽象类

{

public:

//提供一个统一的界面接口

virtual void fun() = 0;//纯虚函数

};

如何理解抽象类提供一个统一的界面接口?

比如说我要实现一个接口,求三角形、原型、正方形的面积,我传给他什么类型的对象,他就给我返回这个对象的面积,显然这里应用到了多态,需要进行虚函数的重写,但是因为父类和子类两个显然是不够的,所以这里要用到多个子类和一个父类,然后多个子类重写父类的纯虚函数。

实现接口形参写父类的指针或引用,这样传不同形状的子类实参就会调用不同形状的返回面积的那个虚函数,达到想要的目的。

 

虽然说如果将三角形作为父类,原形和正方形分别作为子类这样重写返回面积的函数也是可以的,但是显然这样看起来布局很乱,而且每一个子类都多出来父类的成员,即用不到又有二义性。此时抽象类就显然高大上很多了。

 

Figure c1;//错误,抽象类不能建立对象

Figure *p = NULL;//正确,抽象类可以声明自己的指针

Figure fun();//错误,抽象类不能作为返回类型

void fun(Figure);//错误,抽象类不能作为参数类型

Figure &h(Figure &);//正确,抽象类可以声明抽象类的引用

 

最后总结一下各个问题的安全隐患问题:

虚表存在的安全隐患:

①效率太差,因为调用一个函数时得去查找两次,第一次得到虚表的地址,然后在虚表中查看虚函数的地址,会造成性能降低。

②存在安全隐患,例如假如程序不允许你去访问一个虚函数,但是你自己仍可以在虚表中查看虚函数。

虚函数注意要点:

①不要在构造函数和析构函数内部调用虚函数,在构造和析构函数中,对象是不完整的,可能会出现未定义的情况。

例如:在构造函数中,类中有3个成员变量需要初始化,假如在初始化了一个变量后就调用了虚函数,则可能会出现未定义情况。

在析构函数中,假如释放了一块空间后,调用虚函数,也会导致未定义的情况。

 

②在基类中定义了虚函数,则在派生类中该函数始终保持虚函数的特性。

在派生类中重写虚函数时也可以不显示写出virtual关键字,这时编译器会默认该函数为虚函数,为了程序看起来更加清晰,则最好加上virtual关键字。

 

③如果在类外定义虚函数,则只在类中声明时加virtual关键字,在类外定义是不能加virtual关键字。

 

④构造函数为什么不能定义为虚函数?

因为调用虚函数的前提是:对象一定构建成功,获取虚表地址必须通过对象的地址,如果对象还没有构建成功,那么无法获取虚表地址,

所以构造函数不能定义为虚函数。构造函数未执行完时对象是不完整的。

 

⑤虽然operator=可以声明为虚函数,最好不要这样做。,因为容易使用时容易引

起混淆。

本身operator=就是重载,你把它定义为虚函数又不能构成重写(因为参数列表不同,不构成协变)这样会引起歧义,而且让编译器做了很多更加复杂的事情。

 

⑥只有类的成员函数才能定义为虚函数,静态的成员函数不能定义为虚函数。

 

⑦最好把基类的析构函数声明为虚函数。(why?另外析构函数比较特殊,因为派生类的析构函数跟基类的析构函数构成覆盖,这里是因为编译器做了特殊处理)

因为delete一个父类的指针,如果父类指针指向的是子类的对象,delete只会调用父类的析构函数,子类的析构函数没有被调到,有可能会出现内存泄漏。

如果定义为virtual以后,因为构成协变,所以重写了析构函数,此时delete一个父类的指针,父类的指针指向父类对象,则调用父类的析构函数,父类的指针指向子类的对象,则调用父类和子类的析构函数。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值