free-elcmacom头像
关注

sizeof 和 strlen 到底差在哪?四段代码把 C 语言数组指针按在地上摩擦(附 64 位输出 + 面试题)

先说结论:这四段代码,表面上是“打印几个数”,实际上是 C 语言笔试面试里最经典的“指针类型识别 + 数组退化 + 未定义行为”三合一。很多人第一次做,能把 strlen(*arr) 写成 1,把 sizeof(&arr + 1) 写成 8 却不知道为什么,最后面试官一句“你确定?”就开始冒汗。

今天我不只给你答案,还要把背后的“地图”铺开。教育学里有个词叫“先行组织者”,意思是先给你一个认知框架,再往里填细节。否则你一次面对 sizeof、strlen、数组名退化、指针加减、字符串字面量,认知负荷直接拉满,看多少遍都容易忘。

所以先记住三句话:

  1. sizeof 是运算符,不是函数。它只看类型,不看里面存了什么字符串。

  2. strlen 是函数。它从你给的地址开始,一个字节一个字节找 '\0'。

  3. 数组名在大多数表达式里会退化成首元素指针,但 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)char1*arr 就是 arr[0]
sizeof(arr[1])char1单个字符
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)char1p 指向的字符 'a'
sizeof(p[0])char1等价 *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 甚至别的数,都不要惊讶。这不是“答案”,这是未定义行为在碰运气。


六、面试笔试高频清单:这题到底在考什么?

把上面四段代码压缩成一张清单,你复习时直接背这张:

  1. sizeof 是运算符,strlen 是函数。

  2. sizeof 只看类型,编译期求值;strlen 运行期找 '\0'。

  3. 数组名在大多数表达式里退化为首元素指针。

  4. sizeof(arr) 不退化为指针,得到整个数组大小。

  5. &arr 类型是 char (*)[N],&arr + 1 跳过整个数组。

  6. &arr[0] 是首元素指针,&arr[0] + 1 跳过一个元素。

  7. strlen 参数必须是合法的、以 '\0' 结尾的字符串地址。

  8. strlen(*arr)、strlen(arr[1])、strlen(*p) 都是把字符当地址,未定义行为。

  9. 字符串字面量不可修改,建议写 const char* p = "abcdef";。

  10. printf 打印 sizeof/strlen 返回值用 %zu,不要用 %d。

  11. 函数参数里的 char arr[] 会退化成 char*,sizeof(arr) 得到指针大小。

  12. 未定义行为不是“结果随机”,而是“标准不保证任何结果”,可能崩溃,也可能被优化成奇怪逻辑。


七、为什么 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。


十、最后再叮嘱几句

这四段代码,真正要你掌握的不是死记输出,而是三件事:

  1. 看类型:arr、&arr、&arr[0] 到底各是什么类型。

  2. 看退化:数组名什么时候退化成指针。

  3. 看边界: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

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--