ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

C语言入门必看:sizeof与strlen的区别及笔试题解析

C语言入门必看:sizeof与strlen的区别及笔试题解析

做C语言入门的朋友,几乎没有不在 sizeof 和 strlen 上栽过跟头的。这两个名字长得像、出场频率也像,一个是编译期看类型尺寸的运算符,一个是运行期数到字符串结束符为止的函数。搞混了轻则笔试题丢分,重则线上服务把内存读穿。我在带团队面试时,这两道题几乎每轮都出,能一次性答全的人真的不多。这篇文章打算把 sizeof 和 strlen 的区别掰开揉碎,再配上一批经典笔试题做靶子,帮你在“C 语言入门”这个阶段就把这块硬骨头啃下来。适合刚学完指针和数组、准备找实习或校招的同学,也适合想快速捡起 C 语言细节的工程人员。

1. 先看本质:sizeof 是编译期运算符,strlen 是运行期函数

1.1 一个在编译时定型,一个在运行时数数

sizeof 在 C 标准里被定义为一个运算符,不是函数。这意味着它在编译阶段就会被求值,编译器根据表达式或类型名直接算出占用内存的字节数。因为结果在编译期就确定了,所以它通常会被优化成一个常数,比如sizeof(int)在常见平台上是 4,编译器拿到这个信息后,可能直接就替换成数字 4,根本不会在运行时有任何“计算”动作。

strlen 就不一样,它是个标准库函数,声明在<string.h>头文件里。它做的事情很单一:从你给它的地址开始,一个字节一个字节地往后读,直到读到第一个'\0'(数值为 0 的字节)为止,然后返回一共读了多少个字符。整个过程发生在程序运行之后,输入是内存地址,输出是“不包含结束符的字符个数”。所以 strlen 的运行结果取决于内存里实际的内容,同一个变量在不同时刻可能返回不同的值。

一个很常见的考察点是:sizeof 到底需不需要头文件?因为它不是函数,所以不需要。但 strlen 必须带上<string.h>,否则编译器会给你一个隐式声明的警告,甚至直接报错。面试时被问到“sizeof 函数需要头文件吗”,直接回答“它是运算符,不需要”,就能拉开差距。

1.2 一个看类型,一个看内容

这两者看待“字符串”的角度完全不同。sizeof 关心的是“这块内存占多大”,它会老老实实把结尾的'\0'也算进去。strlen 关心的是“有效字符有多少个”,它坚决不数'\0'。

举个例子:

char str[] = "Hello"; printf("%zu %zu\n", sizeof(str), strlen(str));

输出结果是 6 和 5。sizeof(str) 是 6,因为数组总共有 6 个字节:H、e、l、l、o 和结尾的'\0'。strlen(str) 是 5,因为它只数到 o 就停,看到'\0'后不会再往里走。

很多新手容易把两者记反,认为 sizeof 是长度、strlen 是大小。其实只要记住“sizeof 是‘箱子体积’,strlen 是‘货物数量’”。一个空箱子也有它自己的体积,哪怕里面没有货物。一个字符串数组哪怕没有有效字符,只要它是数组,sizeof 也会给出包括结束符在内的完整空间。这种类比在我讲 C 语言的时候用了很多次,几乎没有理解不了的。

2. 经典考点一:数组名、指针和函数传参的 sizeof 问题

2.1 数组名不是指针,却在 sizeof 面前最容易现原形

C 语言里有一句话让大家又爱又恨:“数组名在大部分情况下会退化成指向首元素的指针。”但在 sizeof 这里,它是个例外。当 sizeof 的操作数是数组名时,得到的是整个数组占用的字节数,而不是一个指针的大小。

int a[10]; printf("%zu\n", sizeof(a)); // 40(32位环境下) printf("%zu\n", sizeof(a[0])); // 4 printf("%zu\n", sizeof(&a[0])); // 4或8,取决于平台指针大小

