【C语言:深入理解指针二】

文章目录

  • 1. 二级指针
  • 2. 指针数组
  • 3. 字符指针变量
  • 4. 数组指针变量
  • 5. 二维数组传参的本质
  • 6. 函数指针变量
  • 7. 函数指针数组
  • 8. 转移表
  • 9. 回调函数
  • 10. qsort函数的使用与模拟实现

在这里插入图片描述

1. 二级指针

我们知道,指针变量也是变量,它也有自己的地址,使用什么来存放它的地址呢?答案是:二级指针。

int main()
{int a = 10;int* p = &a;int** pp = &p;  //二级指针变量ppreturn 0;
}

在这里插入图片描述

关于二级指针的运算
在这里插入图片描述

  • *pp先解引用,对pp中的地址进行访问,访问的就是p
  • **pp, 先通过*pp找到p,再对p进行解引用,访问的就是a

2. 指针数组

指针数组,顾名思义,它应该是一个数组,是用来存放指针的。
指针数组中的每一个元素又是一个地址,可以指向另一个区域。

int main()
{int arr1[] = { 1,2,3,4,5 };int arr2[] = { 2,3,4,5,6 };int arr3[] = { 3,4,5,6,7 };//数组名是数组首元素的地址,类型是int*,可以存放在数组指针arr中int* arr[3] = { arr1, arr2, arr3 };return 0;
}

在这里插入图片描述

  • 使用指针数组模拟二维数组
int main()
{int arr1[] = { 1,2,3,4,5 };int arr2[] = { 2,3,4,5,6 };int arr3[] = { 3,4,5,6,7 };//数组名是数组首元素的地址,类型是int*,可以存放在数组指针arr中int* arr[3] = { arr1, arr2, arr3 };int i = 0;for (i = 0; i < 3; i++){int j = 0;for (j = 0; j < 5; j++){printf("%d ", arr[i][j]);//也可以写成下面这种形式//printf("%d ", *(*(arr + i) + j));}printf("\n");}return 0;
}

3. 字符指针变量

在指针的类型中,我们知道有一种指针类型叫字符指针。
一般使用:

int main()
{char ch = 'c';char* pc = &ch;*pc = 'a';printf("%c\n", ch);return 0;
}

还有一种使用方式:

int main()
{char* pc = "abcdef";printf("%s\n", pc);return 0;
}
  1. 可以把字符串想象为一个字符数组,但是这个数组是不能修改的,因此为了避免出错,常常加上const
const char* pc = "abcdef";
  1. 当常量字符串出现在表达式中的时候,他的值是第一个字符的地址。当我们知道存放的是第一个字符的地址的时候,我们就可以这样玩
int main()
{char* pc = "abcdef";printf("%c\n", "abcdef"[3]);   //dprintf("%c\n", pc[3]);		   //dreturn 0;
}

下面来看一道题目:
在这里插入图片描述
str1 不等于 str2 这个很好理解;str3 等于str4怎么理解呢?
在这里插入图片描述
由于str3 与 str4中存放的都是常量字符串,C/C++会把常量字符串存储到单独的⼀个内存区域,当⼏个指针指向同⼀个字符串的时候,他们实际会指向同⼀块内存。(内容相等的常量字符串仅保存一份)
但是⽤相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4相同。

4. 数组指针变量

数组指针变量是数组还是指针呢?答案是:指针。
我们已经熟悉:

  • 整形指针变量: int * pint; 存放的是整形变量的地址,能够指向整形数据的指针。
  • 浮点型指针变量: float * pf; 存放浮点型变量的地址,能够指向浮点型数据的指针。

那数组指针变量应该是:存放的应该是数组的地址,能够指向数组的指针变量

int main()
{int arr[10] = { 0 };int(*parr)[10] = &arr;  //数组指针变量parrreturn  0;
}

在这里插入图片描述

5. 二维数组传参的本质

我们知道一维数组传参的本质是:传的是数组首元素的地址。
那二维数组呢?
过去我们使用二维数组时,是这样的:

