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

目录

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

一、sizeof 和 strlen() 对比

1. sizeof 操作符

sizeof 是一个 C 语言操作符,用于计算变量、数据类型或表达式所占用的字节数。当计算类型名的大小时,必须使用括号括起;计算变量的大小时可以省略。如下代码:

当使用 sizeof 计算类型名大小不加括号时,编译器会报错。且由于操作符 sizeof 的返回值为 size_t 类型,所以输出时需要使用 %zd 格式。

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

如下代码:

c 复制代码
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 中。该函数的原型如下:

c 复制代码
// strlen() 函数原型
size_t strlen(const char *s);

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

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

如果传入 strlen() 函数的地址后面没有空字符,那么 strlen() 函数会往后一直计算字符个数,知道遇到空字符位置,这就造成了越界访问。如下代码:

可以看到,字符数组的长度本应为 3,可计算出的却是 42。由于字符数组 tmp 的末尾没有空字符,所以 strlen() 函数往后一直计算直到遇到空字符,而系统其他内存空间的值是未知的,所以最终显示得是一个随机值。

5. sizeof 和 strlen() 对比表格

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

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

1. 一维数组

c 复制代码
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

c 复制代码
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

c 复制代码
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));

解析:

  1. 从字符数组 arr 首地址开始计算字符串长度,由于该字符数组末尾没有空字符,造成越界访问,结果为随机值

  2. 随机值

  3. *a 是数组首元素字符 'a',其 ASCII 值为 65,而 strlen 函数需要 const char* 类型,那么 65 会被强制类型转换为 const char*,然后从该地址进行计算,这样就会造成非法访问。

  4. 非法访问

  5. &arr 是整个数组的地址,char (*)[6] 类型,会被强制类型转换为 const char*,相当于从数组首元素开始计算,越界访问,随机值

  6. &arr + 1 是数组 arr 后面第一个字节的地址,类型为 char (*)[6],被强制类型转换为 char*,然后从该地址开始计算,非法访问

  7. &arr[0]+1 是数组第二个元素的地址,越界访问,随机值

3. 代码C

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

c 复制代码
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

c 复制代码
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

c 复制代码
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. 二维数组

c 复制代码
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

c 复制代码
#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

c 复制代码
//在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

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

c 复制代码
//假设环境是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

c 复制代码
#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

c 复制代码
#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

c 复制代码
#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 为环境下,代码运行结果如下:

相关推荐
-一杯为品-6 分钟前
【51单片机】程序实验5&6.独立按键-矩阵按键
c语言·笔记·学习·51单片机·硬件工程
以后不吃煲仔饭10 分钟前
Java基础夯实——2.7 线程上下文切换
java·开发语言
进阶的架构师11 分钟前
2024年Java面试题及答案整理(1000+面试题附答案解析)
java·开发语言
前端拾光者15 分钟前
利用D3.js实现数据可视化的简单示例
开发语言·javascript·信息可视化
程序猿阿伟17 分钟前
《C++ 实现区块链:区块时间戳的存储与验证机制解析》
开发语言·c++·区块链
傻啦嘿哟35 分钟前
如何使用 Python 开发一个简单的文本数据转换为 Excel 工具
开发语言·python·excel
大数据编程之光39 分钟前
Flink Standalone集群模式安装部署全攻略
java·大数据·开发语言·面试·flink
初九之潜龙勿用39 分钟前
C#校验画布签名图片是否为空白
开发语言·ui·c#·.net
爱摸鱼的孔乙己1 小时前
【数据结构】链表(leetcode)
c语言·数据结构·c++·链表·csdn
Dola_Pan1 小时前
C语言:数组转换指针的时机
c语言·开发语言·算法