C++继承、多态与虚表
继承
继承的一般形式
子类继承父类,是全盘继承,将父类所有的东西都继承给子类,除了父类的生死,就是父类的构造和析构是不能继承的。
继承的访问权限从两方面看:
1.对象:对象只能直接访问类中公有方法和成员。
2.继承的子类
私有继承就终止了父类再往下继承的能力
c++默认继承为私有继承
像以下程序
class D :public B1 ,public B2,public B3
公有继承B1,B2,B3
class D :public B1,B2,B3;
公有继承B1,私有继承B2,B3
继承是按照继承的顺序,和构造函数的初始化顺序无关,看以下程序
如果,子类中有对象成员,构造顺序是:1.父类,2.对象成员,3.自己
如果父类中有虚基类,应该先构造虚基类
虚基类
虚基类主要解决菱形继承问题,有以下程序
继承模型为:
内存模型:
如果对父类的x进行赋值,如下程序,会引发错误,编译器会报错,因为继承了两份,会产生二义性
如果指定访问A1还是A2就不会报错
内存中只为A1的成员x赋值了
如果希望来自父类的x在子类中只有1份
那么就要用虚拟继承
对于虚继承来的基类,又叫做虚基类
现在对cc.c进行赋值
A1和A2对象中的x成员都变成100
如果不是虚拟机成A1和A2继承来的x各自是各自的空间
虚继承让子类只保持父类的一份成员拷贝,A1和A2的继承的成员的空间是一个
如果是普通继承求C类型的大小为20字节
A1和A2各占8字节,C占4字节,加起来20字节
虚继承时,字节为24个,理论应该是16字节,但是多了两个虚表指针,空间就会增加
继承中的同名隐藏
子类继承父类,子类中有父类的同名方法,访问的是子类的方法,子类会隐藏父类所有的同名方法,即使父类有一个同名的参数不同的方法也是如此。如下程序:
如果子类对象访问父类的fun(int a)方法,编译会报错
但是通过作用域访问父类方法是可以访问的
多态
关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技 术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。
如果通过父类的对象调用fun,当然是调用父类的方法,因为编译时期就决定了
赋值兼容规则
1.可以将子类对象赋值给父类,其实只是把子类中的父类的部分,赋值给了父类。(称为对象的切片)
2.子类的对象赋值给父类的指针
3.子类的对象初始化父类的引用
达到多态
想要实现多态,需要动态绑定,需要父类的指针或父类的引用
父类方法为虚方法,子类覆盖父类的虚方法,才能达到多态
上述程序中的指针和引用调用
pb->fun();
rb.fun();
就会访问到子类的方法
子类中父类没有的方法,父类指针也无法访问到,父类指针只能访问到父类自己有的范围
注意:
virtual与函数重载无关,基类没有virtual,子类的同名方法就是隐藏而不是覆盖
只要没有指针,父类调用子类的,全部是调用父类作用范围内的,就算子类覆盖了父类的virtual方法也是如此。
子类覆盖了父类的虚方法,子类的方法也是虚方法,即使前面不写不写virtual也是如此
子类要覆盖父类的方法,就是要函数名参数都必须一样才叫覆盖
抽象类
如果基类是一个抽象的概念,就可以为其定义纯虚函数构成一个抽象类
以下的抽象类就定义了三个纯虚函数
注意:
如果一个类具有一个或者多个纯虚函数,那么它就是一个抽象类,必须让其他类继承它。
抽象类位于类层次的上层,不能定义抽象类的对象。
基类中的纯虚函数没有代码,而且必须在子类中覆盖基类中的纯虚函数。
子类中如果没有实现其抽象父类的所有纯虚函数,其也是一个抽象类,也不能实例化对象。
抽象类和虚函数的类有区别:
抽象类中的纯虚函数不实现,并且继承他的子类必须实现它
虚函数基类中可以有实现,并且子类中如果重新覆盖了它,还可以实现多态机制
多态的实现
先看一个程序
class Base
{
public:
virtual void fun()
{
cout<<"This is Base::fun()"<<endl;
}
void fun(int a)
{
cout<<"This is Base::fun(int)"<<endl;
}
}; class D : public Base
{
public:
void fun()
{
cout<<"This is D::fun()"<<endl;
}
}; void main()
{
D d;
d.fun();
d.fun(0);
}
当父类没有virtual时,d对象的内存模型
当父类有了virtual时,d对象的内存模型
可以看到,父类会多出来一个虚表指针,保存一个子类的方法
再来看一个程序
class Base
{
public:
Base():y(0)
{
cout<<"Create Base Object."<<endl;
}
public:
virtual void fun()
{
cout<<"This is Base::fun()"<<endl;
}
virtual void list()
{
cout<<"This is Base::list()"<<endl;
} void print()
{
cout<<"This is Base::print()"<<endl;
}
public:
int y;
}; class D : public Base
{
public:
D():x(0)
{
cout<<"Create D Object."<<endl;
}
public:
void fun()
{
cout<<"This is D::fun()"<<endl;
}
void list()
{
cout<<"This is D::list()"<<endl;
} void print()
{
cout<<"This is D::print()"<<endl;
} private:
int x;
}; void main()
{
D d;
Base *pb = &d;
pb->fun();
pb->list();
pb->show();
}
因为创建子类对象前,要先构造父类对象,其内存模型为
虚表中存储的都是父类的虚函数指针
只要有虚方法,创建对象就会自动建立一个虚表,表的最后一个位置为NULL
后面的八个0就是表的最后一个位置NULL (不同平台可能不一样,这是VC平台下的结果)
虚表中存储父类的方法的地址,虚表指针指向这块地址,这就是为什么加了virtual后多了4个指针的原因
子类对象地址为什么能赋值给父类对象指针?
因为,子类对象地址赋值给父类对象指针,父类对象指针就指向了子类的对象空间,父类操作子类的范围是有限制的,只能操作到子类中父类的范围。
上面谈到构造子类对象前先构造一个父类对象
当构造父类对象完成时候,再构造子类时,可以看到,虚表中的方法已经被更改为子类的了
现在我们在父类中加一个虚的show()方法,而子类中不去覆盖父类的show方法,可以看到子类中虚表的成员show还是父类的函数指针
用父类的指针来调用,是在虚表中调用,但是经过子类的覆盖,虚表中的函数地址已经变成了子类的函数的地址了,所以会调用子类的方法
如下图
通过指针访问虚表的函数成员
有以下程序
class Base
{
public:
virtual void f()
{ cout << "Base::f"<<endl;}
virtual void g()
{ cout << "Base::g"<<endl;}
virtual void h()
{ cout << "Base::h"<<endl;}
private:
int a;
int x;
}; typedef void(*pFun)(); void main()
{
Base b;
cout<<"&b = "<<&b<<endl;
cout<<"vfptr = "<<hex<<*(int*)(&b)<<endl; //虚表的地址,前四个字节 pFun pfun = (pFun)*(((int*)*(int*)(&b))+0); //取虚表中第一个虚函数的地址
pfun();
pfun = (pFun)*(((int*)*(int*)(&b))+1);
pfun();
pfun = (pFun)*(((int*)*(int*)(&b))+2);
pfun();
}
注意:虚表是在一个对象空间的开始位置
通过强行把&b转成int *,取得虚函数表的地址,然后,再次取址就可以得到第一个虚函数的地址了,也就是Base::f(),这在上面的程序中得到了验证(把int* 强制转成了函数指针)。
画个图解释一下。如下所示:
注意:在上面这个图中,我在虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符“/0”一样,其标志了虚函数表的结束。这个结束标志的值在不同的编译器下是不同的。
在WinXP+VS2003下,这个值是NULL。而在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下,这个值是如果1,表示还有下一个虚函数表,如果值是0,表示是最后一个虚函数表。(上面内容提到过)
下面,我将分别说明“无覆盖”和“有覆盖”时的子类虚函数表的样子。没有覆盖父类的虚函数是毫无意义的。我之所以要讲述没有覆盖的情况,主要目的是为了给一个对比。在比较之下,我们可以更加清楚地知道其内部的具体实现。
一般继承(无虚函数覆盖)
下面,再让我们来看看继承时的虚函数表是什么样的。假设有如下所示的一个继承关系:
请注意,在这个继承关系中,子类没有重写任何父类的函数。那么,在派生类的实例的虚函数表如下所示:
对于实例:Derive d; 的虚函数表如下:(overload(重载) 和 override(重写),重载就是所谓的名同而签名不同,重写就是对子类对虚函数的重新实现。)
我们可以看到下面几点:
1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。
一般继承(有虚函数覆盖)
覆盖父类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。下面,我们来看一下,如果子类中有虚函数重载了父类的虚函数,会是一个什么样子?假设,我们有下面这样的一个继承关系。
为了让大家看到被继承过后的效果,在这个类的设计中,我只覆盖了父类的一个函数:f()。那么,对于派生类的实例的虚函数表会是下面的样子:
我们从表中可以看到下面几点,
1)覆盖的f()函数被放到了子类虚函数表中原来父类虚函数的位置。
2)没有被覆盖的函数依旧。
这样,我们就可以看到对于下面这样的程序,
Base *b = new Derive();
b->f();
由b所指的内存中的虚函数表(子类的虚函数表)的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。
多重继承(无虚函数覆盖)
下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数
对于子类实例中的虚函数表,是下面这个样子:
我们可以看到:
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)
这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
多重继承(有虚函数覆盖)
下面我们再来看看,如果发生虚函数覆盖的情况。
下图中,我们在子类中覆盖了父类的f()函数。
下面是对于子类实例中的虚函数表的图:
我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以用任一个父类指针来指向子类,并调用子类的f()了。如:
Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()
b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()
安全性
一、尝试:通过父类型的指针(指向子类对象)访问子类自己的虚函数
我们知道,子类没有重载父类的虚函数是一件毫无意义的事情。因为多态也是要基于函数重载的。虽然在上面的图中我们可以看到子类的虚表中有Derive自己的虚函数,但我们根本不可能使用基类的指针来调用子类的自有虚函数:
Base1 *b1 = new Derive();
b1->f1(); //编译出错
任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法,所以,这样的程序根本无法编译通过。
但在运行时,我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为。
二、尝试:通过父类型的指针(指向子类对象)访问父类的non-public虚函数
另外,如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于子类虚函数表中,所以我们同样可以使用访问虚函数表的方式来访问这些non-public的虚函数,这是很容易做到的。
如下程序:
class Base {
private:
virtual void f() { cout << "Base::f" << endl; }
};
class Derive : public Base{
}; typedef void(*Fun)(void); void main() {
Derive d;
Fun pFun = (Fun)*((int*)*(int*)(&d)+0);
pFun();
}
[参考:https://blog.csdn.net/sanfengshou/article/details/4574604]
C++继承、多态与虚表的更多相关文章
- Java开发知识之Java的继承多态跟接口*
Java开发知识之Java的继承多态跟接口 一丶继承 1.继承的写法 在Java中继承的 关键字是 extends 代表一个类继承另一个类. 继承的含义以及作用: 继承就是基于某个父类的扩展.制定出来 ...
- Winform打砖块游戏制作step by step第5节---重构代码,利用继承多态
一 引子 为了让更多的编程初学者,轻松愉快地掌握面向对象的思考方法,对象继承和多态的妙用,故推出此系列随笔,还望大家多多支持. 二 本节内容---重构代码,利用继承多态 1. 主界面截图如下: 2. ...
- java面向对象(封装-继承-多态)
框架图 理解面向对象 面向对象是相对面向过程而言 面向对象和面向过程都是一种思想 面向过程强调的是功能行为 面向对象将功能封装进对象,强调具备了功能的对象. 面向对象是基于面向过程的. 面向对象的特点 ...
- 浅谈学习C++时用到的【封装继承多态】三个概念
封装继承多态这三个概念不是C++特有的,而是所有OOP具有的特性. 由于C++语言支持这三个特性,所以学习C++时不可避免的要理解这些概念. 而在大部分C++教材中这些概念是作为铺垫,接下来就花大部分 ...
- No2_4.接口继承多态_Java学习笔记_经典案例
import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collections; import jav ...
- Java继承多态中的方法访问权限控制
java中的方法天生具有继承多态特性,这点与C++有很大不同(需要在父类方发上加virtual关键字),但用起来确实方便了许多. 最简单的继承多态 声明一个接口BaseIF,只包含一个方法声明 pub ...
- Java三大特性(封装,继承,多态)
Java中有三大特性,分别是封装继承多态,其理念十分抽象,并且是层层深入式的. 一.封装 概念:封装,即隐藏对象的属性和实现细节,仅对外公开接口,控制在程序中属性的读和修改的访问级别:将抽象得到的数据 ...
- -1-2 java 面向对象基本概念 封装继承多态 变量 this super static 静态变量 匿名对象 值传递 初始化过程 代码块 final关键字 抽象类 接口 区别 多态 包 访问权限 内部类 匿名内部类 == 与 equal
java是纯粹的面向对象的语言 也就是万事万物皆是对象 程序是对象的集合,他们通过发送消息来相互通信 每个对象都有自己的由其他的对象所构建的存储,也就是对象可以包含对象 每个对象都有它的类型 也就是 ...
- python面向对象之继承/多态/封装
老师说,按继承/多态/封装这个顺序来讲. 子类使用父类的方法: #!/usr/bin/env python # coding:utf-8 class Vehicle: def __init__(sel ...
- java继承多态和抽象类接口
一.继承 通过扩展一个已有的类,并继承该类的属性和行为,来创建一个新的类.已有的称为父类,新的类称为子类(父类派生子类,子类继承父类).(1)继承的优点: ①代码的可重用性: ②父类的属性 ...
随机推荐
- maven - 一键删除maven仓库无效jar包工具
背景 在进行maven开发时,往往需要下载大量jar包,而由于网络不稳定等其他因素可能导致jar未下载完毕,然后保留了lastUpdated文件,导致无法更新失效的jar包. 现在提供个bat脚本,只 ...
- 翻转引起 cocos studio 引擎与cocos2d 代码相同坐标显示不同
使用setFlippedX后,又改变锚点为1.此时代码中坐标需要相对于cocos studio 中增加它本身的width,因为(0.5,0.5)是相对于自己中点的翻转,不变坐标.而(1,0.5)是相对 ...
- Linux下用Bash语言实现简单排序的功能
题目链接: 题目描述 利用指针,编写一个函数实现三个整数按由小到大的排序. 输入 三个整数 输出 由小到大输出成一行,每个数字后面跟一个空格 样例输入 2 3 1 样例输出 1 2 3 复习下Linu ...
- Appium+python自动化-元素定位uiautomatorviewer的使用
前言 环境搭建好了,下一步元素定位,元素定位本篇主要介绍如何使用uiautomatorviewer,通过定位到页面上的元素,然后进行相应的点击等操作. uiautomatorviewer是androi ...
- asm相关内容想下载(包括 jar 包)
网址:http://download.forge.ow2.org/asm/
- js封装删除数组指定的某个元素的方法
首先可以给JS的数组对象定义一个函数,用于查找指定的元素在数组中的位置,即索引,代码为: Array.prototype.indexOf = function(val) { for (var i = ...
- Unity网络通讯(一)获取计算机的MAC地址
1 string GetMac() { string mac = ""; mac = GetMacAddressBySendARP(); return mac; } [DllImp ...
- 巨杉内核笔记(一)| SequoiaDB 会话(session)简介
SequoiaDB 会话(session)简介 会话(Session)的基本概念 容易弄混淆的两个概念是会话与连接. 通俗来讲,会话(Session) 是通信双方从开始通信到通信结束期间的一个上下文( ...
- vue的一些基础知识点,后续会更新最全的vue知识点
axios中jq的基础 jq语法 $(this).hide() 隐藏当前的html元素 $(''#test").hide() 隐藏id='test'的元素 添加新的 HTML 内容 我们将学 ...
- Codeforces Round #601 (Div. 2) C League of Leesins
把每一次输入的一组数字存下来,然后把每个数字出现的组数存下来 然后找只出现过一次的数字a,那么这个数字a不是开头就是结尾,默认为开头(是哪个都无所谓),然后去找和它出现在同一组的两个数字b和c,而b和 ...