void func(int arr[][5], int row, int col)
{int i = 0;for (i = 0; i < row; i++){int j = 0;for (j = 0; j < col; j++){printf("%d ", arr[i][j]);}printf("\n");}
}
int main()
{int arr[3][5] = { {1,2,3,4,5}, {2,3,4,5,6}, {3,4,5,6,7} };func(arr, 3, 5);return 0;
}

这里实参是⼆维数组,形参也写成⼆维数组的形式,那还有什么其他的写法吗?
首先,我们应该知道⼆维数组其实可以看做是每个元素是⼀维数组的数组,也就是⼆维数组的每个元素是⼀个⼀维数组。那么⼆维数组的首元素就是第一行,是个⼀维数组。
在这里插入图片描述

所以,根据数组名是数组首元素的地址这个规则,⼆维数组的数组名表示的就是第⼀⾏的地址,是⼀维数组的地址。根据上⾯的例⼦,第⼀行的⼀维数组的类型就是 int [5] ,所以第⼀行的地址的类型就是数组指针类型 int(*)[5] 。那就意味着⼆维数组传参本质上也是传递了地址,传递的是第⼀行这个⼀维数组的地址,那么形参也是可以写成指针形式的。如下:

void func(int (*arr)[5], int row, int col)
{int i = 0;for (i = 0; i < row; i++){int j = 0;for (j = 0; j < col; j++){printf("%d ", *(*(arr + i) + j));}printf("\n");}
}

6. 函数指针变量

函数指针变量应该是用来存放函数地址的,未来通过地址能够调用函数,那函数是否真的有地址呢?
在这里插入图片描述
确实打印出来了地址,所以函数是有地址的。

  • 函数名就是函数的地址
  • &函数名 也是函数的地址。

这里和数组相比还是有区别的

  • 数组名是数组首元素的地址;
  • &数组名是整个数组的地址

函数指针类型解析:

在这里插入图片描述
函数指针变量的使用

int add(int x, int y)
{return x + y;
}
int main()
{int (*p1)(int, int) = &add;int ret1 = add(3, 5);printf("%d\n", ret1);  //8int ret2 = (*p1)(3, 5);printf("%d\n", ret2);  //8int (*p2)(int, int) = add;int ret3 = (*p2)(4, 6);printf("%d\n", ret3);  //10int ret4 = p2(4, 6);printf("%d\n", ret4);   //10return 0;
}

因为函数名就是地址,我们调用函数的时候没有使用解引用,所以函数指针也可以不解引用, 如ret4。

两段有趣的代码:

(*(void (*)())0)();
  1. 上述代码是一次函数调用
  2. 将0强制类型转换成一个函数指针,这个函数没有参数,返回类型是void
  3. (*0)()调用0地址处的函数
 void (*  signal(int , void(*)(int))  )(int);
  1. signal是一个函数名,这个函数有两个参数,一个是整型int,一个是函数指针类型的 void (*)(int),这个函数的参数是int,返回值是void
  2. void (*)(int) 去掉函数名和函数参数,剩下的就是函数的返回类型。该signal函数的返回类型是 void ( * )(int)的函数指针。

这样写是不是挺不好理解的,我们可以使用typedef将复杂的类型简单化。
比如,将 int* 重命名为 int_p ,这样写

typedef int* int_p;

但是对于数组指针和函数指针稍微有点区别:新的名字必须在*的旁边
比如我们有数组指针类型 int(*)[5] ,需要重命名为 parr_t ,那可以这样写:

typedef int (*parr_t)[5]

将 void(*)(int) 类型重命名为 pf_t ,就可以这样写:

typedef void (*pf_t)();
pf_t signal(int, pf_t);  //signal函数就可以被这样简化

7. 函数指针数组

数组是⼀个存放相同类型数据的存储空间,而且我们已经学习了函数指针。

int Add(int x, int y)
{return x + y;
}int main()
{int (*p)(int, int) = Add;  //函数指针return 0;
}

那么如果要把多个函数(函数参数的类型、返回值的类型都应相同 )的地址存放起来,那函数指针数组应该如何定义呢?

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 main()
{int (*p)(int, int) = Add;//函数指针int (*pArr[])(int, int) = { Add, Sub, Mul, Div };  //函数指针数组return 0;
}

