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

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

目录

  • 一、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.kler.cn/a/372639.html

相关文章:

  • C++ 中回调函数的实现方式-函数指针
  • JavaFx -- chapter05(多用户服务器)
  • Pytorch学习--神经网络--线性层及其他层
  • SpringBoot驱动的毕业生招聘信息平台
  • Linux-计算机网络-探索epoll是同步阻塞的还是异步非阻塞的
  • 使用VS2019将C#代码生成DLL文件在Unity3D里面使用(一)
  • Java项目实战II基于Spring Boot的火锅店管理系统设计与实现(开发文档+数据库+源码)
  • gpio子系统-通过io来控制gpio
  • 详解:单例模式中的饿汉式和懒汉式
  • lego-loam mapOptmization 源码注释(一)
  • Lua语法基础全面剖析(中篇)
  • uni-app应用级生命周期和页面级生命周期
  • huggingface之tokenization基础结构Trie-代码解读
  • 【缓存与加速技术实践】Redis 主从复制
  • 银河麒麟v10安装Anaconda(python大蟒蛇)+pycharm安装
  • AJAX和JSON
  • K8S 容器可视化管理工具-kuboard 监控管理工具搭建
  • 操作数据表
  • 【蓝桥杯选拔赛真题81】python矩形数量 第十五届青少年组蓝桥杯python选拔赛真题 算法思维真题解析
  • C++ 中回调函数的实现方式-函数指针
  • ICT网络赛道安全考点知识总结1
  • 笔记整理—linux驱动开发部分(2)模块信息与编译
  • 记录一次查询优化
  • 关于Mac打包ipa的配置小结
  • Hyperledger Fabric有那些核心技术,和其他区块链对比Hyperledger Fabric有那些优势
  • Spring Boot 实现文件分片上传和下载