1.类的大小与什么有关系?
与类大小有关的因素:普通成员变量,虚函数,继承(单一继承,多重继承,重复继承,虚拟继承)
与类大小无关的因素:静态成员变量,静态成员函数及普通成员函数
成员函数可以被看作是类作用域的全局函数,不在对象分配的空间里,只有虚函数才会在类对象里有一个指针,存放虚函数的地址等相关信息
一个类的实例化对象所占空间的大小? 注意不要说类的大小,是类的对象的大小。 首先,类的大小是什么?确切的说,类只是一个类型的定义,它是没有大小可言的,用sizeof运算符对一个类型名操作,得到的是具有该类型实体的大小
函数是类的所有对象共享的,而我们计算类的占用空间的时候,只计算对象占的空间
2.空类
空类即什么都没有的类,按上面的说法,照理说大小应该是0,但是,空类的大小为1,因为空类可以实例化,实例化必然在内存中占有一个位置,因此,编译器为其优化为一个字节大小。
某类继承自空类
class base
{
};
class derived:public base
{
private:
int a;
}
3.一般类的大小(注意内存对齐)
首先上两个类的示例:
class base1
{
private:
char a;
int b;
double c;
};
class base2
{
private:
char a;
double b;
int c;
};
虽然上述两个类成员变量都是一个char,一个int,一个double,但是不同的声明顺序,会导致不同的内存构造模型,对于base1,base2,其成员排列是酱紫的:
base1
base2:
base 1类对象的大小为16字节,而base 2类对象的大小为24字节,因为不同的声明顺序,居然造成了8字节的空间差距,因此,我们将来在自己声明类时,一定要注意到内存对齐问题,优化类的对象空间分布。
4.含虚函数的单一继承
首先呈上示意类:(64位,指针大小8字节)
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 类成员函数的编译
对于类成员函数,不是一个对象对应一个单独的成员函数,而是同一类的所有对象共享这个成员函数体。
编译器在编译一个普通成员函数时,会隐式地增加一个形参 this,并把当前对象的地址赋值给 this,所以普通成员函数只能在创建对象后通过对象来调用,因为它需要当前对象的地址。而静态成员函数可以通过类来直接调用,编译器不会为它增加形参 this,它不需要当前对象的地址,所以不管有没有创建对象,都可以调用静态成员函数。
普通成员变量占用对象的内存,静态成员函数没有 this 指针,不知道指向哪个对象,无法访问对象的成员变量,也就是说静态成员函数不能访问普通成员变量,只能访问静态成员变量。
普通成员函数必须通过对象才能调用,而静态成员函数没有 this 指针,无法在函数体内部访问某个对象,所以不能调用普通成员函数,只能调用静态成员函数。
静态成员函数与普通成员函数的根本区别在于:普通成员函数有 this 指针,可以访问类中的任意成员;而静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数)。
当程序编译后,成员函数的地址已经确定,当调用此成员函数时,会将当前对象的this指针传入成员函数,类的成员函数体只有一份,但成员函数之所以可以把各个对象的数据分开是因为,每次执行成员函数时,都会把当前对象的this指针(首地址)传入,对类内成员数据的访问,实际上是通过this指针访问数据;
=====this指针的由来========
一个学生可以有多本书一样,而这些书都是属于这个同学的;同理,如果有很多个同学在一起,那么为了确定他们的书不要拿混淆了,最好的办法我想应该就是每个同学都在自己的书上写上名字,这样肯定就不会拿错了。
同理,一个对象的多个成员就可看作是这个对象所拥有的书;而在很多个对象中间,我们为了证明某个成员是自己的成员,而不是其他对象的成员,我们同样需要给这些成员取上名字。在C++中,我们利用this指针帮助对象做到这一点,this指针记录每个对象的内存地址,然后通过运算符->访问该对象的成员。this指针指向当前的对象
6 含虚函数的多重继承
首先上示意类:
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();
}
首先继承类多重继承了三个基类,此外继承类重写了三个基类中都有的虚函数virtual f(),还添加了自己特有的虚函数derived_func(),那么,新的继承类内存布局究竟是什么样子的呢?请看下图!先来看3个基类的内存布局:
紧接着是继承类Derived的内存布局:
首先,Derived类自己的虚函数表指针与其声明继承顺序的第一个基类Base1的虚函数表指针合并,此外,若Derived类重写了基类中同名的虚函数,则在三个虚函数表的对应项都应该予以修改,Derived中新添加的虚函数位于第一个虚函数表项后面,Derived中新添加的成员变量位于类的最后面,按其声明顺序与内存对齐原则进行排列。
7. 菱形继承的问题及解决方案:虚拟继承
首先在讲这一节之前,先贴出几个重要的信息(干货):
(1)不同环境下虚拟继承对类大小的影响
在vs环境下,采用虚拟继承的继承类会有自己的虚函数表指针(假如基类有虚函数,并且继承类添加了自己新的虚函数)
在gcc环境下及mac下使用clion,采用虚拟继承的继承类没有自己的虚函数表指针(假如基类有虚函数,无论添加自己新的虚函数与否),而是共用父类的虚函数表指针
关于以上这一点请详见博客:https://blog.csdn.net/longjialin93528/article/details/79874558
,这里对此进行了超级详细的讲解。
(2)虚拟继承会给继承类添加一个虚基类指针(virtual base ptr 简称vbptr),其位于类虚函数指针后面,成员变量前面,若基类没有虚函数,则vbptr其位于继承类的最前端
关于虚拟继承,首先我们看看为什么需要虚拟继承及虚极继承解决的问题。
虚极继承主要是为了解决菱形继承下公共基类的多份拷贝问题:
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。
虚拟继承对继承类的内存布局影响可以先看以下示例代码,理解以后,我们在最后列出上述菱形虚拟继承情况下Base1,Base2与Derived代码及内存布局,看到虚拟继承起的作用。
class base
{
public:
int a
virtual void f();
}
class derived:virtual public base
{
public:
double d;
void f();
}
Derived类内存布局如下图,由于虚拟继承,Derived只会有一个最初基类的拷贝,该拷贝位于类对象模型的最下面,而想要访问到基类的元素,需要vbptr指明基类的位置(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也有自己成员变量,则上图全部虚线中的部分都将存在于对象内存布局中。