我们都知道,一个数组,去掉数组名和 [ ] 剩下的就是数组元素的类型。例如 int arr[ ], int 就是数组元素的类型。
因此 int (* pArr[ ] )(int, int),去掉数组名和 [],剩下的int (*)(int ,int)就是这个数组元素的类型,很显然,这是数组元素的类型是函数指针类型。

函数指针数组的使用:
在这里插入图片描述

8. 转移表

当我们学习完函数指针数组以后,你是否也有过这样的疑问:我直接通过函数名调用函数不是更简单吗,干嘛还要放进数组中,然后再调用函数呢?请看下面的代码:
假设我们要实现一个计算器,一般写法是不是这样呢?

void menu()
{printf("****************************\n");printf("******1.Add       2.Sub*****\n");printf("******3.Mul       4.Div*****\n");printf("******0.exit           *****\n");printf("****************************\n");
}int main()
{int input = 0;int x = 0;int y = 0;int ret = 0;do{menu();printf("请选择:\n");scanf("%d", &input);switch (input){case 1:printf("请输入两个操作数:");scanf("%d %d", &x, &y);ret = Add(x, y);printf("%d\n", ret);break;case 2:printf("请输入两个操作数:");scanf("%d %d", &x, &y);ret = Sub(x, y);printf("%d\n", ret);break;case 3:printf("请输入两个操作数:");scanf("%d %d", &x, &y);ret = Mul(x, y);printf("%d\n", ret);break;case 4:printf("请输入两个操作数:");scanf("%d %d", &x, &y);ret = Div(x, y);printf("%d\n", ret);break;case 0:printf("退出计算器\n");break;default:printf("选择错误,请重新选择!\n");}} while (input);return 0;
}

在这里插入图片描述
因此,我们就可以利用转移表来解决代码的冗余问题,首先,我们要知道什么是转移表?

转移表就是用一个函数指针数组存储每一个自定义的函数指针,在调用自定义函数的时候,就可以通过数组下标访问 ----总结于《C和指针》

利用转移表解决问题:

int main()
{int input = 0;int x = 0;int y = 0;int ret = 0;int (*pArr[])(int, int) = { NULL, Add, Sub, Mul ,Div };//                           0     1    2    3    4   使用NULL巧妙地与选择相对应do{menu();printf("请选择:\n");scanf("%d", &input);if (input == 0){printf("退出计算器\n");}else if (input >= 1 && input <= 4){printf("请输入两个操作数:");scanf("%d %d", &x, &y);ret = pArr[input](x, y);printf("%d\n", ret);}else{printf("选择错误,请重新选择!\n");}} while (input);return 0;
}

这样是不是就很好地解决了代码地冗余问题,同时也方便了以后再增加新的功能,只需向函数指针数组中添加函数的地址即,改变以下判断的范围即可,无需再写一大串的case了。

9. 回调函数

  1. 回调函数是什么呢?
    回调函数就是⼀个通过函数指针调用的函数。
    如果你把函数的指针(地址)作为参数传递给另⼀个函数,当这个指针被用来调用其所指向的函数时,被调用的函数就是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的⼀方调用的,用于对该事件或条件进行响应

Calculate函数的参数是函数指针类型的

void Calculate(int (*pfunc)(int, int))
{int x = 0;int y = 0;int ret = 0;printf("请输入两个操作数:");scanf("%d %d", &x, &y);ret = pfunc(x, y);   //通过函数指针调用函数printf("%d\n", ret);
}int main()
{int input = 0;do{menu();printf("请选择:\n");scanf("%d", &input);switch (input){case 1:Calculate(Add);break;case 2:Calculate(Sub);break;case 3:Calculate(Mul);break;case 4:Calculate(Div);break;case 0:printf("退出计算器\n");break;default:printf("选择错误,请重新选择!\n");}} while (input);return 0;
}

我们可以发现,当Calculate函数的参数是函数指针类型时,只要你给Calculate函数传递一个函数指针类型的变量,它都可以调用,这样看它的功能是不是强大了不少。

10. qsort函数的使用与模拟实现

  1. qsort函数是什么?
    qsort函数是可以排序任何类型数据的一个函数。
  2. qsort是如何设计的?
