当前位置: 首页 > news >正文

深入理解指针(最终章):指针运算本质与典型试题剖析

✨ 用 清晰易懂的图解 帮你建立直观认知 ,用通俗的 代码语言 帮你落地理解, 让每个知识点都能 轻松get
🚀 个人主页 :0xCode小新 · CSDN
🌱 代码仓库 :0xCode小新· Gitee
📌 专栏系列

  • 📖 《c语言》

💬 座右铭“ 积跬步,以致千里。”

在之前的学习中,我们已经逐步揭开了指针的神秘面纱,从基本概念到多层指针,从指针与数组的关系到动态内存管理,一步步掌握了指针的核心用法与底层逻辑。指针作为C语言的灵魂,其灵活性与强大功能使我们能够更高效地操作内存与数据结构,但也伴随着复杂性与潜在风险。

本系列最终章——《深入理解指针(5)》将作为指针系列的收官之作,聚焦于sizeofstrlen的深度对比、数组与指针的经典笔试题解析,以及指针运算中的典型陷阱与技巧。通过这些实战性极强的题目,我们将进一步巩固对指针本质的理解,提升代码分析与调试能力,为后续学习复杂数据结构与系统级编程打下坚实基础。

让我们继续深入指针的最后一片领域,彻底掌握这一C语言中最具挑战也最富魅力的特性。


文章目录

  • 1. sizeof和strlen的对比
    • 1.1 sizeof
      • 基本语法
      • 核心特性
      • 注意事项
    • 1.2 strlen
      • 基本语法
      • 核心特性
    • 1.3 sizeof 和 strlen的对比
  • 2.数组和指针笔试题解析
    • 2.1 一维数组
    • 2.2 字符数组
    • 2.3 二维数组
    • 2.4 学习建议
  • 3. 指针运算笔试题解析
    • 3.1 题目1
    • 3.2 题目2
    • 3.3 题目3
    • 3.4 题目4
    • 3.5 题目5
    • 3.6 题目6
    • 3.7 题目7
  • 结语


1. sizeof和strlen的对比

在学习C语言过程中中,sizeofstrlen 这对“孪生兄弟”总是让我们甚至有一定经验的开发者感到困惑。它们看似都与“大小”或“长度”有关,但其背后的原理和适用场景却天差地别,是各类笔试面试中高频出现的经典考点。那么该怎么理解他们才能避免在实际编程和代码阅读时掉入陷阱呢?

1.1 sizeof

sizeof 是 C 语言中的一个运算符(它不是函数),用于计算数据类型或变量所占用的内存字节数。它的结果是一个无符号整数(size_t 类型),具体值取决于操作数的类型和当前系统环境(如 32 位 / 64 位系统)。

基本语法

sizeof 有两种使用形式:

  1. sizeof(数据类型):计算指定数据类型的大小
  2. sizeof(变量名)sizeof 变量名:计算变量所对应类型的大小(括号可省略)
#include <stdio.h>int main() {int a;printf("int 类型大小:%zu\n", sizeof(int));      // 计算类型大小printf("变量 a 的大小:%zu\n", sizeof(a));       // 计算变量大小printf("变量 a 的大小:%zu\n", sizeof a);        // 省略括号(仅变量可用)return 0;
}

核心特性

  1. 编译期计算

sizeof 的结果在编译阶段就已确定,不会在运行时计算。这意味着它不影响程序的运行效率,且不能用于计算动态分配内存(如 malloc 分配的空间)的大小。

示例:

int n = 10;
int arr[n];               // 变长数组
printf("%zu\n", sizeof(arr));  // 编译时无法确定,但 C99 允许运行时计算变长数组的大小
  1. 操作数不执行

sizeof 仅关注操作数的类型,不会执行操作数的表达式。

示例:

int a = 5;
printf("%zu\n", sizeof(a++));  
printf("%d\n", a);             // a 仍为 5,因为 a++ 未执行
  1. 与数组的特殊交互

sizeof 作用于数组名时,计算的是整个数组的总字节数(而非指针大小)。但数组名作为函数参数、参与运算时,会隐式转换为指向首元素的指针。(在之前文章中提到的数组名使用的两种特殊情况之一)

示例:

int arr[5] = {1,2,3,4,5};
printf("%zu\n", sizeof(arr));   // 整个数组大小:5 * 4 = 20
printf("%zu\n", sizeof(arr+0)); // 指针大小(4 或 8),因为 arr 参与了运算
  1. 与指针的关系

所有指针类型的 sizeof 结果在同一系统中相同(与指向的数据类型无关):

  • 32 位系统:指针占 4 字节
  • 64 位系统:指针占 8 字节

示例:

int* p1;
char* p2;
double* p3;
printf("%zu, %zu, %zu\n", sizeof(p1), sizeof(p2), sizeof(p3));  // 均为 4 或 8
  1. 与字符串的交互

字符串字面量(如 "abc")初始化数组时,会包含结尾的 '\0'sizeof 会计算包括 '\0' 在内的总字节数。

示例:

char str[] = "abc";  
printf("%zu\n", sizeof(str));  // 输出 4