这里的规律是:sizeof(数组名)得到整个数组大小,sizeof(数组名[下标])得到单个元素大小,sizeof(&数组元素)得到指针大小。很多人会去背结论,但我建议你理解为什么:sizeof 的处理对象是“数组类型”,int a[10]的类型是“包含 10 个 int 的数组”,所以大小就是 10 乘以单个 int 的大小。而&a[0]的类型是int*,自然就是指针大小。

笔试题里经常出现的变形是:

char s[] = "Hello"; char *p = s; printf("%zu %zu\n", sizeof(s), sizeof(p));

s 是数组,所以 sizeof(s) 是 6;p 是指针,在 32 位平台是 4,在 64 位平台是 8。如果你只回答了“都是 4”,那就把新旧平台一起丢了。现在线上大部分是 64 位,指针大小是 8,而 C 标准并没有规定指针一定占 4 字节,只是保证它能装得下一个地址。所以这类题必须随口问一句“默认是 32 位还是 64 位”,或者直接按 64 位作答,同时说明 32 位下的差异。

2.2 函数参数里的数组其实是指针,这是笔试第一坑

接着上面的问题,把数组传给函数时,情况会再次反转。看这段代码:

void foo(char arr[100]) { printf("%zu\n", sizeof(arr)); } int main(void) { char buf[100]; foo(buf); return 0; }

面试现场经常有人脱口而出“100”。错了。函数参数里写的char arr[100]根本不会真的创建一个包含 100 个元素的数组,它在编译器眼里就是char *arr。C 语言的设计哲学之一就是“传数组就传地址,不复制整个数组”,所以这里的 sizeof(arr) 算的是指针大小:32 位平台是 4,64 位平台是 8。

这个坑之所以经典,是因为它在代码层面几乎无解:你没法在函数内部通过 sizeof 拿到传入数组的元素个数。标准做法是额外传一个长度参数。我见过不少工程事故,都是因为有人想在函数内用sizeof(arr) / sizeof(arr[0])计算数组长度,结果算出来的永远是 1 或 2,然后越界读写。记住:数组只有在数组定义的作用域内,sizeof 才有效;一旦被当作参数传递,它就退化成指针了。

3. 经典考点二:字符串常量、字符数组与 strlen 的边界

3.1 字符串字面量的 sizeof 永远比“看到的字符数”多 1

字符串字面量就是写在双引号里的那串字符,它本质上是一个“静默存在的字符数组”,末尾会隐式补一个'\0'。所以:

printf("%zu\n", sizeof("Hello")); // 6 printf("%zu\n", strlen("Hello")); // 5

有人会追问:“那 sizeof("") 是多少?”答案是 1,因为空字符串也占一个字节,用来放'\0'。同样,char *s = "Hello";里的 s 指向的是一个字符串字面量,而 sizeof(s) 依然是指针大小,不是 6。这里要注意区分“指针指向的字符串长度”和“指针本身的存储空间”。

还有一个容易混的写法:

char str[] = "Hello"; char str2[10] = "Hello";

sizeof(str) 是 6,sizeof(str2) 是 10。数组类型决定了大小,初始化字符串只是往里面放了 5 个字符和 1 个结束符。str2 剩余 4 个字节会被自动初始化为 0,所以 strlen(str2) 依然是 5,因为它读第一个'\0'就停下来了。笔试中经常把这两种定义放在一起考,考察对“数组初始化”和“字符串长度”的理解是否扎实。

3.2 strlen 读不到结束符就会“裸奔”

strlen 最危险的地方在于:它默认你给它的内存里有一个'\0'。一旦没有,它就会顺着地址一直读下去,直到碰运气遇到一个 0 字节才停下来。这个行为在笔试里会被包装成“能运行但结果不确定”的代码,比如:

char a[5] = {'H', 'i', '!'}; printf("%zu\n", strlen(a));

