C++ 由虚基类 虚继承 虚函数 到 虚函数表
//虚基类:一个类可以在一个类族中既被用作虚基类,也被用作非虚基类。
class Base1{
public:
Base1(){cout<<"Construct Base1!"<<endl;};
void foo();//普通函数
virtual void foo1(){cout<<"foo1 in Base1"<<endl;};//虚函数:可以在基类中实现(+{})或者直接定义成虚基类,\
会出现错误:undefined reference to vtable for lass
virtual void foo2() = 0;//纯虚函数:必须要在继承类中实现,类似java中的接口好处是 可以在不清楚具体构建的情况下,给派生类规定好规范
}; class Base2{
public:
void foo();//普通函数
virtual void foo1();//虚函数
//virtual void foo2() = 0;//纯虚函数
};
//虚继承:继承类继承了基类多次,从而产生了多个拷贝,虚基类的基本原则是在内存中只有基类成员的一份拷贝。\
//这样,通过把基类继承声明为虚拟的,就只能继承基类的一份拷贝,从而消除歧义。
class Derived1: public virtual Base1{
public:
Derived1(){cout<<"Construct Derived1~ !"<<endl;};
void foo2(){
cout<<"foo2 in Derive1"<<endl;
}
};
//多继承
class Derived2: public virtual Base1, public virtual Base2{
void foo2(){
cout<<"foo2 in Derive1"<<endl;
}
};
错误解决:
在使用虚函数的程序中,编译时会出现
undefined reference to `vtable for Class
或 undefined reference to typeinfo for Class 的情况
其解决方案就是将类似于
virtual void foo();
Should be defined (inline or in a linked source file):
virtual void foo() {}
Or declared pure virtual:
virtual void foo() = 0;
基类指针指向派生类
基类指针可以指向派生类,但是无法使用不存在于基类只存在于派生类的元素。(所以我们需要虚函数和纯虚函数)
当其指向派生类的时候,由于派生类元素在内存中堆放是:前N个是基类的元素,N之后的是派生类的元素。
于是基类的指针就可以访问到基类也有的元素了,但是此时无法访问到派生类(就是N之后)的元素。
派生类指针不能指向基类
虚函数表
本文部分内容参考了 陈皓专栏的内容.
对c++了解的人都应该知道虚函数是通过一张虚函数表实现的。简称V-Table。 在这个表中,主要是一个类的虚函数的地址表,这张表解决了 继承、覆盖的问题,保证内容真实反映实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子 类的时候,这张虚函数表就显得尤为重要了,它就像一个地图一样,指明了实际所应该调用的函数。C++的编译器保证虚函数表的指针存在与对象实例中的最前面 的位置(这是为了保证取道虚表函数的高性能)。
在每一个包含虚函数的类中,都存在一张虚函数表,用来记录虚函数真正的地址。当派生类继承于基类时,将基类的虚函数表也一同继承,然后根据自己是否覆盖基类的函数,来修改虚函数表。具体看这个:讲的很详细。实际上是每一个类都会创建一张虚表,每一个类的实例也就从类中得到了一张虚表.
一般: 无覆盖
一般继承: 虚函数覆盖
多重继承:无覆盖

可以看到子类函数的虚表就接在第一个基类虚表的后面
多重继承: 有虚函数覆盖

需要注意的问题:
不能通过父类型的指针访问子类自己的函数。即使这种目的可以使用指针的方式访问来达到
包含任一纯虚函数的类 为 抽象类。抽象类是不能够实例化的。
#include <iostream>
#include <cstdio>
using namespace std; class Base{
public:
virtual void f(){ cout<<" Base::f "<<endl; }
virtual void g(){ cout<<" Base::g "<<endl; }
virtual void h(){ cout<<" Base::h "<<endl; }
}; class Base1{
public:
virtual void f(){ cout<<" Base1::f "<<endl; }
virtual void g(){ cout<<" Base1::g "<<endl; }
virtual void h(){ cout<<" Base1::h "<<endl; }
}; class Base2{
public:
virtual void f(){ cout<<" Base2::f "<<endl; }
virtual void g(){ cout<<" Base2::g "<<endl; }
virtual void h(){ cout<<" Base2::h "<<endl; }
}; class Base3{
public:
virtual void f(){ cout<<" Base3::f "<<endl; }
virtual void g(){ cout<<" Base3::g "<<endl; }
virtual void h(){ cout<<" Base3::h "<<endl; }
}; class Derive : public Base{
public:
virtual void f1(){ cout<<" Derive::f1 "<<endl; }
virtual void g1(){ cout<<" Derive::g1 "<<endl; }
virtual void h1(){ cout<<" Derive::h1"<<endl; }
}; class Derive1 : public Base{
public:
virtual void f(){ cout<<" Derive1::f"<<endl; }
virtual void g1(){ cout<<" Derive1::g1 "<<endl; }
virtual void h1(){ cout<<" Derive1::h1"<<endl; }
}; class Derive2 : public Base1, public Base2, public Base3{
public:
virtual void f1(){ cout<<" Derive2::f1"<<endl; }
virtual void g1(){ cout<<" Derive2::g1 "<<endl; }
}; class Derive3: public Base1, public Base2, public Base3{
public:
virtual void f(){ cout<<" Derive3::f"<<endl; }
virtual void g1(){ cout<<" Derive3::g1 "<<endl; }
}; typedef void(*Fun)(void);
//作为函数指针 你得规定好指向的函数的参数类型,返回值类型. int main(void)
{
Base b;
Fun pFun = NULL;
cout<<"虚函数表地址: "<<&b<<endl;
cout<<"虚函数表地址: "<<(void *)&b<<endl;
cout<<"虚函数表地址: "<<(int*)&b<<endl;
cout<<"虚函数表地址: "<<(long*)&b<<endl;
/*上面四种输出结果一致 , b的地址转换成何种类型的指针看似是无所谓*/
cout<<"虚函数表---第一个函数地址: "<<(int *)*(int *)(&b)<<endl;
cout<<"虚函数表---第一个函数地址: "<<(void *)*(int*)(&b)<<endl;
/*
* long:在32位系统是32位整型,取值范围为-2^31 ~ (2^31 - 1);在64位系统是64位整型,取值范围为-2^63 ~ (2^63 - 1)
* 为了能在64位机器上运行,这里要用 long 而不是int
*/
pFun = (Fun)*((long *)*(long*)(&b)); //Base::f
pFun = (Fun)*((long *)*(long*)(&b) + ); //Base::g
pFun = (Fun)*((long *)*(long*)(&b) + ); //Base::h
/* 我们就可以通过上面的方法调用函数 */ /*
*虚函数表明明显是在子类继承父类的时候能够发挥作用, 这里继承可以分为两类:
* 一般继承(无虚函数覆盖 & 有虚函数覆盖) 和 多重继承(有 & 无 虚函数覆盖)
*/ /* 一般: 无覆盖*/
Derive d;
pFun = (Fun)*((long *)*(long*)(&d) + ); //Derive::f1 /*
* 1)虚函数按照其声明顺序放于表中。
* 2)父类的虚函数在子类的虚函数前面。
*/ /* 一般继承: 有虚函数覆盖 */
Derive1 d1;
pFun = (Fun)*((long *)*(long*)(&d1)); //Derive::1
pFun = (Fun)*((long *)*(long*)(&d1) + ); //Base::g
/*
* 1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
* 2)没有被覆盖的函数依旧。
*/ /* 多重继承: 无覆盖*/
Derive2 d2;
pFun = (Fun)*((long *)*(long *)(&d2)); //Base::f
pFun = (Fun)*((long *)*(long *)(&d2) + ); //Derive2::f
pFun = (Fun)*((long *)*( (long *)(&d2) + )); // Base2: f 这里*是取地址得到了虚表上第一个虚函数表Base1中第一个函数的地址 ; + 1 得到第二个虚函数表Base2中第一个函数的地址;
/*
*1) 每个父类都有自己的虚表。
*2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)
*/ /* 多重继承: 有虚函数覆盖*/
Derive3 d3;
pFun = (Fun)*((long *)*( (long *)(&d3) ) ); //Derive3: f
pFun = (Fun)*((long *)*( (long *)(&d3) + )); // Derive3: f
pFun = (Fun)*((long *)*( (long *)(&d3) + )); // Derive3: f 这里*是取地址得到了虚表上第一个虚函数表Base1中第一个函数的地址 ; + 1 得到第二个虚函数表Base2中第一个函数的地址;
/*这里设计到了虚表的概念,也就是每一个基类在子类的虚表中都占有一席 , 我们当然可以通过上面这行code来访问其他虚表,
*但是还可以用二维数组来访问,更加便捷
*/
long **pVtab = (long **)&d3;
//Base1 's vtable
pFun = (Fun)pVtab[][];//Derive3::f
pFun = (Fun)pVtab[][];//Base::g
//Derive's vtable
pFun = (Fun)pVtab[][]; //Derive3::g1
//The tail of the vtable
pFun = (Fun)pVtab[][]; /*
*三个父类虚函数表中的f()的位置被替换成了子类的函数指针。
*我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。
*这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。如:
*/
Base1 *b1 = &d3;
Base2 *b2 = &d3;
Base3 *b3 = &d3;
b1->f(); //Derive3::f()
b2->f(); //Derive3::f()
b3->f(); //Derive3::f()
b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()
/* 当然,父类指针只能指向子类中覆盖的父类中的虚函数, 如下,尝试访问子类中自有的函数 会报错*/
Base1 *b4 = new Derive3();
//b4 -> g1();///error: ‘class Base1’ has no member named ‘g1’| /* 但是并不安全:
* 1) 通过父类型的指针访问子类自己的虚函数: 在运行时,我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为。
* 2) 访问non-public的虚函数:
*/ return ;
}
#########################################################################################################
#再说C++多态 #
#########################################################################################################
. “C++的多态性用一句话概括就是:在基类的函数前+vritual关键字,在派生类中重写该函数,运行时会根据对象的实际类型来调用相应的函数(晚绑定或者叫动态绑定)。如果对象类型是基类,那么调用基类的函数。如果是派生类,那么调用派生类的函数。”
1:用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数。
2:存在虚函数的类都有一个一维的虚函数表叫做虚表,类的对象有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的。
3:多态性是一个接口多种实现,是面向对象的核心,分为类的多态性和函数的多态性。
4:多态用虚函数来实现,结合动态绑定.
5:纯虚函数是虚函数再加上 = 0;
6:抽象类是指包括至少一个纯虚函数的类。
纯虚函数:virtual void fun()=0;即抽象类!必须在子类实现这个函数,即先有名称,没有内容,在派生类实现内容。
编译器在编译的时候,发现Base类中有虚函数,这时候编译器会为每个包含虚函数的类创建一个虚表(vtable),该表是一个一维数组,在这个数组中存放每一个函数的地址。
如何定位虚表?编译器提供另外一个结构虚指针(vptr)。虚指针指向虚表,虚表中存放的是函数的地址。在程序运行时,根据对象的类型去初始化vptr,从而让vptr指向正确的所属类的虚表。由于pFather实际指向的对象类型是Son,所以vptr指向Son的vtable。
C++ 由虚基类 虚继承 虚函数 到 虚函数表的更多相关文章
- C++学习之路—继承与派生(三):多重继承与虚基类
(根据<C++程序设计>(谭浩强)整理,整理者:华科小涛,@http://www.cnblogs.com/hust-ghtao转载请注明) 多重继承是指一个派生类有两个或多个基类.例如,有 ...
- C++ 多继承与虚基类
转载来自:CSDN insistGoGo (http://blog.csdn.net/insistgogo) 多继承的定义:派生类的基类大于一个 语法: class 派生类名:继承方式1 基类名1 ...
- C/C++ 多继承{虚基类,虚继承,构造顺序,析构顺序}
C/C++:一个基类继承和多个基类继承的区别 1.对多个基类继承会出现类之间嵌套时出现的同名问题,如果同名变量或者函数出现不在同一层次,则底层派生隐藏外层比如继承基类的同名变量和函数,不会出现二义性, ...
- C++ 虚基类 派生与继承
在学习设计模式时我就有一个疑问,关联和继承除了用法上的区别,好像在内存上并没有什么区别,继承也是父类作为了子类的元素(内存上),关联也是这样.而且关联好像更占内存一些.这就是设计模式里问题了“依赖倒转 ...
- C++ 虚继承实现原理(虚基类表指针与虚基类表)
虚继承和虚函数是完全无相关的两个概念. 虚继承是解决C++多重继承问题的一种手段,从不同途径继承来的同一基类,会在子类中存在多份拷贝.这将存在两个问题:其一,浪费存储空间:第二,存在二义性问题,通常可 ...
- C++ - 虚基类、虚函数与纯虚函数
虚基类 在说明其作用前先看一段代码 class A{public: int iValue;}; class B:public A{public: void bPrintf(){ ...
- C++学习20 虚基类详解
多继承时很容易产生命名冲突,即使我们很小心地将所有类中的成员变量和成员函数都命名为不同的名字,命名冲突依然有可能发生,比如非常经典的菱形继承层次.如下图所示: 类A派生出类B和类C,类D继承自类B和类 ...
- C++虚基类详解(转)
我们知道,如果一个派生类有多个直接基类,而这些直接基类又有一个共同的基类,则在最终的派生类中会保留该间接共同基类数据成员的多份同名成员.在引用这些同名的成员时,必须在派生类对象名后增加直接基类名,以避 ...
- 从汇编看c++中含有虚基类对象的析构
c++中,当继承结构中含有虚基类时,在构造对象时编译器会通过将一个标志位置1(表示调用虚基类构造函数),或者置0(表示不调用虚基类构造函数)来防止重复构造虚基类子对象.如下图菱形结构所示: 当构造类B ...
随机推荐
- 关于js中的事件委托小案例
需求:页面上有一个按钮,和一个空的ul,要求点击按钮,会给ul中动态添加li元素,然后,点击动态添加的元素,在控制台上输出,这是第几个元素 <ul> </ul> <but ...
- Leetcode 115.不同的子序列
不同的子序列 给定一个字符串 S 和一个字符串 T,计算在 S 的子序列中 T 出现的个数. 一个字符串的一个子序列是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串.(例 ...
- 质因数分解 2012年NOIP全国联赛普及组
时间限制: 1 s 空间限制: 128000 KB 题目等级 : 青铜 Bronze 题目描述 Description 已知正整数 n是两个不同的质数的乘积,试求出较大的那个质数 . 输入描述 Inp ...
- Win32编程API 基础篇 -- 6.菜单和图标
菜单和按钮 例子:菜单1 本小节仅仅向你展示如果向你的窗口中加入一个基本的菜单,通常你会用到一个提前制作好的菜单资源,这会是一份.rc文件并且会被编译链接进你的.exe可执行程序中.这是具体的流程做法 ...
- 洛谷—— P2049 魔术棋子
https://www.luogu.org/problem/show?pid=2049 题目描述 在一个M*N的魔术棋盘中,每个格子中均有一个整数,当棋子走进这个格子中,则此棋子上的数会被乘以此格子中 ...
- symfony2笔记
路由可以在全局定义,也可以在单个bundle内部定义 全局定义:app/config/routing.yml 局部bundle定义:src/Miyaye/webBundle/Resources/con ...
- Vim tips——Working with external commands
A common sequence of events when editing files is to make a change and then need to test by executin ...
- 如何快速查看EPS,AI等矢量文件
使用Adobe Bridge可以快速查看所有这些格式的资源 查看EPS格式图片: 查看AI格式: 某些AI文件则无法预览(此外还有一些CDR的格式) 相比之下,ACDSee的效果则不如Adob ...
- 假设写一段代码引导PC开机这段代码是 ? Here is a tiny "OS" :-D
Hello world -- OS 我找到了华科绍志远博士的相关代码,发现他依据MIT的JOS的boot.S 稍作改动.然后单独剥离出来,能够非常好玩~ 资料下载地址: http://download ...
- poj 1390 Blocks (记忆化搜索)
Blocks Time Limit: 5000MS Memory Limit: 65536K Total Submissions: 4318 Accepted: 1745 Descriptio ...