注意事项

  • sizeof 的结果类型是 size_t,打印时应使用格式符 %zu(C99 标准),而非 %d(可能导致警告或错误)。
  • 对函数名使用 sizeof 会报错,因为函数名不是数据类型或变量。
  • 对空类型 void 使用 sizeof 是非法的(void 表示 “无类型”)。

1.2 strlen

strlen 是 C 标准库 <string.h> 中提供的一个字符串长度计算函数,其核心作用是:

计算从指定内存地址开始,到第一个 '\0'(字符串结束符)为止的字符个数(不包含 '\0' 本身)

基本语法

函数原型:

size_t strlen(const char *str);
  • 参数 str:指向字符串的指针(通常是字符串首元素地址,如数组名、字符指针)。
  • 返回值:size_t 类型(无符号整数,本质是 unsigned intunsigned long,取决于系统),表示字符串的有效长度(不含 '\0')。

strlen 的逻辑非常简单,本质是 “遍历计数”,步骤如下:

  1. 接收一个起始内存地址(由 str 传入);
  2. 从该地址开始,逐个检查内存中的字节内容;
  3. 遇到第一个 '\0' 时停止遍历;
  4. 统计遍历过的字节数(即有效字符数),作为返回值。

关键注意

strlen 完全依赖 '\0' 判断结束,不检查内存越界—— 如果传入的内存中没有 '\0',它会一直向后遍历,直到随机遇到 '\0' 为止,导致结果为 “随机值”。

核心特性

  1. 仅依赖 '\0' 结束,与 “容器大小(一般是数组)” 无关

strlen 不关心字符串存储在 “数组” 还是 “字符常量区”,也不关心容器(如数组)的实际大小,只认 '\0'

#include <string.h>
#include <stdio.h>int main() {char arr1[] = {'a', 'b', 'c', 'd', 'e', 'f'}; printf("strlen(arr1) = %zu\n", strlen(arr1)); // 随机值(直到内存中随机遇到 '\0')char arr2[] = "abcdef"; printf("strlen(arr2) = %zu\n", strlen(arr2)); // 6char arr3[] = {'a', 'b', 'c', 'd', 'e', 'f', '\0'}; printf("strlen(arr3) = %zu\n", strlen(arr3)); // 6return 0;
}
  1. 参数必须是 “指向字符的有效地址”,不能是 “字符值”

strlen 的参数是 const char *(字符指针),必须传入内存地址;如果传入字符值(如 arr[0]'a'),会导致编译错误或运行崩溃。

char arr[] = "abcdef";
// err:arr[0] 是字符 'a'(值为 97),传入后会被当作内存地址 0x61(非法地址)
printf("%zu\n", strlen(arr[0])); 
// err:'a' 是字符常量,同样会被当作非法地址
printf("%zu\n", strlen('a')); 

运行时会触发 “非法内存访问”,导致程序崩溃。

  1. 返回值是 size_t(无符号整数),注意运算逻辑

size_t 是无符号类型,与有符号整数运算时可能出现逻辑错误(因为无符号数不会为负数)。