数组 a 只有 5 个字节,初始化填了 3 个字符,其余字节自动补 0,所以这里能正常输出 3。但如果改成:

char a[3] = {'H', 'i', '!'}; printf("%zu\n", strlen(a));

就麻烦了。这个数组里根本没有'\0',strlen 会一直向后读,直到越界后的某块内存恰好是 0。你得到的长度是随机值,可能 3,可能 17,可能 234。更糟糕的是,如果越界读到的全是非 0 字节,程序可能直接触发段错误。这类代码在面试时会被问“是否安全”,标准答案是:不安全,没有初始化结束符,strlen 的行为是未定义的。

我自己的开发习惯是:所有字符数组在初始化时尽量用字符串赋值,或者显式在最后一个位置写\0。不要赌编译器会帮你补零,尤其是你用char buf[8]然后手动填充数据的时候,一定要留一个位置给结束符。

4. 经典考点三:结构体的 sizeof 不对齐会出大事

4.1 为什么结构体大小不等于成员大小之和

如果结构体成员只是简单顺序排列,那么sizeof(struct)应该是所有成员大小的和。但现代编译器为了让 CPU 更高效地读取变量,会在成员之间插入“填充字节”(padding)。看一个经典例子:

struct A { char c; int i; }; struct B { char c; int i; char d; };

在常见的 32/64 位环境下,sizeof(struct A)是 8,sizeof(struct B)是 12,而不是 5 和 6。因为int通常需要按 4 字节对齐,编译器会在 char 后面补上 3 个填充字节,让i的地址是 4 的倍数。成员顺序的变化会直接改变填充的位置和数量。

这也是笔试里一个很实用的考点:把结构体成员按“大类型在前、小类型在后”排列,可以减少填充字节。比如:

struct C { char c; int i; }; struct D { int i; char c; };

两者成员一样,但 struct C 大小是 8,struct D 大小也是 8,都省不了。真正能省时,是调整多个小类型成员的排列,让它们共享同一个对齐单元。

4.2 位域、嵌套结构体和联合体的 sizeof 估算

位域是另一个高频坑。位域允许你按“位”定义成员,但 sizeof 计算时通常还会按底层类型分配空间。例如:

struct Flags { unsigned char a : 1; unsigned char b : 3; unsigned char c : 2; };

很多编译器会让这个结构体占 1 个字节,因为三个位域总共 6 位,放在一个 unsigned char 里就够了。但如果你把底层类型改成 unsigned int:

struct Flags2 { unsigned int a : 1; unsigned int b : 3; unsigned int c : 2; };

结果往往变成 4 个字节,因为编译器按 unsigned int 的对齐单位分配存储单元。C 标准对位域的布局没有强制规定,所以这类题在笔试中一般会给出“假设环境为常见的 x86/gcc”这样的前置条件,否则答案是不确定的。

结构体嵌套时,内层结构体的大小会作为一个整体参与对齐;联合体则取所有成员中最大的那个大小,且还要满足所有成员的对齐要求。我见过一道很有意思的题:

union U { char c; int i; double d; }; struct S { char c; union U u; };

在常见 64 位平台上,sizeof(union U) 是 8(double 大小和对齐都占 8),struct S 里因为u要对齐到 8 字节,编译器会在char c后面补 7 个字节,所以 sizeof(struct S) 是 16,而不是 9。这类题只要记住“对齐是结构体的命根子”,基本不会算错。

5. 经典考点四:malloc 动态内存与 sizeof 的相爱相杀

5.1 sizeof 永远测不出动态分配的内存大小

很多初学者以为malloc申请了多大,sizeof(指针)就能告诉你多大。这完全理解错了。看代码:

char *p = (char *)malloc(10 * sizeof(char)); printf("%zu\n", sizeof(p));

这里的 sizeof(p) 只取决于 p 的类型char*,在 64 位平台就是 8,和你 malloc 了多少字节没关系。编译器在编译时根本不知道 malloc 在运行时会返回多大的堆空间,它只知道 p 是一个指针变量。

