C++ 类的大小
1. 类的大小与什么有关系?
class base
{};
class derived:public base
{
private:
int a;
};
此时,derived类的大小为4,derived类的大小是自身int成员变量的大小,至于为什么没有加上父类base的大小1是因为空白基优化的问题,在空基类被继承后,子类会优化掉基类的1字节的大小,节省了空间大小,提高了运行效率。
空类继承自空基类:
class base
{};
class derived:public base
{};
如果Base类和derived类可以区分开,则derived类的大小为1,否则为2。
class base1
{
private:
char a;
int b;
double c;
};
class base2
{
private:
char a;
double b;
int c;
};
虽然上述两个类成员变量都是一个char,一个int,一个double,但是不同的声明顺序,会导致不同的内存构造模型,对于base1,base2,其成员排列是酱紫的:
base 1类对象的大小为16字节,而base 2类对象的大小为24字节,因为不同的声明顺序,居然造成了8字节的空间差距,因此,我们将来在自己声明类时,一定要注意到内存对齐问题,优化类的对象空间分布。
4. 含虚函数的单一继承
class Base
{
private:
char a;
public:
virtual void f();
virtual void g();
};
class Derived:public Base
{
private:
int b;
public:
void f();
};
class Derived1:public Base
{
private:
double b;
public:
void g();
virtual void h();
};
基类Base中含有一个char型成员变量,以及两个虚函数,此时Base类的内存布局如下:
内存布局的最一开始是vfptr(virtual function ptr)
即虚函数表指针(只要含虚函数,一定有虚函数表指针,而且该指针一定位于类内存模型最前端),接下来是Base类的成员变量,按照在类里的声明顺序排列,当然啦,还是要像上面一样注意内存对齐原则!
继承类Derived继承了基类,重写了Base中的虚函数f()
,还添加了自己的成员变量,即int型的b,这时,Derived的类内存模型如下:
此种情况下,最一开始的还是虚函数表指针,只不过,在Derived类中被重写的虚函数f()在对应的虚函数表项的Base::f()
已经被替换为Derived::f()
,接下来是基类的成员变量char a,紧接着是继承类的成员变量int b,按照其基类变量声明顺序与继承类变量声明顺序进行排列,并注意内存对齐问题。
继承类Derived1继承了基类,重写了Base中的虚函数g(),还添加了自己的成员变量(即double型的b)与自己的虚函数(virtual h()
),这时,Derived1的类内存模型如下:
此种情况下,Derived1类一开始仍然是虚函数表指针,只是在Derived1类中被重写的虚函数g()在对应的虚函数表项的Base::g()
已经被替换为Derived1::g()
,新添加的虚函数virtual h()
位于虚函数表项的后面,紧跟着基类中最后声明的虚函数表项后,接下来仍然是基类的成员变量,紧接着是继承类的成员变量。
5. 含虚函数的多重继承
class Base1
{
private:
char a;
public:
virtual void f();
virtual void g1();
};
class Base2
{
private:
int b;
public:
virtual void f();
virtual void g2();
};
class Base3
{
private:
double c;
public:
virtual void f();
virtual void g3();
};
class Derived:public Base1, public Base2, public Base3
{
private:
double d;
public:
void f();
virtual void derived_func();
}
derived_func()
紧接着是继承类Derived的内存布局:
首先,Derived类自己的虚函数表指针与其声明继承顺序的第一个基类Base1的虚函数表指针合并,此外,若Derived类重写了基类中同名的虚函数,则在三个虚函数表的对应项都应该予以修改,Derived中新添加的虚函数位于第一个虚函数表项后面,Derived中新添加的成员变量位于类的最后面,按其声明顺序与内存对齐原则进行排列。
6. 虚拟继承
class Base
{
public:
int a;
}
class Base1:virtual public Base
{
}
class Base2:virtual public Base
{
}
class Derived:public Base1,public Base2
{
private:
double b;
public:
}
Base1与Base2本身没有任何自身添加的数据成员与虚函数,因此,Base1与Base2都只含有从Base继承来的int a与一个普通的方法,然后Derived又从Base1与Base2继承,这时会导致二义性问题及重复继承下空间浪费的问题:
二义性问题:
Derived de;
de.a=10; //这里是错误的,因为不知道操作的是哪个a
重复继承下空间浪费:Derived重复继承了两次Base中的int a,造成了无端的空间浪费。
虚拟继承是怎么解决上述问题的?
虚基继承可以使得上述菱形继承情况下最终的Derived类只含有一个Base类,Base类在虚拟继承后,位于继承类内存布局最后面的位置,继承类通过vbptr寻找基类中的成员及vfptr。
虚拟继承对继承类的内存布局影响看以下示例代码:
class base
{
public:
int a
virtual void f();
}
class derived:virtual public base
{
public:
double d;
void f();
}
Derived类内存布局如下图,由于虚拟继承,Derived只会有一个最初基类的拷贝,该拷贝位于类对象模型的最下面,而想要访问到基类的元素,需要vbptr指明基类的位置,假如Base中含有虚函数,而继承类中没有增添自己的新的虚函数,那么Derived类统一的布局如下:
如果添加了自己的新的虚函数(代码如下):
class base
{
public:
int a
virtual void f();
}
class derived:virtual public base
{
public:
double d;
void f();
virtual void g();//这是Derived类自己新添加的虚函数
}
那么Derived在VC下继承类会有自己的虚函数指针,而在Gcc下是共用基类的虚函数指针:
现在有了上述代码的理解我们可以写出菱形虚拟继承代码及每个类的内存布局:
class Base
{
public:
int a;
}
class Base1:public virtual Base
{
}
class Base2:public virtual Base
{
}
class Derived:public Base1,public Base2
{
private:
double b;
public:
}
带实线的框是类确确实实有的,带虚线是针对Base,及Base1,Base2做了扩展后的情况:Base有虚函数,Base1还添加了自己新的虚函数,Base1也有自己成员变量,Base2添加了自己新的虚函数,Base2也有自己成员变量,则上图全部虚线中的部分都将存在于对象内存布局中。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具