从 sizeof 出发,把"类到底占多少内存"审个水落石出:空类为什么是 1 字节、成员函数为什么不要钱、虚函数又凭什么让大小暴涨——每个字节都要有交代
核心方法论:铁面无私
「本官断案,只认一样东西——证据。这一章的证物就是 sizeof:空类到底多大?加了函数变不变?加了虚函数又为什么暴涨?不看教科书,把代码写出来、编译、运行,sizeof 一锤定音,数字不会说谎。每个字节的来龙去脉,本官都要审个水落石出。」
先亮出本章的第一个问题:一个什么都没有的类(空类),占多少内存?直觉上应该是 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 在对象里:它只有一份,在静态存储区,跟对象大小毫无关系。
上一节三种空壳一视同仁,都是 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 扩展) | 0 | C 没有"对象地址唯一"的规则 |
审案要点:占位字节是 C++ 的"法律要求",不是编译器的个人爱好。把 struct A {} 换成 class A {} 或 union A {},结果纹丝不动。会背 1 字节不算本事,能讲出"为什么"才算过关。
注意:跨语言边界要小心。把 C++ 的空结构体交给 C 编译器,可能连编译都过不去(标准 C 不允许空结构体),过了也是 0 字节;反过来,C 代码假设空壳是 0,用 C++ 编译就变成 1——同一个"空壳",两种语言,两种大小,别混着用。
空类只有 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 (*)())是两种东西,语法、大小、用法都不同,别混用。
现在给类加一个虚函数,戏剧性的一幕出现了: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(同一张虚表!)
| 类 | 内容 | sizeof | vptr? |
|---|---|---|---|
class C {} | 空 | 1 | 无 |
class C { 一堆函数 } | 只有成员函数 | 1 | 无(函数不占对象空间) |
class C { virtual void test(); } | 一个虚函数 | 8 | 有(类开头 8 字节) |
class VInt { virtual; int a; } | 虚函数 + int | 16 | 有(vptr 8 + int 4 + 填充 4) |
把 12.1 到 12.4 连起来看,这条曲线就是多态的全部成本:空类 1 字节 → 加函数还是 1 字节 → 加一个 virtual 变 8 字节。vptr 是编译器偷偷塞进来的隐藏成员——你看不见它,但它真实存在,还占着类最前面 8 个字节。这就是"多态不是免费的"的字面意思:每个对象都要为"运行时才知道调哪个函数"预付一个指针。
注意:有虚函数的类不再是标准布局(standard-layout)类型,对它用 offsetof 是未定义行为;上面用 reinterpret_cast 硬读 vptr 只是教学实验,生产代码千万别依赖这种技巧——不同编译器、不同 ABI 的 vptr 布局可能不同。
第 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 : vptr(8) <- 编译器自动塞入的隐藏成员,指向虚表
偏移 8 : int a(4)
偏移 12 : 填充(4) @padding 对齐到 8 的倍数
总计 : sizeof(VInt) = 16
| 类声明 | 成员排布 | sizeof | padding |
|---|---|---|---|
Config | char + int | 8 | 3 字节 |
Config2 | char + int + char | 12 | 6 字节 |
Config3 | int + char + char(重排) | 8 | 2 字节 |
VInt | vptr + int | 16 | 4 字节 |
把本章所有证据串成一条总公式:类的大小 = 隐藏的 vptr(有虚函数才有)+ 成员变量(按第 10 章对齐规则排布)+ padding。成员函数、静态成员都不进来。以后拿到任意一个类,先数虚函数定 vptr,再按对齐规则排成员,手算的 sizeof 和编译器对账——对不上,说明你漏了规矩。
注意:带虚函数的类,第一个成员不在偏移 0——vptr 占了头 8 字节。写序列化、网络协议、跨进程共享结构时,类里藏一个虚函数等于埋雷:你以为 level 在偏移 0,实际它在偏移 8,字节流一错位全盘皆输。协议结构体要么不带虚函数,要么用第 11 章的手法 pack + static_assert 钉死。
包青天式的收尾:把本堂审案的证据链摆齐。起点是空类的 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 1 → 8 —— 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 的成员排布也可能有差异。请务必亲手编译运行本章的每一个示例,以你自己机器上的输出为准。
判断下列说法是否正确: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。
一个类只有构造函数、析构函数、普通成员函数和静态成员函数,它的 sizeof 是多少?如果在这个类里再加一个 virtual void test();,sizeof 变成多少?分别说明理由。
先想"成员函数住哪"(12.3),再想"虚函数会引入什么隐藏成员"(12.4)。
加函数前是 1(成员函数不占对象空间,只有空类的占位字节)。加虚函数后变成 8(本机 64 位):虚函数让编译器在类开头塞进一个 vptr(指向虚函数表的指针),一个指针正好 8 字节。如果你在 32 位平台上编译,答案是 4。
在 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"的实例。
写一个程序完成三组取证:① 实例化 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 一锤定音。