#include <string.h>
#include <stdio.h>int main() {char arr1[] = "abc"; char arr2[] = "abcd"; // 错误逻辑:认为 3-4 = -1,判断为真,但实际是无符号运算if (strlen(arr1) - strlen(arr2) > 0) {printf("arr1 更长\n"); // 实际上是会执行的,因为 3-4 = 4294967295(无符号溢出),远大于 0}return 0;
}
  1. 不修改原字符串(参数是 const char *

strlen 的参数被 const 修饰,表示它仅读取字符串内容,不修改原内存

1.3 sizeof 和 strlen的对比

对比维度strlensizeof
本质字符串长度计算函数(需要包含 <string.h>操作符(不是函数),计算 “数据 / 类型的内存大小”
计算对象从指定地址到 '\0' 的有效字符数变量 / 数组 / 类型占用的总字节数(与 '\0' 无关)
对字符串数组的处理只认 '\0',忽略数组实际大小计算整个数组的字节数(如 char arr[]="abc"sizeof(arr)=4,含 '\0'
对指针的处理从指针指向的地址开始找 '\0'计算指针本身的大小(32 位系统 4 字节,64 位系统 8 字节)
返回值类型size_t(无符号整数)size_t(无符号整数)

示例对比:

#include <string.h>
#include <stdio.h>int main() {char arr[] = "abcdef"; printf("strlen(arr) = %zu\n", strlen(arr));  // 6printf("sizeof(arr) = %zu\n", sizeof(arr));  // 7char *p = arr; printf("strlen(p) = %zu\n", strlen(p));      // 6printf("sizeof(p) = %zu\n", sizeof(p));      // 4/8return 0;
}

2.数组和指针笔试题解析

有了上述知识的铺垫,相信大家已经初步理解了sizeof和strlen的区别,接下来我们会面临更加头疼的问题:数组与指针的混合运算

数组和指针密不可分,所以常常放在一起考查,同时又因为其在不同上下文中的微妙变化而布满了陷阱。

不过大家完全不用有压力!其实博主当初学习这块知识时,和现在的大家一样,也总会在代码上犯错 —— 这真的是学习过程中再正常不过的事了。所以针对每一段代码,我都会做非常细致的拆解和讲解,帮大家把知识点理解透、记扎实。

这里再多提一嘴:即便我当时学完了,过了一段时间回头看这些代码,还是会不小心出错。也正因为这样,想跟大家强调:对于学过的知识点,一定要定期复习!大家可以参考艾宾浩斯遗忘曲线来规划复习周期,这样能更高效地巩固记忆,避免学了又忘~
在这里插入图片描述

2.1 一维数组

int a[] = { 1,2,3,4 };
printf("%d\n", sizeof(a));
printf("%d\n", sizeof(a + 0));
printf("%d\n", sizeof(*a));
printf("%d\n", sizeof(a + 1));
printf("%d\n", sizeof(a[1]));
printf("%d\n", sizeof(&a));
printf("%d\n", sizeof(*&a));
printf("%d\n", sizeof(&a + 1));
printf("%d\n", sizeof(&a[0]));
printf("%d\n", sizeof(&a[0] + 1));

看上面的代码,大家先自己独立思考一下程序会输出什么?思考完毕后,再来看解析内容,看看自己的判断是否正确。

代码解析:

  1. printf("%d\n", sizeof(a));
    • a是数组名,sizeof(a)中数组名表示整个数组
    • 数组有 4 个int元素,每个占 4 字节,总大小为4×4=16字节
  2. printf("%d\n", sizeof(a + 0));
    • a + 0中,数组名a参与运算,隐式转换为指向首元素的指针(int*类型)
    • a + 0仍表示首元素地址,sizeof计算指针大小,结果为 4 /8 字节
  3. printf("%d\n", sizeof(*a));
    • a转换为指向首元素的指针,*a表示首元素(a[0]),类型为int
    • 计算int类型大小,结果为 4 字节
  4. printf("%d\n", sizeof(a + 1));
    • a转换为指针,a + 1表示指向第二个元素的指针(int*类型)
    • 指针大小为 4 / 8 字节
  5. printf("%d\n", sizeof(a[1]));
    • a[1]是数组第二个元素,类型为int
    • 结果为int类型大小 4 字节
  6. printf("%d\n", sizeof(&a));
    • &a表示取整个数组的地址,类型为int(*)[4](指向包含 4 个int的数组的指针)
    • 尽管指针类型不同,但指针大小仍为 4 / 8 字节
  7. printf("%d\n", sizeof(*&a));
    • &a是数组指针,*&a等价于a(表示整个数组)
    • 计算整个数组的大小,结果为 16 字节
  8. printf("%d\n", sizeof(&a + 1));
    • &a + 1表示跳过整个数组后的地址(仍为指针类型)
    • 指针大小为 4 / 8 字节
  9. printf("%d\n", sizeof(&a[0]));
    • &a[0]是首元素的地址(int*类型指针)
    • 指针大小为 4 / 8 字节
  10. printf("%d\n", sizeof(&a[0] + 1));
    • &a[0] + 1是第二个元素的地址(int*类型指针)
    • 指针大小为 4 / 8 字节

2.2 字符数组

代码1:

char arr[] = { 'a','b','c','d','e','f' };
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));
  1. printf("%d\n", sizeof(arr));

    • arr是数组名,在sizeof(arr)中,数组名表示整个数组sizeof对数组名的特殊处理)。

    • 数组arr初始化时包含 6 个char类型元素('a''f'),每个char占 1 字节。

    • 总大小为:6 × 1 = 6字节。

  2. printf("%d\n", sizeof(arr + 0));

    • arr + 0中,数组名arr参与运算,此时退化为指向首元素的指针char*类型)。

    • arr + 0等价于首元素'a'的地址,本质是一个指针。

    • 指针大小取决于系统位数:32 位系统为 4 字节,64 位系统为 8 字节。

  3. printf("%d\n", sizeof(*arr));

    • arr退化为首元素指针(char*),*arr表示首元素'a'(等价于arr[0]),类型为char

    • char类型的大小固定为 1 字节,因此结果为 1。

  4. printf("%d\n", sizeof(arr[1]));

    • arr[1]表示数组的第二个元素('b'),类型为char

    • 同样计算char类型的大小,结果为 1 字节。

  5. printf("%d\n", sizeof(&arr));

    • &arr表示取整个数组的地址,类型为char(*)[6](指向包含 6 个char的数组的指针)。

    • 尽管指针类型与char*不同,但所有指针在同一系统中的大小相同(32 位 4 字节,64 位 8 字节)。

  6. printf("%d\n", sizeof(&arr + 1));

    • &arr + 1表示从整个数组地址向后偏移一个数组长度的地址(跳过 6 个char)。

    • 结果仍为一个指针(类型还是char(*)[6]),因此大小为 4 或 8 字节。(&arr + 1不会参与计算,不理解的看下sizeof的核心特性)

  7. printf("%d\n", sizeof(&arr[0] + 1));

    • &arr[0]是首元素'a'的地址(char*类型),&arr[0] + 1表示第二个元素'b'的地址。

    • 本质是char*类型的指针,大小为 4 或 8 字节。

代码2:

