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

C —— 指针和数组的面试题

指针和数组的面试题

  • 面试题一
  • 面试题二(指针算数)
      • 题目回顾
      • 关键概念
      • 计算三个表达式
        • 1. `p + 0x1`
        • 2. `(unsigned long)p + 0x1`
        • 3. `(unsigned int*)p + 0x1`
      • 最终答案
      • 验证思路
      • 总结
  • 面试题三
  • 面试题四
    • 逗号表达式
  • 面试题五
  • 面试题六
      • 2. `ptr1` 的计算
  • 面试题七
      • 题目分析
      • 关键点
      • 逐步解析
        • 1. `printf("%s\n", **++cpp);`
        • 2. `printf("%s\n", *--*++cpp + 3);`
        • 3. `printf("%s\n", *cpp[-2] + 3);`
        • 4. `printf("%s\n", cpp[-1][-1] + 1);`
      • 最终答案
      • 内存布局变化总结
      • 总结

我们之前了解了指针和数组的一些基本常识,如果大家对这块还不太熟悉可以点击这里:

https://blog.csdn.net/qq_67693066/article/details/146522555?spm=1011.2415.3001.5331

https://blog.csdn.net/qq_67693066/article/details/146526324?spm=1011.2415.3001.5331

今天我们主要来看看一些面试题:

面试题一

int main()
{
    int a[5] = { 1, 2, 3, 4, 5 };
    int *ptr = (int *)(&a + 1);
    printf( "%d,%d", *(a + 1), *(ptr - 1));
    return 0;
}
//程序的结果是什么?

我们可以运行一下看看结果:
在这里插入图片描述
我们画个图来一步一步分析:
在这里插入图片描述首先是 int *ptr = (int *)(&a + 1);
在这里插入图片描述然后是printf( “%d,%d”, *(a + 1), *(ptr - 1));
在这里插入图片描述
在这里插入图片描述

面试题二(指针算数)

//由于还没学习结构体,这里告知结构体的大小是20个字节
struct Test
{
 int Num;
 char *pcName;
 short sDate;
 char cha[2];
 short sBa[4];
}*p;
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
//已知,结构体Test类型的变量大小是20个字节
int main()
{
 printf("%p\n", p + 0x1);
 printf("%p\n", (unsigned long)p + 0x1);
 printf("%p\n", (unsigned int*)p + 0x1);
 return 0;
}

题目回顾

我们有一个结构体 Test,其大小为 20 字节,并定义了一个指向该结构体的指针 p,初始值为 0x100000。然后,在 main 函数中打印了三个表达式的值:

printf("%p\n", p + 0x1);
printf("%p\n", (unsigned long)p + 0x1);
printf("%p\n", (unsigned int*)p + 0x1);

我们需要计算这三个表达式的值。


关键概念

  1. 指针算术(Pointer Arithmetic)

    • 对指针进行加减运算时,步长取决于指针指向的类型的大小。
    • 例如,p + 1 实际上是 p + sizeof(*p)
  2. 类型转换的影响

    • 将指针转换为整数(如 unsigned long)后,加减就是普通的数值运算。
    • 将指针转换为其他类型的指针(如 unsigned int*)后,步长会按新类型的大小计算。
  3. %p 格式说明符

    • 用于打印指针的值,通常以十六进制显示。

计算三个表达式

1. p + 0x1
  • pstruct Test* 类型,指向的结构体大小为 20 字节
  • 指针算术:p + 1 = p + sizeof(struct Test) = 0x100000 + 20
  • 需要将 20 转换为十六进制:
    • 20(十进制)= 0x14(十六进制)。
  • 因此:
    • p + 0x1 = 0x100000 + 0x14 = 0x100014
2. (unsigned long)p + 0x1
  • (unsigned long)p 将指针 p 转换为无符号长整型,此时它只是一个普通的整数 0x100000
  • 整数加法:0x100000 + 1 = 0x100001
  • 注意:%p 可以打印任何地址值,即使是从整数转换来的。
3. (unsigned int*)p + 0x1
  • (unsigned int*)pp 转换为 unsigned int* 类型。
  • unsigned int 的大小通常是 4 字节(取决于平台,但题目未说明,假设为 4 字节)。
  • 指针算术:(unsigned int*)p + 1 = (unsigned int*)p + sizeof(unsigned int) = 0x100000 + 4
  • 4(十进制)= 0x4(十六进制)。
  • 因此:
    • (unsigned int*)p + 0x1 = 0x100000 + 0x4 = 0x100004

最终答案

运行程序后,三个 printf 的输出分别为:

0x100014      // p + 0x1
0x100001      // (unsigned long)p + 0x1
0x100004      // (unsigned int*)p + 0x1

验证思路

  1. p + 0x1
    • pstruct Test*,结构体大小 20 字节 → p + 1 = 0x100000 + 20 = 0x100014
  2. (unsigned long)p + 0x1
    • 转换为整数后直接加 1 → 0x100000 + 1 = 0x100001
  3. (unsigned int*)p + 0x1
    • unsigned int* 步长为 4 字节 → 0x100000 + 4 = 0x100004

