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

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

目录

  • 一、字符指针
    • 1. 指向变量
    • 2. 指向字符串常量
    • 3. 一道与字符串相关的笔试题
  • 二、数组指针
    • 1. 创建和初始化数组指针
    • 2. 通过数组指针打印数组元素
  • 三、二维数组传参的本质
  • 四、函数指针
    • 1. 创建和初始化函数指针
    • 2. 使用函数指针
    • 3. 两段有趣的代码
    • 4. 简化第二段代码
  • 五、函数指针数组
    • 1. 创建函数指针数组
    • 2. 使用函数指针数组来模拟计算器

一、字符指针

1. 指向变量

字符指针我们通常都是用来指向一个字符或者指向字符串和字符数组的开头。如下代码:

char c = 'c';  // 字符
char str1[] = "abcdef";  // 字符串
char str2[] = { 'a','b','c','d','e','f' };  // 字符数组
// 使用字符指针分别指向三个变量
char* pc1 = &c;
char* pc2 = str1;
char* pc3 = str2;

然后通过指针来打印这三个变量:
在这里插入图片描述
可以看到指针 pc1 和 pc2 正常输出,但是指针 pc3 后面输出了一堆垃圾值。这里就要说到 printf() 函数的特性了,当给 printf() 函数传递一个字符指针时,它会从该地址开始往后打印每个字符,直到遇到空字符才停止。 str1 是一个字符串,它的末尾有空字符;而 str2 是一个字符数组,末尾没有空字符。

2. 指向字符串常量

字符指针还可以指向字符串常量:

char* ps = "abcdef";  // 字符串指向字符串常量

然后我们可以使用 printf() 函数来进行打印:
在这里插入图片描述
根据以上种种特征,我们可以得出结论,ps 存储了字符串常量 “abcdef” 的首元素 ‘a’ 的地址。也就是说字符串常量实际上是其首元素的地址,跟数组名是数组首元素的地址有点类似。

3. 一道与字符串相关的笔试题

#include <stdio.h>
int main()
{char str1[] = "hello bit.";char str2[] = "hello bit.";const char *str3 = "hello bit.";const char *str4 = "hello bit.";if(str1 ==str2)printf("str1 and str2 are same\n");elseprintf("str1 and str2 are not same\n");if(str3 ==str4)printf("str3 and str4 are same\n");elseprintf("str3 and str4 are not same\n");return 0;
}

分析一下上述代码,创建了两个字符串 str1 和 str2,它们的内容都是 “hello bit.”;创建了两个字符指针,它们都指向字符串常量 “hello bit.”。如下图:
在这里插入图片描述
在 vs 编译器下,相同的字符串常量通常只有一个实例。所以,str1 != str2,str3 == str4。运行结果如下:
在这里插入图片描述

二、数组指针

我们知道 &数组名 取出的是整个数组的地址,只要是地址就可以存放在相应的指针中。所以,我们就可以创建一个指向数组的指针来存储数组的地址,也就是数组指针。

1. 创建和初始化数组指针

我们先创建一个数组:

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

数组指针首先是一个指针,所以首先需要跟符号 * 结合,*parr,然后去掉 *parr 后,剩下的类型就是该数组的类型,也就是 int [10],所以该数组指针如下:

int (*parr)[10] = &arr;  // 数组指针

由于解引用操作符的优先级低于下标运算符,所以需要添加圆括号。

2. 通过数组指针打印数组元素

int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
int(*parr)[10] = &arr;

指针 parr 指向整个数组 arr,那么 *parr 就等价于 arr,所以 (*parr)[i] 就等价于 arr[i]。那么,就可以通过如下方式访问整个数组:
在这里插入图片描述

三、二维数组传参的本质

我们知道一维数组传参有两种方式,指针传参和数组传参,但本质上都是指针传参。而二维数组也可以看成一维数组,只不过它的每个元素都是一维数组。那么,二维数组传参的本质实际上应该和一维数组一致,都是传递首元素的地址,也就是指针传参。

如下代码:

