十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

面试必问:C语言sizeof与strlen的本质区别及指针数组陷阱

面试必问:C语言sizeof与strlen的本质区别及指针数组陷阱 1. 别再用“都会算长度”糊弄自己sizeof 与 strlen 的本质差异先明白一件事这两个东西在 C 语言里根本不是一个“物种”。sizeof 是运算符编译期就能给你结果strlen 是标准库函数程序运行起来之后才去内存里数数。很多人嘴上知道这点一写代码就混面试一紧张更是错得离谱。我面试过不少候选人问到“char str[] hello; sizeof(str) 是多少strlen(str) 是多少”答案经常是“都不都是 5 吗”——这就是基础没打牢。正确的理解应该从两个问题出发它算的是什么它什么时候算1.1 定义上讲一个是“运算符”一个是“库函数”sizeof 是 C 语言关键字它的操作对象是类型或者表达式。sizeof(char) 是 1sizeof(int) 是 4在常见 32 位/64 位平台sizeof(指针) 是 864 位平台下。它计算的是“占用内存的字节数”跟这个内存里面放了什么内容完全无关。strlen 是 string.h 里的函数它接收一个 const char* 参数从首地址开始一个一个字节地往后扫直到碰见 \0 为止然后返回中间跳过的字节数。换句话说strlen 算的是“字符串有效字符的个数”它依赖运行期的内存内容跟你传入的指针指向哪里、缓冲区多大统统没有直接关系。这里有个关键点sizeof 是编译期就定死的所以它能对“未知长度数组”这种类型做计算strlen 是运行期才执行的它只认内存里的数据。把这两个概念混在一起后面会吃大亏。1.2 编译期求值和运行期遍历的天壤之别我给你一个最直白的例子#include stdio.h #include string.h int main(void) { char str[] hello; printf(sizeof(str) %zu\n, sizeof(str)); printf(strlen(str) %zu\n, strlen(str)); return 0; }输出是sizeof(str) 6 strlen(str) 5为什么因为 char str[] hello 初始化后缓冲区实际是 6 个字节h、e、l、l、o、\0。sizeof 统计的是这个数组总共占的字节数当然包含结尾的 \0所以是 6。strlen 数到 \0 就停下只返回 5。我经常拿一个比喻讲这个问题sizeof 是“量房间的建筑面积”墙打折、公摊面积全算上strlen 是“数房间里住了几个人”没人住的空房不算。这两种统计口径天然不同谁说它们是一回事谁就是没搞懂。1.3 返回值类型相同却总被混为一谈两者返回类型都是 size_t也就是 unsigned long 或 unsigned long long 的别名具体取决于平台。这就引出一个很隐蔽的坑如果你拿 size_t 和负数比较或者把两个 size_t 相减后去判断正负结果往往出乎意料。别急这个问题我在第 4 章专门来讲因为它是经典面试题的重灾区。现在先把结论钉死sizeof 管“类型占多大空间”strlen 管“字符串有多少有效字符”。一个编译期定一个运行期数一个包含 \0一个不含 \0。后面所有陷阱都从这四行推导出来。2. 指针与数组纠缠的三大经典陷阱面试基本必考标题里带了“指针深度剖析”那说到底sizeof 和 strlen 最容易出错的场景就是“数组名”和“指针”混着用的时候。我见过太多人在这个环节翻车因为 C 语言里数组名有时候是数组有时候又退化成指针规则不背下来真的会晕。2.1 数组名在 sizeof 里是“整个数组”在 strlen 里是“首地址”第一个必须背下来的规则当数组名作为 sizeof 的操作数时它代表整个数组所以 sizeof 能算出整个数组的大小当数组名作为函数实参传给 strlen 时它退化成指向首元素的指针strlen 从首元素开始数。看这段代码#include stdio.h #include string.h int main(void) { char arr[] hello world; printf(%zu\n, sizeof(arr)); // 12包含末尾的 \0 printf(%zu\n, strlen(arr)); // 11有效字符个数 return 0; }sizeof(arr) 是 12因为数组类型是 char[12]strlen(arr) 是 11因为有效字符是 11 个。这里没有任何悬念。但一旦把 arr 赋值给指针情况就变了char arr[] hello world; char *p arr; printf(%zu\n, sizeof(p)); // 8在64位系统上是8因为p是指针 printf(%zu\n, strlen(p)); // 11strlen只关心指针指向的内存不在乎是不是指针sizeof(p) 为什么是 8因为 p 的类型是 char*指针变量存的是地址地址在 64 位机器上占 8 字节。至于 p 指向的内存有多大sizeof 才不管。2.2 函数参数里的数组早已不是数组这是面试里最阴的一道变体#include stdio.h void test(char arr[]) { printf(%zu\n, sizeof(arr)); // 你猜是几 } int main(void) { char buf[] hello world; test(buf); return 0; }如果你觉得答案是 12那就中招了。正确答案是 864 位平台下。原因很 C 语言函数参数里的 arr[] 只是一个语法糖C 标准里写明参数声明为数组类型时会被调整为指针类型。所以 void test(char arr[]) 等价于 void test(char *arr)你在函数里 sizeof(arr) 算出来的自然是“指针的大小”。这一点在面试里反复出现。为什么要这么设计因为 C 语言函数传参是值传递你要是真把整个数组拷贝给函数那代价太大了传一个地址进去指针只占 8 字节效率高得多。数组类型作为一个“完整的对象”只在定义它的作用域里存在一旦跨函数传递它就被“退化”成指针。2.3 指针数组、数组指针、二维数组的 sizeof 计算规则这三样东西长得像实际完全不一样面试裸考很容易当场去世。“指针数组”是“装着指针的数组”。比如char *names[] {Alice, Bob, Charlie}; printf(%zu\n, sizeof(names)); // 24? 32? 取决于指针大小和元素个数64 位平台下每个指针 8 字节names 有 3 个元素sizeof(names) 24。strlen 不能用在这个数组上因为它不是单个字符串而是一个“字符串数组”。“数组指针”是“指向数组的指针”。比如char (*p)[5]; printf(%zu\n, sizeof(p)); // 8p 是指针那 sizeof(*p) 呢是 5因为 *p 的类型是 char[5]。二维数组就更有意思了char grid[2][3] {{a, b, c}, {d, e, f}}; printf(%zu\n, sizeof(grid)); // 62 * 3 * 1 printf(%zu\n, sizeof(grid[0])); // 3第一行的字节数很多新手以为 sizeof(grid) 应该是 2那就错了。char grid[2][3] 整体是一个大小为 23sizeof(char) 的连续内存块所以 sizeof 是 6。注意 grid[0] 的类型是 char[3]所以 sizeof(grid[0]) 是 3它也会告诉你第一行占多少字节。我把这个规则浓缩成一句话“sizeof 永远先看‘变量的类型’再看‘类型的尺寸’strlen 永远只看‘地址开始处有没有 \0’。”你把这句话刻进脑子里这一章的坑基本就绕开了。3. 底层视角字符串在内存里到底怎么放sizeof 为什么对数组“开小灶”光会背规则不够想真正在面试里聊出深度你得能讲清楚“为什么”。为什么数组名有时是整个数组、有时退化成指针为什么 sizeof 在运行期啥也不干就能给结果这些回答的背后是 C 语言对“内存”这件事的完整设计思路。3.1 字符串字面量存在哪栈上数组又存在哪先看两段代码char str1[] hello; // 栈上分配6字节内容是 h e l l o \0可修改 char *str2 hello; // 指针在栈上但字符串字面量通常放在只读数据区不可修改str1 是一个真正的数组它在栈上占用 6 个字节这 6 个字节由初始化值“拷贝”而来所以你可以修改 str1[0] H。str2 是一个指针变量它存的是一个地址这个地址指向编译期就放在只读数据区的“hello”字符串。你如果尝试 str2[0] H在很多平台上会直接段错误因为那块内存不允许写。sizeof 在这两者身上的区别特别大sizeof(str1) 是 6因为 str1 的“类型”是 char[6]sizeof(str2) 是 864 位下因为 str2 的“类型”是 char*。而 strlen(str1) 和 strlen(str2) 都是 5因为它们都从首地址开始数到 \0内容一样。为什么要强调这个因为很多人调试时碰见“为什么我改了字符串内容就崩了”十有八九是把 str2 当 str1 用了。这在嵌入式、系统编程里尤其致命栈上数据可以随便改只读区数据一写就崩。3.2 符号表和“退化”的真相C 为何非要这么做回到底层C 程序在编译时编译器会为变量建一个“符号表”记录变量名、类型、地址。对数组来说符号表里记的类型是“char[6]”所以 sizeof 能直接查表算出 6但对指针来说符号表里记的类型是“char*”自然只能算出 8。那为什么数组传到函数里要退化成指针主要是效率和语义的问题。C 语言诞生于资源紧张的上世纪 70 年代函数传参推崇“值传递”如果传数组就整段拷贝开销大到不可接受。传地址则只需要拷贝 8 个字节而且函数内部还能直接改原数据。为了语法上写起来方便干脆规定数组作为函数参数自动调整为指针。后果就是你会遇到这种诡异现象void foo(int arr[10]) { // 这里的 arr 是 int*不是 int[10] printf(%zu\n, sizeof(arr)); // 8 }有人会问我都写了 int arr[10] 了编译器不就知道是 10 个 int 吗不它知道但没用因为标准规定参数里的数组类型一律按指针处理。你写 int arr[10]、int arr[]、int *arr 这三种形式在参数列表里完全等价。这也是为什么真正的工程代码里函数传数组往往要额外再传一个长度参数因为函数内部拿不到数组的大小。3.3 用地址和大小两个维度去理解内存如果你从内存管理的视角来看C 里几乎所有的“疑难杂症”都可以归结为你有一个地址但你不一定知道这个地址背后那块区域有多大。数组名是一个“地址 完整类型”的复合信息所以在定义域内 sizeof 能还原出大小一旦作为参数传入函数地址保留完整类型被剥离只剩下“指向某类型的指针”。指针变量本身是一个“装着地址的盒子”sizeof 量的是盒子大小不是盒子里东西的大小。我习惯把指针和数组的关系类比成“门牌号和房子”门牌号告诉你房子在哪地址但在函数内部你只拿到了门牌号没拿到房产证数组大小。想拿到大小要么在外部先用 sizeof 算好传进去要么在数据末尾放一个哨兵值比如字符串的 \0让接收方自己数。strlen 的本质就是“沿着门牌号走到房子门口数到哨兵停下”。理清这个维度之后你对 gdb 调试时看到的那些地址、大小信息会有一种“原来如此”的疏通感。很多人学指针越学越乱就是因为他试图用“地址”这一个概念解释所有问题却忘了“类型”和“大小”是拴在一起的。4. 经典面试题实战一题一题带你避开暗坑理论讲了这么多真正拉开差距还是做题。我把这些年面试别人和被别人面试遇到的经典题目整理成一套“押题卷”顺着做一遍你的 sizeof/strlen 手感基本就练出来了。4.1 基础送分题但每次都有不少人错第一题char str[] hello; printf(%zu %zu\n, sizeof(str), strlen(str));答案6 5。sizeof 包含 \0strlen 不包含。这道题唯一的坑就是有人记反了。第二题char *str hello; printf(%zu %zu\n, sizeof(str), strlen(str));答案8 564位平台。sizeof(str) 量的是指针大小不是字符串长度。不管字符串写了多长sizeof(str) 永远都是 8。第三题void foo(char str[]) { printf(%zu\n, sizeof(str)); } char buf[] hello; foo(buf);答案8。函数参数里 char str[] 被调整为 char*。这三道题如果你全对说明你已经超过了相当一部分面试者。接下来开始上强度。4.2 进阶陷阱sizeof 到底求不求值表达式有个非常经典的 C 语言考点#include stdio.h int main(void) { int i 0; printf(%zu\n, sizeof(i)); printf(%d\n, i); return 0; }请问程序输出什么很多人以为 i 会变成 1实际上 i 还是 0。原因是 sizeof 的运算对象如果是表达式编译器只会用它的“类型”信息来决定结果并不会真正去求值这个表达式。sizeof(i) 的求值结果是 sizeof(int)也就是 4但 i 这个操作本身根本没被执行。这看起来有点反直觉但 C 标准里明确这么规定sizeof 不会对操作数求值除了变长数组等少数情况。这个特性在实际工程里也能用到比如你想确认某个宏展开后的类型大小又不想真的执行里面的函数调用写 sizeof(表达式) 是最安全的探测方式。面试里只要你能说出“sizeof 不求值”就能加不少印象分。4.3 深度陷阱无符号类型引发的“负数”灾难这道题是真正的杀手级#include stdio.h #include string.h int main(void) { char str[] hello; if (strlen(str) - 10 0) { printf(negative!\n); } else { printf(positive!\n); } return 0; }猜猜输出什么输出是 positive! 因为 strlen(str) 返回 5但 5 - 10 不是 -5而是在 size_t无符号运算下被“包”成了一个巨大的正数再和 0 比较必然不小于 0。我在代码评审里见过不少类似问题典型姿势是把 strlen 或 sizeof 的结果直接和某个有符号整数做减法或者用strlen(str) - 1当循环条件。一旦字符串为空strlen 是 00 - 1 又变成一个巨型正数循环直接失控甚至越界访问。正确的安全写法是size_t len strlen(str); if (len 10) { // 先判断再相减 }或者干脆在相减前把两者都转成有符号类型if ((int)strlen(str) - 10 0) { // ... }但强转有风险万一字符串长度超过 INT_MAX 呢更保险的思路是不要做这种减法比较改成正向比较。这种“无符号陷阱”是 C 安全编程里的常客很多真实漏洞就是从这里来的。4.4 综合难题函数传参、动态内存、二维指针再看一个综合题。假设你有一个函数需要接收一个字符串数组并输出“其中某个元素的长度”你会怎么写原型void print_name_len(char *names[], int count) { // 这里 sizeof(names) 是多少答案是 8因为 names 是 char** 类型 for (int i 0; i count; i) { printf(%zu\n, strlen(names[i])); } } int main(void) { char *names[] {Alice, Bob, Charlie}; print_name_len(names, 3); return 0; }第一个要点print_name_len 的参数 charnames[] 本质上就是 char*所以在函数里 sizeof(names) 永远是 8。想拿到数组元素个数必须在外部算好传进来。第二个要点strlen(names[i]) 完全没问题因为 names[i] 是一个 char*指向字符串开头。它会从那个地址一直扫到 \0 为止。第三个要点如果你想在 main 里拿到 names 的元素个数可以这样size_t count sizeof(names) / sizeof(names[0]);这里 sizeof(names) 是 24sizeof(names[0]) 是 8结果是 3。这就是“数组元素个数公式”但它只在数组声明的作用域里有效一旦数组退化成指针这个公式立刻失效。所以你会发现很多 C 工程的头文件里会这样写宏#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof(arr[0]))这个宏非常好用但前提是 arr 必须是数组不能是函数参数里的指针否则就是面试题照进现实——算出个 1 甚至其他离谱结果。5. 日常开发实战什么时候该用谁以及我已经踩过的坑面试题只是门槛真正写代码的时候怎么选才是让你成为“靠谱工程师”的关键。下面这段是我在实际项目里攒下的经验今天一次性倒出来。5.1 五个开发原则帮你决定用 sizeof 还是 strlen原则一要“缓冲区大小”用 sizeof要“字符串长度”用 strlen。申请内存、拷贝内存、写文件的时候你需要的是缓冲区总大小那必须 sizeof 或显式的长度变量打印用户输入、遍历字符串内容时你需要的是有效字符数那用 strlen。原则二函数内部不要再对“退化的数组参数”用 sizeof。你已经拿不到数组大小了老实让调用者传长度进来。这是个约定俗成的接口设计习惯很多库函数都是(buffer, buffer_size)成对出现比如 fgets、snprintf、memcpy你可以照抄这个模式。原则三不要用 strlen 去算一个非字符串的缓冲区。strlen 必须保证内存里有一个 \0 才安全。如果你从一个文件读了一段二进制数据里面可能一个 \0 都没有strlen 会一路读下去直到碰见某个碰巧是 0 的字节或者直接踩到非法地址崩溃。处理二进制数据请用显式的长度计算和 memcpy。原则四把 sizeof 和 strlen 的结果当 unsigned 看。绝不要用它们相减后再判断正负也不要把它和 int 负数直接比较。这条在代码评审里我会打成“必须修改”因为它真的能引发线上故障。原则五能用宏定义常量就别反复 sizeof。假如你定义了char buf[1024]那缓冲区大小就是 1024。与其到处写 sizeof(buf)不如#define BUF_SIZE 1024可读性更好也避免在指针场景下误用 sizeof。当然在数组原型上临时算一下是可以的但常规代码里最好语义明确。5.2 文件读写、网络协议解析里的实际应用举一个我经常给别人讲的例子你要读一个文件到缓冲区再计算字符串长度最容易写错的版本是这样的char buf[1024]; FILE *fp fopen(test.txt, r); fread(buf, 1, sizeof(buf), fp); // 这一步用 sizeof 没问题 printf(%zu\n, strlen(buf)); // 问题来了如果 fread 正好读满了1024字节没有位置放 \0如果文件内容恰好大于等于 1024 字节fread 把 buf 填满后并没有自动补 \0strlen(buf) 就会越过 buf 的边界继续数轻则读到脏数据重则直接崩溃。正确姿势是先留一个字节给终止符char buf[1025]; size_t n fread(buf, 1, sizeof(buf) - 1, fp); buf[n] \0; printf(%zu\n, strlen(buf));fread 返回实际读入的字节数我们手动在末尾补 \0这样 strlen 才有终止条件。这个模式在网络协议解析里同样适用你从 socket 收了一段数据先拿到长度 n然后数据[n]\0再当字符串处理如果你少加了这一行后面所有用 strlen 的代码都会变成定时炸弹。5.3 用调试器和日志验证“我认为”的内存布局纸上谈兵终觉浅。我建议你动手验证上面的每一条结论这也是我写 C 这些年养成的习惯。在 main 函数里打印几种量的地址和大小配合 gdb 看内存布局理解会深很多。#include stdio.h #include string.h int main(void) { char local[] hello; char *p hello; printf(size of local: %zu, len: %zu\n, sizeof(local), strlen(local)); printf(size of p: %zu, len: %zu\n, sizeof(p), strlen(p)); printf(local address: %p\n, local); printf(p address: %p\n, p); return 0; }你会发现 local 和 p 的地址范围很不一样local 落在栈上p 指向只读区。这两个地址的差别就是“可写”和“不可写”的差别。遇到诡异段错误时第一件事就是想清楚你要写的那块内存到底在哪一块区域。还有一个小技巧打印 size_t 一定要用 %zu这是 C99 的标准格式符。很多人用 %lu在 64 位 Linux/Windows 上有时碰巧没问题但到 32 位平台或者某些编译器上就打印出错乱值。调试的时候一次输出错误信息会白白浪费你半小时。我在实际项目里被 sizeof 和 strlen 坑过不止一次。最惨的一次是写网络报文解析接口返回的并不是字符串而是一段二进制数据我图省事用了 strlen 去算长度结果把一整包内存都数穿了最后程序跑到随机地址崩溃。从那以后我给自己立了条规矩凡是系统编程、网络编程一律用显式长度绝不指望 \\0 作为可靠边界。sizeof 管的是盒子有多大strlen 数的是线有多长一个静态一个动态谁也不能替代谁。希望看完这篇之后你再遇到它们时能多一分笃定少一分翻车。
返回列表