这也是为什么 C 语言里动态数组的长度必须自己拿一个变量记着:

int *arr = (int *)malloc(n * sizeof(int)); // 后面的代码必须一直带着 n,没法通过 sizeof(arr) 找回长度

一旦忘了保存 n,后续遍历就会越界。很多线上的内存破坏问题,本质都是“只动态分配了内存,却把长度信息丢了”。

5.2 一道经典的迷惑题:malloc(10.2 * 1024 * sizeof(char))

网上有个很火的迷因式代码片段:

prt = (char *)malloc(10.2 * 1024 * sizeof(char));

首先,这个写法笔试一定会问三个问题:第一,malloc的参数类型是size_t,也就是无符号整数类型。这里给的是一个浮点表达式10.2 * 1024,结果是一个double类型的 10444.8。C 语言里浮点数转换成整数类型时,会向零截断,也就是变成 10444,而不是四舍五入到 10445。所以实际申请的是 10444 字节,不是你想当然的 10445 字节,更不是 10444.8 字节。

第二,sizeof(char)在 C 标准里被定义为 1,乘不乘都不影响结果,唯一的作用是让代码看起来在“计算字节数”。但真正的问题是,10.2 * 1024 * sizeof(char)里的sizeof(char)是size_t类型,与前面的浮点数相乘后,整个表达式先按浮点数计算,得到 10444.8,然后再传给 malloc。这种隐式转换并不报错,但会让人误以为申请了 10.2KB,实际上丢掉了 0.8 字节的小数部分。

第三,(char *)这个强转在 C 语言中不是必须的,void*可以隐式转换为任意对象指针;但如果你是编译 C++ 代码,就必须写这个强转。所以这段代码在不看上下文的情况下,可以说“能编译,但风格很差,且容易掩盖问题”。真正负责的写法是:

size_t count = (size_t)(10.2 * 1024); char *prt = (char *)malloc(count); if (prt == NULL) { // 处理分配失败 }

你申请动态内存时,永远不要忘记检查 malloc 的返回值。NULL 检查是个老生常谈,可笔试题里十有八九会埋一个“malloc 后不判空”的陷阱。

6. 笔试题实战:从选择题到程序阅读题

6.1 核心答题速查对照表

笔试题目翻来覆去就是这些套路,我把最常见的组合做成了一张表,你直接背下来也能应付大多数题:

表达式32位平台64位平台说明
sizeof(char)11char 恒为 1
sizeof(int)44常见 gcc 环境
char *p; sizeof(p)48指针类型
char arr[10]; sizeof(arr)1010数组类型
void foo(char a[10]) { sizeof(a); }48参数退化为指针
sizeof("Hello")66包含结束符
strlen("Hello")55不含结束符
char s[] = "Hello"; sizeof(s)66自动分配 6 字节
char s[] = "Hello"; strlen(s)55运行期读取
char *s = "Hello"; sizeof(s)48指针大小与字符串无关

这张表没有包含结构体和动态内存,如果题目里出现结构体,要先确认默认对齐规则;出现 malloc,要立刻意识到 sizeof 测不出动态长度。

6.2 五道经典题型逐题拆解

题目 1

#include <stdio.h> #include <string.h> int main(void) { char str[] = "Hello"; printf("%zu %zu\n", sizeof(str), strlen(str)); return 0; }

答案:6 5。sizeof(str) 是整个字符数组的大小,strlen(str) 是除了结束符以外的字符个数。这里如果写成char *str = "Hello";,sizeof(str) 就会变成 4 或 8,这是最常见的变体。

题目 2

void test(char s[]) { printf("%zu\n", sizeof(s)); }

答案:在 32 位平台是 4,64 位平台是 8。题目不会自动告诉你平台,你要在答题时注明“这是指针大小,不是数组大小”。很多面试官就是等着看你能不能点出“数组入参退化为指针”。

题目 3

struct Student { char name[20]; int age; char gender; };

在常见环境下求 sizeof(struct Student)。name 占 20 字节,age 按 4 对齐,因为 20 已经是 4 的倍数,所以可以不填充。gender 占 1 字节,但在结构体末尾,编译器为了保证结构体数组每个元素都能对齐到 4,会在最后补 3 个字节。总大小是 20 + 4 + 1 + 3 = 28。这道题很多人只算 25,忘了末尾填充。

题目 4

char *p = (char *)malloc(100); printf("%zu\n", sizeof(p));

答案:8(64 位平台)。sizeof(p) 跟 malloc 的 100 没有任何关系。如果要验证是否成功申请 100 字节,唯一的办法是判断 p 是不是 NULL,以及你是否在别处记录了长度。

题目 5

char a[] = "abc"; char b[10] = "abc"; printf("%zu %zu\n", sizeof(a), sizeof(b)); printf("%zu %zu\n", strlen(a), strlen(b));

答案:sizeof(a) 是 4,sizeof(b) 是 10。strlen(a) 和 strlen(b) 都是 3,因为它们都以'\0'结尾,有效字符相同。这里要意识到 b 的剩余空间会被自动清零,但就算不清零,strlen 也只在遇到第一个'\0'时停止。

7. 实操心得:我踩过 sizeof 和 strlen 的坑

实战里我吃过最大的亏,是在一个网络协议解析模块里,把一个char buf[64]作为参数传给函数,然后在函数里用sizeof(buf)获取缓冲区长度,结果拿到的只是指针大小 8。原本以为能处理 64 字节的包,结果只处理到第 9 个字节就越界了。最后排查了很久,才发现是函数参数退化了。

后来我给自己定了几条规矩:

能用 sizeof 就别用 strlen 重复计算。如果一个字符串在某个作用域内长度不会变化,你完全可以在编译期拿到它的字节数,不要在循环里反复调用 strlen,每次调用都是从头读到'\0',数组越长开销越大。很多人写循环判断for (int i = 0; i < strlen(s); i++),这在学术上没错,但对长字符串是个性能隐患。正确的做法是先算好长度,再用一个变量保存。

数组传参时一定要带上长度参数。无论是int len还是size_t len,都比在函数里猜长度靠谱。你永远无法通过 sizeof 在子函数中得到外部数组的完整大小,只能靠调用方告诉你。封装 API 时,我习惯把长度参数放在缓冲区指针旁边,并且明确单位是“字节”还是“元素个数”,避免后来的人理解偏差。

涉及字符数组操作时,心里时刻想着'\0'。用 fgets、snprintf 这类函数时,最后一位空间是留给结束符的;用 strncpy 的时候,它不一定帮你补'\0',需要手动指定结尾。很多 strlen 读飞出去的程序,源头都是复制字符串时把结束符覆盖了。检查的办法也很简单:调试时把数组内存打印成十六进制,看看结束符位置对不对。

结构体对齐不要只背结果,要学会用编译器验证。在写跨平台代码时,不同的对齐策略可能导致结构体大小完全不同。你可以在代码里临时加一句:

printf("%zu\n", offsetof(struct A, i));

用<stddef.h>里的 offsetof 查看成员偏移,能帮助你理解填充字节到底加在哪。如果做嵌入式开发,可能需要用__attribute__((packed))禁止对齐,但代价是访问未对齐成员时可能变慢,甚至在某些架构上导致硬件异常,这就要看具体场景取舍了。

最后再分享一个小技巧:笔试中遇到 sizeof 和 strlen 的题,先圈出操作对象到底是什么类型,数组还是指针,这是最关键的一步。只要类型判断对了,答案基本不会错。一旦看到函数参数里的[],马上提醒自己它是指针;一旦看到字符串字面量直接塞给 sizeof,记得加上结束符。希望这份总结能帮你在面试和工程里少踩几个坑。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进