char arr[] = {'a','b','c','d','e','f'};
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));
  1. printf("%d\n", strlen(arr));

    • arr作为数组名,退化为指向首元素'a'的指针(char*类型)。

    • strlen'a'开始向后查找'\0',但数组arr初始化时无'\0'(仅包含'a''f')。

    • 会一直遍历到内存中随机出现'\0'为止,结果为随机值。

  2. printf("%d\n", strlen(arr + 0));

    • arr + 0等价于arr,同样是指向'a'的指针。

    • 与上一条逻辑相同,因无'\0',结果为随机值。

  3. printf("%d\n", strlen(*arr));

    • *arr是首元素'a'(ASCII 值为 97),而非地址。

    • strlen要求参数为指针(地址),此处将 97 当作地址访问,属于非法内存操作,可能导致程序崩溃。

  4. printf("%d\n", strlen(arr[1]));

    • arr[1]是元素'b'(ASCII 值为 98),同样不是地址。

    • 与上一条同理,将字符值当作地址传入,属于非法操作,行为未定义。

  5. printf("%d\n", strlen(&arr));

    • &arr是指向整个数组的指针(char(*)[6]类型),但会被隐式转换为char*类型。

    • 实际指向的地址与arr相同(首元素地址),从'a'开始查找'\0',因为无结束符,结果为随机值。

  6. printf("%d\n", strlen(&arr + 1));

    • &arr + 1是跳过整个数组后的地址(指向'f'之后的位置)。

    • strlen从该位置开始查找'\0',位置不确定,结果为随机值。(不检查越界,可以看之前铺垫的strlen知识)

  7. printf("%d\n", strlen(&arr[0] + 1));

    • &arr[0] + 1是指向'b'的指针(char*类型)。

    • 'b'开始查找'\0',因无结束符,结果为随机值。

代码3:

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));
  1. printf("%d\n", sizeof(arr));

    • arr是数组名,在sizeof(arr)中表示整个数组。

    • 字符串字面量"abcdef"包含 6 个可见字符,会自动添加'\0'作为结束符,共 7 个char元素。

    • 每个char占 1 字节,总大小为7 × 1 = 7字节。

  2. printf("%d\n", sizeof(arr + 0));

    • arr + 0中,数组名arr参与运算,退化为指向首元素'a'的指针(char*类型)。

    • 指针大小取决于系统位数:32 位系统为 4 字节,64 位系统为 8 字节。

  3. printf("%d\n", sizeof(*arr));

    • arr退化为首元素指针,*arr等价于arr[0](字符'a'),类型为char

    • char类型大小为 1 字节,结果为 1。

  4. printf("%d\n", sizeof(arr[1]));

    • arr[1]是数组第二个元素(字符'b'),类型为char

    • 计算char类型大小,结果为 1 字节。

  5. printf("%d\n", sizeof(&arr));

    • &arr表示整个数组的地址,类型为char(*)[7](指向包含 7 个char的数组的指针)。

    • 所有指针在同一系统中大小相同,32 位为 4 字节,64 位为 8 字节。

  6. printf("%d\n", sizeof(&arr + 1));

    • &arr + 1是跳过整个数组后的地址,仍为指针类型(char(*)[7])。

    • 指针大小为 4 或 8 字节(取决于系统位数)。

  7. printf("%d\n", sizeof(&arr[0] + 1));

  • &arr[0] + 1是指向第二个元素'b'的指针(char*类型)。
  • 指针大小为 4 或 8 字节(取决于系统位数)。

代码4:

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));
  1. printf("%d\n", strlen(arr));

    • arr退化为指向首元素'a'的指针(char*类型)。

    • 字符串"abcdef"包含'\0'结束符,strlen'a'开始计数,到'\0'停止(不包含'\0')。

    • 有效字符为'a''f'共 6 个,结果为 6。

  2. printf("%d\n", strlen(arr + 0));

    • arr + 0等价于arr,同样指向'a'的指针。

    • 与上一条逻辑相同,从'a'开始到'\0'结束,结果为 6。

  3. printf("%d\n", strlen(*arr));

    • *arr是首元素'a'(ASCII 值为 97),而非地址。

    • strlen要求参数为指针,此处将 97 当作地址访问,属于非法内存操作,可能导致程序崩溃。

  4. printf("%d\n", strlen(arr[1]));

    • arr[1]是元素'b'(ASCII 值为 98),不是地址。

    • 与上一条同理,将字符值当作地址传入,属于非法操作,行为未定义。

  5. printf("%d\n", strlen(&arr));

    • &arr是指向整个数组的指针(char(*)[7]类型),会隐式转换为char*类型。

    • 实际指向地址与arr相同(首元素'a'的地址),从'a''\0'共 6 个有效字符,结果为 6。

  6. printf("%d\n", strlen(&arr + 1));

    • &arr + 1是跳过整个数组后的地址(指向'\0'之后的位置)。

    • strlen从该位置开始查找'\0',因未知后续内存中'\0'的位置,结果为随机值。

  7. printf("%d\n", strlen(&arr[0] + 1));

    • &arr[0] + 1是指向'b'的指针(char*类型)。

    • 'b'开始计数到'\0',有效字符为'b''f'共 5 个,结果为 5。

