主要有三个因素对对象的内存布局有较大影响:类成员类型(static成员变量,virtual成员函数);继承方式;内存对齐。以下分别详细说明了具体的影响。
一、static与virtual对内存布局的影响
对象的内存分布与类的成员有关,static成员变量与非static成员变量会造成不同的内存分布,virtual成员函数与非virtual成员函数会造成不同的内存分布。具体而言,对象的内存只包含类的非static成员变量,当有virtual成员函数出现时,会多包含一个指向虚函数表的指针。
1. 类只包含非static成员变量与非virtual成员函数
该类包含两非static成员变量,一个非virtual成员函数。
class A{
public:
A(int x=0,int y=0):a(x),b(y) {};
voidprintA(){cout<<a<<”,”<<b<<endl;}
private:
int a;
int b;
}a;
该类的对象a的内存大小为8(假定int型、指针的大小为4个字节,下同),具体分布如图:
Int a |
Int b |
说明非static成员变量被放置在对象中,而非virtual成员函数并不包含在对象中。
2. 类包含static成员变量但不包含virtual成员函数
该类包含两非static成员变量,一个static成员变量,一个非virtual成员函数。
class A{
public:
A(int x=0,int y=0):a(x),b(y) {};
voidprintA(){cout<<a<<”,”<<b<<endl;}
private:
int a;
int b;
staticint c;
}a;
该类的对象a的内存大小为8,具体分布如图:
Int a |
Int b |
说明static成员变量不包含在对象中。这是很容易理解的,因为static成员变量为该类的所有对象共有,若每个对象都保存了static变量,那么修改该变量的值将会十分困难,因为需要修改该类所有对象中的值。
3. 类包含static成员变量与virtual成员函数
该类包含两非static成员变量,一个static成员变量,一个非virtual成员函数和一个virtual成员函数。
class A{
public:
A(int x=0,int y=0):a(x),b(y) {};
voidprintA(){cout<<a<<”,”<<b<<endl;}
virtual void f(){cout<<”I am a virtual function. ”<<endl;}
private:
int a;
int b;
staticint c;
}a;
该类的对象a的内存大小为12字节,具体分布如图:
vptr |
Int a |
Int b |
当类中出现virtual成员函数是,对象会增加一个指针(vptr),该指针指向一张虚函数表(vtbl)。注意,当有多个virtual成员函数时还是只有一个指针,这是的虚函数表会有多个指向函数地址的项。有关虚函数表的内容在中有简单介绍。
总得来说,对象的内存中一般只包含非static成员变量,成员函数与static成员变量并不在对象中。当出现virtual成员函数时会增加一个vptr指针。这是容易理解的,每个对象都应该有自己能够与其他对象区分开来的非static成员变量(当两个对象的成员变量相同时,这两个对象是相等的。例如表示学生的对象中,当两个对象的学号、名字、学院等属性至少有一个不相同时,我们才会说这表示两个不同的学生)。而static成员变量与成员函数是该类所有对象所共有的,为了节省空间或操作简便当然不应该分布存放在每个对象中。正因为一个类的所有对象调用成员函数时调用的都是同一个位置的函数,所以才会需要传递this指针(隐式完成)来保证操作的变量是相应对象的成员变量(若每个对象分别都保存了成员函数就没有必要传递this指针的必要了)。
二、继承方式对内存布局的影响
继承又因为单一继承、多重继承、重复继承、虚拟继承等不同继承方式有不同的内存布局。
1. 单一继承
下述例子中,基类包含两个成员变量,一个非virtual成员函数,两个virtual成员函数,其中一个被子类覆盖。子类也包含两个成员变量,并覆盖了基类的非virtual成员函数和其中一个virtual成员函数。
class base{
public:
base():base_a(0),base_b(1){}
void printA(){ cout<<base_a<<","<<base_b<<endl; }
virtual void printa(){ cout<<base_a<<endl; }
virtualvoid printb(){ cout<<base_b<<endl; }
private:
int base_a;
int base_b;
};
class derived:public base{
public:
derived():derived_a(2),derived_b(3){}
void printA(){ cout<<derived_a<<","<<derived_b<<endl; }
virtual void printa(){ cout<<derived_a<<endl; }
private:
int derived_a;
int derived_b;
}a;
该类的对象内存大小为20字节。
vptr |
Int base_a |
Int base_b |
Int derived_a |
Int derived_b |
该类的对象中首先由一个指向虚函数表的指针(vptr),然后是继承自基类的成员变量,最后类自身的成员变量。
2. 多重继承
下述例子中,共有两个基类,每个基类包含两个成员变量,一个非virtual成员函数,两个virtual成员函数,其中一个被子类覆盖。子类也包含两个成员变量,并覆盖了基类的非virtual成员函数和其中一个virtual成员函数。
class base1{
public:
base():base_a(0),base_b(1){}
void printA(){ cout<<base_a<<","<<base_b<<endl; }
virtual void printa(){ cout<<base_a<<endl; }
virtualvoid printb(){ cout<<base_b<<endl; }
private:
int base_a;
int base_b;
};
class base2{
public:
base():base_a(0),base_b(1){}
void printA(){ cout<<base_a<<","<<base_b<<endl; }
virtual void printa(){ cout<<base_a<<endl; }
virtualvoid printb(){ cout<<base_b<<endl; }
private:
int base_a;
int base_b;
};
class derived:public base1,public base2{
public:
derived():derived_a(2),derived_b(3){}
void printA(){ cout<<derived_a<<","<<derived_b<<endl; }
virtual void printa(){ cout<<derived_a<<endl; }
private:
int derived_a;
int derived_b;
}a;
该类的对象内存大小为32字节。
vptr |
Int base1_a |
Int base1_b |
vptr |
Int base2_a |
Int base2_b |
Int derived_a |
Int derived_b |
该类的对象中首先有继承自基类base1指向虚函数表的指针(vptr),后面是继承自基类base1的成员变量,然后继承自基类base2指向虚函数表的指针(vptr),后面是继承自基类base2的成员变量,最后是类自身的成员变量。该例说明了派生类有多少个具有virtual成员函数的基类,就有相应多个虚函数表,而在对象的内存中就会有相应多个指向对应虚函数表的指针。
3. 重复继承
下述例子中,基类包含两个成员变量,一个非virtual成员函数,两个virtual成员函数,其中一个被子类覆盖。该基类派生了两个子类,base1,base2子类也包含两个成员变量,并覆盖了基类的非virtual成员函数和其中一个virtual成员函数。Base1,base2共同派生了最后的子类。
class base{
public:
base():base_a(0),base_b(1){}
void printA(){ cout<<base_a<<","<<base_b<<endl; }
virtual void printa(){ cout<<base_a<<endl; }
virtualvoid printb(){ cout<<base_b<<endl; }
private:
int base_a;
int base_b;
};
class base1:public base{
public:
base():base_a(0),base_b(1){}
void printA(){ cout<<base_a<<","<<base_b<<endl; }
virtual void printa(){ cout<<base_a<<endl; }
virtualvoid printb(){ cout<<base_b<<endl; }
private:
int base_a;
int base_b;
};
class base2:public base{
public:
base():base_a(0),base_b(1){}
void printA(){ cout<<base_a<<","<<base_b<<endl; }
virtual void printa(){ cout<<base_a<<endl; }
virtualvoid printb(){ cout<<base_b<<endl; }
private:
int base_a;
int base_b;
};
class derived:public base1,public base2{
public:
derived():derived_a(2),derived_b(3){}
void printA(){ cout<<derived_a<<","<<derived_b<<endl; }
virtual void printa(){ cout<<derived_a<<endl; }
private:
int derived_a;
int derived_b;
}a;
该类的对象内存大小为48字节。
vptr |
Int base_a |
Int base_b |
Int base1_a |
Int base1_b |
vptr |
Int base_a |
Int base_b |
Int base2_a |
Int base2_b |
Int derived_a |
Int derived_b |
该类的对象中首先有继承自基类base1指向虚函数表的指针(vptr),后面是继承自基类base1的成员变量,base1的成员变量包括两个继承自base的成员变量和自身定义的两个成员变量;然后是与base2相关的数据与base1类似,最后是类自身的成员变量。该例说明了派生类有多个基类而基类又有相同的父类时,派生类将重复包含这个父类(例子中包含了两次base类的成员变量)。
4. 虚拟继承
下述例子与3中类似,不同的是base1与base2继承base类是为虚继承。
class base{};
class base1:virtual public base{};
class base2:virtual public base{};
class derived:public base1,public base2{};
该类的对象内存大小为44字节。
vptr |
Int base1_a |
Int base1_b |
vptr |
Int base2_a |
Int base2_b |
Int derived_a |
Int derived_b |
Vptr |
Int base_a |
Int base_b |
该类的对象中首先有继承自基类base1指向虚函数表的指针(vptr),后面是继承自基类base1的自身定义的两个成员变量;然后是与base2相关的数据与base1类似;在然后是类自身的成员变量;最后是base1、base2共同的基类base的虚函数表的指针(vptr),后面是base类的两个成员变量。该例说明了虚继承时,派生类只包含一个父类的成员变量。
三、内存补齐对内存布局的影响
类中会有许多不同的数据类型,编译器为了更方便得查找数据,会自动进行对齐。每个成员变量都会按照起始位置为自身大小整数倍地址的规则进行对齐,而最后对象会将内存大小补齐为最大的成员变量的整数倍。下面是几个补齐的例子:
1、class tem{
char a; int b; char c;
}
这个类的大小为12字节,具体分布如下:
a |
|
|
|
b | |||
c |
|
|
|
首先char a占用一个字节,偏移量为0,int b占用4个字节,且其实位置需要为4的整数倍,故补齐3个字节后才是b的起始字节,char c占用1个字节,偏移量为8,共9个字节。最后补齐为最大的变量(int)大小的整数倍12个字节。
2、class tem{
char a; short b; int c; char d;
}
这个类的大小为12字节,具体分布如下:
a |
| b | |
c | |||
d |
|
|
|
首先char a占用一个字节,偏移量为0,short b占用2个字节,且起始位置需要为2的整数倍,故偏移量为2,int b占用4个字节,且其实位置需要为4的整数倍,故偏移量为4正好不需要补齐,char c占用1个字节,偏移量为8,共9个字节。最后补齐为最大的变量大小的整数倍12个字
3、class tem{
char a; char d; short b; int c;
}
这个类的大小为8字节,具体分布如下:
a | d | b |
c |
首先char a占用一个字节,偏移量为0,char b占用一个字节,偏移量为1,short b占用2个字节,偏移量为2不需要补齐,int b占用4个字节,且起始位置需要为4的整数倍,故偏移量为4正好不需要补齐,共8个字节。正好为最大变量int型的整数倍,故最终大小为8字节。
4、class tem{
char a; short b; char d; int c;
}
这个类的大小为12字节,具体分布如下:
a |
| b | |
d |
|
|
|
c |
首先char a占用一个字节,偏移量为0, short b占用2个字节,偏移量为2补齐一个字节,char d占用1个字节,偏移量为4,int b占用4个字节,故偏移量为8补齐三个字节,共12个字节。正好为最大变量int型的整数倍,故最终大小为12字节。
从以上可以看出,将变量声明顺序按从小到大排列能够最大化内存空间的利用。