【14 深入理解指针 (3)】

目录

  1. 字符指针变量
  2. 数组指针变量
  3. 二维数组传参的本质
  4. 函数指针变量
  5. typedef关键字
  6. 函数指针数组
  7. 转移表

1. 字符指针变量

在指针的类型中我们知道有一种指针类型为字符指针 char*
一般使用:

char ch='w';
char* pc=&ch;
*pc='w';

还有一种方式:

const char* pstr="hello bit";
printf("%s\r\n",pstr);

上面的代码很容易让人认为是把字符串放到了指针变量里,但本质上是把字符串的首地址放到了pstr里,是 *pstr=‘h’
在这里插入图片描述

  • 可以把字符串想象为字符数组,但是这个数组不能修改
  • 当常量字符串出现在表达式中时,值是第一个字符的地址,存储在只读数据区

可以通过下列方式验证:

const char* str = "abcdef";
//str[3] = 'o';  err,不可修改
printf("%c\r\n", str[2]);   //数组方式访问字母
printf("%c\r\n", "abcdef"[3]);
printf("%p\r\n", "abcdef");
printf("%p\r\n", str);

在这里插入图片描述在这里插入图片描述

字符指针变量可以像数组一样通过下标访问,也可以在字符串后面添加下标后缀访问。字符串的地址和字符变量的地址相同,都是首字母的地址

下面是《剑指offer》收录的一段代码:

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");
else
		printf("str1 and str2 are not same\n");
if(str3==str4)
		printf("str1 and str2 are same\n");
else
		printf("str1 and str2 are not same\n");

判断if条件分别会输出哪句话?
在这里插入图片描述
str1和str2是字符串首字符的地址,是两个局部变量,会分别申请不同的空间,所以两个的地址也是不一样的,所以输出"not same"
而str3和str4是常量字符串,它们的内容是相同的,就会在只读数据区申请一份空间,因为不能修改,所以只需要存储一份就行

分别输出四个变量地址进行验证:
在这里插入图片描述

2. 数组指针变量

2.1 数组指针变量是什么

指针数组是一种数组,那么数组指针就是指针变量
我们已经熟悉:

  • 整形指针变量: int* p,存放的是整形变量的地址,能指向整形数据的指针
  • 浮点型指针变量: double* p,存放的是浮点型变量的地址,能够指向浮点型数据的指针
    在这里插入图片描述
    定义的区别

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

p1是数组,p1先和中括号结合,表示数组有10个元素,每个元素都是int*
p2有括号,先和*结合,是指针,指向一个有10个元素的数组,每个元素类型是int

因为[]的优先级大于*号,所以数组指针要加一个括号保证和*先结合

2.2 数组指针变量怎么初始化

数组指针是用来存放数组的地址的,那么怎么获得数组的地址呢?可以用**&数组名**

int arr[10] = {0};
&arr;//得到数组的地址
int (*p)[10] = &arr; //定义数组指针并初始化为arr

在调试里也可以看到&arr和p的类型是一致的
在这里插入图片描述

3. 二维数组传参的本质

有了数组指针的理解,就可以讲二维数组传参的本质了

我们在之前一维数组传参的时候,形参部分可以写成数组形式,也可以写成指针形式

  • 写成数组形式更加直观,方便理解
  • 写成指针形式,因为数组传参,传递的是第一个元素的地址

在这里插入图片描述

二维数组也是如此

void Print(int arr[3][5], int r, int c) {

	for (int i = 0; i < r; i++)
	{
		for (int j = 0; j < c; j++) {
			printf("%d ", arr[i][j]);
		}
		printf("\r\n");
	}
}

void Print(int (*p)[5], int r, int c) {

	
	for (int i = 0; i < r; i++)
	{
		for (int j = 0; j < c; j++) {
			//printf("%d ", p[i][j])
			printf("%d ", *(*(p + i) + j));
		}
		printf("\r\n");
	}
}

int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
Print(arr, 3, 5);

在这里插入图片描述在这里插入图片描述

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

4. 函数指针变量

4.1 函数指针变量的创建

函数指针变量应该是用来存放函数地址的,通过地址能够调用函数
那么函数是否有地址呢?

void test(){
}

int main(){
	printf("test: %p",test);
	printf("test: %p",&test);
}

在这里插入图片描述
所以函数是有地址的,函数名就是函数的地址,也可以通过&函数名获得地址,两种是一样的
如果要将函数指针的地址存放起来,就需要创建函数指针变量了,函数指针变量的写法其实和数组指针非常类似

void test(){
}
void (*pfn)() = test;
void (*pfn)() = &test;

函数指针类型解析
在这里插入图片描述

4.2 函数指针变量类型的使用

//函数指针调用
int add(int x, int y) {
	return x + y;
}

int (*pf)(int, int) = add;
//两种调用方式
printf("%d\r\n",(*pf)(3,5));
printf("%d\r\n", pf(3, 5));

