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

C语言:指针(3)

1. 字符指针变量

我们知道,指针变量中有一种类型为 char* ,一般我们会这么使用:

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

将字符变量ch的地址赋值给了字符指针变量pc,再通过解引用操作改变了ch的值。

但是,其实还有另一种使用方法:

int main()
{const char* p = "hello world.";printf("%s ",p);return 0;
}
hello world. 

在这个代码中,很容易让人误解为是将字符串"hello world."放入了指针变量p中。但是很显然,字符串并不是字符指针,不可能是将字符串的整体内容放入了指针变量中,指针变量p中存放的一定是字符指针。

实际上,这里的操作是将字符串的首字符 'h' 的地址传存放到了指针变量p中。

了解了这个知识后我们来看看下面这道题目:

int main()
{char p1[] = "hello world.";char p2[] = "hello world.";char* p3 = "hello world.";char* p4 = "hello world.";if(p1 == p2)printf("p1与p2相同\n");elseprintf("p1与p2不同\n");if(p3 == p4)printf("p3与p4相同\n");elseprintf("p3与p4不同\n");return 0;
}

读完代码后,我们思考一下,再看输出结果:

p1与p2不同
p3与p4相同

p1与p2不同,这是因为p1与p2都是字符数组的数组的数组名,而数组名代表了数组首元素的地址,很明显,p1和p2是两个不相同的数组,尽管数组中元素是一样的,但是两个数组在内存中的地址却是不一样的,所以p1和p2不相同的。

p3与p4相同,这是因为p3与p4都是字符型指针变量,而他们都共同指向字符串常量"hello world.",共用常量的内存。因此,p3和p4都指向同一个地址,p3和p4是相同的。

2. 数组指针变量

2.1 什么是数组指针变量

前面我们学习过指针数组,指针数组是一种数组,内部存放的都是指针。那么数组指针变量又是什么呢?

答案是:数组指针变量也是一种指针变量。

类比一下其他类型的指针变量:

整型指针变量:int * p
字符型指针变量:char * p

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

那么,我们来区分一下数组指针变量与指针数组:

int *p1[10];
int (*p2)[10];

很明显,前面我们学习过,第一行属于指针数组,内部存放指针的数组。那么第二行就是数组指针变量了。

我们发现,这两行代码的差别在于第二行多了一个圆括号,正是这个圆括号起作用,让原本指针数组的语法变为了数组指针变量的语法,那么圆括号的作用是什么呢?

当没有圆括号时,p1会优先与[10]结合,表示这是一个名为p1,拥有十个元素的数组,然后int* 表示元素的类型是int* ,所以p1是指针数组,内部存放指针。而在加上了圆括号后,p2优先与 * 结合,表示p2为指针,而int [10]表示指针指向的是一个拥有十个整型元素的数组,所以p2是数组指针变量,指向一个数组。

这里要注意:[ ]的优先级要高于*号的,所以必须加上()来保证p先和 * 结合。

2.2 数组指针变量的初始化

既然数组指针变量是用来存放数组地址的,那我们怎么获得数组的地址呢?这里就会用到 & 了。

int main()
{int i;int arr[10] = {1,2,3,4,5,6,7,8,9,0};int (*p)[10] = &arr;for (i = 0; i < 10; i++){printf("%d ",(*p)[i]);}return 0;
}
1 2 3 4 5 6 7 8 9 0 

可以看到,我们通过p成功访问到了数组中的元素,证明p成功得到了数组的地址。

3. 二维数组传参的本质

理解了数组指针变量后,我们就可以来了解一下二维数组传参的本质了。

过去我们有⼀个二维数组的需要传参给一个函数的时候,我们是这样写的:

int i,j;
void test(int arr[3][5],int r,int c)
{for(i = 0;i < r;i++){for(j = 0;j < c;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}};test(arr,3,5);return 0;
}
1 2 3 4 5 
2 3 4 5 6 
3 4 5 6 7 

在这里,二维数组是实参,我们把形参也写成了二维数组的形式,那我们能不能将形参的形式换一个写法呢?

我们回顾一下二维数组的知识:二维数组可以看为是每个元素都是一维数组的数组,那么二维数组的首元素就是第一行,是个一维数组。

因此,根据数组名是数组首元素的地址这个规则,二维数组的数组名表示的就是第一行的地址,是一维数组的地址。那第一行的类型就是int [5],地址类型就是int (*)[5] 。那么二维数组传参本质上也是传递了地址传递了第一行的地址。既然是地址,我们就可以用指针变量来表示,那我们就可以将形参写为指针形式,例如:

int i,j;
void test(int (*p)[5],int r,int c)
{for(i = 0;i < r;i++){for(j = 0;j < c;j++){printf("%d ",p[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}};test(arr,3,5);return 0;
}
1 2 3 4 5 
2 3 4 5 6 
3 4 5 6 7 

代码也和预期一样正常执行,证明我们的语法没有出现错误。

总结:二维数组传参,形参的部分可以写成数组,也可以写成指针形式。

4. 函数指针变量

4.1 函数指针变量的创建

那么,什么是函数指针变量呢?

根据前面学习整型指针,数组指针的时候,我们进行类比,我们不难得出结论:

函数指针变量应该是用来存放函数的地址的,指针指向函数,我们可以通过指针来调用函数。

但是我们前面的学习中并没有提到过函数的地址这个概念,函数是否有地址呢?我们可以用进行测试:

void test()
{printf("hello world.\n");
}
int main()
{printf("test  = %p\n",test);printf("&test = %p\n",&test);return 0;
}
test  = 00007FF79DA515E0
&test = 00007FF79DA515E0

我们可以发现,函数的确拥有地址,并且,和数组类似,函数名单独使用时也代表了函数的地址。

那么,我们应该怎么创建函数指针变量来存放函数的地址呢?

我们类比一下数组指针变量的创建,我们需要表示数组元素的类型以及数组的大小。而函数除去函数名外还有函数返回值类型与参数两部分,所以,我们在创建函数指针变量时也要声明这两个部分。

那么,函数指针变量创建的语法如下:

void test()
{printf("hello world.\n");
}
int main()
{void (*p)() = test;printf("p    = %p\n",p);printf("test = %p\n",test);return 0;
}
p    = 00007FF7A42B15E0
test = 00007FF7A42B15E0

我们发现,两行打印结果相同,证明p和test指向的是同一个地址。p成功存放了函数test的地址。

对于拥有参数的函数,函数指针变量的创建语法如下:

int Add(int x,int y)
{return x + y;
}
int main()
{int (*p1) (int x,int y) = Add;int (*p2) (int ,int ) = Add;return 0;
}

对于形参部分,我们只需要写出形参类型即可,形参写不写出都可。

函数指针类型解析:

int (*p1) (int x,int y) = Add;
int表示函数的返回值类型为int
(int x,int y)则表示了形参的个数以及形参的值的类型
(*p)则表示函数指针变量的名称

4.2 函数指针变量的使用

既然我们将函数的地址放入了函数指针变量中,那我们就可以根据指针来调用函数。

int Add(int x,int y)
{return x + y;
}
int main()
{int (*p1) (int x,int y) = Add;printf("%d\n",p1(3,4));printf("%d\n",(*p1)(4,5));return 0;
}
7
9

我们发现,代码能正常运行,证明通过指针调用是可行的。但我们发现,这两行代码中一行有解引用操作,另一行却没有。这是因为,编译器在处理函数指针时,会自动识别并将其作为函数地址处理,从而找到函数并调用。而加上解引用操作后就直接得到了函数的地址,编译器也可以找到函数并调用,正常运行。

4.3 有意思的代码

我们一起来看看两段有趣的代码:

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

这段代码看起来很复杂,我们要逐层来解析:

首先,我们可以从内层中分理出 void (*)() ,结合我们刚刚学习的知识,这就是函数指针类型,指向一个返回值为void,没有参数的函数。那么 (void (*)())0 则就代表对0进行强制类型转换,这里的0就会被作为地址处理,类型被转换为 void (*)() 。最后,(*(void (*)())0)() 则代表通过指针对这个存储地址为0的无参数的函数的调用。

看完了上面的代码,我们接着看看下面这个代码:

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

我们依旧逐层进行分析:

我们找到中间的 signal(int, void(*)(int)) ,很明显,这表示了一个名为signal的函数,拥有两个参数,一个为int类型,另一个为void(*)(int)类型,是一个函数指针,那么,除去函数名与参数部分,剩下的部分就是函数返回值类型了。即为 void(*)(int),很明显,这也是函数指针类型。所以说,这段代码表示的是一个名为signal,参数类型为int和void(*)(int),返回值为void(*)(int)的函数。因为没有函数体部分以及进行传参操作,所以这属于函数声明。

不过,我们发现,这里的函数值返回值类型并没有和一般的函数声明时一样整体放在函数名之前,而是分为了两部分,这是为什么呢?

这是因为,在 C 语言中,函数声明的语法规则是由运算符优先级类型嵌套关系决定的。当函数的返回值是函数指针时,必须通过括号拆分,核心原因是需要明确区分 “函数返回值是指针” 和 “指针指向的函数类型”

4.4 typedef关键字

typedef 可以用来对类型进行重命名,可以将复杂的类型简单化。

例如:我们可以将unsigned int类型重命名为unit

typedef unsigned int uint;

对于指针类型也是一样:

 typedef int* ptr_t;

不过在重命名数组指针类型时则有一点区别。

例如:我们将int (*) [10]类型重命名为parr_t

typedef int(*parr_t)[5]; //新的类型名必须在*的右边

对于函数指针类型的重命名也是一样的:

例如:我们将void (*)(int)类型重命名为pfun_t

typedef void(*pfun_t)(int);//新的类型名必须在*的右边

那么,要简化上面的代码2,就可以这样写:

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

5. 函数指针数组

数组是一个存放相同类型数据的存储空间,我们已经学习了指针数组,那我们是否可以在一个数组中存放函数指针类型的元素呢?

这当然是可以的,这样的话这个数组就叫作函数指针数组。那么,应该如何定义函数指针数组呢?

int (*parr[3])();

让parr先与[3]结合,表示这是一个元素个数为3的数组。剩余的int (*)()则表示数组元素类型,表示指向一个无参数,返回值类型为int的函数。

6. 转移表

函数指针数组的用途:转移表

举例:计算器的一般实现:

我们定义4个简单的加减乘除的函数,并将其放入函数指针数组中。打印出菜单,提醒输入的值对应的操作。读取输入值,根据输入值不同,执行对应操作。

代码如下:

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()
{setbuf(stdout,NULL);int (*parr[5])() = {0, Add, Sub,Mul,Div};int input = 0,x = 0,y = 0;do{printf("***********************\n");printf("******** 0.exit *******\n");printf("*** 1.Add     2.Sub ***\n");printf("*** 3.Mul     4.Div ***\n");printf("***********************\n");if(scanf("%d",&input) == 1 && input >= 0 && input <= 4){if(input == 0){printf("退出成功\n");break;}printf("请输入操作数\n");if(scanf("%d %d",&x,&y) == 2){printf("结果为%d\n",(*parr[input])(x,y));}else{while(getchar() != '\n');printf("输入错误,重新输入");continue;}}else{while(getchar() != '\n');printf("输入错误,重新输入");continue;}}while(input);return 0;
}

利用函数指针数组,我们就可以利用数组的特性,通过指针调用函数,而不需要通过switch语句来一条条的编写,让代码更加简洁。

http://www.dtcms.com/a/325305.html

相关文章:

  • docker集群
  • 【图像处理基石】PCA图像压缩与还原:基于OpenCV的Lena图实验
  • 02Vue3
  • 想冲华为AI认证,怎么选方向?
  • 大模型落地:AI 技术重构工作与行业的底层逻辑
  • Selenium元素定位不到原因以及怎么办?
  • 编译Android版本可用的高版本iproute2
  • AI 健康管家:重构健康管理的未来图景
  • 大模型落地实践:从技术重构到行业变革的双重突破
  • AI生成代码时代的商业模式重构:从“软件即产品”到“价值即服务”
  • 亚马逊广告底层逻辑重构:从流量博弈到价值创造的战略升维
  • uView Pro 正式开源!70+ Vue3 组件重构完成,uni-app 组件库,你会选择它吗?
  • 数据库基本操作
  • 自动化备份全网服务器数据平台项目
  • 掘金数据富矿,永洪科技为山东黄金定制“数智掘金”实战营
  • k8s 部署mysql主从集群
  • kafka 中的Broker 是什么?它在集群中起什么作用?
  • 类银河恶魔城 P20-1 Slime enemy
  • Flutter学习笔记(六)---状态管理、事件、路由、动画
  • 达梦自定义存储过程实现获取表完整的ddl语句
  • Python FastAPI + React + Nginx 阿里云WINDOWS ECS部署实战:从标准流程到踩坑解决全记录
  • 爬虫与数据分析结和
  • NEON性能优化总结
  • Spring MVC 注解参数接收详解:@RequestBody、@PathVariable 等区别与使用场景
  • EXISTS 替代 IN 的性能优化技巧
  • 大数据量下分页查询性能优化实践(SpringBoot+MyBatis-Plus)
  • 基于Spring Data Elasticsearch的分布式全文检索与集群性能优化实践指南
  • Rust:anyhow 高效错误处理库核心用法详解
  • Rust 实战五 | 配置 Tauri 应用图标及解决 exe 被识别为威胁的问题
  • 新人该如何将不同的HTML、CSS、Javascript等文件转化为Vue3文件架构