C++特别适合用于开发压榨机器性能的程序,在资源有限的情况下,C++程序员不能放过机器的每一点计算力以及每一字节内存。当然了,要做到这一点,编程语言本身要提供精准的控制能力,C++语言作为一种强类型的编程语言,“精准控制”自然不在话下。这样看来,将机器性能发挥到极限的责任最后还是落在程序员身上了,要做到这一点,我们至少需要对各种数据类型使用的内存情况了然于胸。
C++是面向对象程序设计语言,自然支持面向对象的三大特性(继承、封装、多态)。在实现中,类的常见组织形式大致有以下几种,空类、普通类、单一继承(非虚继承)、多重继承(非虚继承)、单一继承(虚继承)、重复继承(虚继承)、重复继承(非虚继承),下面对这几种类的组织形式从内存分布方面来逐个学习分析,了解编译器是怎么处理类的内存分布的。
Visual Studio IDE支持配置工程属性,来查看类的内存布局。先选择左侧的C/C++->命令行,然后在其他选项这里写上/d1 reportAllClassLayout,它可以看到所有相关类的内存布局,如果写上/d1 reportSingleClassLayoutXXX(XXX为类名),则只会打出指定类XXX的内存布局。近期的VS版本都支持这样配置,定义一个类后,编译一下,就可以看到输出框里面有内存排布的输出。
一、空类
1. 声明空类
对于空类,在声明时,编译器不会生成任何成员函数,只会生成1个字节的占位符。有时可能会以为编译器会为空类生成默认构造函数等,事实上是不会的,编译器只会在需要的时候(比如定义类变量)生成6个成员函数:一个缺省的构造函数、一个拷贝构造函数、一个析构函数、一个赋值运算符、一对取址运算符和一个this指针。
// 声明
class A
{
};
class B
{
virtual bool compare(int a,int b) = 0;
};
class C:public A,public B
{
};
class D:public A,public B
{
virtual bool compare(int a, int b) = 0;
};
class E :virtual A, virtual B
{
};
class F :virtual A, virtual B
{
virtual bool compare(int a, int b) = 0;
};
分析:
类A是空类,但空类同样可以被实例化,而每个实例在内存中都有一个独一无二的地址,为了达到这个目的,编译器往往会给一个空类隐含的加一个字节,这样空类在实例化后在内存得到了独一无二的地址,所以sizeof(A)的大小为1。
类B里面因有一个纯虚函数,故有一个指向虚函数的指针(vptr),32位系统分配给指针的大小为4个字节,所以sizeof(B)的大小为4。类C继承于A和B,编译器取消A的占位符,保留一虚函数表,故大小为4。类D继承于A和B,派生类基类共享一个虚表,故大小为4。类E虚继承A和B,含有一个指向基类的指针(vftr)和一个指向虚函数的指针。类F虚继承A和B,含有一个指向基类的指针(vftr)和一个指向虚函数的指针。
2. 定义空类
在定义时,会生成6个成员函数,分别是一个缺省的构造函数、一个拷贝构造函数、一个析构函数、一个赋值运算符、一对取址运算符和一个this指针。
class Empty
{
};
等价于:
class Empty
{
public:
Empty(); //缺省构造函数
Empty(const Empty &rhs); //拷贝构造函数
~Empty(); //析构函数
Empty& operator=(const Empty &rhs); //赋值运算符
Empty* operator&(); //取址运算符
const Empty* operator&() const; //取址运算符(const版本)
};
使用时的调用情况:
Empty *e = new Empty(); //缺省构造函数
delete e; //析构函数
Empty e1; //缺省构造函数
Empty e2(e1); //拷贝构造函数
e2 = e1; //赋值运算符
Empty *pe1 = &e1; //取址运算符(非const)
const Empty *pe2 = &e2; //取址运算符(const)
C++编译器对这些函数的实现,
inline Empty::Empty() //缺省构造函数
{
}
inline Empty::~Empty() //析构函数
{
}
inline Empty *Empty::operator&() //取址运算符(非const)
{
return this;
}
inline const Empty *Empty::operator&() const //取址运算符(const)
{
return this;
}
inline Empty::Empty(const Empty &rhs) //拷贝构造函数
{
//对类的非静态数据成员进行以"成员为单位"逐一拷贝构造
//固定类型的对象拷贝构造是从源对象到目标对象的"逐位"拷贝
}
inline Empty& Empty::operator=(const Empty &rhs) //赋值运算符
{
//对类的非静态数据成员进行以"成员为单位"逐一赋值
//固定类型的对象赋值是从源对象到目标对象的"逐位"赋值。
}
二、普通类
普通类的内存布局,成员变量按声明的顺序进行排列(类内偏移为0开始),成员函数不占内存空间,不过通常得考虑内存对齐(字节对齐)的因素。
class Base
{
int a;
int b;
public:
void CommonFunction();
};
这里不想花精力在内存对齐因素上,所以成员变量都设为int型。
三、单一继承(非虚继承)
可见以下几个方面:
1)虚函数指针在最前面的位置。
2)成员变量根据其继承和声明顺序依次放在后面。
3)在单一的继承中,被overwrite的虚函数在虚函数表中得到了更新。
代码(单一继承-非虚继承)实现如下,
#include <iostream>
#include <stdio.h>
#include <stdint.h>
using namespace std;
class Parent {
public:
int iparent;
Parent() :iparent(10) {}
virtual void f() { cout << "Parent::f()" << endl; }
virtual void g() { cout << "Parent::g()" << endl; }
virtual void h() { cout << "Parent::h()" << endl; }
};
class Child : public Parent {
public:
int ichild;
Child() :ichild(100) {}
virtual void f() { cout << "Child::f()" << endl; }
virtual void g_child() { cout << "Child::g_child()" << endl; }
virtual void h_child() { cout << "Child::h_child()" << endl; }
};
class GrandChild : public Child {
public:
int igrandchild;
GrandChild() :igrandchild(1000) {}
virtual void f() { cout << "GrandChild::f()" << endl; }
virtual void g_child() { cout << "GrandChild::g_child()" << endl; }
virtual void h_grandchild() { cout << "GrandChild::h_grandchild()" << endl; }
};
//https://blog.youkuaiyun.com/a3192048/article/details/82259966
//typedef void(*Fun)(void);
// https://blog.youkuaiyun.com/u014221279/article/details/50978204
// 需要知道:对于一个函数glFun(),它的函数名 glFun 其实是一个指针,指向该函数在内存中存的地方
// typedef 返回类型(*新类型)(参数表)
// typedef的功能是定义新的类型。第一句就是定义了一种 Fun 的类型,
// 并定义这种类型为指向某种函数的指针,这种函数以一个void为参数并返回void类型。
// intptr_t是为了跨平台,其长度总是所在平台的位数,所以用来存放地址。
#define intptr_t long long
int main()
{
typedef void(*Fun)(void);
GrandChild gc;
Fun pFun = NULL;
// 运行的电脑是64位的,因此指针是64位的, intptr_t*其实就是long long*
printf("%p , %x\n", &gc, &gc); // %p输出完整的地址,以16进制输出时,会自动省略前面的0
// 在64位系统下, intptr_t*指针占8个字节,64位
cout << &gc << " <- 指针指向的地址,也是虚表指针地址" << endl;
// 但是int类型只占4个字节,32位
cout << &gc.iparent << " <- 变量iparent地址" << endl;
cout << &gc.ichild << " <- 变量ichild地址" << endl;
cout << &gc.igrandchild << " <- 变量igrandchild地址" << endl;
cout << endl;
intptr_t *Vptr = (intptr_t *)&gc; // Vptr就为指针的起始地址
cout << "[0] GrandChild::_vptr-> " << Vptr << endl;
intptr_t *pVtab = (intptr_t*) *(Vptr + 0); //*(Vptr + 0), 解引用后为 虚表指针指向的值,也就是虚函数表地址
for (int i = 0; ; i++){
pFun = (Fun)(*(pVtab + i)); //虚函数地址的指针(pVtab + i),指向的地址*(pVtab + i)
// pFun = (Fun)(pVtab[i]); // pFun = (Fun)(*(pVtab + i)) 的另一种写法
cout << " [" << i << "] ";
if (pFun == NULL){
cout << pFun << endl;
break;
}
pFun();
}
//在64位系统下,对于 intptr_t*类型的指针,intptr_t为long long,进行+1后,增加的是8个字节
// 但是int类型是4个字节的,所以在 虚表指针地址+1后,将指针转换为 int*,这样+1的时候,增加的是4个字节
int* pV = (int*)(Vptr + 1);
cout << "[1] Parent.iparent = " << *(pV + 0) << " " << (pV + 0) << endl;
cout << "[2] Child.ichild = " << *(pV + 1) << " " << (pV + 1) << endl;
cout << "[3] GrandChild.igrandchild = " << *(pV + 2) << " " << (pV + 2) << endl;
cout << endl << endl;
// int* pVtab2 = (int*)&gc;
// intptr_t* pVtab3 = (intptr_t*)&gc;
// printf("%p\n%p\n", pVtab2, pVtab3);
// cout << "[0] GrandChild::_vptr = " << &pVtab2 << endl;
// cout << "[1] offset + 1 = " << &pVtab2[1] << " value:" << *(int*)(&pVtab2[1]) << endl;
// cout << "[2] offset + 2 = " << &pVtab2[2] << " value:" << *(int*)(&pVtab2[2]) << endl;
// cout << "[3] offset + 3 = " << &pVtab2[3] << endl;
return 0;
}
四、多重继承(非虚继承)
(注:图中有误:Derive::Base3中[1]应为Base3::g(),[2]应为Base3::h())
我们可以看到:
1) 每个父类都有自己的虚表指针。
2) 子类的成员函数被放到了第一个父类的虚表指针指向的地址中。
3) 内存布局中,其父类布局依次按声明顺序排列。
4) 每个父类的虚表中的f()函数都被overwrite成了子类的f()。这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
代码(多重继承-非虚继承)实现如下,
#include <iostream>
#include <stdio.h>
#include <stdint.h>
using namespace std;
class Base1 {
public:
int ibase1, int_a;
char char_b;
Base1() :ibase1(10), int_a(11), char_b('1') {}
virtual void f() { cout << "Base1::f()" << endl; }
virtual void g() { cout << "Base1::g()" << endl; }
virtual void h() { cout << "Base1::h()" << endl; }
};
class Base2 {
public:
int ibase2;
Base2() :ibase2(20) {}
virtual void f() { cout << "Base2::f()" << endl; }
virtual void g() { cout << "Base2::g()" << endl; }
virtual void h() { cout << "Base2::h()" << endl; }
};
class Base3 {
public:
int ibase3;
Base3() :ibase3(30) {}
virtual void f() { cout << "Base3::f()" << endl; }
virtual void g() { cout << "Base3::g()" << endl; }
virtual void h() { cout << "Base3::h()" << endl; }
};
class Derive : public Base1, public Base2, public Base3 {
public:
int iderive;
Derive() :iderive(100) {}
virtual void f() { cout << "Derive::f()" << endl; }
virtual void g1() { cout << "Derive::g1()" << endl; }
};
//https://blog.youkuaiyun.com/a3192048/article/details/82259966
int main()
{
typedef void(*Fun)(void);
Derive d;
Fun pFun = NULL;
intptr_t *Vptr = (intptr_t *)&d; // Vptr就为指针的起始地址
intptr_t *pVtab = NULL;
cout << "[0] Base1::_vptr-> " << Vptr + 0 << endl;
pVtab = (intptr_t*) *(Vptr + 0);
for (int i = 0; i < 4; i++){
pFun = (Fun)(pVtab[i]);
cout << " [" << i << "] ";
pFun();
}
int* pV = (int*)(Vptr + 1);
cout << "[1] Base1.ibase1 = " << *(pV + 0) << " " << (pV + 0) << endl;
cout << " Base1.int_a = " << *(pV + 1) << " " << (pV + 1) << endl;
cout << " Base1.char_b = " << *(pV + 2) - '0' << " " << (pV + 2) << endl;
cout << "[2] Base2::_vptr-> " << Vptr + 3 << endl;
// 这里为什么 Vptr + 3呢?这就涉及到内存对齐了,这里默认的对齐字节为8字节 https://www.cnblogs.com/zrtqsk/p/4371773.html
pVtab = (intptr_t*) *(Vptr + 3);
for (int i = 0; i < 3; i++){
pFun = (Fun)(pVtab[i]);
cout << " [" << i << "] ";
pFun();
}
cout << "[3] Base2.ibase2 = " << *(Vptr + 4) << " " << (Vptr + 4) << endl;
cout << "[4] Base3::_vptr-> " << Vptr + 5 << endl;
pVtab = (intptr_t*) *(Vptr + 5);
for (int i = 0; i < 3; i++){
pFun = (Fun)(pVtab[i]);
cout << " [" << i << "] ";
pFun();
}
pV = (int*)(Vptr + 6);
cout << "[5] Base3.ibase3 = " << *(pV + 0) << " " << (pV + 0) << endl;
cout << "[6] Derive.iderive = " << *(pV + 1) << " " << (pV + 1) << endl;
cout << sizeof(d) << endl << endl; // 输出 56
d.f();
d.g1();
// d.g(); // 没有重写,而且因为有多个父类都有g()函数,不能明确用哪个
Base3 *p = &d;
p->g();
// p->g1(); // Base3没有g1()
Base1 *pp = &d;
// pp->g1(); // Base1也没有g1(),子类新增加的,父类是不能调用的
return 0;
}
五、单一继承(虚继承)
六、重复继承(非虚继承)
重复继承,也叫菱形继承,或钻石继承。
运行在32位系统中,指针占4个字节。
1)其中第一个vfptr指向的虚表地址是B1和D共享的,因此其中的函数接口应该覆盖了B1和D的虚函数,而第二个则只有B2的虚函数。
2)从图中可以看出,在派生类D中,存在着两份基类B的成员变量ib和cb,一份是B1继承而来的,另一份是B2继承而来的,这样可能会出现二义性编译错误。我们可以指定类作用域符::进行限定来消除二义性:
D d;
d.ib = 0; //二义性错误
d.B1::ib = 1; //正确
d.B2::ib = 2; //正确
也可以在语言层面利用虚继承机制来解决。
七、重复继承(虚继承)
运行在32位系统中,指针占4个字节。
从上图可以看出,重复继承时用虚继承,派生类D对象在内存中占有56字节,比之前不用虚继承多了8个字节(之前为48字节),主要是因为多了“基类虚表指针”和“vtordisp域”。
注意和非虚多重继承不同的是,
1)第一个虚表指针指向的内容只记录了B1和D的虚函数(不包括B);
2)第二个虚标指针指向的内容只记录了B2和D的虚函数(不包括B);
3)B的虚函数在最后的基类虚表中;
4)B1和B2部分各增加了一个基类虚表指针vbptr,指向后面的基类。
从图中可以看出,VC++编译器在实现虚拟继承时,在派生类的对象中安插了两个vbptr指针。因此,对每个继承自虚基类的类实例,将增加一个隐藏的“基类虚表指针”(vbptr)成员变量,从而达到间接计算基类虚表位置的目的。该变量指向一个全类共享的偏移量表,表中项目记录了对于该类而言,“虚基类表指针”与虚基类之间的偏移量。
由上可以看出,B1虚基类表指针vbptr与虚基类B之间的偏移量是40字节,B2虚基类表指针vbptr与虚基类B之间的偏移量是24字节。第一项中-4的含义:表示的是vfptr和vbptr的距离,如果B1中没有虚函数的定义,这个地方就会是0。
注意到在虚拟继承的C++对象内存布局中,还有一个4个字节的vtordisp字段。如果虚继承中派生类重写了基类的虚函数,并且在构造函数或者析构函数中使用指向基类的指针调用了该函数,编译器会为虚基类添加vtordisp域。
八、改变虚函数表代码
代码如下,
#include <iostream>
#include <stdio.h>
#include <stdint.h>
using namespace std;
class animal
{
protected:
int age_;
animal(int age): age_(age) { }
public:
virtual void print_age(void) = 0;
virtual void print_kind() = 0;
virtual void print_status() = 0;
};
class dog : public animal
{
public:
dog(): animal(2) { }
~dog() { }
virtual void print_age(void) {
cout << "Woof, my age = " << age_ << endl;
}
virtual void print_kind() {
cout << "I'm a dog" << endl;
}
virtual void print_status() {
cout << "I'm barking" << endl;
}
};
class cat : public animal
{
public:
cat(): animal(1) { }
~cat() { }
virtual void print_age(void) {
cout << "Meow, my age = " << age_ << endl;
}
virtual void print_kind() {
cout << "I'm a cat" << endl;
}
virtual void print_status() {
cout << "I'm sleeping" << endl;
}
};
void print_random_message(void* something) {
cout << "I'm crazy" << endl;
}
//https://www.zhihu.com/question/29256578/answer/43725188
int main(void)
{
cat kitty;
dog puppy;
animal* pa = &kitty;
intptr_t* cat_vptr = *((intptr_t**)(&kitty));
intptr_t* dog_vptr = *((intptr_t**)(&puppy));
intptr_t fake_vtable[] = {
dog_vptr[0], // for dog::print_age
cat_vptr[1], // for cat::print_kind
(intptr_t) print_random_message
};
// ((intptr_t**) pa) 是虚表指针, *解引用后,为 虚函数表 地址
// 改变了指向的虚函数表
*((intptr_t**) pa) = fake_vtable;
pa->print_age(); // Woof, my age = 1
pa->print_kind(); // I'm a cat
pa->print_status(); // I'm crazy
return 0;
}
参考:
C++虚继承(一) --- vtordisp字段_H-KING的博客-优快云博客
vfptr和vbptr_temporg的博客-优快云博客_vfptr和vbptr
虚基类指针vbptr和虚函数指针vfptr_程序媛_婷的博客-优快云博客_虚基类指针
C++继承的内存布局_Waves___的博客-优快云博客_继承的内存布局
C++中对象的内存布局(三)_zhuoya_的博客-优快云博客
钻石型继承模型的内存分布_CodeWill的博客-优快云博客
C++ 虚函数表解析---陈皓改进版_啊大1号的博客-优快云博客
C++ 对象的内存布局(上)---陈皓改进版_啊大1号的博客-优快云博客
C++ 对象的内存布局(下)---陈皓改进版_啊大1号的博客-优快云博客
Have Fun