第12章:类的大小:空类、成员函数与虚函数指针

从 sizeof 出发,把"类到底占多少内存"审个水落石出:空类为什么是 1 字节、成员函数为什么不要钱、虚函数又凭什么让大小暴涨——每个字节都要有交代

⚖️

本章导师:包青天

核心方法论:铁面无私

「本官断案,只认一样东西——证据。这一章的证物就是 sizeof:空类到底多大?加了函数变不变?加了虚函数又为什么暴涨?不看教科书,把代码写出来、编译、运行,sizeof 一锤定音,数字不会说谎。每个字节的来龙去脉,本官都要审个水落石出。」

12.1 空类为什么是 1 字节:不同对象必须有不同地址

先亮出本章的第一个问题:一个什么都没有的类(空类),占多少内存?直觉上应该是 0?答案是否定的:在 C++ 里,空类(class)、空结构体(struct)、空联合体(union)的内存大小都是 1,而不是 0。

原因就一句话,这是 C++ 标准定的规矩:不同的对象必须拥有不同的地址。如果空类大小是 0,实例化两个空对象时,它们在内存地址上就无法区分——"谁是谁"根本说不清。所以编译器往往会给空类隐含地加一个字节做占位符,让每个实例都拿到独一无二的地址。换句话说:那 1 个字节不是数据,是"存在感"。下面用素材里的经典三件套实测(本机 macOS 64 位 / Apple Clang 21):

// empty01.cpp —— 空类 / 空结构体 / 空联合体:sizeof 都是多少?
#include <iostream>
#define PR(x) std::cout << #x << ":" << x << std::endl;

struct A {};                    // 空结构体
class C { static int a; };   // 空类:只有一个静态成员声明
union B {};                    // 空联合体

int main()
{
    PR(sizeof(A));
    PR(sizeof(B));
    PR(sizeof(C));
    return 0;
}
// 本机真实输出:sizeof(A):1  sizeof(B):1  sizeof(C):1

三个 1。注意 C 里明明写了一个 static int a;——为什么还是 1?因为静态成员变量不属于任何对象:它只有一份,住在静态存储区,实例化一百个对象也不会有第二份。所以"有没有静态成员"不影响对象大小。接下来验证"地址唯一"这条铁律——两个空对象、三个空对象的数组,地址怎么排:

// empty02.cpp —— 空对象的地址:一个萝卜一个坑
#include <cstdio>

struct A {};

int main()
{
    A x, y;                     // 两个空对象
    printf("&x = %p\n", (void*)&x);
    printf("&y = %p\n", (void*)&y);

    A arr[3];                     // 空类数组:每个元素地址必须不同
    for (int i = 0; i < 3; i++)
        printf("&arr[%d] = %p\n", i, (void*)&arr[i]);
    printf("元素间隔 = %zu 字节\n", (char*)&arr[1] - (char*)&arr[0]);
    return 0;
}
// 本机真实输出:&x = 0x16f0160cb  &y = 0x16f0160ca
// &arr[0..2] = 0x16f0160c7 / 0x16f0160c8 / 0x16f0160c9
// 元素间隔 = 1 字节(每个空对象都实打实占着那 1 字节)
对象声明sizeof为什么
空结构体struct A {}1占位字节,保证对象地址唯一
空类(带静态成员)class C { static int a; }1静态成员不属于对象,类仍是空的
空联合体union B {}1同样占位 1 字节
嵌套空成员struct Wrap { A a; }1占位字节就是那 1 字节
包青天提示

标准只规定"不同对象必须拥有不同的地址",没规定必须用 1 字节实现——几乎所有编译器都用 1 字节,你只需要记住:sizeof(空类) 不为 0。面试被问"空类多大",先答 1,再把"地址唯一"这条标准原话背出来,一锤定音。

注意:别把空类当"零成本"。上面 arr[3] 实打实占了 3 字节——数组里每个空对象都要消耗它那 1 个占位字节std::vector<空类> 存一万个元素就是一万字节。另外别以为 static int a 在对象里:它只有一份,在静态存储区,跟对象大小毫无关系。

12.2 空结构体与空联合体:同样 1 字节

上一节三种空壳一视同仁,都是 1 字节。再深挖一层:在 C++ 里,struct、class、union 只是"关键字不同"——struct 与 class 唯一的差别是成员默认访问权限(struct 默认 public,class 默认 private),内存规则完全一致;union 特殊在"成员共享内存",但空 union 没有成员,同样落到 1 字节占位。一句话:占位字节不看你是哪种壳,只看你是不是空的

