指针进阶(四)(C 语言)

embedded/2024/10/31 6:50:06/

目录

  • 一、sizeof 和 strlen() 对比
    • 1. sizeof 操作符
    • 2. sizeof 操作符不会计算表达式的值
    • 3. strlen() 函数
    • 4. 确保传入 strlen() 函数的地址后面有空字符
    • 5. sizeof 和 strlen() 对比表格
  • 二、数组和指针笔试题解析
    • 1. 一维数组
    • 2. 字符数组
      • 1. 代码 A
      • 2. 代码 B
      • 3. 代码C
      • 4. 代码 D
      • 5. 代码 E
      • 6. 代码 F
    • 3. 二维数组
  • 三、指针运算笔试题解析
    • 1. 题目 A
    • 2. 题目 B
    • 3. 题目 C
    • 4. 题目 D
    • 5. 题目 E
    • 6. 题目 F
    • 7. 题目 G

一、sizeof 和 strlen() 对比

1. sizeof 操作符

sizeof 是一个 C 语言操作符,用于计算变量、数据类型或表达式所占用的字节数。当计算类型名的大小时,必须使用括号括起;计算变量的大小时可以省略。如下代码:
在这里插入图片描述
在这里插入图片描述
当使用 sizeof 计算类型名大小不加括号时,编译器会报错。且由于操作符 sizeof 的返回值为 size_t 类型,所以输出时需要使用 %zd 格式。

2. sizeof 操作符不会计算表达式的值

如下代码:

int a = 10;
short b = 2;
printf("%zd\n", sizeof(b = a + b));
printf("b = %hd\n", b);

按理来说,第三条语句首先计算 b = a + b,然后 b 的值为 12,由于 b 的类型为 short,所以整个 sizeof 表达式的结果为 2。所以上述代码的运行结果应该是打印 2 和 12。我们来看看程序运行结果如何:
在这里插入图片描述
可以看到 b 的值没变,这是因为 sizeof 不会计算表达式的值,b = a + b,那最终结果是 b 的值,b 是 short 类型,那么该表达式的值就直接为 2,并不会去计算表达式。我们要知道,表达式的计算是在程序运行时进行的,而 sizeof 操作符的计算是在编译过程中进行的。而程序的运行的步骤是:预处理->编译->汇编->链接,然后再生成的可执行程序。

3. strlen() 函数

strlen() 是 C 语言标准库中的一个计算字符串长度的函数,包含在头文件 string.h 中。该函数的原型如下:

// strlen() 函数原型
size_t strlen(const char *s);

该函数从传入的字符指针地址处开始往后计算字符数,直到遇到空字符,然后返回计算的字符数,不包括空字符。由于不需要改变字符串的值,所以在参数中加上了 const 进行修饰。下面是对该函数的简单使用:
在这里插入图片描述

4. 确保传入 strlen() 函数的地址后面有空字符

如果传入 strlen() 函数的地址后面没有空字符,那么 strlen() 函数会往后一直计算字符个数,知道遇到空字符位置,这就造成了越界访问。如下代码:
在这里插入图片描述
可以看到,字符数组的长度本应为 3,可计算出的却是 42。由于字符数组 tmp 的末尾没有空字符,所以 strlen() 函数往后一直计算直到遇到空字符,而系统其他内存空间的值是未知的,所以最终显示得是一个随机值。

5. sizeof 和 strlen() 对比表格

sizeofstrlen()
1. sizeof 是一个操作符1. strlen() 是一个标准库函数
2. sizeof 计算操作数所占内存大小(字节)2. strlen() 函数计算字符串的长度,统计 \0 之前的字符个数
3. sizeof 不会计算表达式的值3. strlen() 函数不会关心越界问题,只要没遇到 \0 就会一直往后计算

二、数组和指针笔试题解析

1. 一维数组

int a[] = {1,2,3,4};
1. printf("%d\n",sizeof(a));
2. printf("%d\n",sizeof(a+0));
3. printf("%d\n",sizeof(*a));
4. printf("%d\n",sizeof(a+1));
5. printf("%d\n",sizeof(a[1]));
6. printf("%d\n",sizeof(&a));
7. printf("%d\n",sizeof(*&a));
8. printf("%d\n",sizeof(&a+1));
9. printf("%d\n",sizeof(&a[0]));
10.printf("%d\n",sizeof(&a[0]+1));

