https://blog.csdn.net/fengxinlinux/article/details/72836199

C++中类所占的大小计算,因为涉及到虚函数成员,静态成员,虚继承,多继承以及空类等,不同情况有对应的计算方式。

首先要明确一个概念,平时所声明的类只是一种类型定义,它本身是没有大小可言的。 我们这里指的类的大小,其实指的是类的对象所占的大小。因此,如果用sizeof运算符对一个类型名操作,得到的是具有该类型实体的大小。

关于类/对象大小的计算:
    1、类大小的计算遵循结构体的对齐原则
    2、类的大小与普通数据成员有关,与成员函数和静态成员无关。即普通成员函数,静态成员函数,静态数据成员,静态常量数据成员均对类的大小无影响
    3、虚函数对类的大小有影响,是因为虚函数表指针带来的影响
    4、虚继承对类的大小有影响,是因为虚基表指针带来的影响
    5、空类的大小是一个特殊情况,空类的大小为1

解释说明
    1、静态数据成员之所以不计算在类的对象大小内,是因为类的静态数据成员被该类所有的对象所共享,并不属于具体哪个对象,静态数据成员定义在内存的全局区。
    2、空类的大小,以及含有虚函数,虚继承,多继承是特殊情况,接下来会一一举例说明

注意:因为计算涉及到内置类型的大小,接下来的例子运行结果是在64位gcc编译器下得到的。int的大小为4,指针大小为8。

一、简单情况的计算

#include<iostream>

class Base
{
public:
Base() { }
~Base() { } private:
static int a;
int b;
char c;
}; int main()
{
Base obj;
std::cout << sizeof(obj) << std::endl;
}

计算结果:8
静态变量a不计算在对象的大小内,由于字节对齐,结果为4+4=8。

二、空类的大小
    C++的空类是指这个类不带任何数据,即类中没有非静态(non-static)数据成员变量,没有虚函数(virtual function),也没有虚基类(virtual base class)。

直观地看,空类对象不使用任何空间,因为没有任何隶属对象的数据需要存储。然而,C++标准规定,凡是一个独立的(非附属)对象都必须具有非零大小。换句话说,c++空类的大小不为0。

#include <iostream>

class NoMembers
{ }; int main()
{
NoMembers num;
std::cout << sizeof(num) << std::endl;
}

输出: 1

C++标准指出,不允许一个对象(当然包括类对象)的大小为0,不同的对象不能具有相同的地址。这是由于:new需要分配不同的内存地址,不能分配内存大小为0的空间,避免除以 sizeof(T)时得到除以0错误,故使用一个字节来区分空类。

但是,有两种情况值得我们注意。
    第一种情况,涉及到空类的继承。 
    当派生类继承空类后,派生类如果有自己的数据成员,而空基类的一个字节并不会加到派生类中去。

#include <iostream>

class Empty
{ }; struct D : public Empty
{
int a;
}; int main()
{
D data;
std::cout << sizeof(data) << std::endl;
}

输出:4。

第二种情况,一个类包含一个空类对象数据成员。

#include <iostream>

class Empty
{ }; class HoldsAnInt
{
int x;
Empty e;
}; int main()
{
HoldsAnInt holds;
std::cout << sizeof(holds) << std::endl;
}

输出:8。

因为在这种情况下,空类的1字节是会被计算进去的。而又由于字节对齐的原则,所以结果为4+4=8。

继承空类的派生类,如果派生类也为空类,大小也都为1。

三、含有虚函数成员
    首先,介绍一下虚函数的工作原理:

  虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。编译器必需要保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量)。
  每当创建一个包含有虚函数的类或从包含有虚函数的类派生一个类时,编译器就会为这个类创建一个虚函数表(VTABLE)保存该类所有虚函数的地址,其实这个VTABLE的作用就是保存自己类中所有虚函数的地址,可以把VTABLE形象地看成一个函数指针数组,这个数组的每个元素存放的就是虚函数的地址。在每个带有虚函数的类中,编译器秘密地置入一指针,称为vpointer(缩写为VPTR),指向这个对象的VTABLE。当构造该派生类对象时,其成员VPTR被初始化指向该派生类的VTABLE。所以可以认为VTABLE是该类的所有对象共有的,在定义该类时被初始化;而VPTR则是每个类对象都有独立一份的,且在该类对象被构造时被初始化。

假设我们有这样的一个类:

#include <iostream>

class Base
{
public:
virtual void f() { std::cout << "Base::f" << std::endl; }
virtual void g() { std::cout << "Base::g" << std::endl; }
virtual void h() { std::cout << "Base::h" << std::endl; }
};

当我们定义一个这个类的实例,Base b时,其b中成员的存放如下:

指向虚函数表的指针在对象b的最前面。

虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符”\0”一样,其标志了虚函数表的结束。这个结束标志的值在不同的编译器下是不同的。在vs下,这个值是NULL。而在linux下,这个值如果是1,表示还有下一个虚函数表,如果值是0,表示是最后一个虚函数表。
    因为对象b中多了一个指向虚函数表的指针,而指针的sizeof是8,因此含有虚函数的类或实例最后的sizeof是实际的数据成员的sizeof加8。

例如:

#include <iostream>

class Base
{
public:
int a; virtual void f() { std::cout << "Base::f" << std::endl; }
virtual void g() { std::cout << "Base::g" << std::endl; }
virtual void h() { std::cout << "Base::h" << std::endl; }
}; int main()
{
Base data;
std::cout << sizeof(data) << std::endl;
}

输出为16。

vptr指针的大小为8,又因为对象中还包含一个int变量,字节对齐得8+8=16。

下面将针对基类含有虚函数的继承进行讨论:

(1)在派生类中不对基类的虚函数进行覆盖,同时派生类中还拥有自己的虚函数,比如有如下的派生类:

#include <iostream>

class Base
{
public:
int a; virtual void f() { std::cout << "Base::f" << std::endl; }
virtual void g() { std::cout << "Base::g" << std::endl; }
virtual void h() { std::cout << "Base::h" << std::endl; }
}; class Derived: public Base
{
public:
virtual void f1() { std::cout << "Derived::f1" << std::endl; }
virtual void g1() { std::cout << "Derived::g1" << std::endl; }
virtual void h1() { std::cout << "Derived::h1" << std::endl; }
}; int main()
{
Derived data;
std::cout << sizeof(data) << std::endl;
}

基类和派生类的关系如下:

当定义一个Derived的对象d后,其成员的存放如下:

可以发现:

1)虚函数按照其声明顺序放于表中。

2)基类的虚函数在派生类的虚函数前面。

此时基类和派生类的sizeof都是数据成员的大小+指针的大小8。

(2)在派生类中对基类的虚函数进行覆盖,假设有如下的派生类:

#include <iostream>

class Base
{
public:
int a; virtual void f() { std::cout << "Base::f" << std::endl; }
virtual void g() { std::cout << "Base::g" << std::endl; }
virtual void h() { std::cout << "Base::h" << std::endl; }
}; class Derived: public Base
{
public:
virtual void f() { std::cout << "Derived::f" << std::endl; }
virtual void g1() { std::cout << "Derived::g1" << std::endl; }
virtual void h1() { std::cout << "Derived::h1" << std::endl; }
}; int main()
{
Derived data;
std::cout << sizeof(data) << std::endl;
}

基类和派生类之间的关系:其中基类的虚函数f在派生类中被覆盖了

当我们定义一个派生类对象d后,其d的成员存放为:

可以发现:

1)覆盖的f()函数被放到了虚表中原来基类虚函数的位置。

2)没有被覆盖的函数依旧。

派生类的大小仍是基类和派生类的非静态数据成员的大小+一个vptr指针的大小

这样,我们就可以看到对于下面这样的程序,

Base *b = new Derive();
b->f();

由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。

(3)多重继承:无虚函数覆盖
    假设基类和派生类之间有如下关系:

对于派生类实例中的虚函数表,是下面这个样子:

我们可以看到:

1) 每个基类都有自己的虚表。

2) 派生类的成员函数被放到了第一个基类的表中。(所谓的第一个基类是按照声明顺序来判断的)

由于每个基类都需要一个指针来指向其虚函数表,因此d的sizeof等于d的数据成员加上三个指针的大小。

