一、基本原则
一个 类对象在内存中的大小(sizeof(T))主要由以下因素决定:
- 成员变量的大小(内置类型、对象、指针等)。
- 对齐填充(padding) —— 为了满足 CPU 内存对齐要求,编译器会在成员之间和末尾加填充字节。
- 虚函数表指针(vptr) —— 如果类有虚函数,编译器会在对象中额外存一根指针(通常是 4 或 8 字节)。
- 继承 —— 父类成员 + 对齐规则。
- 空类特殊规则 —— 空类对象大小至少为 1 字节(保证对象能有唯一地址)。
- 类的成员函数不占用类对象的内存空间。
一个类对象的大小 = 所有非静态成员变量大小 + 对齐填充 + 继承带来的子对象大小 + 虚函数表指针开销(如果有)。
二、举例分析
普通类:
- 每个非静态数据成员都会占用空间。
- 静态数据成员不属于对象,而是存储在类的静态区,所以不影响对象大小。
class A {
int x; // 4字节
char y; // 1字节
};
x 需要 4 字节;y 需要 1 字节,但后面会对齐填充 3 字节,使整个对象大小对齐到 4 的倍数;总大小 = 8 字节。
含虚函数:
class B {
int x;
virtual void foo() {}
};
x 需要 4 字节;对象里有一个 虚函数表指针 vptr,在 64 位系统下一般是 8 字节。再加上对齐填充,总大小 = 16 字节。
继承:
class Base {
int a;
};
class Derived : public Base {
char b;
};
Base 占 4 字节;Derived 增加 1 字节的 b,但是要对齐成 4 的倍数;所以 总大小 = 8 字节。
空类:
class Empty {};
虽然类里啥都没有,但 C++ 要保证不同对象有唯一地址。所以空类对象大小 = 1 字节。
内存对齐:
- C++ 为了提高内存访问效率,通常会在成员之间插入 padding(填充字节),使得每个成员地址满足其对齐要求。
- 对象的大小还需要是 最大对齐量的整数倍。
struct B {
char a; // 1字节
int b; // 4字节
};
内存布局可能是:a (1字节) + 填充 (3字节) + b (4字节) = 总共 8 字节
三、成员变量的地址访问
在 C++ 中:
- 对象的地址(&obj)指向对象在内存中的起始位置。
- 成员变量的地址(&obj.member)指向该成员在对象内存布局中的位置。
由于成员变量在对象中是按一定的内存偏移量存放的,所以 成员变量的地址 = 对象的地址 + 偏移量。
示例:
#include <iostream>
using namespace std;
struct A {
int x; // 4字节
char y; // 1字节(可能有对齐填充)
double z;
};
int main() {
A a;
cout << "对象地址: " << &a << endl;
cout << "成员 x 地址: " << &a.x << endl;
cout << "成员 y 地址: " << (void*)&a.y << endl;
cout << "成员 z 地址: " << &a.z << endl;
return 0;
}
可能输出(具体依赖编译器和对齐方式):
四、类的静态成员变量
静态成员变量属于类,而不是对象:静态成员变量在程序的静态存储区(通常是数据段 .data 或 .bss)分配内存。所有对象共享同一份静态变量。因此,它不计入对象的大小。
#include <iostream>
using namespace std;
struct A {
int x;
static int s; // 静态成员
};
int A::s = 42; // 必须在类外定义和初始化
int main() {
A a1, a2;
cout << "sizeof(A): " << sizeof(A) << endl;
cout << "&a1: " << &a1 << endl;
cout << "&a2: " << &a2 << endl;
cout << "&A::s: " << &A::s << endl;
}
输出结果: