做开发这么多年,我发现自己对sizeof的态度经历了三个阶段:刚开始学C语言时觉得它就是个“求大小”的函数,用法记一下就完了;后来写代码多了,开始在各种诡异Bug里跟它打交道,才意识到以前很多理解是模糊的;再到现在,sizeof反而成了我在做协议解析、跨平台开发、内存优化时的一项基础工具,时不时要回来看一眼它的边界条件到底在哪。
网上关于sizeof的博客一抓一大把,但大多在堆结论——“数组名在sizeof里不退化”“结构体有对齐”“sizeof是运算符不是函数”,背完就忘,真到用的时候照样踩坑。这篇小结,我想把这些散落的点完整串一遍:它为什么不需要头文件、它到底在什么时候求值、数组参数退化和结构体对齐这些老大难问题背后的原理是什么,最后附上一份我在实际项目里一直在用的避坑清单。不管你是刚学C/C++的初学者,还是写了好几年程序想彻底理清这块知识的开发者,这篇内容应该都能帮你省下不少折腾的时间。
1. 先从“sizeof到底是函数还是运算符”说起
1.1 不需要头文件,这一条就暴露了它的真实身份
很多人第一次搜“sizeof函数需要头文件”时,心里默认它是个函数,所以觉得应该有对应的头文件。但这个问题本身就是个误解。sizeof不是函数,它永远不会要求你include任何头文件。
原因很简单:函数是一个运行期实体,你需要通过头文件拿到它的声明,编译器才能知道这个函数长什么样、参数怎么传、返回值是什么。而sizeof是C/C++语言内置的运算符,程序里根本没有一段叫做“sizeof”的机器指令。编译器在解析源码阶段就直接把它处理掉了,压根不走“声明—链接—调用”这条链路。换句话说,你不需要为sizeof引入任何头文件,就像你不会为加法运算符去包含一个头文件一样。
真有那么多人在搜这个问题,说明两件事:一是sizeof的书写形式太像函数了,二是编程教学里大家长期用“sizeof()”这种带括号的写法,把人的直觉带偏了。后面我会专门解释括号的规则。
1.2 括号的作用是“有条件的”,这决定了你写不写它
sizeof这个运算符在语法上有个非常有意思的规则:对类型名使用时必须加括号,对表达式使用时括号是可选的。
int a = 42; sizeof(int); // 类型名,必须加括号 sizeof a; // 表达式,括号可以省略,合法 sizeof(a); // 表达式,加括号也合法,日常推荐这个规则经常被人忽视,但理解它是有实际意义的。比如你看到一个宏定义#define ARRAY_LEN(arr) (sizeof(arr) / sizeof((arr)[0])),里面把数组元素写成sizeof((arr)[0]),多出的这对括号是为了防止宏参数展开时出现优先级问题。如果你不理解sizeof本身对表达式是允许不加括号的,就很难看懂为什么宏里要写这么多括号,更不敢自己写类似的宏。
另外还有一个运算符优先级的问题。sizeof的优先级高于算术运算符,所以sizeof a + b会被解析成(sizeof a) + b,而不是sizeof(a + b)。要是你想求两个变量之和的类型大小,必须明确写成sizeof(a + b)。这类“看着对、实际错”的表达式,在代码评审里我见过不止一次。
1.3 为什么它看起来像函数,以及返回类型是size_t
一个运算符为什么长得像函数?历史原因很简单:早期C语言教材普遍书写成“sizeof()”的样式,加上很多人在讲的时候都会说“sizeof返回大小”,这种表述方式让它在初学者眼里跟函数调用没区别。但真正写底层代码的老手都知道,它跟函数完全是两码事。
真正区分它们的方法也简单:看编译之后去哪找它。函数调用在汇编层面对应call指令,调用前需要压栈传参,执行完再从栈里取返回值;sizeof则没有任何运行期指令,编译结束它就消失了,你永远在可执行文件里找不到一个叫做“sizeof”的函数入口。
还有一个细节经常被忽略,就是sizeof的返回值类型是size_t,这是一个无符号整数类型,定义在<stddef.h>或C++的<cstddef>里。使用它时要注意两件事:
- 打印时用
%zu格式,不要用%d,否则在64位系统上可能因为类型不匹配打印出错误的值; - 因为它是无符号类型,不要直接拿它跟负数比较,
sizeof(arr) > -1这个表达式永远是false,编译器通常也会给警告。
从C++的角度看,sizeof还有一个特殊之处:它在模板元编程里经常作为编译期常量被使用。比如std::integral_constant<size_t, sizeof(T)>这种写法,配合模板特化可以在编译期根据类型大小分发不同的实现。这也是它作为运算符而非函数带来的优势——函数不可能在编译期参与类型计算,运算符可以。
2. 编译期求值:sizeof不会真的“运行”你的表达式
2.1 sizeof(i++)不增加i的值,这是我见过最神奇的入门实验
如果你刚接触C语言,一定要亲手跑一次下面这段代码:
#include <stdio.h> int main(void) { int i = 0; int n = sizeof(i++); printf("n = %d, i = %d\n", n, i); return 0; }多数人第一次看到输出时会愣住。按照“表达式会被求值”的直觉,i++应该把i变成1,但实际输出是n = 4, i = 0。
原因在于:sizeof只需要知道i++这个表达式的类型(int)就能确定大小为4,它根本不会真的去执行i++。语言规范里把这种场景称作“未求值操作数”——编译器只提取类型信息,忽略所有运行期副作用。这跟你在纸上计算“一堆苹果的总重量”时,并不会真的把苹果秤一遍是一个道理。
这个特性对新手来说很反直觉,但对老手来说是一道安全护城河。它保证了sizeof永远不会因为表达式里有函数调用而触发运行时开销或副作用,你可以放心把任意表达式塞进去,只需要关心它的类型。
需要注意的是,sizeof不会求值的规则同样适用于C++,但在C++中还有更复杂的地方:如果操作数是类类型,比如sizeof(MyClass()),编译器仍然不会调用构造函数,但会考虑这个类的完整定义。如果一个类只有前置声明而没有完整定义,sizeof是无法使用的,编译器会报“incomplete type”错误。这个点很重要,因为很多人在写跨文件代码时,以为把sizeof(不完整类型)放在函数里就能躲过检查,实际上是行不通的。
2.2 一个例外:变长数组的大小只能运行时确定
在C99之后,C语言支持变长数组(VLA),这时候sizeof的行为出现了一个边界案例:
int n = 10; int arr[n]; // VLA,长度依赖运行时的 n size_t s = sizeof(arr); // 无法在编译期确定,必须运行时计算也就是说,当一个数组的长度不是编译期常量时,sizeof不得不退化为一段运行时计算代码。这种情况下,sizeof确实有了“运行期执行”的味道,但它依然不是一个函数调用,而是编译器生成的、在栈上计算VLA字节数的指令。
不过,在C++标准里并不支持VLA,GCC和Clang只是作为扩展支持它,如果你想写出跨编译器、跨平台的代码,最好不要依赖这个特性。就算在纯C环境里,VLA也有栈溢出的风险,动态分配内存是更安全的选择。
2.3 编译期常量:为什么这个特性值得好好利用
因为sizeof在绝大多数情况下是编译期常量,它可以出现在那些“必须是编译期常量”的场合。最典型的是数组长度和C语言的case标签:
int arr[(int)sizeof(long)]; // 在64位平台展开为 int arr[8]; switch (x) { case sizeof(char): // 常量表达式,等于1 break; default: break; }这种用法在写跨平台代码时非常有用。比如你想写一个针对32位和64位指针不同行为的程序,普通的if在运行时判断、两个分支的代码都会被编译;而用sizeof配合预处理或者C++的模板特化,可以在编译期就把不必要的分支剔除掉,既省空间又省时间。
在C++的模板元编程里,sizeof更是常客。典型的std::aligned_storage、std::common_type这些工具的实现中,都大量使用了sizeof(T)来确定类型存储空间。它和decltype一样,是C++编译期类型计算的重要基石。
3. 数组、指针与参数退化——sizeof在哪一步悄悄变回指针大小
3.1 对数组名取sizeof:整段内存的大小,不只是首元素的
这是面试里最经典的sizeof题目,也是个很好的思维实验。假设有一个数组变量:
int arr[10]; printf("%zu\n", sizeof(arr)); // 通常是 40(10 * 4) printf("%zu\n", sizeof(arr[0])); // 4 printf("%zu\n", sizeof(arr) / sizeof(arr[0])); // 10sizeof(arr)返回的是整个数组占据的字节数,sizeof(arr[0])是单个元素大小,两者相除得到的就是元素个数。这是C语言里最通用的数组长度计算方式。
但这里有一个极其重要的前提:这个操作必须在数组变量还保有完整数组类型的地方进行。也就是说,在同一个函数内部、直接对数组名使用sizeof是可以的;一旦数组名作为函数参数被传递,情况就完全变了(下一节详述)。
二维数组的情况更值得展开:
int matrix[3][4]; printf("%zu\n", sizeof(matrix)); // 3*4*4 = 48 printf("%zu\n", sizeof(matrix[0])); // 一行的大小,4*4 = 16 printf("%zu\n", sizeof(matrix[0][0])); // 单个元素大小,4你既可以拿sizeof(matrix) / sizeof(matrix[0])算出行数3,也可以拿sizeof(matrix[0]) / sizeof(matrix[0][0])算出一行有4列。理解了数组的“嵌套”结构,多维数组的sizeof计算就有了统一的思路——永远是用整体大小除以当前维度一个元素的大小。
3.2 函数参数里的“数组”其实是指针,sizeof量出来永远是8/4
接着上面的问题,我们看一个堪称“经典陷阱”的例子:
#include <stdio.h> void print_size(int arr[]) // 等价于 int *arr { printf("%zu\n", sizeof(arr)); // 在64位系统上是8 } int main(void) { int data[10]; printf("%zu\n", sizeof(data)); // 40 print_size(data); // 8 return 0; }为什么同一个数组,在main里量出来是40,传进函数就变成8了?因为在C语言中,函数形参的数组语法只是语法糖。编译器在编译函数签名时,会把int arr[]自动调整为int *arr,也就是说,函数内部拿到的是一个指针,而不是数组本体。这个规则叫“数组参数退化”,是C语言为了效率和使用方便做的一个历史性设计。
想绕过这个坑,有几种做法,我整理成了一个表格:
| 方案 | 写法 | 原理 |
|---|---|---|
| 传长度参数 | void print_size(int arr[], size_t len) | 由调用者负责告知真实长度 |
| 传指向数组的指针 | void print_size(int (*arr)[10]) | 保留了完整数组类型,但只能匹配固定长度 |
| 用宏统计 | #define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0])) | 在调用点展开,数组还保有原始类型 |
| C++ 用模板推导 | template <size_t N> void f(int (&arr)[N]) | 引用参数保留数组类型 |
在C++里,模板引用是首选方案,它既能保留数组类型,又能自动推导长度,是最安全的写法。在纯C项目里,最朴素的“数组长度作为参数传递”反而是最不容易出错的,不要嫌麻烦。
3.3 指针的代价:你拿到的是地址,不是背后的数据
对应地,对指针变量本身求sizeof,得到的永远是指针大小,而不是它指向的那块内存的大小:
int *p = malloc(100 * sizeof(int)); printf("%zu\n", sizeof(p)); // 8(64位系统),不是400 char *s = "hello"; printf("%zu\n", sizeof(s)); // 8,不是6,末尾的 '\0' 不计入指针大小这其实是很多内存相关Bug的根源:你分配了一份内存,拿到一个指针,再把指针传来传去,任何sizeof都只能告诉你“这个指针变量占几个字节”,完全无法得知它指向的内存有多大。C语言在设计上就没有在动态内存块头部记录长度的机制(不同于某些托管语言),所以管理动态内存时,长度信息必须自己额外保存,否则必然吃亏。
我在做协议解析时经常遇到这种场景:收到一个缓冲区和长度,长度对不上就会越界。所以我的习惯是,一旦代码里出现sizeof(指针变量),就默认这是一个危险信号,要马上检查它前面是不是应该有一个显式的长度变量。
4. 结构体大小:sizeof面试题里最大的坑——内存对齐
4.1 为什么结构体大小不等于成员大小之和
假设你有这样的结构体:
struct A { char c; // 1字节 int i; // 4字节 char d; // 1字节 };直觉上成员大小相加是1+4+1=6,但在最常见的32位或64位平台上,sizeof(struct A)实际上是12。这不是计算错误,而是编译器在成员之间以及结构体末尾插入了填充字节,目的是让每个成员的地址满足“对齐”要求。
为什么要对齐?现代CPU在读取内存时,通常以CPU字长为单位进行访问,数据对齐在自然边界上时可以一次取完,不对齐则可能需要两次内存访问甚至触发总线错误。编译器选择的策略是:宁可浪费一点空间,也要让每个成员都落在“顺滑”的地址上。你可以类比成搬家工人推箱子进仓库:箱子不按托盘规格摆的话,每搬一箱都要额外调整方向,整体效率反而更低。
4.2 对齐规则与一个完整的计算实例
C/C++并没有在语言层面规定每一个平台的精确对齐数值,但绝大多数平台遵循“自然对齐”原则,可以概括为三条:
- 每个成员的起始偏移量必须是该成员对齐数的整数倍;
- 成员的对齐数通常是其自身大小与编译器默认对齐数中的较小值;
- 结构体的整体大小必须是全体成员中最大对齐数的整数倍。
看一个经典例子:
struct B { char a; // 偏移0,占1字节 double b; // 按8对齐,所以从偏移8开始,占8字节 int c; // 按4对齐,从偏移16开始,占4字节 };逐步计算一下偏移位置:
a放在偏移0,占1字节;b需要按8字节对齐,1不是8的倍数,所以编译器在a后面填充7个字节,b从偏移8开始占8字节,结束后偏移到16;c需要按4字节对齐,16正好是4的倍数,直接放,占4字节,到20;- 结构体最大对齐数是8,总大小必须是8的倍数,20向上取整到24。
所以sizeof(struct B)是24,而不是直觉的1+8+4=13。
更有意思的是,把成员顺序调换一下:
struct B2 { double b; // 偏移0,占8 int c; // 偏移8,占4 char a; // 偏移12,占1 };这次成员紧凑排列后总大小13,向上对齐到8的倍数,最终结果为16。同样的成员,仅仅因为排列顺序不同,结构体大小就从24变成16,节省了三分之一的空间。在大量对象常驻内存的程序里,这种优化带来的差异可能相当可观。所以我在写结构体时,通常会按照成员大小从大到小排列,让填充字节最少化。
4.3 #pragma pack与位域:需要手工控制布局的场景
默认对齐能提升访问效率,但在协议解析、文件格式解析、内存映射这类场景中,结构体布局必须和外部二进制格式完全一致,不允许编译器随意插入填充字节。这时可以用编译指令强制按1字节对齐:
#pragma pack(push, 1) struct FileHeader { char magic[4]; // 4字节 int version; // 4字节 short flags; // 2字节 }; #pragma pack(pop)pack(1)表示按1字节对齐,即不填充任何字节,sizeof(struct FileHeader)就等于4+4+2=10。这么做最大的好处是:可以直接把内存块映射成结构体指针,方便读取文件头。坏处也明显:结构体内部成员可能不再是自然对齐地址,在某些平台上访问未对齐成员会崩溃或严重降低性能。x86和ARM的行为就不一样,ARM有些内核配置下未对齐访问直接触发异常。所以我只在解析外部协议时使用pragma pack,不会在普通业务结构体上滥用。
位域(bit-field)是另一个让sizeof变得不直观的地方:
struct Flags { unsigned int a : 3; unsigned int b : 5; unsigned int c : 8; };你可能会认为3+5+8=16比特,应该占2字节。但位域的存储单元分配规则由编译器决定,常见的实现是:把位域放在一个unsigned int(4字节)存储单元里,全部位域放得下就只分配4字节。所以sizeof(struct Flags)可能是4而不是2。如果总位数超过32,才可能分配8字节。这个行为在不同编译器之间还有差异,所以在跨平台代码里,位域的二进制布局不应当被当作标准来依赖。
4.4 联合体、空结构体与带虚函数的类:几个边界情况
联合体(union)的大小计算方式跟结构体完全不同:它所有成员共享同一块内存,大小等于最大成员的大小,但同样要考虑对齐。
union U { char c[9]; // 9字节,对齐数为1 double d; // 8字节,对齐数为8 }; // sizeof(union U) 会向上对齐到8,结果为16c数组需要9字节,double需要8字节对齐,所以联合体至少得能放得下9字节,同时整体大小必须是8的倍数,于是结果是16,而不是9。这个例子清楚说明:即使是“取最大成员”的联合体,也逃不过对齐规则。
空结构体则是另一个故事。在C语言里,struct Empty {}是非法的,GCC会扩展支持并给大小为0;在C++里,struct Empty {}是合法的,但sizeof(Empty)的结果通常是1。为什么一个不包含任何数据的类会是1字节?因为C++标准要求同一个类型的每个对象必须有唯一的地址,如果空类大小为0,数组里多个对象就会在内存里重叠。所以编译器强制给它1字节,保证每个实例拥有独立的存储位置。
C++里还有一类容易被忽略的隐藏开销:带虚函数的类会包含一个虚函数表指针(vptr),sizeof会把这个指针计算进去。这在设计基类时会影响对象大小,尤其是当你需要一个对象数组、又希望把派生类对象塞进去时,很容易因为切片问题引发错误。对性能敏感的开发来说,对象大小的变化直接影响缓存命中率,所以看到sizeof(类)时,脑子里要有个意识:这个大小包含了隐藏的布局开销,把成员加在一起算是不靠谱的。
5. sizeof与strlen的对比:一个在编译期,一个在运行期
5.1 一个测量类型空间,一个测量字符长度
“sizeof和strlen有什么区别”是另一类高频问题。先看这段代码:
char s[] = "hello"; printf("%zu\n", sizeof(s)); // 6,包含末尾的 '\0' printf("%zu\n", strlen(s)); // 5,不包含末尾的 '\0'"hello"这个字符串字面量在内存里实际上是6个字节,最后有一个隐藏的'\0'作为结束符;而strlen扫描到第一个'\0'就停下来,返回之前字符的个数。两者在结果上的差异由此而来。
从本质上看:
sizeof是编译期运算符,接受的是一个类型或表达式,返回的是存储空间的大小,它不关心这块空间里存的是什么;strlen是运行期函数,它必须真正对内存做一次线性扫描,逐字节检查直到遇到'\0',返回的是字符串内容的长度。
一个简单的判断方法:想知道“容量”用sizeof,想知道“内容长度”用strlen。容量是没有运行时开销的,内容长度则需要付出遍历的代价。
5.2 strlen的代价:每次调用都是O(n)扫描
因为strlen是线性扫描,它的开销跟字符串长度成正比。如果你在循环条件里反复调用它,问题就来了:
for (size_t i = 0; i < strlen(s); i++) { // do something }这段代码每循环一次都会重新扫描一遍strlen(s),复杂度从O(n)变成O(n^2)。当字符串长度是几千几万时,性能差异会非常明显。正确做法是先缓存长度:
size_t len = strlen(s); for (size_t i = 0; i < len; i++) { // do something }很多人对这个“性能优化”不以为然,觉得现代编译器肯定会自动优化掉重复调用。但实际上,如果strlen的参数是外部传入的const char*,编译器无法证明字符串内容在循环过程中没有变化(比如被别的函数修改了),所以通常不会做这种优化。这是我在实际项目里实测过的,别指望优化器替你的失误买单。
5.3 一个典型误用:把sizeof当strlen用
几乎所有C语言项目里都能看到类似的bug:
char name[64]; fgets(name, sizeof(name), stdin); // 这行是对的,sizeof(数组)安全 printf("length = %zu\n", sizeof(name)); // 这行是错的,输出固定64fgets用sizeof(name)没问题,因为它需要知道缓冲区容量;但随后又用sizeof(name)打印“长度”,这个值永远等于64,跟用户实际输入了多少字符毫无关系。要拿实际输入长度,必须用strlen(name)。
这个错误背后的原因是把“空间大小”和“内容长度”混为一谈。一个数组变量有确定的容量,但里面的内容长度是可变的。sizeof回答的是前面那个问题,strlen回答的才是后面那个。搞清楚你想问什么问题,自然就不会用错工具。
6. 实际项目里的sizeof使用习惯与避坑清单
6.1 三个最常用的合法场景
第一个是动态内存分配时的元素大小:
int *p = (int *)malloc(10 * sizeof(int));写成sizeof(int)有三个好处:一是代码意图清楚,读者一眼看出是在分配10个int;二是在不同平台编译时,即使int宽度有差异也会正确计算;三是后续如果改成long或自定义类型,只需要改一处。在C语言里其实可以不写(int *)强制转换,自动从void*转过去是合法的;但在C++里必须强转,所以我还见过不少团队为了保证代码能同时被两种编译器编译,统一保留强转的写法。这属于风格争执,不影响正确性。
第二个是计算数组长度:
int arr[] = {1, 2, 3, 4, 5}; size_t n = sizeof(arr) / sizeof(arr[0]);这个写法只能在数组尚未退化的作用域内使用。一旦进入函数参数或经过指针转换,结果就错了,所以很多项目里会把它封装成宏:
#define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0]))注意宏里给a加括号是必须的,防止传入表达式时出现运算符优先级问题。
第三个是内存操作的字节数:
memcpy(dest, src, sizeof(src));这个写法的前提仍然是src必须是真实数组,不是指针。如果src是函数参数传入的指针,sizeof(src)就退化成8或4,memcpy只会复制8字节,Bug随之而来。
6.2 一份可以直接抄的避坑清单
结合我近几年做项目积累的经验,下面这份表格可以说是血泪换来的,建议直接保存在笔记里:
| 场景 | 错误示例 | 正确做法 |
|---|---|---|
| 跨函数传数组 | 在void f(int arr[])里用sizeof(arr)当数组长度 | 额外传size_t len;C++可用模板引用保留数组类型 |
| 同一函数内算长度 | 用sizeof(ptr)计算目标数组长度 | 用sizeof(数组名) / sizeof(数组名[0]) |
| 字符串长度 | 用sizeof(str)当字符串内容长度 | 用strlen,并且注意循环里要缓存 |
| 结构体大小 | 把成员大小直接相加 | 考虑内存对齐,用offsetof与静态断言验证 |
| 动态内存 | 拿到指针后又想“自动知道”分配大小 | 单独保存长度,或用C++的std::vector等容器 |
| 打印size_t | 用%d打印sizeof结果 | 用%zu格式,避免截断或警告 |
6.3 一个辅助工具:用offsetof和sizeof验证结构体布局
做协议解析时,我需要精确知道结构体里每个成员的偏移量。C语言标准库提供了offsetof宏,配合sizeof可以快速验证布局:
#include <stddef.h> #include <stdio.h> #include <stdint.h> struct Packet { uint16_t head; // 偏移0 uint16_t type; // 偏移2 uint32_t len; // 偏移4,天然对齐 uint8_t data; // 偏移8 }; int main(void) { printf("offset of head: %zu\n", offsetof(struct Packet, head)); printf("offset of type: %zu\n", offsetof(struct Packet, type)); printf("offset of len: %zu\n", offsetof(struct Packet, len)); printf("offset of data: %zu\n", offsetof(struct Packet, data)); printf("total size: %zu\n", sizeof(struct Packet)); return 0; }在不同平台编译这份代码,打印结果能直观反映编译器是否插入了填充字节。我在排查“为什么这个结构体在A平台是12字节、在B平台变成16字节”的跨平台问题时,就是靠这种方法快速定位的。
6.4 用sizeof做编译期静态断言的老办法
C11提供了_Static_assert,C++11提供了static_assert,但在一些兼容老标准或老编译器的嵌入式项目里,我见过用sizeof构造编译期检查的老派技巧:
typedef char BUILD_ERROR[-1]; // 条件为假时编译失败 #define CHECK_SIZE(type, expected) \ extern char check_size_##type[sizeof(type) == (expected) ? 1 : -1]定义之后,可以这样使用:
CHECK_SIZE(struct FileHeader, 10); // 如果大小不是10,编译直接报错原理是利用“数组长度为负数”在编译期会被拒绝的规则。只要结构体大小不符合预期,编译器就会报错,问题在编译阶段就暴露了,而不是等到运行时解析出乱码。这个方法虽然没有static_assert优雅,但理解它能帮你把握住sizeof作为编译期常量的本质——它不只是求一个数,而是可以参与编译期的逻辑判断。
这些年来我的体会是,sizeof这个运算符看起来简单,但每深挖一层,都能碰到语言设计和编译器行为的底层逻辑。搞清楚它的边界,写起跨平台代码来会踏实很多。希望这篇小结能帮你把零散的知识点串成一张网,下次再遇到跟它相关的Bug时,能少走几步弯路。