面试题答案
一键面试C++类的内存布局
- 数据成员:
- 非静态数据成员:类的对象中包含非静态数据成员,它们按照声明顺序依次存储在对象的内存空间中。例如:
class A {
int a;
double b;
};
在这个类A
的对象中,int
类型的a
先存储,然后是double
类型的b
。对象的大小至少为sizeof(int)+sizeof(double)
,通常还会考虑内存对齐,可能会更大。
- 静态数据成员:静态数据成员不包含在类的对象中,它们存储在全局数据区,属于整个类,而不是某个具体对象。所有对象共享静态数据成员。例如:
class B {
static int s;
};
B::s
存储在全局数据区,不影响B
类对象的大小。
2. 成员函数:
- 普通成员函数:普通成员函数代码存储在代码段,并不占用类对象的内存空间。普通成员函数通过
this
指针访问类的非静态数据成员。例如:
class C {
int data;
public:
void func() {
// 通过this指针访问data
this->data = 10;
}
};
C
类的对象大小只由data
决定,func
函数代码存储在代码段。
- 虚函数:
- 虚函数表(vtable):当类中包含虚函数时,类的对象会包含一个指向虚函数表的指针(vptr)。虚函数表是一个存储虚函数地址的数组,位于只读数据段。例如:
class D {
public:
virtual void vfunc() {}
};
D
类对象中包含一个vptr
,其大小通常为指针大小(如在64位系统中为8字节)。虚函数表中存储了vfunc
的地址。如果D
类有多个虚函数,虚函数表中会按顺序存储这些虚函数的地址。
- 动态绑定:当通过基类指针或引用调用虚函数时,会根据对象实际类型对应的虚函数表来调用正确的虚函数,实现动态绑定。
不同继承方式对内存布局的影响
- 公有继承:
- 公有继承保持基类成员的访问属性。在内存布局上,派生类对象包含基类对象的所有非静态数据成员,且在派生类对象内存空间中,基类对象部分先存储,然后是派生类自己的非静态数据成员。例如:
class Base {
int a;
public:
virtual void vfunc() {}
};
class Derived : public Base {
double b;
public:
void newfunc() {}
};
Derived
类对象内存布局中,先存储Base
类对象部分(包括vptr
和a
),然后是Derived
类自己的b
。Derived
类对象可以访问Base
类的公有和保护成员。
2. 私有继承:
- 私有继承将基类的公有和保护成员变为派生类的私有成员。在内存布局上,与公有继承类似,派生类对象先存储基类对象部分,然后是派生类自己的非静态数据成员。但派生类的外部代码无法访问从基类继承的公有和保护成员,只有派生类内部成员函数可以访问。
- 保护继承:
- 保护继承将基类的公有成员变为派生类的保护成员。内存布局同样是派生类对象先存储基类对象部分,再存储自己的非静态数据成员。派生类的外部代码无法访问从基类继承的公有成员,派生类的派生类可以访问这些保护成员。
多重继承和虚继承场景下的对象模型
- 多重继承:
- 当一个类从多个基类继承时,派生类对象内存布局包含多个基类对象部分。例如:
class Base1 {
int a;
};
class Base2 {
double b;
};
class Derived : public Base1, public Base2 {
char c;
};
Derived
类对象内存布局中,先存储Base1
类对象部分(包含a
),然后是Base2
类对象部分(包含b
),最后是Derived
类自己的c
。这种情况下可能会出现菱形继承问题(多个基类继承自同一个基类,导致数据冗余和歧义)。
2. 虚继承:
- 虚继承用于解决菱形继承问题。在虚继承中,虚基类的成员在派生类对象中只存储一份。例如:
class Base {
int data;
};
class Derived1 : virtual public Base {};
class Derived2 : virtual public Base {};
class Final : public Derived1, public Derived2 {};
Final
类对象内存布局中,Base
类的data
只存储一份。派生类对象会包含一个指向虚基类偏移量表的指针(vbptr),通过这个表可以找到虚基类成员在对象中的位置,避免数据冗余。