以上两种调用方式都是可以的

4.3 两段代码解读

代码1

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

首先回忆一下,(int)5是什么意思,这是将5强制转为int类型
函数指针没有名字,是函数指针类型
将0这个数转换成一个无返回值无参数的函数指针,然后*函数指针() 是对这个0地址函数的调用

在这里插入图片描述

代码2

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

  1. signal是一个函数
  2. 参数有两个,一个是int,一个是函数指针类型,这个函数指针一个int类型的参数,无返回值
  3. 函数signal的返回值是无返回值的函数指针类型,参数是int

此代码是一个函数声明

上面的两段代码均出自《c陷阱和缺陷》这本书

5. typedef关键字

用来重命名,将复杂的类型简单化
如果觉得 unsigned int 用起来不方便,可以写成uint就方便多了

typedef unsigned int uint;
//将unsigned int重命名为int
uint n;

对于数组指针和函数指针的重命名稍微有区别:
数组指针类型

typedef int (*par)[5];

函数指针类型

typedef void(*pfun)(int);

有了typedef,就可以简化理解上面的代码2

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

5. 函数指针数组

数组是一个存放相同类型数据的存储空间,函数指针数组存放的元素类型就是函数指针,定义方法:

int (*parr[3])();

parr先和[]结合,是数组,然后数组的类型是int(*)(),函数指针

6.转移表

函数指针数组的用途: 转移表
转移表是c语言一种常见的数据结构,用于实现条件分支和跳转的功能,它可以将一个输入值映射到相应的输出值,并根据不同的输入值进行不同的处理
就是利用函数指针数组,通过不同的输入值跳转不同的函数处理

实例: 计算器的实现

简单写法

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>

int add(int a, int b) {
	return a + b;
}
int sub(int a, int b) {
	return a - b;
}
int mul(int a, int b) {
	return a * b;
}
int div(int a, int b) {
	return a / b;
}

void menu() {
	printf("********************\r\n");
	printf("*** 1.add  2.sub ***\r\n");
	printf("*** 3.mul  4.div ***\r\n");
	printf("*** 0.退出       ***\r\n");
	printf("********************\r\n");
}
int main()
{
	int input = 1;
	int n1, n2;
	int ret = 0;
	do
	{
		menu();
		printf("请选择功能\r\n");
		scanf("%d", &input);
		switch (input)
		{
		case 0:
			//退出
			printf("退出游戏\r\n");
			break;
		case 1:
			printf("请输入两个数\r\n");
			scanf("%d %d", &n1, &n2);
			ret = add(n1, n2);
			printf("ret=%d\r\n", ret); 
			break;
		case 2:
			printf("请输入两个数\r\n");
			scanf("%d %d", &n1, &n2);
			ret = sub(n1, n2);
			printf("ret=%d\r\n", ret);
			break;
		case 3:
			printf("请输入两个数\r\n");
			scanf("%d %d", &n1, &n2);
			ret = mul(n1, n2);
			printf("ret=%d\r\n", ret);
			break;
		case 4:
			printf("请输入两个数\r\n");
			scanf("%d %d", &n1, &n2);
			ret = div(n1, n2);
			printf("ret=%d\r\n", ret);
			break;
		default:
			printf("输入错误\r\n");
			break;
		}

	} while (input);
	return 0;
}

运行截图:
在这里插入图片描述
上述写法的问题在于switch语句中有很多重复代码,如果计算器再添加新的计算符,需要重写很多冗余的代码,所以用函数指针数组进行优化

优化写法

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>

int add(int a, int b) {
	return a + b;
}
int sub(int a, int b) {
	return a - b;
}
int mul(int a, int b) {
	return a * b;
}
int div(int a, int b) {
	return a / b;
}

void menu() {
	printf("********************\r\n");
	printf("*** 1.add  2.sub ***\r\n");
	printf("*** 3.mul  4.div ***\r\n");
	printf("*** 0.退出       ***\r\n");
	printf("********************\r\n");
}
int main()
{
	int input = 1;
	int n1, n2;
	int ret = 0;
	//转移表
	//为了使下标对应,0下标设置个空
	int (*pf[5])(int,int) = {NULL,add,sub,mul,div};
	do
	{
		menu();
		printf("请选择功能\r\n");
		scanf("%d", &input);
		if (input >= 1 && input <= 4) {
			printf("请输入两个数\r\n");
			scanf("%d %d", &n1, &n2);
			ret = pf[input](n1, n2);
			printf("ret=%d\r\n", ret);
		}
		else if (input == 0) {
			//退出
			printf("退出游戏\r\n");
			break;
		}
		else {
			printf("输入错误\r\n");
		}
	} while (input);
		
	return 0;
}

使用函数指针可以实现跳转的功能,根据输入值找到不同的功能
在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值