void qsort (void* base, size_t num, size_t size,int (*compar)(const void*,const void*));

这个函数有四个参数,各参数的说明如下:

在这里插入图片描述

第四个参数有点特别,它是一个函数指针,指针指向的函数的功能是比较两个元素的大小,这个函数需要qsort函数的使用者自己实现,并且函数的返回值要符合qsort函数的要求

在这里插入图片描述
那么我们先来使用以下qsort函数吧:

struct Stu
{int age;char name[20];
};
//使用者自己实现两个元素的比较函数
int cmp(const void* p1, const void* p2)
{return   *((int*)p1) - *((int*)p2);//此处是将p1、p2变量强制转换为 整型指针变量 然后解引用
}int cmp_struct_name(const void* p1, const void* p2)
{return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
}void print1(int arr[], int sz)
{int i = 0;for (i = 0; i < sz; i++){printf("%d ", arr[i]);}printf("\n");
}void print2(struct Stu arr[],int sz)
{int i = 0;for (i = 0; i < sz; i++){printf("%d %s\n", arr[i].age, arr[i].name);}printf("\n");
}int main()
{int arr1[] = { 2,1,8,5,6,3,4,9,7,0 };int sz1 = sizeof(arr1) / sizeof(arr1[0]);print1(arr1, sz1);qsort(arr1, sz1, sizeof(arr1[0]), cmp);print1(arr1, sz1);struct Stu arr2[] = { {18, "zhangsan"}, {38, "lisi"}, {25, "wangwu"} };int sz2 = sizeof(arr2) / sizeof(arr2[0]);print2(arr2, sz2);qsort(arr2, sz2, sizeof(arr2[0]), cmp_struct_name);print2(arr2, sz2);return 0;
}

在这里插入图片描述

接下来,我们就来模拟实现一个qsort函数,由于qsort的实现使用的是快速排序,我们在此就使用冒泡排序

