CPP琐记 04 类的空间布局

空类

class Empty{};

Empty empty;
auto *empty2 = new Empty();

std::cout << sizeof(empty) << std::endl;
std::cout << sizeof(*empty2) << std::endl;

输出sizeof的时候直接被编译器优化掉了,以及空类只有1个字节,显然创建对象必须要有一个最小的内存空间。

auto *empty2 = new Empty();
auto *empty3 = new Empty();

std::cout << (&empty2 == &empty3) << std::endl;

试比较两者,结果编译器又看穿了一切:

另外这是C++的一个设计(空类也可被继承与比较)。

There is an interesting rule that says that an empty base class need not be represented by a separate byte:

struct X : Empty {

int a;

// …

};

void f(X* p)

{

void* p1 = p;

void* p2 = &p->a;

if (p1 == p2) cout << “nice: good optimizer”;

}

成员变量与静态和常量

class CNumber {
private:
int m_num1, m_num2;
public:
CNumber(){
m_num1 = 1;
m_num2 = 2;
}
CNumber(int mNum1, int mNum2) : m_num1(mNum1), m_num2(mNum2) {}
int getSum() const { return m_num1 + m_num2; }
};

CNumber cnum;
std::cout << sizeof(cnum) << std::endl;
std::cout << cnum.getSum() << std::endl;

auto *cnum2 = new CNumber(2, 3);
std::cout << sizeof(*cnum2) << std::endl;
std::cout << cnum2->getSum() << std::endl;

可见函数并不占用类的空间,类在栈上分配了8个字节,new的时候赋值给类指针,分配了8个字节。

class CNumber {
private:
int m_num1, m_num2;
public:
static int m_num3;
CNumber(){
m_num1 = 1;
m_num2 = 2;
m_num3++;
}
CNumber(int mNum1, int mNum2) : m_num1(mNum1), m_num2(mNum2) {}
int getSum() const { return m_num1 + m_num2; }
};

int CNumber::m_num3 = 0;

我们尝试增加静态成员变量,发现并不会影响类的大小,我们的静态变量去哪里了呢?

答案是.bss节,可能还不够明显,我们再声明一个string

public:
static const std::string s;

const std::string CNumber::s = “CNumber class”;

字符串被作为常量存储在.rdata节:

静态常量在初始化的时候被初始化(MinGW GCC编译器的环境):

虚函数无继承

class CNumber {
private:
int m_num1, m_num2;
public:
CNumber() {
m_num1 = 1;
m_num2 = 2;
}

CNumber(int mNum1, int mNum2) : m_num1(mNum1), m_num2(mNum2) {}

int getSum() const { return m_num1 + m_num2; }

virtual int getSum2() { return 1; }
};

sizeof输出了16,因为类成员中多了个vptr,占了8个字节,因此我们也可以知道,无论有多少虚函数,也只会有1个虚表指针。

而它的默认实现我们也可以跟踪过去:

单继承

先空类继承一下看看:

class CNumber2 : CNumber{

};

CNumber2 cnum3;
std::cout << sizeof(cnum3) << std::endl;

sizeof结果与基类一致,为16。

CNumder2有自己的虚表,且包含了一个getSum2的函数指针。

我们尝试在CNumber中加入成员后,会发现:

class CNumber2 : CNumber{
private:
int m_num1, m_num2;
};

sizeof为24,大小为基类 + 派生类,所有成员之和。

补充链式继承的虚表布局

class Base{
virtual int funBase() {return 1;};
};

class A : Base
{
virtual int fun() {return 2;}
};

class C : public A
{
public:
virtual int fun3() {return 4;}
};

内存对齐

class CNumber {
private:
int m_num1, m_num2;
char a;
public:
CNumber() {
m_num1 = 1;

a = ‘a’;
m_num2 = 2;
}

CNumber(int mNum1, int mNum2) : m_num1(mNum1), m_num2(mNum2) {}
int getSum() const { return m_num1 + m_num2; }
virtual int getSum2() {return 1;};
};