代码5:

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));
  1. printf("%d\n", sizeof(p));

    • p是字符指针变量,指向字符串常量"abcdef"的首地址。

    • sizeof(p)计算的是指针变量本身的大小,与指向的数据无关。

    • 32 位系统中指针占 4 字节,64 位系统中占 8 字节。

  2. printf("%d\n", sizeof(p + 1));

    • p + 1表示指针向后偏移 1 个char类型的地址(指向'b'),仍为指针类型(char*)。

    • 指针大小取决于系统位数,结果为 4 或 8 字节。

  3. printf("%d\n", sizeof(*p));

    • *p表示指针p指向的首元素('a'),类型为char

    • char类型的大小固定为 1 字节,结果为 1。

  4. printf("%d\n", sizeof(p[0]));

    • p[0]等价于*(p + 0),表示首元素'a',类型为char

    • 结果为char类型的大小 1 字节。

  5. printf("%d\n", sizeof(&p));

    • &p表示取指针变量p本身的地址,类型为char**(指向指针的指针)。

    • 无论指针层级如何,同一系统中所有指针大小相同,结果为 4 或 8 字节。

  6. printf("%d\n", sizeof(&p + 1));

    • &p + 1表示指针p的地址向后偏移 1 个char*类型的地址,仍为指针类型(char**)。

    • 指针大小为 4 或 8 字节(取决于系统位数)。

  7. printf("%d\n", sizeof(&p[0] + 1));

    • &p[0]等价于p(指向'a'的指针),&p[0] + 1指向'b',类型为char*

    • 指针大小为 4 或 8 字节(取决于系统位数)。

代码6:

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));
  1. printf("%d\n", strlen(p));

    • p是指向字符串"abcdef"首元素'a'的指针(char*类型)。

    • strlen'a'开始计数,直到遇到'\0'停止,字符串包含'a''f'共 6 个有效字符,结果为 6。

  2. printf("%d\n", strlen(p + 1));

    • p + 1指向字符串中'b'的地址(char*类型)。

    • 'b'开始计数到'\0',有效字符为'b''f'共 5 个,结果为 5。

  3. printf("%d\n", strlen(*p));

    • *pp指向的首元素'a'(ASCII 值为 97),并非地址。

    • strlen要求参数为指针(地址),此处将 97 当作地址访问,属于非法内存操作,可能导致程序崩溃。

  4. printf("%d\n", strlen(p[0]));

    • p[0]等价于*p,即字符'a'(ASCII 值 97),不是地址。

    • 与上一条同理,将字符值当作地址传入,属于非法操作,行为未定义。

  5. printf("%d\n", strlen(&p));

    • &p是指针变量p本身的地址(char**类型),会被隐式转换为char*类型。

    • strlen从该地址开始查找'\0',但此处内存中是否有'\0'及位置未知,结果为随机值。

  6. printf("%d\n", strlen(&p + 1));

    • &p + 1是指针p地址向后偏移一个char*类型的地址,仍为指针(char**类型)。

    • strlen从该位置开始查找'\0',因内存内容未知,结果为随机值。

  7. printf("%d\n", strlen(&p[0] + 1));

    • &p[0]等价于p(指向'a'),&p[0] + 1指向'b'char*类型)。

    • 'b'开始计数到'\0',有效字符为 5 个,结果为 5。

2.3 二维数组

int a[3][4] = {0};
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(a[0][0]));
printf("%d\n",sizeof(a[0]));
printf("%d\n",sizeof(a[0]+1));
printf("%d\n",sizeof(*(a[0]+1)));
printf("%d\n",sizeof(a+1));
printf("%d\n",sizeof(*(a+1)));
printf("%d\n",sizeof(&a[0]+1));
printf("%d\n",sizeof(*(&a[0]+1)));
printf("%d\n",sizeof(*a));
printf("%d\n",sizeof(a[3]));
  1. printf("%d\n", sizeof(a));

    • a是二维数组名,在sizeof(a)中表示整个二维数组。

    • 数组定义为int a[3][4],包含 3 行 4 列共 12 个int元素,每个int占 4 字节。

    • 总大小为:3 × 4 × 4 = 48字节。

  2. printf("%d\n", sizeof(a[0][0]));

    • a[0][0]是二维数组第 0 行第 0 列的元素,类型为int

    • int类型大小为 4 字节,结果为 4。

  3. printf("%d\n", sizeof(a[0]));

    • a[0]表示二维数组的第 0 行(可看作一个一维数组int[4])。

    • 每行包含 4 个int元素,大小为:4 × 4 = 16字节。

  4. printf("%d\n", sizeof(a[0] + 1));

    • a[0]作为一维数组名参与运算,退化为指向第 0 行首元素的指针(int*类型)。

    • a[0] + 1指向第 0 行第 1 列元素,仍为指针类型,大小为 4 或 8 字节(取决于系统位数)。

  5. printf("%d\n", sizeof(*(a[0] + 1)));

    • a[0] + 1是指向第 0 行第 1 列元素的指针,*(a[0] + 1)表示该元素(int类型)。

    • 结果为int类型的大小 4 字节。

  6. printf("%d\n", sizeof(a + 1));

    • a作为二维数组名参与运算,退化为指向第 0 行的指针(int(*)[4]类型,数组指针)。

    • a + 1指向第 1 行,仍为数组指针类型,大小为 4 或 8 字节(取决于系统位数)。

  7. printf("%d\n", sizeof(*(a + 1)));

    • a + 1是指向第 1 行的数组指针,*(a + 1)表示第 1 行整个一维数组(int[4]类型)。

    • 每行大小为 16 字节,结果为 16。

  8. printf("%d\n", sizeof(&a[0] + 1));

    • &a[0]是指向第 0 行的数组指针(int(*)[4]类型)。

    • &a[0] + 1指向第 1 行,仍为数组指针类型,大小为 4 或 8 字节(取决于系统位数)。

  9. printf("%d\n", sizeof(*(&a[0] + 1)));

    • &a[0] + 1是指向第 1 行的数组指针,*(&a[0] + 1)表示第 1 行整个一维数组。

    • 每行大小为 16 字节,结果为 16。

  10. printf("%d\n", sizeof(*a));

    • a退化为指向第 0 行的数组指针,*a表示第 0 行整个一维数组(int[4]类型)。

    • 大小为 16 字节。

  11. printf("%d\n", sizeof(a[3]));

    • a[3]看似越界访问(数组只有 3 行,索引 0~2),但sizeof仅计算类型大小,不实际访问内存。

    • a[3]的类型与其他行一致(int[4]),大小为 16 字节。

