C++ 类在内存中的存储方式
空类
class Test {
};
Test t0;
cout << sizeof(t0) << endl;
// 运行结果:1
空类,没有任何成员变量和成员函数,编译器是支持空类实例化对象的,对象必须要被分配内存空间才有意义,这里编译器默认分配了 1Byte 内存空间(不同的编译器可能不同)
含有成员变量的类
// ====== 测试一 ======
class Test {
private:
int i; // 4 -> 4
char c; // 1 -> 4
double d; // 8 -> 8
};
Test t11;
cout << sizeof(t11) << endl;
// 运行结果:16 = 4+4+8
// ====== 测试二 ======
class A{};
class Test {
private:
int i; // 4 -> 4
char c; // 1 -> 4
double d; // 8 -> 8
A a; // 1 -> 8
};
Test t12;
cout << sizeof(t12) << endl;
// 运行结果:24 = 4+4+8+8
// ====== 测试三 ======
class A {
private:
double dd; // 8 -> 8
int ii; // 4 -> 8
int* pp; // x86: 4 x64: 8
};
class Test {
private:
int i; // 4 -> 8
A a; // 24 -> 24
double d; // 8 -> 8
char* p; // x86: 4 x64: 8
};
Test t13;
cout << sizeof(t13) << endl;
// x86 目标平台运行结果:40 = 8+(8+8+4)+8+4
// x64 目标平台下运行结果:48 = 8+(8+8+8)+8+8
解释:
-
这里的类的内存对齐原则与前面写的结构体的内存对齐原则是一样的
-
测试三中,32bit 目标平台寻址空间是 4Byte(32bit),所以指针是 4Byte 的;64bit 目标平台寻址空间是 8Byte(64bit),所以指针是 8Byte
-
另外,静态成员变量是在编译阶段就在静态区分配好内存的,所以静态成员变量的内存大小不计入类空间
含有成员变量和成员函数的类
// ====== 测试一 ======
class Test {
private:
int n; // 4 -> 4
char c; // 1 -> 2
short s; // 2 -> 2
};
Test t21;
cout << sizeof(t21) << endl;
// 运行结果:8
// ====== 测试二 ======
class Test {
public:
Test() {
}
int func0() {
return n;
}
friend int func1();
int func2() const {
return s;
}
inline void func3() {
cout << "inline function" << endl;
}
static void func4() {
cout << "static function" << endl;
}
~Test() {
}
private:
int n; // 4 -> 4
char c; // 1 -> 2
short s; // 2 -> 2
};
int func1() {
Test t;
return t.c;
}
Test t22;
cout << sizeof(t22) << endl;
// 运行结果:8
// ====== 测试三 ======
class Test {
public:
Test() {
}
int func0() {
return n;
}
friend int func1();
int func2() const {
return s;
}
inline void func3() {
cout << "inline function" << endl;
}
static void func4() {
cout << "static function" << endl;
}
virtual void func5() {
cout << "virtual function" << endl;
} // x86: 4 x64: 8
~Test() {
}
private:
int n; // 4 -> 4
char c; // 1 -> 2
short s; // 2 -> 2
};
int func1() {
Test t;
return t.c;
}
Test t23;
cout << sizeof(t23) << endl;
// x86 目标平台运行结果:12;x64 目标平台下运行结果:16
解释:
-
因 C++ 中成员函数和非成员函数都是存放在代码区的,故类中一般成员函数、友元函数,内联函数还是静态成员函数都不计入类的内存空间,测试一和测试二对比可证明这一点;
-
测试三中,因出现了虚函数,故类要维护一个指向虚函数表的指针,分别在 x86 目标平台和 x64 目标平台下编译运行的结果可证明这一点;
空类的派生类
// ====== 测试一 ======
class A {
};
class Test : public A{
private:
char c;
};
Test test;
cout << sizeof(test) << endl;
// 运行结果:1
// ====== 测试二 ======
class A {
};
class Test : public A{
private:
A a;
char c;
};
Test test;
cout << sizeof(test) << endl;
// 在MinGW 64bit下编译运行结果:3;在MSVC2017 64bit下编译运行结果:2
解释:
-
若基类是空类,C++ 标准允许派生类的第一个成员与基类共享地址,故此时的基类不占内存空间
-
若空类的派生类第一个数据成员是被继承的空类,此时的编译器就会给继承过来的基类分配 1Byte(不同的编译器可能不同) 的内存空间,因为 C++ 标准规定类型相同的对象地址必须不同
基类有数据成员的派生类
// ====== 测试一 ======
class A {
private:
double d;
char c;
};
class Test : public A{
private:
char c;
};
Test test;
cout << sizeof(test) << endl;
// 在 MinGW 64bit 下编译运行结果:16;在 MSVC2017 64bit 下编译运行结果:24
解释:
-
在 MinGW 64bit 下编译环境下,基类和派生类的数据成员是合在一起计算的内存大小,double(8Byte),char(1Byte),8 + 1 + 1 = 10Byte,补齐最终结果为 16Byte
-
在 MSVC2017 64bit 下编译环境下,基类和派生类的数据成员是先分开计算然后合在一起计算的内存大小,double(8Byte),char(1Byte),基类 8 + 1 = 9Byte,补齐为16Byte;派生类 16 + 1 = 17Byte,补齐最终结果为 24Byte
基类有虚函数的派生类
// ====== 测试一 ======
class A {
public:
virtual void func() {
cout << "class A virtual function" << endl;
}
private:
double d;
char c;
};
class Test : public A{
private:
char cc;
};
Test test;
cout << sizeof(test) << endl;
// 在MinGW 64bit下编译运行结果:24;在MSVC2017 64bit下编译运行结果:32
// ====== 测试二 ======
class A {
public:
virtual void func() {
cout << "class A virtual function" << endl;
}
private:
double d;
char c;
};
class Test : public A{
public:
virtual void func1() {
cout << "class Test virtual function" << endl;
}
private:
char cc;
};
Test test;
cout << sizeof(test) << endl;
// 在MinGW 64bit下编译运行结果:24;在MSVC2017 64bit下编译运行结果:32
解释:
-
测试一可看出派生类中继承了基类的虚表指针,内存的计算方式与 5 相同
-
测试二可看出派生类的虚函数和基类的虚函数共用一张虚函数表,故派生类中只有一个虚表指针
虚继承的派生类
// ====== 测试一 ======
class A {
private:
double d;
char c;
};
class Test : virtual public A{
private:
char cc;
};
Test test;
cout << sizeof(test) << endl;
// 运行结果:32
解释:
- 虚继承与虚函数一样同样是多了一个虚表的指针
总结
-
C++ 编译系统中,数据和函数是分开存放的(函数放在代码区;数据主要放在栈区和堆区,静态/全局区以及文字常量区也有),实例化不同对象时,只给数据分配空间,各个对象调用函数时都都跳转到(内联函数例外)找到函数在代码区的入口执行,可以节省拷贝多份代码的空间;
-
类的静态成员变量编译时被分配到静态/全局区,因此静态成员变量是属于类的,所有对象共用一份,不计入类的内存空间;
-
静态成员函数和非静态成员函数都是存放在代码区的,是属于类的,类可以直接调用静态成员函数,不可以直接调用非静态成员函数,两者主要的区别是有无 this 指针;
-
内联函数(声明和定义都要加 inline)也是存放在代码区,内联函数在被调用时,编译器会用内联函数的代码替换掉函数,避免了函数跳转和保护现场的开销(实际上到底替不替换还要由编译器决定,即使声明为内联函数也有可能不替换,未声明成内联函数也有可能被编译器替换到调用位置,主要由编译器决定);
-
派生类的内存大小 = 基类的内存空间(非静态成员函数 + 虚函数指针(若有虚函数)) + 派生类特有的非静态数据成员的内存空间 + 派生类虚指针(在基类没有虚函数,且派生类有特有的虚函数或虚继承的条件下)