但同样的问题放到 C 语言里,答案截然不同:C 语言中空结构体、空联合体的内存大小为 0(标准 C 根本不允许空结构体——能编译是 GNU 扩展在网开一面)。为什么 C 可以 0?因为"不同对象必须拥有不同地址"是 C++ 的规则,C 语言没这条规矩。下面是 C 与 C++ 的对照实测:

// empty03.cpp —— 空结构体当成员 / 塞进 union,逐一实测
#include <cstdio>

struct A {};
union U { A a; int x; };       // 空结构体塞进联合体
struct Wrap { A a; };            // 空结构体当成员

int main()
{
    printf("sizeof(A)   = %zu\n", sizeof(A));
    printf("sizeof(Wrap)= %zu\n", sizeof(Wrap));
    printf("sizeof(U)   = %zu\n", sizeof(U));   // 最大成员 int 决定(第 11 章规则)
    return 0;
}
// 本机真实输出:sizeof(A) = 1  sizeof(Wrap) = 1  sizeof(U) = 4
// empty_c.c —— 同一个"空壳",C 语言里是 0(GNU 扩展)
#include <stdio.h>

struct A {};
union B {};

int main()
{
    printf("sizeof(A) = %zu\n", sizeof(struct A));
    printf("sizeof(B) = %zu\n", sizeof(union B));
    return 0;
}
// 本机真实输出(gcc -std=gnu11 编译):sizeof(A) = 0  sizeof(B) = 0
场景sizeof说人话
C++ 空结构体 struct A {}1占位字节,对象地址必须唯一
C++ 空联合体 union B {}1没有成员,同样占位 1 字节
C++ 嵌套 struct Wrap { A a; }1占位字节就是那 1 字节
C++ union { A a; int x; }4最大成员 int 决定(第 11 章规则)
C 语言空结构体 / 空联合体(GNU 扩展)0C 没有"对象地址唯一"的规则
包青天提示

审案要点:占位字节是 C++ 的"法律要求",不是编译器的个人爱好。把 struct A {} 换成 class A {}union A {},结果纹丝不动。会背 1 字节不算本事,能讲出"为什么"才算过关。

注意:跨语言边界要小心。把 C++ 的空结构体交给 C 编译器,可能连编译都过不去(标准 C 不允许空结构体),过了也是 0 字节;反过来,C 代码假设空壳是 0,用 C++ 编译就变成 1——同一个"空壳",两种语言,两种大小,别混着用

12.3 加成员函数:大小不变(函数住在代码段)

空类只有 1 字节,那往里面加函数呢?答案可能让新手意外:加普通函数、构造函数、析构函数、静态函数、const 函数——sizeof 纹丝不动,还是 1。原因是一个贯穿全章的根本事实:成员函数不占用这个类的内存空间。对象里装的只有数据(成员变量),函数是"类"这个集体户口,不随对象搬家。

那函数住哪?住在代码段(text segment,可执行文件里存放机器指令的区域)——每个函数在编译产物里只有一份拷贝,一万个对象调用的都是同一份代码。所谓"调用 obj.sum()",本质是编译器把 obj 的地址作为隐藏参数 this 传进去,跳到那份唯一的代码执行。用素材里的经典"函数全家桶"实测:

// methods01.cpp —— 塞满成员函数,sizeof 纹丝不动
#include <cstdio>

class C
{
public:
    C()          { /* 构造函数 */ }
    ~C()         { /* 析构函数 */ }
    int sum()    { int a = 10; return a; }
    static void add() { /* 静态函数:连 this 都没有 */ }
    const int getA() { return 10; }
};

int main()
{
    printf("sizeof(C) = %zu\n", sizeof(C));   // 还是 1!
    C obj;
    printf("obj.sum() = %d, obj.getA() = %d\n", obj.sum(), obj.getA());
    return 0;
}
// 本机真实输出:sizeof(C) = 1  obj.sum() = 10  obj.getA() = 10

构造函数、析构函数、普通成员函数、静态成员函数、const 成员函数——五路大军齐上阵,sizeof(C) 依然是 1。为了把"函数住在代码段"钉死,再做一个取证实验:读出两个成员函数的地址,它们应落在代码段(远离栈上的对象):