总结

  • 指针加减:步长由指针指向的类型决定。
  • 强制类型转换
    • 转整数 → 直接数值运算。
    • 转其他指针 → 按新类型的步长运算。
  • 本题的关键是清楚不同类型指针的步长。

面试题三

int main()
{
    int a[4] = { 1, 2, 3, 4 };
    int *ptr1 = (int *)(&a + 1);
    int *ptr2 = (int *)((int)a + 1);
    printf( "%x,%x", ptr1[-1], *ptr2);
    return 0;
}

这里的ptr[-1]和上面的*(ptr - 1)是一个意思:
在这里插入图片描述
在这里插入图片描述

面试题四

#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;
}

逗号表达式

这里注意一下坑,数组初始化时用了逗号表达式, 逗号表达式会计算所有子表达式,但最终取 最后一个值。

例如 (0, 1) 的值是 1,(2, 3) 的值是 3,(4, 5) 的值是 5。

所以这个三行两列的数组实际上有效的数字只有三个:1,3,5
在这里插入图片描述
在这里插入图片描述

面试题五

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;
}

这里有的平台上运行会报错,会报类型不匹配的错误,如果报错,记得强转一下:
在这里插入图片描述
这里,a会发生强转,由int (*) [5] 强转为 int ()[4] 的类型,这暗含了一个条件p指向的数组,一行只有4个元素

  • p[4][2]a[4][2] 的地址计算方式不同:
    • a[4][2]a 的第 4 行第 2 列,地址为 &a[0][0] + 4 * 5 * sizeof(int) + 2 * sizeof(int)
    • p[4][2]p 的第 4 行第 2 列,地址为 &p[0][0] + 4 * 4 * sizeof(int) + 2 * sizeof(int)
  1. 地址计算

    • &a[4][2]
      • aint [5][5]a[4][2] 的偏移量 = 4 * 5 + 2 = 22int
    • &p[4][2]
      • pint (*)[4]p[4][2] 的偏移量 = 4 * 4 + 2 = 18int
    • 由于 p = a&p[0][0] = &a[0][0],所以:
      • &p[4][2] - &a[4][2] = (&a[0][0] + 18) - (&a[0][0] + 22) = -4(以 int 为单位)。
  2. 指针减法

    • 指针减法的结果是两个地址之间的元素个数(int 为单位)。
    • -4%p 打印时会被转换为无符号数(通常是补码表示),以 %d 打印则是 -4

在这里插入图片描述

面试题六

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;
}

这道题和前面思路其实大同小异:


int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

aa 是一个 2 行 5 列的二维数组,内存布局如下:

aa[0][0] = 1   aa[0][1] = 2   aa[0][2] = 3   aa[0][3] = 4   aa[0][4] = 5  
aa[1][0] = 6   aa[1][1] = 7   aa[1][2] = 8   aa[1][3] = 9   aa[1][4] = 10

2. ptr1 的计算

int* ptr1 = (int*)(&aa + 1);

&aa 是整个数组的地址(类型是 int (*)[2][5])。
&aa + 1 会跳过整个 aa 的大小(2×5=10 个 int),指向 aa 末尾的下一个位置。
(int*) 强制转换为 int*,所以 ptr1 指向 aa 之后的位置。

ptr1 - 1 指向哪里?
ptr1 指向 aa 末尾之后,ptr1 - 1 回退 1 个 int,指向 aa[1][4](即 10)。
• 因此 *(ptr1 - 1) 的值是 10


int* ptr2 = (int*)(*(aa + 1));

aa 是二维数组名,在表达式中退化为指向第一行的指针(类型 int (*)[5])。
aa + 1 指向第二行(aa[1])。
*(aa + 1) 解引用得到第二行的数组(类型 int[5]),再退化为指向 aa[1][0] 的指针(int*)。
• 因此 ptr2 指向 aa[1][0](即 6)。

ptr2 - 1 指向哪里?
ptr2 指向 aa[1][0]ptr2 - 1 回退 1 个 int,指向 aa[0][4](即 5)。
• 因此 *(ptr2 - 1) 的值是 5
在这里插入图片描述

面试题七

题目分析

这段代码定义了一个多层指针结构,并通过指针运算打印字符串:

const char *c[] = { "ENTER","NEW","POINT","FIRST" };
const char **cp[] = { c + 3, c + 2, c + 1, c };
const char ***cpp = cp;

printf("%s\n", **++cpp);       // 输出: POINT
printf("%s\n", *--*++cpp + 3); // 输出: ER
printf("%s\n", *cpp[-2] + 3);  // 输出: ST
printf("%s\n", cpp[-1][-1] + 1); // 输出: EW