// 二维数组打印函数// 数组传参
void print1_2arr(int arr[][10], int row, int col)
{int i;for (i = 0; i < row; ++i){int j;for (j = 0; j < 10; ++j)printf("%d ", arr[i][j]);// 下一行printf("\n");}
}// 指针传参
void print2_2arr(int(*parr)[10], int row, int col)
{int i;for (i = 0; i < row; ++i){int j;for (j = 0; j < 10; ++j)printf("%d ", parr[i][j]);// 下一行printf("\n");}
}int main()
{int arr[10][10] = { 0 };print1_2arr(arr, 10, 10);  // 数组传参print2_2arr(arr, 10, 10);  // 指针传参
}

数组 arr 代表其首元素的地址,而它的首元素是一个数组,大小为 10,每个元素的类型是 int。所以 arr 的值应该存放在类型为 int (*)[10] 的数组指针中。也就对应了上面的指针传参函数 print2_2arr(),而数组传参只是形式上和数组对应,更加容易理解和使用,其本质上还是指针传参。

所以,不管是几维数组,都可以看成一维数组,传参时传递的都是其首元素的地址。因为传递的都是数组名,而数组名除了两种特殊情况,都是代表数组首元素的地址。

运行程序进行验证:
在这里插入图片描述
所以,数组传参都可以写成数组传递和指针传递两种形式,但是要知道其本质都是指针传递。

四、函数指针

不管是库函数还是自定义函数,都是要存放在内存空间中的,那么我们就可以使用指针存放这些函数的地址,然后通过指针调用这些函数。

1. 创建和初始化函数指针

假设有如下的加法函数:

// 加法函数
int Add(int x, int y)
{return x + y;
}

那么如何创建指向该函数的函数指针 pf ?首先,pf 需要和解引用操作符结合,表示 pf 是一个指针,然后去掉 *pf 剩下的就是该函数的类型,而函数 Add 的类型就是去掉函数名之后剩下的部分。所以,函数指针 pf 的创建如下:

int (*pf)(int, int) = &Add;  // 函数指针 pf

而实际上,函数名就是函数的地址,也就是说 Add 和 &Add 是等价的,如下代码:
在这里插入图片描述
可以看到两个输出结果是同一个地址,所以函数指针初始化就可以有两种形式:

int (*pf1)(int, int) = &Add;  // 函数指针 pf1
int (*pf2)(int, int) = Add;  // 函数指针 pf2

2. 使用函数指针

从上述代码中我们可以得出结论:*pf1 等价于 Add,而 pf2 也等价于 Add。那么理论上,我们可以通过 *pf1 或者 pf2 来调用该函数。如下代码:
在这里插入图片描述
可以看到,三条打印语句的结果都是一样的。

3. 两段有趣的代码

(1)

(*(void (*)())0)();

我们通过画图板来逐步解析这条语句,首先,void (*)() 是一个函数指针,该指针指向一个函数,该函数没有参数也不返回任何值。
在这里插入图片描述

然后相邻外边的圆括号就对数字 0 进行强制类型转换,把 0 转换成该类型的函数指针。
在这里插入图片描述
然后左边的解引用操作符和外面的圆括号一起,对函数指针 0 进行解引用操作,因为函数调用操作符的优先级高于解引用操作符。
在这里插入图片描述
最后通过函数调用操作符调用该函数。
在这里插入图片描述
所以,该代码的运行是:先把数字 0 强制类型转换成函数指针,该指针指向没有参数没有返回值的函数。然后对其解引用得到该函数,然后调用该函数。

(2)

void (*signal(int , void(*)(int)))(int);

首先,signal 先和后面的圆括号结合,表明其是一个函数,其参数为 int 和一个指向没有参数也没有返回值的函数的函数指针。
在这里插入图片描述
然后去掉函数名和参数列表,得到该函数的返回类型。
在这里插入图片描述
该函数的返回类型也是一个指向没有参数也没有返回值的函数的函数指针。

所以,上述代码就是一个函数声明,该函数有两个参数,一个 int,一个函数指针,然后返回类型也是一个函数指针。

4. 简化第二段代码

关键字 typedef 的作用是为类型重命名,那么通过就可以使用它来为函数指针 void (*)() 重命名,从而简化代码。如下:

// 重命名函数指针 void (*)()
typedef void (*pf)();

现在 pf 就代表 void (*)() 函数指针类型,那么第二段带代码就可以按照如下形式编写:

pf signal(int, pf);

五、函数指针数组

1. 创建函数指针数组

假设需要创建一个函数指针数组 arr_pf,大小为 4,每个元素都是指向有两个 int 参数且返回值为 int 的函数指针。

首先,arr_pf 需要先跟下标运算符结合,表示 arr_pf 是一个数组,然后去掉 arr_pf[4] 之后,剩下的就是数组元素的类型,也就是 int (*)(int, int)。所以,代码如下:

int (*arr_pf[4])(int, int);  // 函数指针数组

2. 使用函数指针数组来模拟计算器

这里只模拟两个整数进行加减乘除的简单计算器。那么就需要支持两个整数进行加减乘除的四个函数:

// 加法
int Add(int x, int y)
{return x + y;
}// 减法
int Sub(int x, int y)
{return x - y;
}// 乘法
int Mul(int x, int y)
{return x * y;
}// 除法
int Div(int x, int y)
{return x / y;
}

然后使用一个函数指针数组来存放这四个函数:

int (*arr_pf[5])(int, int) = { NULL, Add, Sub, Mul, Div };

这里为什么把数组的大小设置为 5,第一个元素设置为空,是为了与下面的 switch 选择语句配合。

下面就是通过一个循环,打印菜单,让用户选择加减乘除,然后进行相应的运算,直到用户退出程序。

// 菜单
void menu()
{printf("*********************************************\n");printf("*********    1. Add      2. Sub    **********\n");printf("*********    3. Mul      4. Div    **********\n");printf("*********    0. Exit               **********\n");printf("*********************************************\n");
}// 计算
void calc(int (*pf)(int, int))
{int x, y;printf("请输入需要计算的两个数:");scanf("%d %d", &x, &y);printf("结果为:%d\n", pf(x, y));
}int main()
{int (*arr_pf[5])(int, int) = { NULL, Add, Sub, Mul, Div };int select = 0;// 模拟计算机实现do{// 菜单menu();// 选择printf("请选择:");scanf("%d", &select);// 判断switch (select){case 1:calc(arr_pf[1]);  // 加法break;case 2:calc(arr_pf[2]);  // 减法break;case 3:calc(arr_pf[3]);  // 乘法break;case 4:calc(arr_pf[4]);  // 除法break;case 0:printf("退出计算器\n");break;default:printf("选择错误,请重新选择!\n");break;}} while (select);return 0;
}

上述代码额外创建了一个函数 calc(),通过传递的函数指针来调用相应的函数。这样提高了代码的可读性,也减少了代码的冗余。而在 calc() 中通过函数指针被调用的函数也叫回调函数。

回调函数是一种通过函数指针调用的函数,使得某个特定的动作在特定的时间点被执行。

代码运行测试结果如下:
在这里插入图片描述


http://www.mrgr.cn/news/57481.html

相关文章:

  • (成功解决)ubuntu22.04不小心更新成了atzlinux12.7.1,右上角出现红色错误符号
  • kotlin 中 ::class ::class.java 和 .javaClass 区别
  • 桑黄辅助放、化疗比单独使用更有效
  • 项目中使用markdown-editor-django版
  • 常用分布的数学期望、方差、特征函数
  • WPS宏合并多表格指定Sheet页内容(去多余表头版)
  • 【1024特辑 | 机器学习-无监督学习】EM算法
  • 从文本到知识图谱:GraphRAG 各步骤的技术拆解与实现
  • django教育网站-计算机设计毕业源码89335
  • QML----Webengineview点击网页上的下载没反应,下载文件
  • 关于搜索接口被攻击,如何优化思路
  • 背包九讲——混合背包问题
  • 华为OD机试真题---We Are A Team
  • paddleocr使用FastDeploy 部署工具部署 rknn 模型
  • 智能扭矩系统Torque在新能源领域的应用_SunTorque
  • threejs中的小案例
  • autMan奥特曼机器人-出现argument list too long报错的解决方法
  • 哈希——哈希的基本概念
  • 两个开源AI应用让Claude 3.5 直接操作你的电脑;构建和部署多智能体系统课程;简化PDF文档管理并提供智能聊天功能
  • 通过运行窗口呼出Windows功能的快捷命令集合
  • Swarm集群管理常用命令与详解
  • 在 Spring 框架中,@ComponentScan` 扫描的注解类型
  • Bros!使用 focus 和 blur 事件时别忽略了这一点!
  • CentOS 6 修改 yun 源
  • 【Linux】 su 和 sudo 的区别剖析
  • C#,自动驾驶技术,ASAM OpenDRIVE BS 1.8.0 规范摘要与C# .NET Parser