//使用者自己实现两个元素的比较函数
int cmp(const void* p1, const void* p2)
{return   *((int*)p1) - *((int*)p2);//此处是将p1、p2变量强制转换为 整型指针变量 然后解引用
}int cmp_struct_name(const void* p1, const void* p2)
{return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
}int cmp_struct_age(const void* p1, const void* p2)
{return (*(struct Stu*)p1).age - (*(struct Stu*)p2).age;
}//由于被交换的数据的类型不是固定的,但是数据类型的大小是知道的
//因此我们可以交换数据的每一个字节的数据
void _Swap(const void* p1, const void* p2, int sz)
{int i = 0;//交换数据的每一个字节for (i = 0; i < sz; i++){char tmp = *((char*)p1 + i);*((char*)p1 + i) = *((char*)p2 + i);*((char*)p2 + i) = tmp;}
}void my_qsort(void* base, int num, int size, int (*compar)(const void*, const void*))
{int i = 0;for (i = 0; i < num - 1; i++){int j = 0;for (j = 0; j < num - 1 - i; j++){
//此处应该是传给compar()两个参数arr[j]与arr[j+1],让其进行比较
//但是怎么拿到要传的数呢?
//qsort函数只有这个数组首元素的地址,和数组元素类型的大小
//因此我可以让base指针加上 j个类型的大小 找到某个元素的首地址,具体比较多大内容的数据,看比较什么类型的数据,由使用者决定
//但是我得到的数组首元素的地址也是void* 类型的,所以我们可以将base转换位char*类型的指针,一次访问 j*size 大小if (compar((char*)base + j * size, (char*)base + (j + 1) * size) > 0){//交换_Swap((char*)base + j * size, (char*)base + (j + 1) * size, size);}}}
}
int main()
{//比较整型的数据int arr1[] = { 2,1,8,5,6,3,4,9,7,0 };int sz1 = sizeof(arr1) / sizeof(arr1[0]);printf("排序整型\n");print1(arr1, sz1);my_qsort(arr1, sz1, sizeof(arr1[0]), cmp);print1(arr1, sz1); //比较结构体类型的数据struct Stu arr2[] = { {38, "lisi"}, {18, "zhangsan"}, {25, "wangwu"} };int sz2 = sizeof(arr2) / sizeof(arr2[0]);printf("排序结构体型-按姓名\n");print2(arr2, sz2);my_qsort(arr2, sz2, sizeof(arr2[0]), cmp_struct_name);print2(arr2, sz2);printf("排序结构体型-按年龄\n");print2(arr2, sz2);my_qsort(arr2, sz2, sizeof(arr2[0]), cmp_struct_age);print2(arr2, sz2);return 0;
}

在这里插入图片描述
本次的分享就到这里啦~在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/164374.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【面试】jvm中堆是分配对象存储的唯一选择吗

目录 一、说明二、逃逸分析2.1 说明2.2 参数设置 一、说明 1.在《深入理解Java虚拟机》中关于Java堆内存有这样一段描述:随着JIT编译期的发展与逃逸分析技术逐渐成熟&#xff0c;栈上分配、标量替换优化技术将会导致一些微妙的变化&#xff0c;所有的对象都分配到堆上也渐渐变得…

牛客 最小公配数 golang版实现

题目请参考: HJ108 求最小公倍数 题解: 在大的数的倍数里面去找最小的能整除另外一个数的数&#xff0c;就是最小公倍数&#xff0c;按照大的来找&#xff0c;循环次数能够降到很少&#xff0c;提升效率 golang实现: package mainimport ("fmt" )func main() {a : …

CSDN最新最全python+pytest接口自动化(12)-自动化用例编写思路 (使用pytest编写一个测试脚本)

经过之前的学习铺垫&#xff0c;我们尝试着利用pytest框架编写一条接口自动化测试用例&#xff0c;来厘清接口自动化用例编写的思路。 我们在百度搜索天气查询&#xff0c;会出现如下图所示结果&#xff1a; 接下来&#xff0c;我们以该天气查询接口为例&#xff0c;编写接口测…

JVM 之 class文件详解

目录 一. 前言 二. class文件结构 2.1. 文件格式 2.2. 魔数与版本号 2.3. 常量池 2.4. 访问标志 2.5. 类索引、父类索引和接口索引集合 2.6. 字段表集合 2.7. 方法表集合 2.8. 属性表集合 2.8.1. Code 属性表 2.8.2. Exceptions 属性 2.8.3. LineNumberTable 属性…

R语言数据缩放-1到1

目录 普通scale -1到1限定范围scale 普通scale R语言实战&#xff1a;scale&#xff08;&#xff09;函数 - 知乎 (zhihu.com) scale(x, center TRUE, scale TRUE) 过程&#xff1a; 对每个变量&#xff08;列&#xff09;计算平均值&#xff08;mean&#xff09;和标准…

QT中样式表常见属性与颜色的设置与应用

常见样式表属性 在Qt中的样式表(QSS)中,有一些特定的英文单词和关键字用于指定不同的样式属性。以下是常见的一些英文单词和关键字: 颜色(Colors): color: 文本颜色 background-color: 背景颜色 border-color: 边框颜色 字体(Fonts): font: 字体 font-family: 字体…

任意文件下载漏洞(CVE-2021-44983)

简介 CVE-2021-44983是Taocms内容管理系统中的一个安全漏洞&#xff0c;可以追溯到版本3.0.1。该漏洞主要源于在登录后台后&#xff0c;文件管理栏存在任意文件下载漏洞。简言之&#xff0c;这个漏洞可能让攻击者通过特定的请求下载系统中的任意文件&#xff0c;包括但不限于敏…

python3实现grep命令

由于windows上面没有类似linux上面的grep命令&#xff0c;所以下面的python脚本来代替其能力。 grep.py import argparse import reif __name__ __main__:arg_parser argparse.ArgumentParser()arg_parser.add_argument("grep")arg_parser.add_argument("fil…

图像导向滤波

导向滤波&#xff08;Guided Filter&#xff09;是一种基于局部线性模型的滤波方法&#xff0c;用于图像处理中的去噪、图像增强和边缘保留等任务。它结合了引导图像&#xff08;guide image&#xff09;和输入图像来实现对输入图像的滤波操作。 原理 数学原理&#xff1a; …

文件名称管理文件:抓关键字归类文件,让文件管理变得简单明了

在当今数字时代&#xff0c;每天都要处理大量的文件&#xff0c;无论是文本、图片、视频还是其他类型的文件。如何有效地管理这些文件&#xff0c;能够迅速找到所需的信息&#xff0c;已经成为了一个重要的问题。文件名称是文件内容的第一反映&#xff0c;也是识别和检索文件的…

408-数据结构-代码题

2014 2014 二叉树&#xff08;链式存储&#xff09; #include<iostream> #include<bits/stdc.h> using namespace std;typedef struct Node{struct Node *left;struct Node *right;int high0;double weight; }node;double sum0;void visit(node *t){int lop0;if…

算法刷题-动态规划2(继续)

算法刷题-动态规划2 珠宝的最高价值下降路径最小和使用最小花费爬楼梯整数拆分 珠宝的最高价值 题目 大佬思路 多开一行使得代码更加的简洁 移动到右侧和下侧 dp[ i ][ j ]有两种情况&#xff1a; 第一种是从上面来的礼物最大价值&#xff1a;dp[ i ][ j ] dp[ i - 1 ][ j ]…

【CCF-PTA】第03届Scratch第02题 -- 计算天数

计算天数 【题目描述】 一年有 365 天还是有 366 天呢&#xff1f;要看这一年是不是闰年。有个计算方法可以帮助我们判断&#xff0c;那就是闰年能够除尽 4 但不能除尽 100 或者能够除尽 400 的年份。如果这一年是闰年&#xff0c;2 月份的天数就是 29 天。小明决定编写一个程…

排序算法--希尔排序

实现逻辑 ① 先取一个小于n的整数d1作为第一个增量&#xff0c;把文件的全部记录分成d1个组。 ② 所有距离为d1的倍数的记录放在同一个组中&#xff0c;在各组内进行直接插入排序。 ③ 取第二个增量d2小于d1重复上述的分组和排序&#xff0c;直至所取的增量dt1(dt小于dt-l小于……

JSP:Servlet

Servlet处理请求过程 B/S请求响应模型 Servlet介绍 JSP是Servlet的一个成功应用&#xff0c;其子集。 JSP页面负责前台用户界面&#xff0c;JavaBean负责后台数据处理&#xff0c;一般的Web应用采用JSPJavaBean就可以设计得很好了。 JSPServletJavaBean是MVC Servlet的核心…

【实验笔记】C语言实验——降价提醒机器人

降价提醒机器人 题目&#xff1a; 小 T 想买一个玩具很久了&#xff0c;但价格有些高&#xff0c;他打算等便宜些再买。但天天盯着购物网站很麻烦&#xff0c;请你帮小 T 写一个降价提醒机器人&#xff0c;当玩具的当前价格比他设定的价格便宜时发出提醒。 输入格式&#xf…

人工智能教程(一):基础知识

目录 前言 什么是人工智能&#xff1f; 教学环境搭建 向量和矩阵 前言 如果你是关注计算机领域最新趋势的学生或从业者&#xff0c;你应该听说过人工智能、数据科学、机器学习、深度学习等术语。作为人工智能系列文章的第一篇&#xff0c;本文将解释这些术语&#xff0c;并搭…

k8s部署-kuboard安装(工具kuboard-spary)

Kuboard-Spray Kuboard-Spray 是一款可以在图形界面引导下完成 Kubernetes 高可用集群离线安装的工具 配置要求 对于 Kubernetes 初学者&#xff0c;在搭建K8S集群时&#xff0c;推荐在阿里云或腾讯云采购如下配置&#xff1a;&#xff08;您也可以使用自己的虚拟机、私有云等…

HCIP --- HCIA(部分汇总)--- 点对点网络

抽象语言 --- 电信号 抽象语言 --- 编码 编码 --- 二进制 二进制 --- 电信号 处理电信号 OSI/RM ---- 开放式系统互联参考模型 --- 1979 --- ISO --- 国际标准化组织 核心思想 --- 分层 应用层 --- 提供各种应用程序&#xff0c;抽象语言转换成编码&#xff0c;人机交互…