C 语言指针拔高篇(五):sizeof 与 strlen 辨析 + 经典笔试题通关指南
前四篇我们从指针基础一路进阶到回调函数与qsort,但想要真正吃透指针,还得攻克两个核心难点:sizeof与strlen的区别、数组指针笔试题的底层逻辑。这两个知识点是面试高频考点,也是很多同学的 “易错重灾区”。
本篇作为指针系列的拔高篇,会通过概念辨析 + 代码实战 + 笔试题深度解析的方式,帮你彻底打通任督二脉!
目录
一、 sizeof 与 strlen 的深度辨析
1.1 sizeof:计算内存大小的操作符
1.2 strlen:计算字符串长度的库函数
1.3 sizeof 与 strlen 核心对比表
二、 数组与指针经典笔试题解析
2.1 一维数组笔试题
2.2 字符数组笔试题(4 种场景)
场景 1:无\0的字符数组
场景 2:有\0的字符数组(字符串)
场景 3:字符指针指向字符串
2.3 二维数组笔试题
三、 指针运算经典笔试题解析
3.1 题目 1:数组地址的加减运算
3.2 题目 2:结构体指针的运算(32位)
3.3 题目 3:逗号表达式的坑
3.4 题目 4:指针相减的本质
3.5 题目 5:二维数组的数组地址
3.6 题目 6:指针数组的遍历(阿里巴巴笔试题)
3.7 题目 7:三级指针的终极考验
四、 核心知识点总结
一、 sizeof 与 strlen 的深度辨析
sizeof和strlen是 C 语言中最容易混淆的两个工具,一个是操作符,一个是库函数,核心功能天差地别。
1.1 sizeof:计算内存大小的操作符
- 本质:C 语言内置操作符,不是函数,不需要头文件。
- 功能:计算操作数所占内存的字节数,只关心内存大小,不关心内存中存放的数据。
- 操作数:可以是变量、类型、数组名等。
-
关键特性:
sizeof(数组名)中,数组名代表整个数组,计算的是数组总字节数。
代码示例:
#include <stdio.h>
int main()
{
int a = 10;
// 计算变量a的大小:int占4字节
printf("%zd\n", sizeof(a));
// 等价写法,操作数是变量时可省略括号
printf("%zd\n", sizeof a);
// 计算int类型的大小
printf("%zd\n", sizeof(int));
int arr[10] = {0};
// 数组名代表整个数组,10*4=40字节
printf("%zd\n", sizeof(arr));
return 0;
}
运行结果(32/64 位平台一致):

1.2 strlen:计算字符串长度的库函数
-
本质:C 语言标准库函数,定义在
<string.h>头文件中。 -
功能:计算字符串中
\0之前的字符个数,只针对字符串有效。 -
关键特性:
- 必须接收
char*类型的参数(字符串首地址)。 - 从起始地址开始向后查找,直到遇到
\0才停止。 - 如果内存中没有
\0,会越界访问,结果是随机值。
- 必须接收
代码示例:
#include <stdio.h>
#include <string.h>
int main()
{
// 无\0,strlen会越界,结果随机
char arr1[3] = {'a', 'b', 'c'};
// 有\0,字符串长度是3
char arr2[] = "abc";
printf("strlen(arr1) = %zd\n", strlen(arr1));
printf("strlen(arr2) = %zd\n", strlen(arr2));
printf("sizeof(arr1) = %zd\n", sizeof(arr1));
printf("sizeof(arr2) = %zd\n", sizeof(arr2));
return 0;
}
运行结果(示例):

注意:上面运行结果strlen(arr1)=35是随机值,取决于内存中后续的\0位置
1.3 sizeof 与 strlen 核心对比表

