【整理】C++虚函数及其继承、虚继承类大小
参考文章:
http://blog.chinaunix.net/uid-25132162-id-1564955.html
http://blog.csdn.net/haoel/article/details/1948051/
一、虚函数与继承
1、空类,空类单继承,空类多继承的sizeof
#include <iostream>
using namespace std; class Base1
{ }; class Base2
{ }; class Derived1:public Base1
{ }; class Derived2:public Base1, public Base2
{ }; int main()
{
Base1 b1;
Base2 b2;
Derived1 d1;
Derived2 d2;
cout<<"sizeof(Base1) = "<<sizeof(Base1)<<" sizeof(b1) = "<<sizeof(b1)<<endl;
cout<<"sizeof(Base2) = "<<sizeof(Base2)<<" sizeof(b2) = "<<sizeof(b2)<<endl;
cout<<"sizeof(Derived1) = "<<sizeof(Derived1)<<" sizeof(d1) = "<<sizeof(d1)<<endl;
cout<<"sizeof(Derived2) = "<<sizeof(Derived2)<<" sizeof(d1) = "<<sizeof(d1)<<endl; return ;
}
结果为:
sizeof(Base1) = 1 sizeof(b1) = 1
sizeof(Base2) = 1 sizeof(b2) = 1
sizeof(Derived1) = 1 sizeof(d1) = 1
sizeof(Derived2) = 1 sizeof(d1) = 1
可以看出所有的结果都是1。
2、含有虚函数的类以及虚继承类的sizeof
虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。编译器必需要保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量)。
假设我们有这样的一个类:
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
当我们定义一个这个类的实例,Base b时,其b中成员的存放如下:
指向虚函数表的指针在对象b的最前面。
虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符"\0"一样,其标志了虚函数表的结束。这个结束标志的值在不同的编译器下是不同的。在WinXP+VS2003下,这个值是NULL。而在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下,这个值是如果1,表示还有下一个虚函数表,如果值是0,表示是最后一个虚函数表。
因为对象b中多了一个指向虚函数表的指针,而指针的sizeof是4,因此含有虚函数的类或实例最后的sizeof是实际的数据成员的sizeof加4。
下面将讨论针对基类含有虚函数的继承讨论
(1)在派生类中不对基类的虚函数进行覆盖,同时派生类中还拥有自己的虚函数,比如有如下的派生类:
class Derived: public Base
{ public: virtual void f1() { cout << "Derived::f1" << endl; } virtual void g1() { cout << "Derived::g1" << endl; } virtual void h1() { cout << "Derived::h1" << endl; } };
基类和派生类的关系如下:
当定义一个Derived的对象d后,其成员的存放如下:
可以发现:
1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。
此时基类和派生类的sizeof都是数据成员的sizeof加4。
(2)在派生类中对基类的虚函数进行覆盖,假设有如下的派生类:
class Derived: public Base
{ public: virtual void f() { cout << "Derived::f" << endl; } virtual void g1() { cout << "Derived::g1" << endl; } virtual void h1() { cout << "Derived::h1" << endl; } };
基类和派生类之间的关系:其中基类的虚函数f在派生类中被覆盖了
当我们定义一个派生类对象d后,其d的成员存放为:
可以发现:
1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
2)没有被覆盖的函数依旧。
这样,我们就可以看到对于下面这样的程序,
Base *b = new Derive();
b->f();
由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。
(3)多继承:无虚函数覆盖
假设基类和派生类之间有如下关系:
对于子类实例中的虚函数表,是下面这个样子:
我们可以看到:
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)
由于每个基类都需要一个指针来指向其虚函数表,因此d的sizeof等于d的数据成员加3*4=12。
(4)多重继承,含虚函数覆盖
假设,基类和派生类又如下关系:派生类中覆盖了基类的虚函数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()
3、一个关于含虚函数及虚继承的sizeof计算
#include <iostream>
using namespace std; class Base
{
public:
virtual void f();
virtual void g();
virtual void h();
}; class Derived1: public Base
{
public:
virtual void f1();
virtual void g1();
virtual void h1();
}; class Derived2:public Base
{
public:
virtual void f();
virtual void g1();
virtual void h1();
}; class Derived3:virtual public Base
{
public:
virtual void f1();
virtual void g1();
virtual void h1();
}; class Derived4:virtual public Base
{
public:
virtual void f();
virtual void g1();
virtual void h1();
}; class Derived5:virtual public Base
{
public:
virtual void f();
virtual void g();
virtual void h();
}; class Derived6:virtual public Base
{ }; int main()
{
cout<<sizeof(Base)<<endl; //
cout<<sizeof(Derived1)<<endl; //
cout<<sizeof(Derived2)<<endl; //
cout<<sizeof(Derived3)<<endl; //
cout<<sizeof(Derived4)<<endl; //
cout<<sizeof(Derived5)<<endl; //
cout<<sizeof(Derived6)<<endl; // return ;
}
对于Base, Derived1和Derived2的结果根据前面关于继承的分析是比较好理解的,不过对于虚继承的方式则有点不一样了,根据结果自己得出的一种关于虚继承的分析,如对Derived3或Derived4定义一个对象d,其里面会出现三个跟虚函数以及虚继承的指针,因为是虚继承,因此引入一个指针指向虚继承的基类,第二由于在基类中有虚函数,因此需要指针指向其虚函数表,由于派生类自己本身也有自己的虚函数,因为采取的是虚继承,因此它自己的虚函数不会放到基类的虚函数表的后面,而是另外分配一个只存放自己的虚函数的虚函数表,于是又引入一个指针,从例子中看到Derived5和Derived6的结果是8,原因是在派生类要么没有自己的虚函数,要么全部都是对基类虚函数的覆盖,因此就少了指向其派生类自己的虚函数表的指针,故结果要少4。(这个是个人的分析,但原理不知道是不是这样的)
二、不同编译器下的虚继承
1、对虚继承层次的对象的内存布局,在不同编译器实现有所区别。
首先,说说GCC的编译器.
它实现比较简单,不管是否虚继承,GCC都是将虚表指针在整个继承关系中共享的,不共享的是指向虚基类的指针。
class A { int a; virtual ~A(){} }; class B:virtual public A{ virtual void myfunB(){} }; class C:virtual public A{ virtual void myfunC(){} }; class D:public B,public C{ virtual void myfunD(){} };
以上代码中sizeof(A)=8,sizeof(B)=12,sizeof(C)=12,sizeof(D)=16.
解释:A中int+虚表指针。B,C中由于是虚继承因此大小为A+指向虚基类的指针,B,C虽然加入了自己的虚函数,但是虚表指针是和基类共享的,因此不会有自己的虚表指针。D由于B,C都是虚继承,因此D只包含一个A的副本,于是D大小就等于A+B中的指向虚基类的指针+C中的指向虚基类的指针。
如果B,C不是虚继承,而是普通继承的话,那么A,B,C的大小都是8(没有指向虚基类的指针了),而D由于不是虚继承,因此包含两个A副本,大小为16.注意此时虽然D的大小和虚继承一样,但是内存布局却不同。
然后,来看看VC的编译器
vc对虚表指针的处理比GCC复杂,它根据是否为虚继承来判断是否在继承关系中共享虚表指针,而对指向虚基类的指针和GCC一样是不共享,当然也不可能共享。
代码同上。
运行结果将会是sizeof(A)=8,sizeof(B)=16,sizeof(C)=16,sizeof(D)=24.
解释:A中依然是int+虚表指针。B,C中由于是虚继承因此虚表指针不共享,由于B,C加入了自己的虚函数,所以B,C分别自己维护一个虚表指针,它指向自己的虚函数。(注意:只有子类有新的虚函数时,编译器才会在子类中添加虚表指针)因此B,C大小为A+自己的虚表指针+指向虚基类的指针。D由于B,C都是虚继承,因此D只包含一个A的副本,同时D是从B,C普通继承的,而不是虚继承的,因此没有自己的虚表指针。于是D大小就等于A+B的虚表指针+C的虚表指针+B中的指向虚基类的指针+C中的指向虚基类的指针。
同样,如果去掉虚继承,结果将和GCC结果一样,A,B,C都是8,D为16,原因就是VC的编译器对于非虚继承,父类和子类是共享虚表指针的。
利用visual studio 命令提示(2008),到xx.cpp 文件目录下 运行cl /d1 reportSingleClassLayoutB xx.cpp
第一个vfptr 指向B的虚表,第二个vbptr指向A,第三个指向A的虚表,因为是虚拟继承,所以子类中有一个指向父类的虚基类指针,防止菱形继承中数据重复,这样在菱形继承中,不会出现祖先数据重复,而只指向祖先数据的指针。
【整理】C++虚函数及其继承、虚继承类大小的更多相关文章
- C++学习笔记(十二):类继承、虚函数、纯虚函数、抽象类和嵌套类
类继承 在C++类继承中,一个派生类可以从一个基类派生,也可以从多个基类派生. 从一个基类派生的继承称为单继承:从多个基类派生的继承称为多继承. //单继承的定义 class B:public A { ...
- C++ 虚函数、纯虚函数、虚继承
1)C++利用虚函数来实现多态. 程序执行时的多态性通过虚函数体现,实现运行时多态性的机制称爲动态绑定:与编译时的多态性(通过函数重载.运算符重载体现,称爲静态绑定)相对应. 在成员函数的声明前加上v ...
- c++ 虚函数和纯虚函数
在你设计一个基类的时候,如果发现一个函数需要在派生类里有不同的表现,那么它就应该是虚的.从设计的角度讲,出现在基类中的虚函数是接口,出现在派生类中的虚函数是接口的具体实现.通过这样的方法,就可以将对象 ...
- C++ - 虚基类、虚函数与纯虚函数
虚基类 在说明其作用前先看一段代码 class A{public: int iValue;}; class B:public A{public: void bPrintf(){ ...
- C++ 虚函数与纯虚函数
#include<iostream> #include<string> using namespace std; class A{ public: virtual void f ...
- c++中虚函数和纯虚函数定义
只有用virtual声明类的成员函数,使之成为虚函数,不能将类外的普通函数声明为虚函数.因为虚函数的作用是允许在派生类中对基类的虚函数重新定义.所以虚函数只能用于类的继承层次结构中. 一个成员函数被声 ...
- c/c++ 基金会(七) 功能覆盖,虚函数,纯虚函数控制
1.功能覆盖 ClassA , ClassB ,其中ClassB继承ClassA 类的定义如下面的: #ifndef _CLASSA_H #define _CLASSA_H #include < ...
- C++ 虚函数 、纯虚函数、接口的实用方法和意义
也许之前我很少写代码,更很少写面向对象的代码,即使有写多半也很容易写回到面向过程的老路上去.在写面向过程的代码的时候,根本不管什么函数重载和覆盖,想到要什么功能就变得法子的换个函数名字,心里想想:反正 ...
- C++\virtual 虚函数、纯虚函数
前提摘要: 虚函数联系到多态,多态联系到继承.所以本文中都是在继承层次上做文章.没了继承,什么都没得谈. 虚函数定义: 指向基类的指针或引用在操作它的多态类(子类/派生类)对象时,会根据不同的类对象, ...
- C++多态、虚函数、纯虚函数、抽象类、虚基类
一.C++多态 C++的多态包括静态多态和动态多态.静态多态包括函数重载和泛型编程,动态多态包括虚函数.静态多态是指在编译期间就可以确定,动态多态是指在程序运行时才能确定. 二.虚函数 1.虚函数为类 ...
随机推荐
- NOSQL Mongo入门学习笔记 - 数据的基本插入(二)
成功运行起来mongo之后,进入了命令行模式,mongo默认会选择test数据库 1. 使用db命令打印出来当前选定的数据库: > db test 2. 使用show dbs 命令可以打印出数据 ...
- Matlab中bsxfun和unique函数解析
一.问题来源 来自于一份LSH代码,记录下来. 二.函数解析 2.1 bsxfun bsxfun是一个matlab自版本R2007a来就提供的一个函数,作用是”applies an element-b ...
- "reactive programming"的概念
下面的内容大多是翻译来的. Reactive Programming? What is Reactive Programming? 为了了解Reactive——从编程范式至其背后的动机,有必要了解现在 ...
- eclipse查看jar包中class的中文注释乱码问题的解决
1,问题来源是在eclipse中直接查看springside的class(由eclipse自动反编译)里面注释的乱码问题: Preferences-General-Workspace-Text fil ...
- 解决ListView 跟ScroolView 共存 listItem.measure(0, 0) 空指针
在网上找到ListView 和ScroolView 共存的方法无非是给他每个listview 重新增加高度,但是android 的设计者始终认为这并不是一种好的实现方法.但是有的时候有必须要用这种蛋疼 ...
- linux ubuntu卸载软件
1.通过deb包安装的情况: 安装.deb包: 代码:sudo dpkg -i package_file.deb反安装.deb包: 代码:sudo dpkg -r package_name 2.通过a ...
- http://www.ibm.com/developerworks/cn/java/j-lo-hotswapcls/
http://www.ibm.com/developerworks/cn/java/j-lo-hotswapcls/
- SaaS系列介绍之三: SaaS的特性与作用
1 SaaS的特性 最早的SaaS服务之一当属在线电子邮箱,极大地降低了个人与企业使用电子邮件的门槛,进而改变了人与人.企业与企业之间的沟通方式.发展至今,SaaS服务的种类与产品已经非常丰富,面向个 ...
- 利用循环removeChild删除节点只删除一半问题
<!DOCTYPE html> <html> <head> <title>adduser.html</title> ...
- 数据段、代码段、堆栈段、BSS段
在linux中,进程在内存中一般会分为5个段,用来存放从磁盘载入的程序代码,等. 这五个段分别是: BSS段: 通常用来存放程序中未初始化的全局变量的一块内存区域.属于静态内存分配. 问题:全局变量不 ...