// methods02.cpp —— 函数地址取证:一份代码,万人共享
#include <cstdio>
#include <cstring>

class C
{
public:
    int sum()    { return 10; }
    const int getA() { return 10; }
};

int main()
{
    C obj1, obj2;
    int (C::*pmf_sum)()  = &C::sum;    // 成员函数指针
    const int (C::*pmf_getA)() = &C::getA;
    void* p1 = nullptr, *p2 = nullptr;
    memcpy(&p1, &pmf_sum,  sizeof(p1));  // Itanium ABI 上等于 void* 大小
    memcpy(&p2, &pmf_getA, sizeof(p2));
    printf("&C::sum  = %p(代码段地址)\n", p1);
    printf("&C::getA = %p(代码段地址)\n", p2);
    printf("obj1.sum() = %d, obj2.sum() = %d\n", obj1.sum(), obj2.sum());
    return 0;
}
// 本机真实输出:
// &C::sum = 0x10001c6c4(代码段地址)  &C::getA = 0x10001c6e0(代码段地址)
// obj1.sum() = 10, obj2.sum() = 10
加进空类的"东西"sizeof 变化原因
普通成员函数不变(1)函数代码在代码段,不属于对象
构造函数 / 析构函数不变(1)同上,调用时才执行
静态成员函数不变(1)静态函数连 this 都没有,更不占对象空间
const 成员函数不变(1)只是签名里的承诺,不产生任何成员
静态成员变量不变(1)静态变量全局只有一份,不在对象里
包青天提示

这一课是对象模型的第一块基石:对象 = 数据(成员变量);类 = 数据 + 行为(成员函数)。函数不进对象,是因为"行为"是全班共享的——一万个对象不需要一万份 sum(),一份就够了,大家轮流把 this 递进去用。想通这一点,后面虚函数表、多态的机制就顺了。

注意:"成员函数不占空间"有个著名的例外——虚成员函数。一旦函数前加上 virtual,情况立刻反转,下一节就是这场反转的现场。另外留意:成员函数指针(int (C::*)())和普通函数指针(void (*)())是两种东西,语法、大小、用法都不同,别混用。

12.4 加虚函数:vptr 出现了(8 字节的隐藏成员)

现在给类加一个虚函数,戏剧性的一幕出现了:sizeof 从 1 直接跳到 8(本机 64 位系统实测)。原因:虚函数会引入一个隐藏成员——虚表指针(vptr,virtual table pointer,指向虚函数表的指针)。虚函数表(vtable,virtual table,存放这个类所有虚函数地址的表)不定义在类里,类定义时只是在类的开头加了一个指向虚函数表的指针,而不是塞一个"虚函数数组"。所以每次实例化一个带虚函数的对象,都要多付一个指针的代价——64 位系统上一个指针是 8 字节。

几个必须审清的细节:vptr 布局在类的开头(偏移 0);编译器在构造函数中创建/初始化这个 vptr,让它指向本类的虚函数表;这个类产生多个对象时,所有对象共用同一张虚函数表——表只有一份,每个对象只存一个指向它的指针;纯虚函数同理(抽象类照样带 vptr)。先复现素材里的"虚函数空类":

// vptr01.cpp —— 加一个虚函数,sizeof 从 1 跳到 8
#include <cstdio>

class C
{
public:
    C()  { /* 构造函数 */ }
    ~C() { /* 析构函数 */ }
    virtual void test() {}   // 一个虚函数:编译器偷偷塞进一个 vptr
};

int main()
{
    printf("sizeof(C) = %zu\n", sizeof(C));
    return 0;
}
// 本机真实输出:sizeof(C) = 8  (64 位系统,一个指针 = 8 字节)

再看 vptr 到底站在哪、以及"共享虚表"怎么取证。给类加一个 int a 成员,量出它的偏移:如果 a 不在偏移 0 而在偏移 8,说明前 8 字节被 vptr 占了;再创建两个对象,读出各自的 vptr 值,如果两个值相同,说明它们指向同一张虚表:

// vptr02.cpp —— vptr 站在类开头,所有对象共享同一张虚表
#include <cstdio>

class VInt
{
public:
    virtual void run() {}
    int a;                     // 虚函数 + 一个 int
};

