C 语言指针拔高篇(五):sizeof 与 strlen 辨析 + 经典笔试题通关指南

前四篇我们从指针基础一路进阶到回调函数与qsort,但想要真正吃透指针,还得攻克两个核心难点:sizeofstrlen的区别数组指针笔试题的底层逻辑。这两个知识点是面试高频考点,也是很多同学的 “易错重灾区”。

本篇作为指针系列的拔高篇,会通过概念辨析 + 代码实战 + 笔试题深度解析的方式,帮你彻底打通任督二脉!

目录

一、 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 的深度辨析

sizeofstrlen是 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 位平台一致):

C 语言指针拔高篇(五):sizeof 与 strlen 辨析 + 经典笔试题通关指南

1.2 strlen:计算字符串长度的库函数

  • 本质:C 语言标准库函数,定义在 <string.h> 头文件中。
  • 功能:计算字符串中\0之前的字符个数,只针对字符串有效。
  • 关键特性

    1. 必须接收char*类型的参数(字符串首地址)。
    2. 从起始地址开始向后查找,直到遇到\0才停止。
    3. 如果内存中没有\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;
}

运行结果(示例):

C 语言指针拔高篇(五):sizeof 与 strlen 辨析 + 经典笔试题通关指南

注意:上面运行结果strlen(arr1)=35随机值,取决于内存中后续的\0位置

1.3 sizeof 与 strlen 核心对比表

C 语言指针拔高篇(五):sizeof 与 strlen 辨析 + 经典笔试题通关指南

二、 数组与指针经典笔试题解析

笔试题的核心考点只有一个:数组名的本质。回顾之前的结论:

数组名在绝大多数情况下是首元素地址,但有两个例外:

  1. sizeof(数组名):数组名代表整个数组,计算总字节数。
  2. &数组名:数组名代表整个数组,取出的是整个数组的地址

下面我们分三类数组,逐个解析经典笔试题。

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 位为例):

  1. sizeof(a)a是数组名,代表整个数组 → 4*4=16
  2. sizeof(a+0)a+0是首元素地址(指针)→ 4
  3. sizeof(*a)*a等价于a[0],是 int 类型 → 4
  4. sizeof(a+1)a+1是第二个元素的地址(指针)→ 4
  5. sizeof(a[1])a[1]是 int 类型变量 → 4
  6. sizeof(&a)&a是整个数组的地址(指针)→ 4
  7. sizeof(*&a)&a是数组地址,*&a等价于a,代表整个数组 → 16
  8. sizeof(&a+1)&a+1是跳过整个数组后的地址(指针)→ 4
  9. sizeof(&a[0])&a[0]是首元素地址(指针)→ 4
  10. 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;
}

解析

  1. &a是整个数组的地址,&a+1跳过整个数组,指向数组末尾的下一个位置。
  2. ptr强制转换为int*ptr-1指向数组最后一个元素5
  3. *(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;
}

解析

  1. p是结构体指针,+1跳过整个结构体(20 字节)→ 0x100000 + 20 = 0x100014
  2. (unsigned long)p是数值,+1就是数值 + 1 → 0x100001
  3. (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;
}

解析

  1. 注意初始化列表中的是逗号表达式(0,1)结果是1(2,3)结果是3(4,5)结果是5
  2. 数组实际初始化:a[3][2] = {1,3,5},其余元素默认 0。
  3. 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;
}

解析

  1. pint(*)[4]类型,p[i]跳过4*i字节;aint[5][5]a[i]跳过5*i字节。
  2. &p[4][2]&a[4][2]的地址差为-4(十进制)。
  3. 指针相减的结果是元素个数差,以补码形式存储,十六进制为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;
}

解析

  1. &aa是整个二维数组的地址,&aa+1跳过整个数组,ptr1-1指向最后一个元素10
  2. aa+1是第二行地址,*(aa+1)等价于aa[1],指向第二行首元素6ptr2-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;
}

解析

  1. a是指针数组,a[0]指向"work"a[1]指向"at"a[2]指向"alibaba"
  2. 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;
}

解析(核心是指针的加减和取值):

  1. ++cpp指向cp[1]c+2),**cppc[2]指向的"POINT" → 输出POINT
  2. ++cpp指向cp[2]*cppc+1--*cppc*c"ENTER"+3后是"ER" → 输出ER
  3. cpp[-2]等价于*(cpp-2),指向cp[0]*cp[0]"FIRST"+3后是"ST" → 输出ST
  4. cpp[-1]cp[2]cpp[-1][-1]c[1]指向的"NEW"+1后是"EW" → 输出EW

四、 核心知识点总结

  1. sizeofstrlen的核心区别sizeof算内存字节数,不关心内容;strlen算字符串长度,必须有\0
  2. 数组名的两个例外sizeof(数组名)&数组名中,数组名代表整个数组,其余情况都是首元素地址。
  3. 指针运算的步长:指针 ± 整数的步长由指针类型决定,比如int*+1 跳 4 字节,char*+1 跳 1 字节。
  4. 指针相减的本质:两个同类型指针相减,结果是元素个数差,不是地址差。

如果本篇内容对你有帮助,欢迎点赞、收藏、关注!有任何疑问,评论区留言交流~

© 版权声明

相关文章