(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()

此情况派生类的大小也是类的所有非静态数据成员的大小+三个指针的大小

举一个例子:

#include<iostream>

class A
{ }; class B
{
public:
char ch;
virtual void func0() { }
}; class C
{
public:
char ch1;
char ch2;
virtual void func() { }
virtual void func1() { }
}; class D: public A, public C
{
public:
int d;
virtual void func() { }
virtual void func1() { }
}; class E: public B, public C
{
public:
int e;
virtual void func0() { }
virtual void func1() { }
}; int main()
{
std::cout << sizeof(A) << std::endl; //result=1
std::cout << sizeof(B) << std::endl; //result=16
std::cout << sizeof(C) << std::endl; //result=16
std::cout << sizeof(D) << std::endl; //result=16
std::cout << sizeof(E) << std::endl; //result=32 return ;
}

结果分析:
    1、A为空类,所以大小为1
    2、B的大小为char数据成员大小+vptr指针大小。由于字节对齐,大小为8+8=16
    3、C的大小为两个char数据成员大小+vptr指针大小。由于字节对齐,大小为8+8=16
    4、D为多继承派生类,由于D有数据成员,所以继承空类A时,空类A的大小1字节并没有计入当中,D继承C,此情况D只需要一个vptr指针,所以大小为数据成员加一个指针大小。由于字节对齐,大小为8+8=16
    5、E为多继承派生类,此情况为我们上面所讲的多重继承,含虚函数覆盖的情况。此时大小计算为数据成员的大小+2个基类虚函数表指针大小
考虑字节对齐,结果为8+8+2*8=32

四、虚继承的情况
    对虚继承层次的对象的内存布局,在不同编译器实现有所区别。
    在这里,我们只说一下在gcc编译器下,虚继承大小的计算。

它在gcc下实现比较简单,不管是否虚继承,GCC都是将虚表指针在整个继承关系中共享的,不共享的是指向虚基类的指针。

#include<iostream>

class A
{
public:
int a;
virtual void myfuncA() { }
}; class B : virtual public A
{
public:
virtual void myfunB() { }
}; class C : virtual public A
{
public:
virtual void myfunC() { }
}; class D : public B, public C
{
public:
virtual void myfunD() { }
}; int main()
{
std::cout << sizeof(A) << std::endl;
std::cout << sizeof(B) << std::endl;
std::cout << sizeof(C) << std::endl;
std::cout << sizeof(D) << std::endl; return ;
}

sizeof(A)=16,sizeof(B)=24,sizeof(C)=24,sizeof(D)=32.
    解释:A的大小为int大小加上虚表指针大小。B,C中由于是虚继承,因此大小为int大小加指向虚基类的指针的大小。B,C虽然加入了自己的虚函数,但是虚表指针是和基类共享的,因此不会有自己的虚表指针,他们两个共用虚基类A的虚表指针。D由于B,C都是虚继承,因此D只包含一个A的副本,于是D大小就等于int变量的大小+B中的指向虚基类的指针+C中的指向虚基类的指针+一个虚表指针的大小,由于字节对齐,结果为8+8+8+8=32。

Appendix:

1.空类

class A
{
};

sizeof(A); //1
解析:类的实例化就是为每个实例在内存中分配一块地址;每个类在内存中都有唯一的标识,因此空类被实例化时,编译器会隐含地为其添加一个字节,以作区分。

2.虚函数类

class A
{
virtual void Fun();
};

sizeof(A); //4
解析:当一个类中包含虚函数时,会有一个指向其虚函数表的指针vptr,系统为类指针分配大小为4个字节(即使有多个虚函数)。

3.普通数据成员

class A
{
int a;
char b;
};

sizeof(A); //8

解析:普通数据成员,按照其数据类型分配大小,由于字节对齐,所以a+b=8字节。

4.静态数据成员

class A
{
int a;
static int b;
};

sizeof(A); //4

解析:静态数据成员存放的是全局数据段,即使它是类的一个成员,但不影响类的大小;不管类产生多少实例或者派生多少子类,静态成员数据在类中永远只有一个实体存在。而类的非静态数据成员只有被实例化时,才存在,但类的静态数据成员一旦被声明,无论类是否被实例化,它都已存在,类的静态数据成员可以说是一种特殊的全局变量。

5.普通成员函数

class A
{
void Fun();
};

sizeof(A); //1
解析:类的大小与它的构造函数、析构函数以及其他成员函数无关,只与它的数据成员相关。

6.普通继承

class A
{
int a;
};

class B:public A
{
int b;
};

sizeof(B); //8

解析:普通类的继承,类的大小为本身数据成员大小+基类数据成员大小。

7.虚函数继承

virtual class A
{
int a;
};

class B:virtual public A
{
int b;
};

sizeof(B); //12

解析:虚函数类的继承,派生类大小=派生类自身成员大小+基类数据成员大小+虚拟指针大小(即使继承多个虚基类,也只有一个指向其虚函数表的指针vptr,大小为4字节)。

C++: class sizeof的更多相关文章

  1. 聊聊 C 语言中的 sizeof 运算

    聊聊 sizeof 运算 在这两次的课上,同学们已经学到了数组了.下面几节课,应该就会学习到指针.这个速度的确是很快的. 对于同学们来说,暂时应该也有些概念理解起来可能会比较的吃力. 先说一个概念叫内 ...

  2. c/c++中关于sizeof、strlen的使用说明

    sizeof: 一般指类型.变量等占用的内存大小(由于在编译时计算,因此sizeof不能用来返回动态分配的内存空间的大小) strlen: c字符串的长度(参数必须是字符型指针 char*,当数组名作 ...

  3. sizeof(转载)

    原文地址:http://blog.sina.com.cn/s/blog_5da08c340100bmwu.html 转载至:http://www.cnblogs.com/wangkangluo1/ar ...

  4. C语言中的sizeof()

    sizeof,一个其貌不扬的家伙,引无数菜鸟竟折腰,小虾我当初也没少犯迷糊,秉着"辛苦我一个,幸福千万人"的伟大思想,我决定将其尽可能详细的总结一下. 但当我总结的时候才发现,这个 ...

  5. 你必须知道的指针基础-4.sizeof计算数组长度与strcpy的安全性问题

    一.使用sizeof计算数组长度 1.1 sizeof的基本使用 如果在作用域内,变量以数组形式声明,则可以使用sizeof求数组大小,下面一段代码展示了如何使用sizeof: ,,,,,}; int ...

  6. c++面试常用知识(sizeof计算类的大小,虚拟继承,重载,隐藏,覆盖)

    一. sizeof计算结构体 注:本机机器字长为64位 1.最普通的类和普通的继承 #include<iostream> using namespace std; class Parent ...

  7. c语言 sizeof理解

    1.基本数据类型 char :1     short:2   int 4    long 4   long long :8    float:4    double :8字节. 2.数组:对应的基本数 ...

  8. sizeof与strlen的区别

    1 sizeof是操作符,而strlen是库函数: 2 sizeof的参数可以为任意变量或类型,而strlen必须以char*做参数,且字符串必须以‘/0’结尾: 3 数组名用作sizeof参数时不会 ...

  9. sizeof

    一.sizeof使用的场合: 1.sizeof操作符的一个主要用途是与存储分配和I/O系统那样的例程进行通信.例如: void* malloc(size_t size); size_t fread(v ...

  10. strlen()和sizeof()求数组长度

    在字符常量和字符串常量的博文里有提: 求字符串数组的长度 标准库函数strlen(s)可以返回字符串s的长度,在头文件<string.h>里. strlen(s)的判断长度的依据是(s[i ...

随机推荐

  1. mybatis执行test07测试类却显示test05测试类调用的sql语句出错

    1.测试类 @Test public void test07() { IStudentDao studentDao = new IStudentDaoImpl(); Student student = ...

  2. Keepalived 双主虚拟路由配置实例

    Keepalived 双主虚拟路由配置实例 演示前说明: 2台centos7.2 主机:node-00,node-01 VIP1:10.1.38.19预定node-00占有 VIP2:10.1.38. ...

  3. Spring MVC源码分析(三):SpringMVC的HandlerMapping和HandlerAdapter的体系结构设计与实现

    概述在我的上一篇文章:Spring源码分析(三):DispatcherServlet的设计与实现中提到,DispatcherServlet在接收到客户端请求时,会遍历DispatcherServlet ...

  4. VMware中 CentOS7挂载windows共享文件夹

    在编译自己的hadoop时,不想再次在虚拟机中下载jar包,就想到了挂载自己本地的maven仓库,使用本地仓库来进行编译,这里就需要使用VMware的VMware Tools了,直接复制官方文档如下 ...

  5. dnslog小技巧

    一.dnslog利用场景 主要针对无回显的情况. Sql-Blind RCE SSRF RFI(Remote File Inclusion) 二.原理 将dnslog平台中的特有字段payload带入 ...

  6. Ubuntu 18.04 切换使用Python3

    我安装的Ubuntu 默认的python是2.7.5 python -V 我参考网上照到的文章,如果需要默认python为 python3 python命令默认是 python 3 sudo cp / ...

  7. java常用类——比较器

    Comparable和Comparator接口都是为了对类进行比较,众所周知,诸如Integer,double等基本数据类型,java可以对他们进行比较,而对于类的比较,需要人工定义比较用到的字段比较 ...

  8. delphi mysql

    Delphi2006连接Mysql5.1 2.DBExpress+dbxopenmysql50.dll可能很多人会奇怪,dbxopenmysql50.dll是什么东东?DBExpress不就是数据库连 ...

  9. CF696B Puzzles(期望dp)

    传送门 解题思路 比较有意思的一道题.首先假如这个点\(x\)只有\(1\)个儿子\(u\),那么显然可得\(dp[u]=dp[x]+1\).继续如果多加一个儿子\(p\),那么\(p\)在\(u\) ...

  10. 牛客多校第六场 G Is Today Friday? 蔡勒公式/排列

    题意: 有一堆日期,这些日期都是星期五,但是数字被映射成了字母A~J,现在让你求逆映射,如果存在多种答案,输出字典序最小的那个. 题解: 用蔡勒公式解决关于星期几的问题. 对于映射,可以用笔者刚刚学会 ...