2.4 学习建议

面对这些题目,切忌死记硬背答案。最好的方法是:

  1. 画内存布局图:将数组、指针、字符串在内存中的样子画出来。
  2. 写出每一步的类型:仔细分析每个表达式的类型是什么(int*? int(*)[4]?),+1操作会跳过多少字节。
  3. 结合编译器验证:自己编写代码并打印结果和地址(%p),观察地址的变化是否与你的分析一致。

那么现在通过这种抽丝剥茧式的分析,你会发现这些题目的规律性极强。掌握这些规律,是你理解sizeofstrlen的关键

3. 指针运算笔试题解析

在理解了上面的题目后我们会发现,对指针的深入掌握,绝不是靠死记硬背几条规则就能实现的。它更需要我们沉下心来,细致拆解每一层间接引用的逻辑,这样才能真正吃透指针的核心原理。

本讲中的指针运算笔试题,正是为此而设计的。它们看似复杂甚至刁钻,实则是对指针核心概念最凝练的考察:

  • 指针的类型决定了指针算术运算的步长
  • 数组名在大多数情况下会“退化”为首元素的地址。
  • &(取地址)和 *(解引用)操作符的深层含义。
  • 多级指针与指针数组的协同工作方式。

这些复杂的题目,你第一次看到时觉得无从下手完全没关系 —— 当初刚接触的我,也和现在的你们一样。接下来我会先带着大家一步步拆解、理解,不过最后还是希望大家能试着独立思考这些题目,尤其是要能清晰地画出每一块内存的布局,这才是真正掌握知识点的关键。

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 + 1:数组指针&a向后偏移 1 个数组长度(即跳过整个数组的 5 个元素),指向数组a最后一个元素(5)的下一个位置。
  • int *ptr = (int *)(&a + 1):将&a + 1的结果(也就是数组指针类型)强制转换为int*类型,赋值给ptr。此时ptr指向数组a末尾的下一个位置。
  • *(a + 1)a 退化为指向首元素的指针(int*),a + 1 指向第二个元素(2),解引用后结果为 2
  • *(ptr - 1)ptrint*类型指针,ptr - 1 向前偏移 1 个int大小,指向数组最后一个元素(5),解引用后结果为 5
    在这里插入图片描述

3.2 题目2

//在X86环境下
//假设结构体的⼤⼩是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;
}

在 X86 环境下(32 位系统,指针大小为 4 字节),结合结构体大小为 20 字节的前提,我们来逐条分析:

  1. printf("%p\n", p + 0x1); // 00100014
    • pstruct Test*类型的指针(指向结构体的指针)。
    • 指针加法运算时,偏移量 = 指针类型大小 × 增量。此处增量为0x1(即 1),结构体大小为 20 字节(0x14)。
    • 计算:0x100000 + (20 × 1) = 0x100000 + 0x14 = 0x100014,输出格式化为%p00100014
  2. printf("%p\n", (unsigned long)p + 0x1); // 00100001
    • (unsigned long)p将指针p的地址值(0x100000)强制转换为无符号长整型(数值)。
    • 数值加法直接累加:0x100000 + 0x1 = 0x100001,输出为00100001
  3. printf("%p\n", (unsigned int*)p + 0x1); // 00100004
    • (unsigned int*)pp强制转换为unsigned int*类型(指向无符号整数的指针)。
    • X86 环境下unsigned int大小为 4 字节,指针加法偏移量 = 4 × 1 = 4 字节(0x4)。
    • 计算:0x100000 + 0x4 = 0x100004,输出为00100004

在这里插入图片描述

3.3 题目3

#include <stdio.h>
int main()
{int a[3][2] = { (0, 1), (2, 3), (4, 5) };int* p;p = a[0];printf("%d", p[0]);return 0;
}
  1. 数组初始化的关键问题

    • 数组定义为 int a[3][2] = { (0, 1), (2, 3), (4, 5) },但这里使用的是逗号表达式而非花括号初始化。(肯定有很多人掉进这个坑)
    • 逗号表达式的结果是最后一个表达式的值,因此:
      • (0, 1) 的结果为 1
      • (2, 3) 的结果为 3
      • (4, 5) 的结果为 5
    • 数组实际初始化后的值为:a[3][2] = { {1, 3}, {5, 0}, {0, 0} }

    在这里插入图片描述

  2. 指针赋值与访问

    • p = a[0] 中,a[0] 是二维数组第 0 行的数组名,退化为指向第 0 行首元素的指针(int* 类型),因此 p 指向 a[0][0]
    • p[0] 等价于 *(p + 0),即访问 p 指向的首元素,也就是 a[0][0],其值为 1

