先说结论:这四段代码,表面上是“打印几个数”,实际上是 C 语言笔试面试里最经典的“指针类型识别 + 数组退化 + 未定义行为”三合一。很多人第一次做,能把 strlen(*arr) 写成 1,把 sizeof(&arr + 1) 写成 8 却不知道为什么,最后面试官一句“你确定?”就开始冒汗。
今天我不只给你答案,还要把背后的“地图”铺开。教育学里有个词叫“先行组织者”,意思是先给你一个认知框架,再往里填细节。否则你一次面对 sizeof、strlen、数组名退化、指针加减、字符串字面量,认知负荷直接拉满,看多少遍都容易忘。
所以先记住三句话:
-
sizeof 是运算符,不是函数。它只看类型,不看里面存了什么字符串。
-
strlen 是函数。它从你给的地址开始,一个字节一个字节找
'\0'。 -
数组名在大多数表达式里会退化成首元素指针,但
sizeof(arr)、&arr是例外。
下面默认环境是 64 位 Linux/GCC/Clang,指针大小 8 字节。如果是 32 位环境,所有指针大小从 8 变成 4。另外,原代码用 %d 打印 sizeof 和 strlen 的返回值,严格说格式不匹配,正确应该用 %zu。笔试里为了省事常写 %d,但工程里别这么干。
一、先把“演员表”认清楚
c
char arr[] = "abcdef";
这行代码等价于:
c
char arr[7] = {'a', 'b', 'c', 'd', 'e', 'f', '\0'};
注意,"abcdef" 是 6 个可见字符,但字符串字面量末尾自带一个 '\0',所以数组长度是 7。
c
char* p = "abcdef";
这行代码里,p 是一个指针变量,指向字符串字面量 "abcdef" 的首字符 'a'。字符串字面量通常放在只读区,所以更规范写法是:
c
const char* p = "abcdef";
你如果尝试 p[0] = 'x';,那是未定义行为,可能直接段错误。
现在演员有:
-
arr:数组名,类型char[7] -
arr + 0:表达式里数组名退化,类型char* -
*arr:等价arr[0],类型char -
&arr:指向整个数组的指针,类型char (*)[7] -
&arr[0]:首元素地址,类型char* -
p:指针变量,类型char* -
*p:p指向的字符,类型char -
&p:指针变量p自己的地址,类型char**
这些类型一乱,后面全乱。
二、代码一:sizeof 版字符数组
c
#include <stdio.h>
int main()
{
char arr[] = "abcdef";
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(arr + 0));
printf("%d\n", sizeof(*arr));
printf("%d\n", sizeof(arr[1]));
printf("%d\n", sizeof(&arr));
printf("%d\n", sizeof(&arr + 1));
printf("%d\n", sizeof(&arr[0] + 1));
return 0;
}
逐行拆:
| 表达式 | 类型 | 64 位结果 | 解释 |
|---|---|---|---|
sizeof(arr) | char[7] | 7 | 整个数组大小,7 个 char |
sizeof(arr + 0) | char* | 8 | 数组名退化,加 0 还是指针 |
sizeof(*arr) | char | 1 | *arr 就是 arr[0] |
sizeof(arr[1]) | char | 1 | 单个字符 |
sizeof(&arr) | char (*)[7] | 8 | 指向数组的指针,还是指针 |
sizeof(&arr + 1) | char (*)[7] | 8 | 指针加 1,类型不变 |
sizeof(&arr[0] + 1) | char* | 8 | 首元素指针加 1,还是指针 |
所以 64 位输出是:
text
7 8 1 1 8 8 8
这里最容易被忽略的是 &arr + 1。&arr 的类型是 char (*)[7],它指向整个数组。&arr + 1 不是跳过 1 个字节,而是跳过整个数组 7 个字节。但 sizeof 只关心类型,它不会真的去访问内存,所以结果仍然是指针大小 8。
面试常问:
arr和&arr的值一样吗?
答:大多数情况下地址值一样,但类型不一样,步长不一样。arr + 1跳 1 字节,&arr + 1跳 7 字节。
三、代码二:strlen 版字符数组,这里开始有坑
c
#include <stdio.h>
#include <string.h>
int main()
{
char arr[] = "abcdef";
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr + 0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr + 1));
printf("%d\n", strlen(&arr[0] + 1));
return 0;
}
逐行看:
| 表达式 | 结果 | 解释 |
|---|---|---|
strlen(arr) | 6 | 从 'a' 开始数到 '\0' 前,6 个字符 |
strlen(arr + 0) | 6 | 同上 |
strlen(*arr) | 未定义行为 | *arr 是字符 'a',ASCII 97。strlen 会把 97 当地址 |
strlen(arr[1]) | 未定义行为 | arr[1] 是 'b',ASCII 98,同样被当地址 |
strlen(&arr) | 通常 6 | &arr 地址值等于 arr,但类型是 char (*)[7],类型不匹配 |
strlen(&arr + 1) | 未定义行为 | 跳过整个数组,指向数组后面,越界读 |
strlen(&arr[0] + 1) | 5 | 从 'b' 开始数,剩下 bcdef,长度 5 |
所以正常能确定的是:
text
6 6 崩溃/未定义 崩溃/未定义 6(但不建议依赖) 未定义 5
这里必须重点讲 strlen(*arr)。
strlen 的原型是:
c
size_t strlen(const char *str);
它要的是地址。你给它 *arr,它拿到的是字符 'a',也就是整数 97。于是它把内存地址 0x0000000000000061 当成字符串起点,去那里找 '\0'。这地址通常不可访问,所以程序大概率直接 segmentation fault。
这类错误在面试里非常爱考,因为它能区分“你只是背了答案”还是“你真的知道指针和字符的区别”。
strlen(&arr) 也值得说一句。&arr 类型是 char (*)[7],传给 strlen 类型不兼容,编译器一般会警告。只是它的地址值和 arr 相同,所以运行时很多环境会输出 6。但工程里不要这么写,考试里如果问“输出什么”,可以答“常见环境是 6,但类型不匹配,规范上应避免”。
四、代码三:sizeof 版字符指针
c
#include <stdio.h>
int main()
{
char* p = "abcdef";
printf("%d\n", sizeof(p));
printf("%d\n", sizeof(p + 1));
printf("%d\n", sizeof(*p));
printf("%d\n", sizeof(p[0]));
printf("%d\n", sizeof(&p));
printf("%d\n", sizeof(&p + 1));
printf("%d\n", sizeof(&p[0] + 1));
return 0;
}
这次 p 是指针变量,不是数组。
| 表达式 | 类型 | 64 位结果 | 解释 |
|---|---|---|---|
sizeof(p) | char* | 8 | 指针变量大小 |
sizeof(p + 1) | char* | 8 | 指针加 1,还是指针 |
sizeof(*p) | char | 1 | p 指向的字符 'a' |
sizeof(p[0]) | char | 1 | 等价 *p |
sizeof(&p) | char** | 8 | 指针变量自己的地址 |
sizeof(&p + 1) | char** | 8 | 指向指针的指针,加 1 还是指针 |
sizeof(&p[0] + 1) | char* | 8 | &p[0] 是 char*,加 1 还是 char* |
输出:
text
8 8 1 1 8 8 8
注意,sizeof(p) 不是 6,也不是 7。p 只是指针,它不携带“我指向的字符串有多长”的信息。这是 C 语言和很多高级语言不一样的地方。C 里字符串长度要靠 '\0' 标记,或者你另外存一个长度变量。
五、代码四:strlen 版字符指针,坑更深
c
#include <stdio.h>
#include <string.h>
int main()
{
char* p = "abcdef";
printf("%d\n", strlen(p));
printf("%d\n", strlen(p + 1));
printf("%d\n", strlen(*p));
printf("%d\n", strlen(p[0]));
printf("%d\n", strlen(&p));
printf("%d\n", strlen(&p + 1));
printf("%d\n", strlen(&p[0] + 1));
return 0;
}
| 表达式 | 结果 | 解释 |
|---|---|---|
strlen(p) | 6 | 从 'a' 开始数 |
strlen(p + 1) | 5 | 从 'b' 开始数 |
strlen(*p) | 未定义行为 | *p 是 'a',被当地址 |
strlen(p[0]) | 未定义行为 | 同上 |
strlen(&p) | 未定义/不确定 | &p 是 p 变量自己的地址,不是字符串地址 |
strlen(&p + 1) | 未定义行为 | 越界读 |
strlen(&p[0] + 1) | 5 | 等价 strlen(p + 1) |
能确定的是:
text
6 5 崩溃/未定义 崩溃/未定义 不确定/未定义 未定义 5
重点说 strlen(&p)。
p 是一个指针变量,它自己占 8 字节,里面存的是字符串字面量的地址。&p 是“存放这个指针变量的内存地址”,类型是 char**。strlen(&p) 会把 p 这个变量所占的内存字节当成字符串来数。它会从栈上读字节,直到遇到某个 0 字节。结果取决于栈布局、地址值、编译器,完全不可移植。
所以如果你在某些环境看到 strlen(&p) 输出 3、0、6 甚至别的数,都不要惊讶。这不是“答案”,这是未定义行为在碰运气。
六、面试笔试高频清单:这题到底在考什么?
把上面四段代码压缩成一张清单,你复习时直接背这张:
-
sizeof是运算符,strlen是函数。 -
sizeof只看类型,编译期求值;strlen运行期找'\0'。 -
数组名在大多数表达式里退化为首元素指针。
-
sizeof(arr)不退化为指针,得到整个数组大小。 -
&arr类型是char (*)[N],&arr + 1跳过整个数组。 -
&arr[0]是首元素指针,&arr[0] + 1跳过一个元素。 -
strlen参数必须是合法的、以'\0'结尾的字符串地址。 -
strlen(*arr)、strlen(arr[1])、strlen(*p)都是把字符当地址,未定义行为。 -
字符串字面量不可修改,建议写
const char* p = "abcdef";。 -
printf打印sizeof/strlen返回值用%zu,不要用%d。 -
函数参数里的
char arr[]会退化成char*,sizeof(arr)得到指针大小。 -
未定义行为不是“结果随机”,而是“标准不保证任何结果”,可能崩溃,也可能被优化成奇怪逻辑。
七、为什么 2026 年还要学这种“老古董”?
因为 C 语言的内存模型仍然是现代计算机的底座。
当前计算机前沿里,内存安全是绝对热点。大量安全漏洞都和缓冲区溢出、越界读写、悬空指针、未定义行为有关。strlen(*arr) 这种代码在真实项目里一旦被触发,轻则崩溃,重则可能被利用。
现在工业界常用这些工具来抓这类问题:
-
AddressSanitizer:抓越界、use-after-free
-
UndefinedBehaviorSanitizer:抓未定义行为
-
Valgrind:内存检测
-
Fuzzing:模糊测试
-
静态分析:Coverity、Clang Static Analyzer、MISRA
-
Rust:用所有权和借用检查在编译期挡住大量内存错误
-
CHERI、指针 provenance:硬件和编译器层面追踪指针来源
但注意,Rust 再火,C 仍然在操作系统、嵌入式、数据库、编译器、驱动里大量存在。你不可能只会“上层语言”,底层指针类型一塌糊涂。AI 编程助手能帮你写代码,但它也会生成 char* p = "abc"; 这种有隐患的代码。真正能兜底的人,还是得懂 sizeof 和 strlen 的区别。
八、从教育学角度看:为什么你一看就会,一考就废?
这里插一点学习方法,不是鸡汤,是认知科学。
第一,先行组织者。先记“sizeof 看类型,strlen 找 \0”,再做题。没有这个框架,细节越多越乱。
第二,认知负荷控制。不要一上来就同时背 arr、&arr、p、&p。先分数组组,再分指针组。每组只变一个维度,这叫变式教学。
第三,正例反例对照。strlen(arr) 是正例,strlen(*arr) 是反例。反例比正例更能让你记住边界。
第四,最近发展区。你先会算 sizeof(arr),再学 sizeof(arr + 0),再学 sizeof(&arr + 1),每一步只比当前能力高一点,这叫脚手架。
第五,形成性评价。不要只看答案,要自己先预测输出,再运行,再解释。预测错的地方,才是你真正的知识漏洞。
第六,元认知。做完题问自己:我到底是在背答案,还是能解释为什么?如果面试官把 char 换成 int,我还会不会?
九、练习题与针对性答案(C 语言实现)
下面这些题,建议你先盖住答案,自己写输出。
练习 1
c
#include <stdio.h>
#include <string.h>
int main() {
char s[] = "hello";
printf("%zu %zu\n", sizeof(s), strlen(s));
return 0;
}
答案:6 5
解析:"hello" 是 5 个字符加 '\0',数组大小 6;strlen 不算 '\0',所以 5。
练习 2
c
#include <stdio.h>
#include <string.h>
int main() {
char s[10] = "hi";
printf("%zu %zu\n", sizeof(s), strlen(s));
return 0;
}
答案:10 2
解析:数组显式大小是 10;字符串内容是 hi,长度 2。剩余字节自动补 0。
练习 3
c
#include <stdio.h>
#include <string.h>
int main() {
char *s = "hello";
printf("%zu %zu\n", sizeof(s), strlen(s));
return 0;
}
答案:64 位下 8 5
解析:s 是指针,大小 8;strlen 数到 '\0',长度 5。
练习 4
c
#include <stdio.h>
int main() {
int a[5];
printf("%zu %zu %zu\n", sizeof(a), sizeof(a + 0), sizeof(*a));
return 0;
}
答案:64 位下 20 8 4
解析:a 是 int[5],5 × 4 = 20;a + 0 退化为 int*,指针 8;*a 是 int,4。
练习 5
c
#include <stdio.h>
int main() {
char s[] = "abc";
printf("%zu %zu %zu\n", sizeof(&s), sizeof(&s + 1), sizeof(&s[0] + 1));
return 0;
}
答案:64 位下 8 8 8
解析:三个都是指针,大小都是 8。但步长不同:&s + 1 跳过整个数组,&s[0] + 1 跳过一个字符。
练习 6
c
#include <stdio.h>
#include <string.h>
int main() {
char s[] = "abc";
printf("%zu\n", strlen(&s[1]));
return 0;
}
答案:2
解析:从 'b' 开始,剩下 bc,长度 2。
练习 7
下面代码为什么危险?
c
#include <string.h>
int main() {
char *s = "abc";
strlen(*s);
return 0;
}
答案:*s 是字符 'a',不是地址。strlen 会把 'a' 的 ASCII 值当地址使用,导致未定义行为,通常段错误。
练习 8:手写 my_strlen
c
#include <stdio.h>
size_t my_strlen(const char *s) {
const char *p = s;
while (*p != '\0') {
p++;
}
return (size_t)(p - s);
}
int main() {
char s[] = "abcdef";
printf("%zu\n", my_strlen(s));
return 0;
}
答案:输出 6。
练习 9:写一个求数组元素个数的宏
c
#include <stdio.h>
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
int main() {
int a[5];
printf("%zu\n", ARRAY_SIZE(a));
return 0;
}
答案:5
注意:这个宏只在数组名可见的同一作用域有效。如果传进来的是指针,结果就错了。
练习 10:函数参数里的数组
c
#include <stdio.h>
void f(char arr[]) {
printf("%zu\n", sizeof(arr));
}
int main() {
char s[7] = "abcdef";
f(s);
return 0;
}
答案:64 位下 8
解析:函数参数中的 char arr[] 等价于 char *arr,数组名退化成指针,所以 sizeof(arr) 是指针大小,不是 7。
十、最后再叮嘱几句
这四段代码,真正要你掌握的不是死记输出,而是三件事:
-
看类型:
arr、&arr、&arr[0]到底各是什么类型。 -
看退化:数组名什么时候退化成指针。
-
看边界:
strlen只接受合法字符串地址,传字符、传char**、传越界地址都是未定义行为。
如果面试官问你:“sizeof(arr) 和 sizeof(&arr) 有什么区别?”
你就答:前者是整个数组大小,后者是指向数组的指针大小。
如果问:“strlen(&p) 输出什么?”
你就答:未定义行为,因为 &p 不是字符串地址,而是指针变量自己的地址。
如果问:“为什么 strlen(*arr) 会崩?”
你就答:*arr 是字符,不是地址,strlen 把 ASCII 码当地址用了。
把这几句话吃透,这组题就不再是“混合双打”,而是你面试时的送分题。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2303_77568009/article/details/166601681