sizeof的输出是24,char被对齐到了8字节保证访问速度。

继承的时候类的数据成员按其声明顺序加入内存,与声明顺序相关。

多继承

class CNumber {
private:
int m_num1, m_num2;
public:
CNumber() {
m_num1 = 1;
m_num2 = 2;
}

CNumber(int mNum1, int mNum2) : m_num1(mNum1), m_num2(mNum2) {}

int getSum() const { return m_num1 + m_num2; }

virtual int getSum2() { return 1; };
};

class CNumber2 : CNumber {
private:
int m_num1, m_num2;
};

class CNumber3 : CNumber2, CNumber {

};

在暂时不考虑代码是否合理(CNumber是无法访问的)的情况下,CNumber的大小为,((4+4)+8) + (((4+4)+8)+4+4) = 40字节,计算规则是被继承的类的大小之和。

同时也收获了2个虚表指针:

虚继承

先来个特殊的菱形继承例子

class Base{
public:
virtual int funBase() {return 1;};
};

class A : public Base
{
public:
virtual int fun() {return 2;}
};

class B : public Base
{
public:
virtual int fun2() {return 3;}
};

class C : public A, public B
{
public:
virtual int fun3() {return 4;}
};

sizeof A B C 分别是8 8 16,内存布局:

可见有两个指向了Base的指针。

我们假定这种场景是,C是Base,但我们无法通过C的指针来使用Base的方法,因此需要使用static_cast来转为B或者C来间接访问Base的方法。

C c;
Base &a = static_cast<A&>(c);
Base &b = static_cast<B&>(c);

a.funBase();
b.funBase();

而虚继承中,我们将代码改为下面的样子:

class Base{
public:
virtual int funBase() {return 1;}
};

class A : public virtual Base
{
public:
virtual int fun() {return 2;}
};

class B : public virtual Base
{
public:
virtual int fun2() {return 3;}
};

class C : public A, public B
{
public:
virtual int fun3() {return 4;}
};

此时sizeof(c)的大小居然是16,而期间我换了MSVC编译器,是24,这个问题困扰了我N多天,因为IDA也没有正确的分析虚表的结构,本来打算放弃深究这个问题的时候,我发现GCC编译器支持输出类的结构。

GCC Ver < 8 使用g++ -fdump-class-hierarchy main.cpp

GCC Ver >8 使用 g++ -fdump-lang-class main.cpp

Class C

size=16 align=8

base size=16 base align=8

C (0x0x8782930) 0

vptridx=0 vptr=((& C::_ZTV1C) + 32)

A (0x0x85e6ea0) 0 nearly-empty

primary-for C (0x0x8782930)

subvttidx=8

Base (0x0x87d0ba0) 0 nearly-empty virtual

primary-for A (0x0x85e6ea0)

vptridx=40 vbaseoffset=-32

B (0x0x85e6f08) 8 nearly-empty

lost-primary

subvttidx=24 vptridx=48 vptr=((& C::_ZTV1C) + 88)

Base (0x0x87d0ba0) alternative-path

Vtable for C

C::_ZTV1C: 13 entries

0     0

8     0

16    (int (*)(…))0

24    (int (*)(…))(& _ZTI1C)

32    (int (*)(…))Base::funBase

40    (int (*)(…))A::fun

48    (int (*)(…))C::fun3

56    18446744073709551608

64    18446744073709551608

72    (int (*)(…))-8

80    (int (*)(…))(& _ZTI1C)

88    0

96    (int (*)(…))B::fun2

看起来只是节约了一些空间,后面有深刻的理解之后再补充一下为什么编译器要这么处理吧,有时候情况会很复杂,个人认为虚继承也不是常用的模式,具体生成的代码也与编译器相关,不去研究编译器的话,了解即可。

发表评论

电子邮件地址不会被公开。 必填项已用*标注