int main()
{
    printf("sizeof(VInt) = %zu\n", sizeof(VInt));

    VInt vi;
    char* base = reinterpret_cast<char*>(&vi);
    printf("对象地址   = %p\n", (void*)base);
    printf("成员 a 偏移 = %zu 字节(前 8 字节是 vptr)\n",
           reinterpret_cast<char*>(&vi.a) - base);

    VInt v1, v2;                // 两个对象,读出各自的 vptr
    void** vt1 = *(void***)&v1;
    void** vt2 = *(void***)&v2;
    printf("v1 的 vptr 指向 %p\n", (void*)vt1);
    printf("v2 的 vptr 指向 %p\n", (void*)vt2);
    return 0;
}
// 本机真实输出:
// sizeof(VInt) = 16
// 对象地址   = 0x16bcf6098   成员 a 偏移 = 8 字节(前 8 字节是 vptr)
// v1 的 vptr 指向 0x10410c028  v2 的 vptr 指向 0x10410c028(同一张虚表!)
内容sizeofvptr?
class C {}1
class C { 一堆函数 }只有成员函数1无(函数不占对象空间)
class C { virtual void test(); }一个虚函数8有(类开头 8 字节)
class VInt { virtual; int a; }虚函数 + int16有(vptr 8 + int 4 + 填充 4)
包青天提示

把 12.1 到 12.4 连起来看,这条曲线就是多态的全部成本:空类 1 字节 → 加函数还是 1 字节 → 加一个 virtual 变 8 字节。vptr 是编译器偷偷塞进来的隐藏成员——你看不见它,但它真实存在,还占着类最前面 8 个字节。这就是"多态不是免费的"的字面意思:每个对象都要为"运行时才知道调哪个函数"预付一个指针。

注意:有虚函数的类不再是标准布局(standard-layout)类型,对它用 offsetof 是未定义行为;上面用 reinterpret_cast 硬读 vptr 只是教学实验,生产代码千万别依赖这种技巧——不同编译器、不同 ABI 的 vptr 布局可能不同。

12.5 回扣第 10 章:类也要对齐

第 10 章我们审过结构体的对齐:sizeof 不是成员之和,padding 藏在中间或尾巴。现在把结论搬进类——在成员布局这件事上,class 和 struct 用的是同一把尺子。看三个成员一模一样、顺序不同的类,sizeof 天差地别(本机实测):

// align01.cpp —— class 的对齐:和 struct 用的是同一把尺子
#include <cstdio>

class Config  { public: char level; int timeout; };      // char + int
class Config2 { public: char level; int timeout; char mode; }; // char + int + char
class Config3 { public: int timeout; char level; char mode; }; // 重排:int 在前

int main()
{
    printf("sizeof(Config)  = %zu\n", sizeof(Config));
    printf("sizeof(Config2) = %zu\n", sizeof(Config2));
    printf("sizeof(Config3) = %zu\n", sizeof(Config3));
    return 0;
}
// 本机真实输出:
// sizeof(Config)  = 8    (char(1) + 填充3 + int(4))
// sizeof(Config2) = 12   (char(1) + 填充3 + int(4) + char(1) + 填充3)
// sizeof(Config3) = 8    (int(4) + char(1) + char(1) + 填充2)

和 struct 一模一样的剧情。更关键的是:vptr 也参与对齐排布。带虚函数的类,布局是"vptr 打头,成员变量按对齐规则往后排"。用地址差把偏移量一个个量出来,把 Config2 和 12.4 节的 VInt 并排审一遍:

// align02.cpp —— 用地址差把成员偏移量出来(含 vptr 的类)
#include <cstdio>

class Config2
{
public:
    char level;      // 1 字节
    int  timeout;    // 4 字节
    char mode;       // 1 字节
};

class VInt
{
public:
    virtual void run() {}
    int a;           // 虚函数 + int
};