3.4 题目4

//假设环境是x86环境,程序输出的结果是啥?
#include <stdio.h>
int main()
{int a[5][5];int(*p)[4];p = a;printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);return 0;
}
  1. 数组与指针类型

    • int a[5][5]:二维数组,每行包含 5 个int元素,每行大小为5×4=20字节(int占 4 字节)。
    • int(*p)[4]p是数组指针,指向包含 4 个int的一维数组,每次指针移动的步长为4×4=16字节。
  2. 指针赋值与偏移计算

    • p = a:将二维数组a的首地址赋值给pa退化为指向首行的指针,此处强制转换为int(*)[4]类型)。

    • p[4][2]的地址计算:

      p每次移动 1 步跳过 16 字节,p[4]表示偏移 4 步:4×16=64字节;

      再偏移 2 个int2×4=8字节;

      总偏移:64+8=72字节。

    • a[4][2]的地址计算:

      a每行 20 字节,a[4]表示偏移 4 行:4×20=80字节;

      再偏移 2 个int2×4=8字节;

      总偏移:80+8=88字节。

  3. 地址差值分析

    • 地址差:72 - 88 = -4(以字节为单位,两个地址的实际差值)。
    • 在 x86 的 32 位环境中,%p打印有符号整数的补码:-4的 32 位补码为0xFFFFFFFC
    • %d直接打印有符号整数结果-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. 数组初始化与存储

    • int aa[2][5] 是一个 2 行 5 列的二维数组,初始化值为 {{1,2,3,4,5}, {6,7,8,9,10}},在内存中连续存储。
  2. 指针 ptr1 分析

    • &aa 表示取整个二维数组的地址,类型为 int(*)[2][5](指向 2 行 5 列数组的指针)。
    • &aa + 1 会跳过整个二维数组(共 10 个int元素,总大小为10×4=40字节),指向数组末尾的下一个位置。
    • int* ptr1 = (int*)(&aa + 1) 将该地址强制转换为int*类型,ptr1 指向数组最后一个元素(10)的下一个位置。
    • *(ptr1 - 1)ptr1 - 1 向前偏移 1 个int大小,指向最后一个元素10,解引用结果为 10
  3. 指针 ptr2 分析

    • aa 是二维数组名,退化为指向第 0 行的指针(int(*)[5]类型)。
    • aa + 1 指向第 1 行(跳过 5 个int元素),*(aa + 1) 表示第 1 行的数组名(退化为指向第 1 行首元素6的指针int*)。
    • int* ptr2 = (int*)(*(aa + 1)) 中,ptr2 指向第 1 行首元素6
    • *(ptr2 - 1)ptr2 - 1 向前偏移 1 个int大小,指向第 0 行最后一个元素5,解引用结果为 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. 数组与指针定义

    • char* a[] = {"work","at","alibaba"} 定义了一个指针数组,数组 a 的每个元素都是 char* 类型(指向字符串的指针)。
    • 数组 a 存储了 3 个字符串的首地址:a[0] 指向 "work"a[1] 指向 "at"a[2] 指向 "alibaba"
  2. 二级指针 pa 的操作

    • char**pa = apa 是二级指针(指向指针的指针),初始指向指针数组 a 的首元素(即 a[0] 的地址)。
    • pa++:二级指针 pa 向后偏移 1 个位置(跳过一个 char* 类型的大小),此时 pa 指向 a[1] 的地址。
  3. 打印内容分析

    • *pa 表示解引用 pa,得到 a[1] 的值(即指向字符串 "at" 的指针)。
    • printf("%s\n", *pa) 以字符串格式打印该指针指向的内容,结果为 "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;
}

先明确各指针的初始指向关系:

  • c 是指针数组,存储 4 个字符串地址:c[0]="ENTER"c[1]="NEW"c[2]="POINT"c[3]="FIRST"
  • cp 是二级指针数组,存储 4 个指向c的指针:cp[0]=c+3cp[1]=c+2cp[2]=c+1cp[3]=c
  • cpp 是三级指针,初始指向cp[0]