关键点

  1. 指针结构

    • c 是一个字符串数组,存储 4 个字符串的地址。
    • cp 是一个指针数组,存储 c 中元素的地址(c + 3 指向 "FIRST"c + 2 指向 "POINT",依此类推)。
    • cpp 指向 cp 的首元素。
  2. 初始内存布局

    c[0] = "ENTER" (地址 &"ENTER")
    c[1] = "NEW"   (地址 &"NEW")
    c[2] = "POINT" (地址 &"POINT")
    c[3] = "FIRST" (地址 &"FIRST")
    
    cp[0] = c + 3 = &c[3] = &"FIRST"
    cp[1] = c + 2 = &c[2] = &"POINT"
    cp[2] = c + 1 = &c[1] = &"NEW"
    cp[3] = c + 0 = &c[0] = &"ENTER"
    
    cpp = &cp[0]
    
  3. 指针运算规则

    • ++cppcpp 指向 cp 的下一个元素。
    • *cpp:解引用 cpp,得到 cp[i]
    • **cpp:解引用 cp[i],得到 c[j](即字符串地址)。
    • +n:对字符串指针进行偏移,跳过前 n 个字符。

逐步解析

1. printf("%s\n", **++cpp);
  • 初始状态
    • cpp 指向 cp[0]
  • ++cpp
    • cpp 现在指向 cp[1]
  • *cpp
    • cp[1] 的值是 c + 2(即 &c[2])。
  • **cpp
    • c[2]"POINT"
  • 输出
    POINT
    
2. printf("%s\n", *--*++cpp + 3);
  • 当前状态
    • cpp 指向 cp[1]
  • ++cpp
    • cpp 现在指向 cp[2]
  • *++cpp
    • cp[2] 的值是 c + 1(即 &c[1])。
  • --*++cpp
    • *cpp&c[1],对其减 1 后变为 &c[0]
    • 注意:cp[2] 的值被修改为 &c[0](副作用)。
  • *--*cpp
    • c[0]"ENTER"
  • + 3
    • "ENTER" + 3 跳过前 3 个字符,指向 "ER"
  • 输出
    ER
    
3. printf("%s\n", *cpp[-2] + 3);
  • 当前状态
    • cpp 指向 cp[2](因为上一步 ++cpp 后未移动)。
  • cpp[-2]
    • cpp 指向 cp[2]cpp[-2] = *(cpp - 2) = cp[0]
  • *cpp[-2]
    • cp[0] 的值是 c + 3(即 &c[3]),解引用后是 c[3] = "FIRST"
  • + 3
    • "FIRST" + 3 跳过前 3 个字符,指向 "ST"
  • 输出
    ST
    
4. printf("%s\n", cpp[-1][-1] + 1);
  • 当前状态
    • cpp 指向 cp[2]
  • cpp[-1]
    • *(cpp - 1) = cp[1]
  • cpp[-1][-1]
    • cp[1] 的值是 c + 2(即 &c[2]),cp[1][-1] = *(c + 2 - 1) = c[1] = "NEW"
  • + 1
    • "NEW" + 1 跳过前 1 个字符,指向 "EW"
  • 输出
    EW
    

最终答案

运行程序后,输出为:

POINT
ER
ST
EW

在这里插入图片描述

内存布局变化总结

操作cpp 指向cp 的值变化关键解引用
初始cp[0]cp[0]=&c[3], cp[1]=&c[2], cp[2]=&c[1], cp[3]=&c[0]
**++cppcp[1]无变化cp[1]c[2] = "POINT"
*--*++cppcp[2]cp[2]&c[1] 改为 &c[0]cp[2]c[0] = "ENTER""ER"
*cpp[-2] + 3cp[2]无变化cp[0]c[3] = "FIRST""ST"
cpp[-1][-1] + 1cp[2]无变化cp[1]c[1] = "NEW""EW"

总结

  • 指针运算优先级++ > * > +
  • 副作用*--*++cpp 修改了 cp[2] 的值(从 &c[1] 变为 &c[0])。
  • 数组下标等价性
    • cpp[-1] = *(cpp - 1)
    • cpp[-1][-1] = *(*(cpp - 1) - 1)

http://www.kler.cn/a/614012.html

相关文章:

  • Redis的缓存雪崩和缓存穿透的理解和如何避免
  • 后端开发基础:语言选择与 RESTful API 设计
  • C#使用用户名密码连接共享文件夹
  • 招聘面试季--金融系统常用的系统架构的特征
  • (C语言)指针运算 习题练习1.2(压轴难题)
  • python并发爬虫
  • SpringMVC的请求与响应
  • 如何使用Python爬虫获取1688商品评论?
  • pyspark学习rdd处理数据方法——学习记录
  • TDengine 中的系统信息统计
  • 【leetcode hot 100 45】跳跃游戏Ⅱ
  • SpringBoot 7 种实现 HTTP 调用的方式
  • Maven 多模块项目(如微服务架构)中,父 POM(最外层) 和 子模块 POM(具体业务模块)的区别和联系
  • 深入理解 Linux 基础 IO:从文件操作到缓冲区机制
  • 如何利用 CSS 的clip - path属性创建不规则形状的元素,应用场景有哪些?
  • ngx_http_core_init_main_conf
  • windows免密ssh登录linux
  • 项目代码第10讲【数据库运维知识——如何优化数据库查询效率?】:各种日志查看;主从复制;分库分表(MyCat);读写分离;区别数据分区、分表、分库
  • uni-app AES 加密
  • 判断质数及其优化方法