尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

sizeof与strlen深度对比:C语言字符串与内存操作的经典陷阱

sizeof与strlen深度对比:C语言字符串与内存操作的经典陷阱 1. 写在前面sizeof和strlen的爱恨情仇做过C语言笔试题的朋友应该都有过这种体验明明觉得自己已经把字符串搞明白了结果一道char s[] hello; printf(%zu %zu, sizeof(s), strlen(s));直接把自信打回原形。我当年就是在这个题上翻的车——纸上工工整整写下 5 和 5老师红笔一个大叉sizeof(s) 是 6strlen(s) 才是 5。那一瞬间我才意识到这两个在代码里长得像双胞胎的东西底层逻辑完全不是一个路子。sizeof 和 strlen 的对比说它是C语言学习路上的劝退题一点都不夸张。一个连数组和指针都没彻底分清的人碰到这两个函数必然头大哪怕是写过几年C的开发者也经常在函数传参、结构体对齐、字符串字面量这些细节上栽跟头。这篇文章我会把两者的本质差异、运行时行为、常见陷阱全部拆开揉碎再配合十几道经典题目带着你一步步推演答案。不管你是刚学C语言的学生、正在准备面试的求职者还是工作中被指针坑过的老手只要把这篇吃透以后再看到 sizeof 和 strlen 出现在同一行代码里就可以直接秒答。1.1 一个让无数人翻车的经典场景先回到最经典的入门题。很多教科书会告诉你sizeof计算的是所占内存字节数strlen计算的是字符串长度。听起来很简单对吧但题目换个花样就露馅了。比如char str[] hello; char *p str; printf(sizeof(str) %zu\n, sizeof(str)); printf(strlen(str) %zu\n, strlen(str)); printf(sizeof(p) %zu\n, sizeof(p)); printf(strlen(p) %zu\n, strlen(p));同样的内容hello数组和指针的结果完全不一样。数组的 sizeof 是 6指针的 sizeof 在64位系统上是 8。很多初学者在这里直接就懵了我都定义了hello凭什么大小不一样答案就藏在两者的出身里——sizeof 是一个运算符strlen 是一个函数一个在编译期干活一个在运行时干活。这种本质差异决定了它们在数组、指针、函数参数、结构体等场景下的表现截然不同。1.2 这篇文章能帮你解决什么我打算用五个大块把这件事讲透先讲本质再讲高频场景接着上实战题目然后整理面试考点最后给出项目中真正实用的建议。文章里的每道题我都用标准C语言写过、验证过所有代码的注释都标注了实际运行结果你完全可以复制到本地编译器里跑一遍对照着看。我会明确区分哪些结果是 C 标准保证的哪些是在常见平台上的通常表现避免你被网上那些含糊其辞的说法带偏。2. 本质拆解一个运算符与一个函数的出身差异很多资料喜欢把 sizeof 和 strlen 放在一起对比但严格来说这俩根本不是同一类东西。sizeof 是 C 语言的关键字属于运算符和 、-、*、/ 是同一个等级strlen 则是一个标准库函数需要包含 string.h 头文件才能用。这个出身差异直接导致了两者在计算时机、计算方式、适用对象上的全面分叉。2.1 sizeof编译期就定下来的尺子sizeof 的本质是类型系统的一部分。编译器在解析代码的时候看到 sizeof 就会通过静态类型分析算出结果然后直接把这个结果作为编译期常量替换进去。所以绝大多数情况下sizeof 根本不会生成任何运行时代码它就像一把在编译阶段就量好的尺子不关心内存里实际放了什么东西只看你给它的类型声明长什么样。举个例子int main(void) { int a 1; size_t s sizeof(a); printf(s %zu, a %d\n, s, a); return 0; }运行结果是s 4, a 1。你可能会奇怪a 不是会自增吗为什么 a 还是 1因为 sizeof 拿到表达式 a 之后只需要知道它的类型是 int根本不会去执行这个表达式。这就是编译期运算符的典型特征——只看类型不求值。sizeof 的结果类型是 size_t这是 C 标准里定义的无符号整数类型在64位系统上通常是 8 字节打印时应该用%zu格式化符。另外sizeof 的使用不需要包含任何头文件它是语言本身的一部分这一点和 strlen 有根本区别。有个细节必须强调C 语言里sizeof(char)恒等于 1这是标准明确规定的所有平台都一样。其他类型的大小则由平台决定比如 int 在绝大多数桌面系统上是 4 字节但不是所有平台都如此写跨平台代码时不要硬编码类型大小。2.2 strlen运行时要数到\0的计数器strlen 是一个不折不扣的库函数声明在 string.h 里。它的工作方式特别朴素从你传入的指针位置开始一个字节一个字节地往内存后面读数每读一个字符就计数加一直到遇到 \0 才停下来返回计数值。你可以把它理解为strlen 是在现场数数必须真的跑到那块内存里去访问字符串内容才能知道结果。所以它有两个硬性前提第一指针不能是 NULL第二指针指向的内存必须是一个以 \0 结尾的合法字符串区域。这两个前提任何一个不满足结果都是未定义行为轻则读到随机值重则段错误。手写一个 strlen 是C语言入门的经典练习最常见的长这样size_t my_strlen(const char *s) { size_t len 0; while (*s) { len; } return len; }原理一目了然指针逐步后移只要当前字符不是 \0 就继续数。理解了这一点你就能明白为什么 strlen 的结果永远不包括结尾的 \0——因为 \0 是它停下来的标志不是它统计的对象。2.3 核心差异一张表看懂把两者的核心差异整理成一张对照表方便你随时查阅对比维度sizeofstrlen本质身份运算符关键字库函数头文件要求不需要需要 string.h计算时机绝大多数在编译期运行时计算对象类型或表达式的字节大小字符串的有效长度是否包含\0统计内存时包含不包含是否执行表达式不执行不求值会真正遍历内存适用场景任何类型、变量、数组、结构体必须以\0结尾的字符串空指针安全对类型求值不访问内存直接解引用不安全常见返回值示例sizeof(hello) 6strlen(hello) 5看完这张表你应该已经能感觉到sizeof 回答的是我声明的这个东西占多大空间strlen 回答的是这个字符串实际有多长。二者的关注点完全不同所以才会在同一段代码里给出截然不同的答案。3. 五个最容易翻车的经典场景深挖很多人觉得自己懂了定义就万事大吉结果做题还是错。问题就出在sizeof 和 strlen 的分岔路口其实藏在各种具体场景里。我挑出五个高频翻车点逐个给你讲透。3.1 字符数组sizeof为什么总是比strlen多1这是最基础也最常见的场景。当你写char str[] hello;编译器会数一下字符串字面量 hello 包含的所有字符h、e、l、l、o最后还要加上一个隐式的 \0 作为字符串结束标志所以数组实际大小是 6。sizeof(str)得到 6strlen(str) 从 str 首地址开始数数到 o 是 5然后遇到 \0 停下返回 5。如果把数组显式指定大小情况又有变化char buf[100] hello;sizeof(buf) 直接就是 100因为数组声明就是这么大strlen(buf) 依然是 5因为虽然剩余空间都被初始化为 \0但 strlen 在第一个 \0 处就停了根本不会继续往后数。还有一个进阶变体容易被忽略char s[] abc\0def;这个字符串字面量实际包含 a、b、c、\0、d、e、f 再加末尾隐式 \0一共 8 个字符所以 sizeof(s) 是 8。但 strlen(s) 只要数到第一个 \0 就停结果只有 3。很多人第一次看到 8 和 3 这个组合会愣半天其实就是没搞明白数组大小看声明字符串长度看第一个\0的位置这个基本原则。3.2 指针变量sizeof(p)别再当成字符串长度再看指针版本char *p hello;p 是一个指针变量里面存放的是字符串首字符 h 的地址。sizeof(p) 计算的是指针变量本身要占多少字节——64位系统上指针占 8 字节32位系统上占 4 字节和它指向的字符串长度没有任何关系。strlen(p) 才是真正沿着指针去访问内存数出字符串长度 5。这里有一个特别容易误导人的地方如果你把数组名和指针放在一起对比会产生一种它们明明一样的错觉char str[] hello; char *p str; printf(%zu\n, sizeof(str)); // 6 printf(%zu\n, sizeof(p)); // 864位系统同样是打印一个hello结果差了 2 个字节。原因在于数组名 str 在 sizeof 语境下代表整个数组对象而指针 p 只是保存了数组首地址的变量。C语言里数组名在大多数表达式中会退化成指针但在 sizeof 运算符这里是个例外——sizeof 作用于数组名时得到的是整个数组的大小。3.3 函数传参陷阱数组形参的退化实录这是面试和笔试里最阴险的考点没有之一。看代码#include stdio.h #include string.h void func(char arr[100]) { printf(inside: sizeof(arr) %zu\n, sizeof(arr)); printf(inside: strlen(arr) %zu\n, strlen(arr)); } int main(void) { char buf[100] hello; printf(outside: sizeof(buf) %zu\n, sizeof(buf)); func(buf); return 0; }main 函数里 sizeof(buf) 是 100这个没问题。但 func 函数内部sizeof(arr)打印出来的却是 864位系统或 432位系统绝不是 100。原因在于数组作为函数参数传递时并不会把整个数组复制进去而是退化成指向首元素的指针。C语言规定函数形参声明为char arr[100]和声明为char *arr完全等价方括号里的数字只是给人看的编译器一律按指针处理。所以函数内部对数组形参取 sizeof得到的永远是指针大小。这个坑的实用教训是如果一个函数需要知道传入数组的长度不能在函数体内用 sizeof 现算必须在外面算好作为参数传进去。对于字符串场景可以用 strlen 来获取长度但要保证字符串确实以 \0 结尾。3.4 sizeof不求值那个著名的a坑前面提过 sizeof 不求值的问题这里展开细讲。看这段代码int a 1; int *p a; printf(%zu\n, sizeof(a)); // 4 printf(%d\n, a); // 还是 1 printf(%zu\n, sizeof(*p)); // 4sizeof(a) 的表达式里包含自增操作但因为 sizeof 只在编译期分析类型不会真的执行 a所以 a 的值保持不变。同理sizeof(*p) 只是通过类型系统知道 *p 是 int并不会真的去解引用指针访问内存。这个特性在实际编程中反而是一件好事你可以在不产生副作用的前提下写复杂的类型表达式给 sizeof 算尺寸。比如你临时想确认某个表达式结果的类型大小直接放进去就行不用担心它真的执行会产生副作用或无效访问。有一个边界情况需要单独说明C99 引入的变长数组VLA会让 sizeof 从编译期计算变成运行期计算。比如int n 10; char buf[n]; printf(%zu\n, sizeof(buf)); // 10buf 的大小依赖运行时变量 n编译器在编译时无法确定所以 sizeof 必须推迟到运行时求值。这是 sizeof 的非典型场景除了 VLA 之外其他几乎所有情况下 sizeof 都是编译期常量。3.5 字符与字符串字面量a和a的天壤之别另一个高频翻车点藏在字符字面量和字符串字面量的区分上。看这段代码printf(%zu\n, sizeof(a)); printf(%zu\n, sizeof(a));在 C 语言里字符字面量 a 的类型是 int不是 char所以 sizeof(a) 通常等于 sizeof(int)在大多数平台上结果是 4。而字符串字面量 a 是一个 char 数组包含 a 和 \0 两个字符sizeof(a) 的结果是 2。这里有个有趣的对比如果你在 C 里运行同样的代码sizeof(a) 的结果是 1因为 C 把字符字面量定义为 char 类型。不少从 C 转过来学 C 的人或者面试时被问到这个问题都会在这个细节上翻车。顺带一提如果你确实想让某个东西占用 1 字节应该写sizeof((char)a)通过显式类型转换把类型说清楚。4. 实战题目演练15道题由浅入深彻底吃透光讲理论不练题等于白看。我把各类题目按难度分成三组每组 5 道题目下面直接给答案和解析。建议你自己先闭上眼睛想一遍结果再对照解析这样记忆最深刻。4.1 基础题数组、指针、字符串的 sizeof 与 strlen第一组题目考察的是最常见的数组 vs 指针基础场景。第1题char s[] hello; printf(%zu %zu\n, sizeof(s), strlen(s));答案6 5。s 是字符数组由字符串字面量初始化数组大小包含隐式 \0所以 sizeof 是 6strlen 数到 \0 前为止是 5。第2题char s[100] hello; printf(%zu %zu\n, sizeof(s), strlen(s));答案100 5。s 被显式声明为 100 字节的数组sizeof 只看声明strlen 数到第一个 \0是 5。第3题char *s hello; printf(%zu %zu\n, sizeof(s), strlen(s));答案8 564位系统或4 532位系统。sizeof 得到的是指针变量本身的大小strlen 得到的是字符串实际长度。第4题int arr[10]; printf(%zu %zu\n, sizeof(arr), sizeof(arr[0]));答案40 4int 为 4 字节的常见平台。sizeof(arr) 是整个数组 10 个 int 的大小sizeof(arr[0]) 是单个元素的大小。这也是计算数组元素个数的经典思路sizeof(arr) / sizeof(arr[0])。第5题void func(char s[]) { printf(%zu\n, sizeof(s)); } int main(void) { char s[] hello; func(s); return 0; }答案864位系统或432位系统。数组形参退化为指针函数内部的 sizeof(s) 是指针大小和外部 main 函数里的 sizeof(s)结果为 6完全不同。4.2 进阶题多维数组、结构体与内存对齐第二组题目开始加入多维数组和结构体的知识点。第6题char s[] abc\0def; printf(%zu %zu\n, sizeof(s), strlen(s));答案8 3。sizeof 统计整个字符串字面量的所有 8 个字符a、b、c、\0、d、e、f、隐式\0strlen 在第一个 \0 处停下只数到 3。第7题int a[3][4]; printf(%zu %zu %zu\n, sizeof(a), sizeof(a[0]), sizeof(a[0][0]));答案48 16 4int 为 4 字节的平台。a 是 3 行 4 列的二维数组总共 12 个 inta[0] 是第一行包含 4 个 inta[0][0] 是单个 int。第8题struct Node { char c; int x; }; printf(%zu\n, sizeof(struct Node));答案常见平台上为8。这里涉及结构体内存对齐char c 占 1 字节后编译器为了保证 int 类型按 4 字节对齐会在其后填充 3 个字节所以结构体总大小为 1 3填充 4 8。如果你把成员顺序改成int x; char c;依然是 8但改成两个 char 在前就能节省空间。结构体成员排列顺序不同会造成不同的内存开销。第9题union U { char c; int i; double d; }; printf(%zu\n, sizeof(union U));答案常见平台上为8。联合体所有成员共享一块内存大小取最大成员的大小并满足对齐要求。double 是 8 字节所以整个联合体就是 8 字节。第10题int a 10; printf(%zu %d\n, sizeof(a), a);答案4 10。sizeof 不求值表达式a 不会自增。这是 sizeof 的编译期特性最直观的体现。4.3 拔高题联合体、位段、柔性数组与变长数组第三组题目针对那些看起来不太常见但一考就翻车的冷门场景。第11题struct Flex { int len; char data[]; }; printf(%zu\n, sizeof(struct Flex));答案常见平台上为4。C99 引入了柔性数组成员data 不占用结构体空间sizeof 只计算 len 的大小。注意柔性数组必须是结构体的最后一个成员并且结构体不能直接作为普通数组元素使用。第12题int n 10; char buf[n]; printf(%zu\n, sizeof(buf));答案10。buf 是变长数组大小在运行时由 n 决定此时 sizeof 会在运行时求值而不是编译期常量。第13题char *p NULL; printf(%zu\n, sizeof(p));答案864位系统或432位系统。sizeof 只看类型不访问内存对空指针依然能安全算出指针大小。但如果你在后面加一句 strlen(p)程序大概率直接崩溃因为 strlen 会尝试从 NULL 地址开始读取内存。第14题char *p hello; printf(%zu %zu\n, sizeof(*p), sizeof(p 1));答案1 864位系统。sizeof(*p) 得到的是单个 char 的大小 1p 1 仍然是指针类型所以 sizeof(p 1) 是指针大小 8而不是字符 e 的大小。第15题struct Bits { unsigned int a : 3; unsigned int b : 5; unsigned int c : 6; }; printf(%zu\n, sizeof(struct Bits));答案常见平台上为4。位段成员按底层存储单元分配a、b、c 合计 14 位小于一个 unsigned int32位的容量所以整个结构体只占一个存储单元的大小即 4 字节。如果把总位数调到超过 32就可能占用两个存储单元大小变为 8。这类题目考察的是位段对内存布局的影响属于偏冷门但面试官偶尔会拿出来压场的题型。5. 真实项目中的sizeof与strlen从笔试到落地学会做题只是第一步真正考验人的是这些知识在项目里怎么用。这一节我把面试高频考点和日常编码中的实用建议一起整理出来。5.1 高频面试题与阴险陷阱整理面试官问 sizeof 和 strlen 的区别通常不是让你背定义而是考察你是否真正理解编译期与运行时的差异。最经典的五个问法sizeof 和 strlen 的本质区别是什么至少要答出三点运算符 vs 函数、编译期 vs 运行时、sizeof包含\0而strlen不包含。加分项是提一句 sizeof 不求值表达式。为什么函数参数中的数组不能直接用 sizeof 得到长度因为形参数组退化为指针。追问一般会跟着来那怎么正确传数组长度答在调用处计算好传进去或者用宏#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))但注意这个宏也只能用在真数组上对指针使用会得到完全错误的结果。对 malloc 返回的指针调用 sizeof 能得到分配的大小吗不能。sizeof(p) 只能得到指针大小8或4malloc 分配的内存大小不会记录在指针里所以你必须自己记住分配了多少。char *s hello; sizeof(s) 和 strlen(s) 分别是多少在不同位数的系统上分别说明。strlen 的返回类型是什么size_t无符号整数。这里也有一个隐藏的坑拿 strlen 的结果和 int 类型做比较或减法时要注意无符号整数的隐式转换。比如if (strlen(s) 0)永远是成立的因为 strlen 返回的是无符号数。我自己见过不止一次因为length - 1在 length 为 0 时变成一个巨大正数而导致的隐蔽 bug。5.2 写代码时真正该注意的性能与安全细节理论知识落到实际项目里有几个细节是资深开发者才会特别注意的第一不要在循环条件里反复调用 strlen。很多人一开始都会写出这种代码for (size_t i 0; i strlen(s); i) { // 处理字符串 }strlen 每次循环都会从头遍历一遍字符串复杂度直接变成 O(n²)。字符串一长性能肉眼可见地变差。正确做法是提前把长度存到局部变量size_t len strlen(s); for (size_t i 0; i len; i) { // 处理字符串 }这是一个成本极低却收益明显的优化习惯。第二内存分配时一定要把 \0 的空间算进去。复制一个字符串前用malloc(strlen(s) 1)已经是老生常谈但依然有人写错。我见过一次真实事故有人用malloc(strlen(s))分配内存结果拷贝字符串时把 \0 写到了堆内存之外程序偶尔崩溃还很难复现排查了大半天才定位到是少加了一个1的问题。这类 bug 属于典型的平时不炸炸的时候让你怀疑人生。第三对不信任的输入避免直接使用 strlen 造成的越界风险。strlen 会一直向前读到 \0 为止如果数据源不可靠且没有结束标志就等于在裸奔。C11 提供了strnlen_s可选实现很多平台也有strnlen可以限制最大扫描长度。项目里处理网络数据、文件内容这类不确定输入时建议优先使用带长度限制的版本或者在读入数据时就明确确保 \0 终止。第四尽量用 sizeof 配合操作符来保证类型安全。手动写malloc(100)或malloc(100 * sizeof(char))都没有大问题但涉及到 int、结构体等非 char 类型时我建议永远写成malloc(n * sizeof(*ptr))这种形式。这样即使以后把 int 换成别的类型分配大小也会跟着变不会出现改类型忘改动大小的低级错误。5.3 速记口诀与自查清单为方便你快速记忆我总结了一个顺口溜式的口诀sizeof 看类型strlen 上内存sizeof 带 \0strlen 数到零数组作参数退化成指针表达式不求值运行时不执行。对照做题时建议按下面这个清单逐项排查先明确你拿到的到底是什么数组名、指针还是结构体变量sizeof 后面跟的是类型还是表达式是不是 VLA变长数组strlen 的参数是不是确定以 \0 结尾指针是否为 NULL如果出现在函数内部数组形参已经被当作指针了吗字符串里有没有内嵌的 \0内嵌的话 strlen 会提前停下来。打印时是否用了 %zu 而不是 %dsize_t 类型混用格式化符是 UB。这个清单基本覆盖了所有常见坑位。你每遇到一道相关题目就按顺序过一遍正确率会直线上升。我个人在实际项目里还有一个习惯凡是看到sizeof(某个指针)的代码都会下意识停下来确认作者是不是以为能得到数组大小凡是看到strlen出现在循环条件里都会直接提醒改成先存变量。原因很简单这两个错误在代码评审里出现的频率太高了高到我都快产生条件反射。熟练掌握 sizeof 和 strlen 的差异不只是为了应付考试更是为了让你的 C 代码从根本上少踩内存相关的坑。把这篇文章里的题目挨个亲手跑一遍再结合口诀回看每一道题你就能把这两个相爱相杀的家伙彻底驯服。
返回列表