int main()
{
    Config2 c2;
    char* b = reinterpret_cast<char*>(&c2);
    printf("Config2.level   偏移 = %zu\n", reinterpret_cast<char*>(&c2.level)   - b);
    printf("Config2.timeout 偏移 = %zu\n", reinterpret_cast<char*>(&c2.timeout) - b);
    printf("Config2.mode    偏移 = %zu\n", reinterpret_cast<char*>(&c2.mode)    - b);

    VInt vi;
    char* v = reinterpret_cast<char*>(&vi);
    printf("VInt 的 vptr 偏移 = 0\n");
    printf("VInt::a     偏移 = %zu(前面 8 字节是 vptr)\n",
           reinterpret_cast<char*>(&vi.a) - v);
    return 0;
}
// 本机真实输出:
// Config2.level = 0  Config2.timeout = 4  Config2.mode = 8
// VInt 的 vptr 偏移 = 0  VInt::a 偏移 = 8(前面 8 字节是 vptr)
# VInt 对象内部布局(64 位,本机实测,单位:字节)
偏移 0  : vptr8<- 编译器自动塞入的隐藏成员,指向虚表
偏移 8  : int a4)
偏移 12 : 填充(4@padding 对齐到 8 的倍数
总计     : sizeof(VInt) = 16
类声明成员排布sizeofpadding
Configchar + int83 字节
Config2char + int + char126 字节
Config3int + char + char(重排)82 字节
VIntvptr + int164 字节
包青天提示

把本章所有证据串成一条总公式:类的大小 = 隐藏的 vptr(有虚函数才有)+ 成员变量(按第 10 章对齐规则排布)+ padding。成员函数、静态成员都不进来。以后拿到任意一个类,先数虚函数定 vptr,再按对齐规则排成员,手算的 sizeof 和编译器对账——对不上,说明你漏了规矩。

注意:带虚函数的类,第一个成员不在偏移 0——vptr 占了头 8 字节。写序列化、网络协议、跨进程共享结构时,类里藏一个虚函数等于埋雷:你以为 level 在偏移 0,实际它在偏移 8,字节流一错位全盘皆输。协议结构体要么不带虚函数,要么用第 11 章的手法 pack + static_assert 钉死。

12.6 章节小结与悬念:继承之后内存怎么排?

包青天式的收尾:把本堂审案的证据链摆齐。起点是空类的 1 字节占位(对象地址必须唯一);接着证明成员函数不占对象空间(函数住在代码段);然后虚函数登场,vptr 在类开头出现,一加就是 8 字节,所有对象共享同一张虚表;最后回扣第 10 章——class 与 struct 用同一把对齐尺子,vptr 也参与排布。五步走完,"类多大"不再是一道背数字的题,而是一道推理题

最后一个必须埋的钩子:本章审的全是"单打独斗"的类。那么继承之后,内存怎么排?子类的成员是接在父类后面,还是另起炉灶?父类的 vptr 子类继承吗——还是子类生成自己的虚表?多态调用 son.print() 时,CPU 是怎么通过 vptr 找到正确函数的?预告一个实验(本机实测):

// inherit_preview.cpp —— 第 13 章预告:继承之后内存怎么排?
#include <cstdio>

class Base
{
public:
    int a;
private:
    int b;
    virtual void print() { printf("Base\n"); }
};

class Son : public Base
{
public:
    int c;
    void print() { printf("Son\n"); }
};

int main()
{
    Son son;
    son.print();
    printf("sizeof(Base) = %zu\n", sizeof(Base));
    printf("sizeof(Son)  = %zu\n", sizeof(Son));
    return 0;
}
// 本机真实输出:
// Son
// sizeof(Base) = 16  (vptr 8 + int a 4 + int b 4,正好对齐)
// sizeof(Son)  = 24  (Base 16 + int c 4 + 填充 4)
# 本章知识地图(包青天审案记录)
12.1 空类:  sizeof = 1 —— 不同对象必须有不同地址(占位字节)
12.2 空壳:  struct / union 同为 1,C 语言里是 0(GNU 扩展)
12.3 函数:  成员函数不占对象空间 —— 函数代码住在代码段
12.4 虚函数:sizeof 18 —— vptr 出现在类开头,指向共享虚表
12.5 对齐:  class 与 struct 同一把尺子 —— padding 照塞,vptr 也参与
12.6 悬念:  继承之后内存怎么排?->13
小节一句话核心带走的能力
12.1 空类占位 1 字节,保证对象地址唯一能解释 sizeof(空类) = 1 的标准依据
12.2 空壳struct / union 同样 1 字节,C 里是 0知道 C / C++ 在这个问题上的差别
12.3 成员函数函数不进对象,代码在代码段建立"对象 = 数据"的对象模型直觉
12.4 虚函数vptr 8 字节,在类开头,指向共享虚表能解释多态的内存成本
12.5 对齐class 与 struct 同规则,vptr 也参与会手算带虚函数的类的大小
12.6 悬念继承之后内存怎么排?带着问题进第 13 章
包青天提示

本官的断案口诀只有八个字:sizeof 一锤定音。别人问你某个类多大,别背数字、别猜——把类写进程序,printf("%zu", sizeof(类)) 跑一遍,数字自己会说话。本章所有结论(空类 1、函数不加量、虚函数加 8、对齐照排)你都可以在十分钟内亲手复现,这就是铁面无私的力量:不靠权威,靠验证。

注意:本章所有 sizeof 数字都是本机(macOS 64 位,Apple Clang 21)编译运行的真实输出,素材核心结论原样保留。换环境数字可能不同:32 位平台上指针是 4 字节(vptr 就是 4),MSVC 与 Itanium ABI 的成员排布也可能有差异。请务必亲手编译运行本章的每一个示例,以你自己机器上的输出为准。

章末练习

练习 1:概念判断题 入门

判断下列说法是否正确:A. sizeof(空类) 在 C++ 中等于 0;B. 空类占 1 字节是为了保证不同对象拥有不同地址;C. 静态成员变量会撑大对象的大小;D. 空联合体也是 1 字节。

提示

回顾 12.1 节的"三个 1"实测和那段标准原话;再想想静态成员住在哪、属于谁。

参考答案

B、D 正确;A、C 错误。A:C++ 规定不同对象必须有不同地址,空类占 1 字节(0 是 C 语言 GNU 扩展里的行为)。C:静态成员变量全局只有一份,住在静态存储区,不占任何对象的空间——class C { static int a; } 实测 sizeof 仍是 1。

练习 2:预测 sizeof 的变化 进阶

一个类只有构造函数、析构函数、普通成员函数和静态成员函数,它的 sizeof 是多少?如果在这个类里再加一个 virtual void test();,sizeof 变成多少?分别说明理由。

提示

先想"成员函数住哪"(12.3),再想"虚函数会引入什么隐藏成员"(12.4)。

参考答案

加函数前是 1(成员函数不占对象空间,只有空类的占位字节)。加虚函数后变成 8(本机 64 位):虚函数让编译器在类开头塞进一个 vptr(指向虚函数表的指针),一个指针正好 8 字节。如果你在 32 位平台上编译,答案是 4。

练习 3:手算类的大小 进阶

在 64 位系统上,class Foo { char c; int n; char d; }; 的 sizeof 是多少?各成员的偏移分别是多少?如果给 Foo 加一个 virtual void f();,sizeof 又变成多少?

提示

先按第 10 章的对齐规则排 char + int + char,再用 12.5 节的总公式:类大小 = vptr(有虚函数时)+ 成员 + padding。

参考答案

不加虚函数:char c(偏移 0)+ 填充 3 + int n(偏移 4)+ char d(偏移 8)+ 填充 3 = 12。加了虚函数:vptr(偏移 0,8 字节)+ char c(偏移 8)+ 填充 3 + int n(偏移 12)+ char d(偏移 16)+ 填充 7 = 24(对齐到 8 的倍数)。注意第一个成员被挤到偏移 8——这正是 12.5 节 warning 里"带虚函数的类第一个成员不在偏移 0"的实例。

练习 4:亲手审案 挑战

写一个程序完成三组取证:① 实例化 3 个空类对象组成数组,打印地址,验证元素间隔是 1 字节;② 定义一个带虚函数的类(含一个 int 成员),打印 sizeof 和 int 成员的偏移,确认 vptr 在偏移 0;③ 把本章的 class 全部换成 struct 重跑一遍,确认结果一致。把每步输出记录下来,并解释为什么。

提示

① 用 (char*)&arr[1] - (char*)&arr[0] 量间隔;② 用 reinterpret_cast<char*> 减法量成员偏移;③ 直接全局替换 class 为 struct 再编译。

参考答案

① 间隔是 1——每个空对象都占满它的占位字节,地址不撞车;② sizeof = 8 + 4 对齐到 16,int 成员偏移 8——前 8 字节是 vptr,证明虚函数引入了隐藏成员;③ struct 与 class 结果完全一致——两者在成员布局上只有默认访问权限的差别,占位、vptr、对齐规则统统相同。三组取证全过,你就算把本章的案子审完了——记住包青天的话:sizeof 一锤定音