在这里插入图片描述

  1. printf("%s\n", **++cpp); // POINT

    • ++cppcpp 从指向 cp[0] 变为指向 cp[1]cpp 现在指向 cp[1]

    • 第一次解引用 *cpp:得到 cp[1] 的值 c+2(指向 c[2]

    • 第二次解引用 **cpp:得到 c[2] 的值(指向字符串 "POINT"

    • 输出字符串:POINT

在这里插入图片描述

  1. printf("%s\n", *-- * ++cpp + 3); // ER

    • ++cppcpp 从指向 cp[1] 变为指向 cp[2]

    • 第一次解引用 *cpp:得到 cp[2] 的值 c+1

    • --*cpp:将 c+1 减 1,变为 c(指向 c[0]

    • 第二次解引用 *(--*cpp):得到 c[0] 的值(指向 "ENTER"

    • +3:指针向后偏移 3 个字符,指向 "ENTER" 中第 3 个字符后(即 "ER"

    • 输出字符串:ER

    在这里插入图片描述

  2. printf("%s\n", *cpp[-2] + 3); // ST

    • cpp[-2] 等价于 *(cpp-2)cpp 当前指向 cp[2],减 2 后指向 cp[0],解引用得到 cp[0] 的值 c+3

    • 解引用 *cpp[-2]:得到 c[3] 的值(指向 "FIRST"

    • +3:指针向后偏移 3 个字符,指向 "FIRST" 中第 3 个字符后(即 "ST"

    • 输出字符串:ST

    在这里插入图片描述

  3. printf("%s\n", cpp[-1][-1] + 1); // EW

    • cpp[-1] 等价于 *(cpp-1)cpp 当前指向 cp[2],减 1 后指向 cp[1],解引用得到 cp[1] 的值 c+2

    • cpp[-1][-1] 等价于 *((c+2)-1) = *(c+1):得到 c[1] 的值(指向 "NEW"

    • +1:指针向后偏移 1 个字符,指向 "NEW" 中第 1 个字符后(即 "EW"

    • 输出字符串:EW

    在这里插入图片描述


结语

穿越这一系列指针笔试题的“魔鬼训练”,我们仿佛进行了一场深入内存腹地的探险。从最初的 sizeofstrlen 的对比,到一维、二维数组的层层剥茧,再到多级指针的曲折迂回,我们一次又一次地验证了那个核心法则:指针的类型,是理解其一切行为的钥匙。它决定了指针算术的步长,定义了解引用的深度,描绘了内存的访问边界。

这些题目或许曾让你感到困惑甚至挫败,但这正是深入理解指针的必经之路。它们迫使你跳出直觉,去思考编译器如何看待你的代码,数据在内存中如何实际排布。掌握这些知识,不仅能让你在面试中游刃有余,更能让你在日后的程序生涯中,写出更高效、更健壮、更接近机器本质的代码。

请记住,指针并不可怕,它只是需要一份细心和一份耐心。当你下次再遇到复杂的指针声明时,不妨从变量名开始,由内向外,结合运算符优先级逐步解析;当你对一段指针操作代码的行为不确定时,尝试在纸上画出内存布局图,一步一步追踪指针的移动和数据的流向——这是最有效的学习方法。

至此,我们的指针系列博客就告一段落了。希望这段学习经历,能让你不仅学会了指针的语法规则,更培养了一种从内存视角思考问题的能力。这种能力,将伴随你在编程的道路上不断前行,帮助你写出更优雅、更高效的代码。

如今,指针系列虽已结束,但这并不意味着学习的终止。C 语言的世界广阔无垠,指针只是其中一块重要的基石。掌握了指针,你在后续学习链表、树、图等数据结构时将会更加得心应手,在进行系统编程、嵌入式开发等领域的探索时也会更有底气。

感谢你一路的陪伴与坚持。愿你带着这份对指针的理解,在 C 语言的世界里继续探索,不断精进,开启新的编程征程!


思考与互动

在你学习指针的过程中,哪个概念或哪道题目曾让你有“顿悟”的感觉?欢迎在评论区分享你的经历与见解!

http://www.dtcms.com/a/395452.html

相关文章:

  • SCI 期刊验证!苏黎世大学使用 ALINX FPGA 开发板实现分子动力学模拟新方案
  • C# OnnxRuntime yolov8 纸箱分割
  • SQLite3的API调用实战例子
  • LeetCode 60. 排列序列
  • springboot2.7.11 + quartz2.3.2,单机,集群实战,增删改查任务,项目一启动就执行任务
  • Hive 调优
  • 王晨辉:RWA注册登记平台赋能资产数字化转型
  • 周末荐读:美 SEC 推出加密货币 ETF 上市标准,Base 发币在即
  • HTTP API获取 MQTT上报数据
  • Apache HTTP基于端口的多站点部署完整教程
  • 新网站如何让百度快速收录的方法大全
  • 企业非结构化数据治理与存储架构优化实践探索
  • dagger.js 实现嵌套路由导航:对比 React Router 的另一种思路
  • React自定义同步状态Hook
  • 系统架构设计能力
  • 安卓图形系统架构
  • 《ZooKeeper终极指南》
  • 软考 系统架构设计师系列知识点之杂项集萃(154)
  • 算法提升之单调数据结构-单调栈与单调队列
  • 【Linux】初识进程(Ⅰ)
  • VMware登录后没有网络解决方法
  • Infoseek助力品牌公关升级:从成本中心到价值引擎
  • AR 运维系统与 MES、EMA、IoT 系统的融合架构与实践
  • 牛客周赛 Round 110
  • AutoMQ x Lightstreamer: Kafka 金融数据实时分发新方案
  • Vulkan原理到底学什么
  • 第14讲 机器学习的数据结构
  • MATLAB的宽频带频谱感知算法仿真
  • Adobe Fresco下载教程Adobe Fresco 2025保姆级安装步骤(附安装包)
  • MQTT 服务质量 (QoS) 深度解析