解析:

  1. sizeof + 数组名,计算整个数组的大小,16
  2. a + 0 表示数组首元素的地址,int* 类型,4/8
  3. *a 是数组首元素,int 类型,4
  4. a + 1 表示数组第二个元素的地址,4/8
  5. a[1] 表示数组第二个元素,int 类型,4
  6. &a 表示整个数组的地址,int (*)[4] 类型,4/8
  7. *&a 等价于 a,等同于 sizeof + 数组名,16
  8. &a + 1 是数组 a 后第一个字节的地址,int (*)[4] 类型,4/8
  9. &a[0] 是数组首元素地址 4/8
  10. &a[0] + 1 是数组第二个元素的地址,4/8

下面是 64 位环境下,程序运行结果:
在这里插入图片描述

2. 字符数组

1. 代码 A

char arr[] = {'a','b','c','d','e','f'};
1. printf("%d\n", sizeof(arr));
2. printf("%d\n", sizeof(arr+0));
3. printf("%d\n", sizeof(*arr));
4. printf("%d\n", sizeof(arr[1]));
5. printf("%d\n", sizeof(&arr));
6. printf("%d\n", sizeof(&arr+1));
7. printf("%d\n", sizeof(&arr[0]+1));

解析:

  1. sizeof + 数组名,计算整个数组的大小 6
  2. arr + 0 是数组首元素的地址,4/8
  3. *arr 是数组首元素,1
  4. arr[1] 是数组第二个元素,1
  5. &arr 是整个数组的地址,char (*)[6] 类型,4/8
  6. &arr + 1 是数组 arr 后第一个字节的地址,char (*)[6] 类型,4/8
  7. &arr[0]+1 是数组第二个元素的地址 4/8

在 64 位环境下,程序运行结果:
在这里插入图片描述

2. 代码 B

char arr[] = {'a','b','c','d','e','f'};
1. printf("%d\n", strlen(arr));
2. printf("%d\n", strlen(arr+0));
3. printf("%d\n", strlen(*arr));
4. printf("%d\n", strlen(arr[1]));
5. printf("%d\n", strlen(&arr));
6. printf("%d\n", strlen(&arr+1));
7. printf("%d\n", strlen(&arr[0]+1));

解析:
8. 从字符数组 arr 首地址开始计算字符串长度,由于该字符数组末尾没有空字符,造成越界访问,结果为随机值
9. 随机值
10. *a 是数组首元素字符 ‘a’,其 ASCII 值为 65,而 strlen 函数需要 const char* 类型,那么 65 会被强制类型转换为 const char*,然后从该地址进行计算,这样就会造成非法访问。
11. 非法访问
12. &arr 是整个数组的地址,char (*)[6] 类型,会被强制类型转换为 const char*,相当于从数组首元素开始计算,越界访问,随机值
13. &arr + 1 是数组 arr 后面第一个字节的地址,类型为 char (*)[6],被强制类型转换为 char*,然后从该地址开始计算,非法访问
14. &arr[0]+1 是数组第二个元素的地址,越界访问,随机值

3. 代码C

char arr[] = "abcdef";
1. printf("%d\n", sizeof(arr));
2. printf("%d\n", sizeof(arr+0));
3. printf("%d\n", sizeof(*arr));
4. printf("%d\n", sizeof(arr[1]));
5. printf("%d\n", sizeof(&arr));
6. printf("%d\n", sizeof(&arr+1));
7. printf("%d\n", sizeof(&arr[0]+1));

解析:

  1. sizeof + 数组名,计算整个数组的大小,后面还有一个空字符,7
  2. arr+0 是数组首元素的地址,4/8
  3. *arr 是数组首元素,1
  4. arr[1] 是数组第二个元素,1
  5. &arr 取出的是整个数组的地址,char (*)[7] 类型,4/8
  6. &arr+1 指向数组 arr 后面第一个字节,char (*)[7] 类型,4/8
  7. &arr[0]+1 是数组第二个元素的地址,4/8

在 64 位环境下,程序运行结果:
在这里插入图片描述

4. 代码 D

char arr[] = "abcdef";
1. printf("%d\n", strlen(arr));
2. printf("%d\n", strlen(arr+0));
3. printf("%d\n", strlen(*arr));
4. printf("%d\n", strlen(arr[1]));
5. printf("%d\n", strlen(&arr));
6. printf("%d\n", strlen(&arr+1));
7. printf("%d\n", strlen(&arr[0]+1));

解析:

  1. arr 字符串的首地址,计算字符串的长度 6
  2. arr+0 是字符串的首地址,6
  3. *arr 是字符串的首字符,非法访问
  4. arr[1] 是字符串的第二个字符,非法访问
  5. &arr 是整个字符串的地址,char (*)[7] 类型,传入 strlen() 函数时,被强制类型转换为 char*,从字符串首字符开始计算,6
  6. &arr+1 是字符串后面第一个字符的地址,类型 char (*)[7],传入 strlen() 函数时,被强制类型转换为 char*,然后开始计算,非法访问
  7. &arr[0]+1 时字符串第二个字符的地址,5

5. 代码 E

char *p = "abcdef";
1. printf("%d\n", sizeof(p));
2. printf("%d\n", sizeof(p+1));
3. printf("%d\n", sizeof(*p));
4. printf("%d\n", sizeof(p[0]));
5. printf("%d\n", sizeof(&p));
6. printf("%d\n", sizeof(&p+1));
7. printf("%d\n", sizeof(&p[0]+1));

解析:

  1. p 是 char* 类型的指针,指向字符串第一个字符,4/8
  2. p+1 指向字符串第二个字符,4/8
  3. *p 是字符串第一个字符,1
  4. p[0] 是字符串第一个字符,1
  5. &p 是指针 p 的地址,char** 类型,二级指针,4/8
  6. &p+1 是指针 p 的地址的下一个字节的地址,char** 类型,4/8
  7. &p[0]+1 是字符串第二个字符的地址,4/8

在 64 位环境下,程序的运行结果:
在这里插入图片描述

6. 代码 F

char *p = "abcdef";
1. printf("%d\n", strlen(p));
2. printf("%d\n", strlen(p+1));
3. printf("%d\n", strlen(*p));
4. printf("%d\n", strlen(p[0]));
5. printf("%d\n", strlen(&p));
6. printf("%d\n", strlen(&p+1));
7. printf("%d\n", strlen(&p[0]+1));

解析:

  1. p 是字符串第一个字符的地址,计算整个字符串的长度 6
  2. p+1 是字符串第二个字符的地址,5
  3. *p 是字符串第一个字符,非法访问
  4. p[0] 是字符串第一个字符,非法访问
  5. &p 是指针 p 的地址,char** 类型,传入 strlen() 函数时,被强制类型转换为 char*,然后从该地址开始计算,越界访问,随机值
  6. &p+1 是指针 p 的地址后面第一个字节的地址,char** 类型,传入 strlen() 函数时,被强制类型转换为 char*,然后从该地址开始计算,非法访问
  7. &p[0]+1 是字符串第二个字符的地址,5

3. 二维数组

int a[3][4] = {0};
1. printf("%d\n",sizeof(a));
2. printf("%d\n",sizeof(a[0][0]));
3. printf("%d\n",sizeof(a[0]));
4. printf("%d\n",sizeof(a[0]+1));
5. printf("%d\n",sizeof(*(a[0]+1)));
6. printf("%d\n",sizeof(a+1));
7. printf("%d\n",sizeof(*(a+1)));
8. printf("%d\n",sizeof(&a[0]+1));
9. printf("%d\n",sizeof(*(&a[0]+1)));
10. printf("%d\n",sizeof(*a));
11. printf("%d\n",sizeof(a[3]));

解析:

  1. sizeof + 数组名,计算整个数组的大小,48
  2. a[0][0] 是二维数组的首元素,4
  3. a[0] 是二维数组 a 的第一行,也是一个数组,sizeof + 数组名 16
  4. a[0]+1 是二维数组 a 的第一行的第二个元素的地址,int (*)[4] 类型,4/8
  5. *(ar[0]+1) 是二维数组 a 的第一行的第二个元素,4
  6. a+1 是二维数组 a 的第二行的地址,int (*)[4] 类型,4/8
  7. *(a+1) 是二维数组 a 的第二行,也就是 sizeof + 第二行数组名,16
  8. &a[0]+1 是二维数组 a 的第二行的地址,int(*)[4] 类型,4/8
  9. *(&a[0]+1) 是二维数组的第二行,16
  10. *a 是二维数组 a 的第一行,也就是 sizeof + 第一行数组名,16
  11. a[3]是二维数组 a 的第四行,实际上并没有第四行,但是 sizeof 可以根据前三行推测出第四行的类型,这里并没有进行访问,所以不存在越界的问题,16

三、指针运算笔试题解析

1. 题目 A

#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;
}
//程序的结果是什么?  

解析:
指针 ptr 指向数组 a 后面的第一个字节,*(a+1) 是数组 a 的第二个元素,而 *(ptr - 1) 是数组的第 5 个元素。如下图:
在这里插入图片描述
所以应该输出 2,5

64 位环境下,运行结果如下:
在这里插入图片描述

2. 题目 B

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

解析:
在 p + 0x1 中由于 p 是 struct Test* 类型,所以其加 1 应该增加 sizeof(struct Test) 也就是 20 个字节,而 %p 又是按照十六进制输出,所以第一个 printf() 函数打印 100014。
在 (unsigned long)p + 0x1 中,p 被强制转换为 unsigned long 类型,现在就是算数运算,其加 1 就是加 1,所以第二个 printf() 函数打印 1000001。
在 (unsigned int*)p + 0x1 中,p 被强制类型转换为 unsigned int* 类型,所以其加 1 应该增加 sizeof(unsigned int) 也就是 4 个字节,所以第三个 printf() 函数打印 1000004。

在 64 位环境下,运行结果如下:
在这里插入图片描述

3. 题目 C

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

解析:
a[0] 是二维数组 a 的第一行,在 p = a[0] 中,a[0] 代表第一行首元素的地址,所以 p[0] 是第一行的首元素。然后回过头来观察二维数组 a 的初始化语句,其中使用了逗号表达式,逗号表达式从左往右依次计算各个表达式,然后最终的结果是右侧表达式的值。所以实际上应该是: int a[3][2] = {1,3,5},所以程序应该输出 1。

在 64 为环境下,程序运行结果如下:
在这里插入图片描述

4. 题目 D

//假设环境是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;
}

解析:
a 是二维数组首行地址,在表达式 p = a 中,a 的值被强制类型转换为 int (*)[4] 然后赋值给 p。所以现在 p 和 a 的值都是数组 a 的第一个元素的第一个字节的地址,但是它们的类型分别为 int (*)[5] 和 int (*)[4],所以 p[4][2] 是数组 a 的第 19 个元素,而 a[4][2] 是数组 a 的第 23 个元素。而当两个指针指向同一块空间时,它们相减的结果是它们直接差的元素个数,所以 &p[4][2] - &a[4][2] 的值为 -4。它的二进制补码为:
11111111111111111111111111111100
当使用 %p 十六进制地址输出时,显示 fffffffc,使用 %d 输出时,-4

在 32 位环境下,输出结果如下:
在这里插入图片描述

5. 题目 E

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

解析:
在 int *ptr1 = (int *)(&aa + 1); 中,&aa + 1 是二维数组 aa 后的第一个字节的地址,然后被强制类型转换为 int* 赋值给 ptr1。
在 int *ptr2 = (int *)(*(aa + 1)); 中,aa +1 是二维数组第二行的地址,然后解引用获得第二行,也就是第二行首元素的地址,然后再被强制类型转换为 int* 被赋值给 ptr2。

现在两个指针指向如下:
在这里插入图片描述

所以 ptr1 - 1 指向 10,也就是 a[1][4],而 ptr2 - 1 指向 5 也就是 a[0][4]。

在 64 位环境下,程序运行结果如下:
在这里插入图片描述

6. 题目 F

#include <stdio.h>
int main()
{char *a[] = {"work","at","alibaba"};char**pa = a;pa++;printf("%s\n", *pa);return 0;
}

解析:
指针数组 a 的每个元素分别指向了一个字符串,每个元素的类型都是 char*,而在表达式 char**pa = a 中,a 是数组首元素的地址,char ** 类型,现在 pa 也指向数组 a 的首元素。然后 pa++,pa 指向数组 a 的第二个元素,然后解引用拿到第二个元素,也就是指向字符串常量 “at” 的指针,然后按照 %s 的格式输出,打印 at。具体关系如下图:
在这里插入图片描述

在 64 为环境下,程序运行结果如下:
在这里插入图片描述

7. 题目 G

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

解析:
在写这种复杂指针关系运算的题目时,最好是先把指针的关系表示出来:
在这里插入图片描述
现在来看第一条 printf() 语句,**++cpp,解引用操作符和前置底层操作符优先级相同,两个操作符都是右结合,所以先计算 ++cpp,然后 cpp 指向数组 cp 的第二个元素,
在这里插入图片描述
然后解引用 *++cpp,拿到数组 CP 的第二个元素,然后再次解引用 **++cpp,拿到数组 C 的第三个元素,然后打印字符串 POINT。

然后看第二条 printf() 语句,*–*++cpp+3,首先 ++cpp,cpp 指向数组 cp 的第三个元素,
在这里插入图片描述
接着解引用 *++cpp,拿到数组 cp 的第三个元素,然后进行前置自减操作,–*++cpp,让数组 cp 的第三个元素指向数组 c 的第一个元素,
在这里插入图片描述
然后解引用 *–*++cpp 拿到数组 cp 的第一个元素,然后加 3,*–*++cpp+3,拿到指向字符串 “ENTER” 第四个字符的地址,然后打印 ER

接下来看第三条 printf() 语句,*cpp[-2]+3,首先 cpp[-2],也就是 *(cpp-2) 拿到数组 cp 的第一个元素,然后解引用拿到数组 c 的第四个元素,然后加 3,拿到指向字符串 “FIRST” 的第四个字符的地址,然后打印 ST

接下来看第四条 printf() 语句,cpp[-1][-1]+1,首先 cpp[-1],拿到数组 cp 的第二个元素,然后 cpp[-1][-1] 拿到数组 c 的第二个元素,然后加 1 得到字符串 “NEW” 第二个字符的地址,然后打印 EW

在 64 为环境下,代码运行结果如下:
在这里插入图片描述


http://www.ppmy.cn/embedded/133816.html

相关文章

python-opencv给图片或视频去水印

文章目录 引言inpaint函数的使用方法鼠标事件回调函数cv2.setMouseCallback介绍去水印步骤实现代码 引言 本文主要基于cv2.inpaint函数实现图片的水印去除。 inpaint函数基于图像修复算法&#xff0c;通过对缺陷区域周围像素的分析和插值&#xff0c;生成合适的像素值来填充缺…

初体验鸿蒙 HarmonyOS NEXT开发

上个星期三就下载了鸿蒙 HarmonyOS NEXT&#xff0c;安装好了后测试了一下&#xff0c;感觉界面和功能设计与IntelliJ IDEA很像&#xff0c;对初学者非常友好&#xff0c;所见即所得。不知道什么原因&#xff0c;写了代码后测试起来很慢&#xff0c;简单测试后就没有再动。 今天…

【Java网络编程】从套接字(Socket)概念到UDP与TCP套接字编程

目录 网络编程 1.socket套接字 2.udp数据报套接字编程 DatagramSocket API DatagramPacket API Java基于UDP实现客户端-服务器代码实例 3.tcp流套接字编程 ServerSocket API Socket API TCP中的长短连接 Java基于TCP客户端-服务器代码实例 网络编程 1.socket套接字 S…

【热门主题】000018 人工智能深度学习模型:探索与应用

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享一篇文章&#xff01;并提供具体代码帮助大家深入理解&#xff0c;彻底掌握&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495; 目录 【热…

网站攻击,XSS攻击的类型

XSS&#xff08;跨站脚本&#xff09;攻击是一种网络安全攻击方式&#xff0c;攻击者通过在网站页面中注入恶意脚本&#xff0c;使脚本在其他用户的浏览器中执行&#xff0c;从而窃取用户信息、篡改页面内容或操控用户账户。这类攻击通常利用网站对输入数据的过滤不严格&#x…

Element

Element 是由饿了么前端团队开发的一个基于 Vue 2.0 的桌面端组件库&#xff0c;它提供了一系列高质量的Vue组件&#xff0c;帮助开发者快速构建功能丰富、风格统一的前端界面。官网&#xff1a;Element - The worlds most popular Vue UI framework 引入 一、安装 ElementUI …

二、Go快速入门之数据类型

&#x1f4c5; 2024年4月27日 &#x1f4e6; 使用版本为1.21.5 Go的数据类型 &#x1f4d6;官方文档&#xff1a;https://go.dev/ref/spec#Types 1️⃣ 布尔类型 ⭐️ 布尔类型只有真和假,true和false ⭐️ 在Go中整数0不会代表假&#xff0c;非零整数也不能代替真&#…

论文笔记:通用世界模型WorldDreamer

整理了WorldDreamer: Towards General World Models for Video Generation via Predicting Masked Tokens 论文的阅读笔记 背景模型实验 背景 现有的世界模型仅限于游戏或驾驶等特定场景&#xff0c;限制了它们捕捉一般世界动态环境复杂性的能力。针对这一挑战&#xff0c;本文…