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