二、 数组与指针经典笔试题解析
笔试题的核心考点只有一个:数组名的本质。回顾之前的结论:
数组名在绝大多数情况下是首元素地址,但有两个例外:
sizeof(数组名):数组名代表整个数组,计算总字节数。&数组名:数组名代表整个数组,取出的是整个数组的地址。
下面我们分三类数组,逐个解析经典笔试题。
2.1 一维数组笔试题
题目代码:
#include <stdio.h>
int main()
{
int a[] = {1,2,3,4};
printf("1. %zd\n", sizeof(a));
printf("2. %zd\n", sizeof(a+0));
printf("3. %zd\n", sizeof(*a));
printf("4. %zd\n", sizeof(a+1));
printf("5. %zd\n", sizeof(a[1]));
printf("6. %zd\n", sizeof(&a));
printf("7. %zd\n", sizeof(*&a));
printf("8. %zd\n", sizeof(&a+1));
printf("9. %zd\n", sizeof(&a[0]));
printf("10. %zd\n", sizeof(&a[0]+1));
return 0;
}
逐题解析(32 位平台指针大小为 4,64 位为 8,以下以 32 位为例):
-
sizeof(a):a是数组名,代表整个数组 →4*4=16 -
sizeof(a+0):a+0是首元素地址(指针)→4 -
sizeof(*a):*a等价于a[0],是 int 类型 →4 -
sizeof(a+1):a+1是第二个元素的地址(指针)→4 -
sizeof(a[1]):a[1]是 int 类型变量 →4 -
sizeof(&a):&a是整个数组的地址(指针)→4 -
sizeof(*&a):&a是数组地址,*&a等价于a,代表整个数组 →16 -
sizeof(&a+1):&a+1是跳过整个数组后的地址(指针)→4 -
sizeof(&a[0]):&a[0]是首元素地址(指针)→4 -
sizeof(&a[0]+1):&a[0]+1是第二个元素地址(指针)→4
2.2 字符数组笔试题(3 种场景)
字符数组的笔试题是面试重点,核心区别在于是否包含\0,以及是数组还是指针。
场景 1:无\0的字符数组
#include <stdio.h>
#include <string.h>
int main()
{
char arr[] = {'a','b','c','d','e','f'};
// ====== sizeof系列 ======
printf("1. %zd\n", sizeof(arr)); // 数组总大小:6*1=6
printf("2. %zd\n", sizeof(arr+0)); // 首元素地址(指针)→4
printf("3. %zd\n", sizeof(*arr)); // 首元素a,char→1
printf("4. %zd\n", sizeof(arr[1])); // 元素b,char→1
printf("5. %zd\n", sizeof(&arr)); // 数组地址(指针)→4
printf("6. %zd\n", sizeof(&arr+1)); // 跳过数组后的地址→4
printf("7. %zd\n", sizeof(&arr[0]+1));// 第二个元素地址→4
// ====== strlen系列 ======
printf("8. %zd\n", strlen(arr)); // 无\0,越界→随机值
printf("9. %zd\n", strlen(arr+0)); // 同8→随机值
// printf("10. %zd\n", strlen(*arr)); // 错误:*arr是'a',ASCII值97,当成地址会崩溃
// printf("11. %zd\n", strlen(arr[1]));// 错误:同理,'b'是98,非法地址
printf("12. %zd\n", strlen(&arr)); // 数组地址等价于首元素地址→随机值
printf("13. %zd\n", strlen(&arr+1)); // 跳过数组后的地址→随机值
printf("14. %zd\n", strlen(&arr[0]+1));// 从b开始→随机值-1
return 0;
}
场景 2:有\0的字符数组(字符串)
#include <stdio.h>
#include <string.h>
int main()
{
char arr[] = "abcdef"; // 隐含\0,总长度7
// ====== sizeof系列 ======
printf("1. %zd\n", sizeof(arr)); // 整个数组→7
printf("2. %zd\n", sizeof(arr+0)); // 首元素地址→4
printf("3. %zd\n", sizeof(*arr)); // 首元素a→1
printf("4. %zd\n", sizeof(arr[1])); // 元素b→1
printf("5. %zd\n", sizeof(&arr)); // 数组地址→4
printf("6. %zd\n", sizeof(&arr+1)); // 跳过数组后的地址→4
printf("7. %zd\n", sizeof(&arr[0]+1));// 第二个元素地址→4
// ====== strlen系列 ======
printf("8. %zd\n", strlen(arr)); // \0前字符数→6
printf("9. %zd\n", strlen(arr+0)); // 同8→6
// printf("10. %zd\n", strlen(*arr)); // 错误:非法地址
// printf("11. %zd\n", strlen(arr[1]));// 错误:非法地址
printf("12. %zd\n", strlen(&arr)); // 等价于首元素地址→6
printf("13. %zd\n", strlen(&arr+1)); // 跳过数组后的地址→随机值
printf("14. %zd\n", strlen(&arr[0]+1));// 从b开始→5
return 0;
}
场景 3:字符指针指向字符串
#include <stdio.h>
#include <string.h>
int main()
{
char *p = "abcdef"; // p是指针,指向字符串首地址
// ====== sizeof系列 ======
printf("1. %zd\n", sizeof(p)); // 指针大小→4
printf("2. %zd\n", sizeof(p+1)); // p+1指向b,指针→4
printf("3. %zd\n", sizeof(*p)); // *p是a,char→1
printf("4. %zd\n", sizeof(p[0])); // p[0]等价于*p→1
printf("5. %zd\n", sizeof(&p)); // &p是指针的地址→4
printf("6. %zd\n", sizeof(&p+1)); // 跳过p后的地址→4
printf("7. %zd\n", sizeof(&p[0]+1)); // 指向b的地址→4
// ====== strlen系列 ======
printf("8. %zd\n", strlen(p)); // 字符串长度→6
printf("9. %zd\n", strlen(p+1)); // 从b开始→5
// printf("10. %zd\n", strlen(*p)); // 错误:非法地址
// printf("11. %zd\n", strlen(p[0])); // 错误:非法地址
printf("12. %zd\n", strlen(&p)); // &p是指针地址,和字符串无关→随机值
printf("13. %zd\n", strlen(&p+1)); // 跳过p后的地址→随机值
printf("14. %zd\n", strlen(&p[0]+1)); // 从b开始→5
return 0;
}
2.3 二维数组笔试题
二维数组的核心理解:二维数组是 “数组的数组”,比如int a[3][4],a的每个元素都是int[4]的一维数组。(32位)
题目代码:
#include <stdio.h>
int main()
{
int a[3][4] = {0};
printf("1. %zd\n", sizeof(a)); // 整个数组:3*4*4=48
printf("2. %zd\n", sizeof(a[0][0])); // 首元素,int→4
printf("3. %zd\n", sizeof(a[0])); // a[0]是第一行,一维数组→4*4=16
printf("4. %zd\n", sizeof(a[0]+1)); // a[0]+1是第一行第二个元素地址→4
printf("5. %zd\n", sizeof(*(a[0]+1)));// 第一行第二个元素→4
printf("6. %zd\n", sizeof(a+1)); // a+1是第二行地址→4
printf("7. %zd\n", sizeof(*(a+1))); // 第二行整个数组→16
printf("8. %zd\n", sizeof(&a[0]+1)); // &a[0]+1是第二行地址→4
printf("9. %zd\n", sizeof(*(&a[0]+1)));// 第二行整个数组→16
printf("10. %zd\n", sizeof(*a)); // *a等价于a[0],第一行→16
printf("11. %zd\n", sizeof(a[3])); // a[3]是int[4]类型→16(编译时计算,不越界)
return 0;
}
三、 指针运算经典笔试题解析
指针运算的核心是步长由指针类型决定,下面 7 道题是面试高频题,我们逐一解析。
3.1 题目 1:数组地址的加减运算
#include <stdio.h>
int main()
{
int a[5] = { 1, 2, 3, 4, 5 };
int *ptr = (int *)(&a + 1);
printf("%d,%d", *(a + 1), *(ptr - 1));
return 0;
}
解析:
-
&a是整个数组的地址,&a+1跳过整个数组,指向数组末尾的下一个位置。 -
ptr强制转换为int*,ptr-1指向数组最后一个元素5。 -
*(a+1)等价于a[1],值为2。结果:2,5
3.2 题目 2:结构体指针的运算(32位)
#include <stdio.h>
// 结构体大小20字节(假设)
struct Test
{
int Num;
char *pcName;
short sDate;
char cha[2];
short sBa[4];
}*p = (struct Test*)0x100000;
int main()
{
printf("%p\n", p + 0x1);
printf("%p\n", (unsigned long)p + 0x1);
printf("%p\n", (unsigned int*)p + 0x1);
return 0;
}
解析:
-
p是结构体指针,+1跳过整个结构体(20 字节)→0x100000 + 20 = 0x100014 -
(unsigned long)p是数值,+1就是数值 + 1 →0x100001 -
(unsigned int*)p是 int 指针,+1跳过 4 字节 →0x100004
3.3 题目 3:逗号表达式的坑
#include <stdio.h>
int main()
{
int a[3][2] = { (0, 1), (2, 3), (4, 5) };
int *p = a[0];
printf("%d", p[0]);
return 0;
}
解析:
- 注意初始化列表中的是逗号表达式,
(0,1)结果是1,(2,3)结果是3,(4,5)结果是5。 - 数组实际初始化:
a[3][2] = {1,3,5},其余元素默认 0。 -
p[0]等价于a[0][0],值为1。结果:1
3.4 题目 4:指针相减的本质
#include <stdio.h>
int main()
{
int a[5][5];
int(*p)[4] = a;
printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
return 0;
}
解析:
-
p是int(*)[4]类型,p[i]跳过4*i字节;a是int[5][5],a[i]跳过5*i字节。 -
&p[4][2]和&a[4][2]的地址差为-4(十进制)。 - 指针相减的结果是元素个数差,以补码形式存储,十六进制为
0xfffffffc。
结果:fffffffc,-4
3.5 题目 5:二维数组的数组地址
#include <stdio.h>
int main()
{
int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int *ptr1 = (int *)(&aa + 1);
int *ptr2 = (int *)(*(aa + 1));
printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));
return 0;
}
解析:
-
&aa是整个二维数组的地址,&aa+1跳过整个数组,ptr1-1指向最后一个元素10。 -
aa+1是第二行地址,*(aa+1)等价于aa[1],指向第二行首元素6,ptr2-1指向5。结果:10,5
3.6 题目 6:指针数组的遍历(阿里巴巴笔试题)
#include <stdio.h>
int main()
{
char *a[] = {"work","at","alibaba"};
char**pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}
解析:
-
a是指针数组,a[0]指向"work",a[1]指向"at",a[2]指向"alibaba"。 -
pa是二级指针,pa++指向a[1],*pa就是"at"。结果:at
3.7 题目 7:三级指针的终极考验
#include <stdio.h>
int main()
{
char *c[] = {"ENTER","NEW","POINT","FIRST"};
char**cp[] = {c+3,c+2,c+1,c};
char***cpp = cp;
printf("%s\n", **++cpp);
printf("%s\n", *--*++cpp+3);
printf("%s\n", *cpp[-2]+3);
printf("%s\n", cpp[-1][-1]+1);
return 0;
}
解析(核心是指针的加减和取值):
-
++cpp指向cp[1](c+2),**cpp是c[2]指向的"POINT"→ 输出POINT。 -
++cpp指向cp[2],*cpp是c+1,--*cpp是c,*c是"ENTER",+3后是"ER"→ 输出ER。 -
cpp[-2]等价于*(cpp-2),指向cp[0],*cp[0]是"FIRST",+3后是"ST"→ 输出ST。 -
cpp[-1]是cp[2],cpp[-1][-1]是c[1]指向的"NEW",+1后是"EW"→ 输出EW。
四、 核心知识点总结
-
sizeof与strlen的核心区别:sizeof算内存字节数,不关心内容;strlen算字符串长度,必须有\0。 -
数组名的两个例外:
sizeof(数组名)和&数组名中,数组名代表整个数组,其余情况都是首元素地址。 -
指针运算的步长:指针 ± 整数的步长由指针类型决定,比如
int*+1 跳 4 字节,char*+1 跳 1 字节。 - 指针相减的本质:两个同类型指针相减,结果是元素个数差,不是地址差。
如果本篇内容对你有帮助,欢迎点赞、收藏、关注!有任何疑问,评论区留言交流~