【C语言】指针还不会?这一篇就够了

news2024/11/24 22:31:53

在这里插入图片描述

👦个人主页:Weraphael
✍🏻作者简介:目前正在回炉重造C语言(2023暑假)
✈️专栏:【C语言航路】
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注😍


目录

  • 一、什么是指针
      • 1.1 内存
      • 1.2 内存的管理与使用
      • 1.3 指针变量的使用
      • 1.4 指针的大小
  • 二、指针和指针类型
      • 2.1 指针类型的意义
      • 2.2 指针+ 或 - 整数
      • 2.3 指针解引用
  • 三、野指针
      • 3.1 野指针成因
        • 3.1.1 指针未初始化
        • 3.1.2 指针越界访问
      • 3.1.3 指针指向的空间被释放
      • 3.2 如何规避野指针
  • 四、指针运算
      • 4.1 指针+-整数
      • 4.2 指针 - 指针
        • 4.2.1 指针-指针的运用
      • 4.3 指针的关系运算(比较大小)
  • 五、指针和数组
  • 六、二级指针
      • 6.1 什么是二级指针
      • 6.2 二级指针的使用
  • 七、指针数组
      • 7.1 什么是指针数组
      • 7.2 用一维数组模拟二维数组
  • 八、字符指针
      • 8.1 笔试题
  • 九、数组指针
      • 9.1 数组指针的概念
      • 9.2 数组指针的定义
      • 9.3 &数组名和数组名的区别
      • 9.4 数组指针的使用
      • 9.5 剖析数组指针和指针数组的区别
  • 十、数组传参和指针传参
      • 10.1 一维数组传参
      • 10.2 二维数组传参
      • 10.3 一级指针传参
      • 10.4 二级指针传参
  • 十一、函数指针
      • 11.1 函数指针的定义
      • 11.2 函数指针的使用
      • 11.3 有趣的代码
  • 十二、函数指针数组
      • 12.1 函数指针数组的定义
      • 12.2 函数指针数组的用途
  • 十三、指向函数指针数组的指针
      • 13.1 定义
  • 十四、回调函数
      • 14.1 回调函数的使用

一、什么是指针

想要理解什么是指针,必须先了解什么是 内存

1.1 内存

内存是电脑上的存储设备,一般都是4G/8G/16G等,程序运行时会加载到内存中,也会使用内存空间。我们可以看看电脑的任务管理器:

在这里插入图片描述

1.2 内存的管理与使用

我们将内存划分为一个个小格子,每一个格子是一个 内存单元,大小为 一个字节,对每一个内存单元进行 编号,假设未来要找一个内存单元,就可以通过编号(地址)很快的找到,我们把这些 编号叫做地址,而地址在C语言中又叫做指针。

在这里插入图片描述

举一个例子,在下图中,假设定义一个变量int a = 10,一个int类型的变量,需要占4个字节的空间,而每个字节都有地址,&a取出的是4个字节中的哪一个的地址呢?其实取出的是第一个字节的地址(也就是较小的地址),也就是说,&a最终取出来的地址是0x0012ff40。当然,可以把这个地址存到一个变量中,int* pa = &a*表示pa是一个指针,int代表pa所指向的类型是int类型,这个pa也叫做指针变量(它是专门用来存放地址的)。

在这里插入图片描述

总结指针理解的2个要点:

  1. 指针是内存中一个最小的单元编号,也就是地址。
  2. 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量。

总结:指针就是地址,口语中说的指针通常指的是指针变量。

1.3 指针变量的使用

在这里插入图片描述

通过以上代码就验证了,指针变量p存放的就是a的地址。

接下来,可以使用*解引用操作符来对其使用

在这里插入图片描述

1.4 指针的大小

  • 指针变量,就是用来存放地址的变量。(存放在指针中的值都被当成地址处理)。
  • 那么问题来了:一个内存单元到底是多大?刚刚讲过,就是一个字节。那它又是如何编址的呢?
    经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。 对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0)
    那么32根地址线产生的地址就会是:
    00000000000000000000000000000000
    00000000000000000000000000000001
    .....
    11111111111111111111111111111111
    这里就有232个地址。 一个地址管理一个内存单元,那么232个地址就能管理232个内存单元,也就是232个字节,那2的32次方个字节又是多大空间呢?根据进制转化:
    232 Byte = 232÷1024 KB ÷1024 MB ÷ 1024 GB = 4GB
    同样的方法,64位机器,也可以计算出来。
    264 Byte = 8 GB

这里我们就明白:

  • 在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以 一个指针变量的大小就应该是4个字节。
  • 那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。

总结:

  • 指针变量是用来存放地址的,地址是唯一标示一个内存单元的。
  • 指针的大小在32位平台是4个字节,在64位平台是8个字节。

二、指针和指针类型

2.1 指针类型的意义

假设在32位或64位机器上,指针大小都是4个字节或8个字节,直接搞一个通用指针不就完事了,那为什么要区分int*char*double*这些呢?那它肯定就有特殊的意义。

先看看下面的代码:

在这里插入图片描述

我们可以通过调试来观察变量a内存 中的变化

在这里插入图片描述

为了方便观察,我们把它调成4列

在这里插入图片描述

我们发现,内存中确实存的是44332211,只不过是倒着放的(为什么是在内存中倒着存放会在后期讲解)

接着按F10再往下走

在这里插入图片描述

我们发现,4个字节全部被改为0,这说明a的值确实被修改成0了

假设我把指针变量pa的类型改为char*结果又会是如何呢?

可以继续通过观察其内存变化

在这里插入图片描述

继续按F10

在这里插入图片描述

它只改变了1个字节!!

总结:指针类型的意义

  1. 指针类型决定了,指针在进行解引用操作的时候,一次性访问几个字节
    如果是char*类型的指针,解引用访问内存中的一个字节
    如果是int*类型的指针,解引用访问内存中的四个字节
    float*double*也同样如此…

指针类型还要其它意义,接着往下看:

在这里插入图片描述

我们发现,pa + 1跳过了4个字节,pc + 1跳过了1个字节

总结:指针类型的意义:
2. 指针类型决定指针的步长(指针+1到底跳过几个字节)
字符指针+1,跳过1个字节
整型指针+1,跳过4个字节
其它类型的指针也是如此…

2.2 指针+ 或 - 整数

在这里插入图片描述

注意:指针+一个数,是往高地址走,指针-一个数,是往低地址走

2.3 指针解引用

指针的类型决定了,对指针解引用的时候有多大权限(能操作几个字节)。比如:char*类型的指针解引用就只能访问1个字节,int*解引用就只能访问4个字节

在这里插入图片描述

三、野指针

野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

3.1 野指针成因

3.1.1 指针未初始化

在这里插入图片描述

3.1.2 指针越界访问

在这里插入图片描述

3.1.3 指针指向的空间被释放

在这里插入图片描述

首先test函数中的a是一个局部变量,根据局部变量的生命周期,出了它的作用域,生命周期结束。返回的时候a的地址就已经被销毁了,此时的指针变量p就是一个野指针。但是运行的时候结果还是10,这只是侥幸,这是因为之前的内存空间还没有被覆盖。我们加上个输出语句就能破坏掉这个内存空间。

在这里插入图片描述

3.2 如何规避野指针

  • 指针初始化
    在这里插入图片描述
  • 小心指针越界
  • 指针指向空间释放,及时置NULL
  • 避免放回局部变量的地址
  • 指针使用之前要检查有效性

四、指针运算

4.1 指针±整数

#include <stdio.h>
#define N 5 // #define定义的标识符常量
float a[N];
float* p; // 全局变量默认初始化为0,而NULL本质上就是0
int main()
{
	for (p = &a[0]; p < &a[N]; )
	{
		*p++;
	}
	return 0;
}

看下图就很容易理解代码循环了,但这里要主要循环体内的代码,*的优先级是比++操作符要高的,而++是后置的(先使用,后++),所以这代码的意思是把数组内5个元素全部赋值成0

在这里插入图片描述

4.2 指针 - 指针

  • 运算的前提条件:两个指针要指向同一块空间(同个数组)
  • 运算结果是:相减绝对值的结果就是两个指针之间的元素个数

在这里插入图片描述

【解析】

在这里插入图片描述

4.2.1 指针-指针的运用

之前我们求字符串长度是这么写的(不使用库函数strlen的情况下),计数法

#include <stdio.h>
int my_strlen(char* str)
{
	// 计数法
	int count = 0;
	while (*str != '\0')
	{
		count++;
		str++;
	}
	return count;
}

int main()
{
	char a[] = "abcdef";
	int len = my_strlen(a);
	printf("%d\n", len);
	return 0;
}

当然还可以使用递归

#include <stdio.h>
int my_strlen(char* str)
{
	if (*str == '\0')
		return 0;
	else
		return 1 + my_strlen(str + 1);
}

int main()
{
	char a[] = "abcdef";
	int len = my_strlen(a);
	printf("%d\n", len);
	return 0;
}

除了以上两种方法,还可以使用 指针 - 指针

#include <stdio.h>

int my_strlen(char* str)
{
	// 记录起始地址
	char* start = str;
	// 记录'\0'的位置
	while (*str != '\0')
	{
		str++;
	}
	return str - start;
}

int main()
{
	char a[] = "abcdef";
	int len = my_strlen(a);
	printf("%d\n", len);
	return 0;
}

【解析】

在这里插入图片描述

注意:++千万不要放到循环表达式中,因为不管是前置还是后置++,它都会有副作用,会导致return的结果有误差。

4.3 指针的关系运算(比较大小)

#include <stdio.h>
#define N 5 // #define定义的标识符常量
float a[N];
float* p; // 全局变量默认初始化为0,而NULL本质上就是0
int main()
{
	for (p = &a[N]; p > &a[0]; )
	{
		*--p;
	}
	return 0;
}

【详解】
在这里插入图片描述

上面的代码的写法有点难以理解,如果写成以下这样,结果还会是一样吗?

#include <stdio.h>
#define N 5 // #define定义的标识符常量
float a[N];
float* p; // 全局变量默认初始化为0,而NULL本质上就是0
int main()
{
	for (p = &a[N - 1]; p >= &a[0];p-- )
	{
		*p = 0;
	}
	return 0;
}

【解析】

在这里插入图片描述

其实,简化过的代码实际在绝大部分的编译器上是可以顺利完成任务的,然而我们应该避免这样书写,因为标准规定并不保证它可行

标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。

五、指针和数组

  1. 指针和数组是不同的对象
  • 指针是一种变量,存放地址的,大小4/8字节的
  • 数组是一组相同类型元素的集合,是可以放多个元素的,大小是取决于元素个数和元素类型
  1. 数组的数组名是数组首元素的地址,地址是可以放在指针变量中,可以通过指针访问数组

如何用指针来访问数组呢?

在这里插入图片描述

或者还能这么写

在这里插入图片描述

【总结】

int arr[10];
int* p = arr;//首元素地址
这里有一层等价关系
arr[i] == *(arr + i) == *(i + arr) == i [arr]
i[arr]是可以正常使用的,因为[]只是一个操作符,i和arr是[ ]的操作数而已
(就像a + b同样也能写成b + a)。在编译的过程中,arr[i]也会被翻译成*(arr+i)

六、二级指针

6.1 什么是二级指针

在这里插入图片描述

变量a的地址存放在指针变量pa中,我们称pa是一级指针。指针变量也是变量,是变量就得有地址,pa的的地址存放在ppa中,所以我们称ppa二级指针

这里再解释一下,pa前面一颗的*是告诉我们pa是指针变量,而pa指向的aint类型,所以pa的类型就是int*;同样的,ppa前一颗的*告诉我们ppa是指针变量,而ppa指向的paint*类型的,所以ppa的类型就是int**

apappa关系如下:

在这里插入图片描述

形象点就是如下所示:

在这里插入图片描述

6.2 二级指针的使用

在这里插入图片描述

七、指针数组

7.1 什么是指针数组

指针数组是指针还是数组? 
--- 是数组(存放指针的数组)

可以类比整型数组和字符数组
整型数组是存放整型的数组
字符数组是存放字符的数组
那么指针数组就是存放指针(地址)的数组

举一个简单的例子:

在这里插入图片描述

7.2 用一维数组模拟二维数组

假设要模拟三行四列的数组

思路:开辟一个数组用来存放3个数组首元素的地址,因为数组在内存中是连续存放的,所以知道首元素的地址,后面也自然而然也就跟着知道了。

在这里插入图片描述

【代码实现】

#include <stdio.h>
int main()
{
	int a[] = { 1,2,3,4 };
	int b[] = { 5,6,7,8 };
	int c[] = { 8,10,11,12 };
	int* arr[] = { a,b,c };
	for (int i = 0; i < 3; i++)
	{
		// 偏移量
		for (int j = 0; j < 4; j++)
		{
			printf("%d ", *(arr[i] + j));
		}
		printf("\n");
	}
	return 0;
}

八、字符指针

以往我们都是用一个字符指针指向一个字符变量:

int main()
{
	char ch = 'x';
 
    //ptr前的*是在告诉我们ptr是指针
    //字符变量的地址用字符指针ptr接收
	char* ptr = &ch;
	return 0;
}

现在还有另外一种使用方式:

#include <stdio.h>
int main()
{
    //常量字符串不能被修改,用const修饰
	const char* ptr = "hello world!";
	printf("%s\n", ptr);
	return 0;
}

以上代码很容易认为是把hello world!放到字符指针ptr里了。其实它 本质是把字符串hello world!的首字母h的地址放到了ptr中。
在这里插入图片描述

8.1 笔试题

#include <stdio.h>
int main()
{
	char a[] = "hello";
	char b[] = "hello";
 
	const char* c = "hello";
	const char* d = "hello";
 
	if (a == b)
		printf("a和b相同\n");
	else
		printf("a和b不相同\n");
 
	if (c == d)
		printf("c和d相同\n");
	else
		printf("c和d不同\n");
 
	return 0;
}

【答案】

在这里插入图片描述

【解析】

  1. 不同的数组在创建时,会在内存中开辟不同的空间。由于地址不同,则ab一定不相同。
    在这里插入图片描述
  2. 因为cd是常量字符串,这说明它们不会被随意修改。并且它们字符的首字母都是h,说明字符指针变量cd都存储的是首字符h
    在这里插入图片描述

九、数组指针

9.1 数组指针的概念

  • 字符指针char* - 存放字符地址的指针 - 也是指向字符的指针
  • 整型指针int* - 存放整型地址的指针 - 也是指向整型的指针
  • 浮点型指针float double - 存放浮点数地址的指针 - 也是指向浮点数的指针
  • 数组指针 - 存放数组地址的指针 - 指向数组的指针(数组指针本质上是指针)

9.2 数组指针的定义

同样可以类比字符指针、整型指针…

int main()
{
	// 以整型指针为例
	int n = 0;
	int* pn = &n;
	
	//数组指针仿照上面也就是
	int arr[10] = {0};
	int (*p)[10] = &arr;
	// *的优先级比[]低,首先要保证p是指针
	
	return 0;
}

注意要与指针数组区分开来

指针数组本质上是一个数组,是存放指针(地址)的数组

int arr[] = {1,2,3,4};
// 存放数组首元素地址
int *p[10] = {arr};
 
解释:
优先级 * < [],所以p指向的是数组,并且每一个元素的类型都是int*,
所以它是一个指针数组
 
int arr[10];
int (*p)[10] = &arr;
 
解释:
由于加了括号,p先和*结合,这说明了p是一个指针,然后指向一个大小
为10的数组,并且每一个元素都是int类型,所以它是一个数组指针

9.3 &数组名和数组名的区别

往期博客:点击跳转

我们都知道arr是数组名,数组名表示数组首元素的地址

&arr是什么意思,我们分别打印出它们的地址来观察:

#include <stdio.h>
int main()
{
	int arr[10] = { 0 };
	printf("arr:%p\n", arr);
	printf("&arr:%p\n", &arr);
 
	return 0;
}

【程序结果】

在这里插入图片描述

可见arr&arr地址都是一样的,难道它们真的是一样的吗?我们再来看一段代码:

#include <stdio.h>
int main()
{
	int arr[10] = { 0 };
	printf("arr:%p\n", arr);
	printf("arr:%p\n", arr + 1);
 
	printf("&arr:%p\n", &arr);
	printf("&arr:%p\n", &arr + 1);
 
	return 0;
}

【程序结果】

在这里插入图片描述

根据上面的代码我们发现,其实&arrarr,虽然值是一样的,但是意义却不一样

实际上:&arr是的是数组的地址(整个数组)。&arr + 1,跳过的是整个数组的大小 因此,&arr的类型是int (*)[N],是一种数组指针类型
在这里插入图片描述

9.4 数组指针的使用

一般用于二维数组

例:用一个数组指针打印一个二维数组

#include <stdio.h>
void Print(int(*p)[3], int row, int col)
                                         
{
	for (int i = 0; i < row; i++)
	{
		for (int j = 0; j < col; j++)
		{
			printf("%d ", *(p+i)[j]);
			// 本质p[i][j]
		}
		printf("\n");
	}
}
  
int main()
{
	int arr[3][3] = { {1,2,3},{4,5,6},{7,8,9} };
	
	//封装一个数组来打印
	Print(arr, 3, 3);
 
	return 0;
}

二维数组传参可以是传数组:arr[][3]arr[3][3],当然也可以传指针,二维数组的数组名是第一行数组的地址,也就是一个一维数组的地址,因此可以用数组指针接收。

数组指针还可用于一维数组,不过比较少

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

p进行解引用,访问的是整个数组,再用下标访问操作符[],访问对应下标元素。

9.5 剖析数组指针和指针数组的区别

int arr[5];  
//整型数组,数组有5个元素
 
int* p[10];  
//p是指针数组,数组有10个元素,每个元素的类型是int*
 
int(*p)[10]; 
//p是数组指针,指向10个元素的数组,每个元素是int类型
 
int(*p[10])[5]; 
// 优先级:* < [],因此p是一个数组,数组有10个元素
// 数组的每个元素的类型是:int(*)[5]的数组指针类型

十、数组传参和指针传参

10.1 一维数组传参

int main()
{
	int arr[10] = { 0 };
	//函数传参传参
	test(arr);
	return 0;
}
//1. 一维数组传参的形参可以这么写
void test(int arr[])
void test(int arr[10])

//2. arr是数组首元素的地址,整型的地址,用整型指针接收
void test(int* arr) 

int main()
{
	int* arr[10] = { 0 };
 
	test(arr);
 
	return 0;
}
//1. 数组传参,形参可以用数组接收
// 大小也可以不写
void test(int *arr[10])
void test(int *arr[])
 
//2. arr是首元素地址,数组每一个元素类型都是int*
// 一级指针传参可以拿二级指针接收
void test(int **arr) 

10.2 二维数组传参

int main()
{
	int arr[3][3] = { 0 };
 
	test(arr);
 
	return 0;
}
// 1.二维数组传参,二维数组接收
//注意:行可省,列不可省
void test(int arr[3][3])
void test(int arr[][4])

//2. 二维数组arr是第一行的地址
//第一行的地址就是整个一维数组的地址,所以用数组指针接收
void test(int (*p)[3]) 

10.3 一级指针传参

#include <stdio.h>

int main()
{
	char* p = "hello world!";
	test(p);
	return 0;
}
// 一级指针传参,一级指针接收
void test(char* p)

10.4 二级指针传参

思考:函数的参数为二级指针,实参可以是什么??

void test(int** p)
{
	;
}

int main()
{
	//1. 二级指针传参,二级指针接收
	int** p;
	test(p);
 
 	//2. 一级指针地址传参,二级指针接收
	int* p;
	test(&p);
 
 	//3. 当arr作为首元素地址,每个元素都是int*类型,二级指针接收
	int* arr[10];
	test(arr);
 
	return 0;
}

十一、函数指针

11.1 函数指针的定义

函数指针的本质上是指针,它是指向函数的指针

这可以类比数组指针(指向数组的指针)

int arr[10];
int (*p)[10] = &arr;

我们也能不能 &函数名,拿到函数的地址呢?然后再把函数的地址存到一个函数指针变量里呢?

来看看下面的一段代码:

#include <stdio.h>
int Add(int x, int y)
{
	return x + y;
}
int main()
{
	printf("%p", &Add);
	return 0;
}

【程序结果】

在这里插入图片描述

通过以上代码我们发现,函数名也是有地址的,所以我们能不能创建一个变量并把这个地址存起来呢?

#include <stdio.h>
int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int (*p)(int, int) = &Add;
	return 0;
}

【解析】

在这里插入图片描述

接下来还能类比,&arr不加&arr则是数组首元素的地址,如果不&数组名,那么单独的数组名是否是函数首元素的地址呢?

在这里插入图片描述

总结:&数组名数组名都是函数的地址

11.2 函数指针的使用

其实就是对指针进行解引用操作:

#include <stdio.h>
int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int (*p)(int, int) = &Add;
	
	//对指针p解引用找到Add函数
	int res = (*p)(100, 200);
	printf("%d\n", res);
	return 0;
}

【程序结果】

在这里插入图片描述

通过上述代码我们发现:*p 等价于Add,又因为&数组名数组名都是函数的地址,因此Add又等价于&Add,而指针变量p又存储Add的地址。所以可以得出关系:*p == Add == &Add == p

所以上述的代码中,解引用操作符就可以去掉了

在这里插入图片描述

11.3 有趣的代码

int main()
{
	(*(void(*)())0)()
	// 拆分
	// void(*)()是函数指针类型
	// 对int类型的0进行强制类型转化函数指针类型
	// 意思是0被当做一个函数的地址
	// *(void(*)())0 再对地址解引用,就是调用函数
	// 最右边的括号表示在调用函数时没有传参
	// 因此以上代码就是一次函数调用

	void (*p(int, void(*)(int)))(int);
	//该代码是函数的声明,声明的名字是p
	//p函数有两个参数,一个是int类型
	//另一个是函数指针类型void(*)(int),并且该函数指针能够指向的那个函数的参数是int,返回类型是void
	//p的返回类型是函数指针,类型是:void (*)(int)

	return 0;
}

十二、函数指针数组

12.1 函数指针数组的定义

函数指针数组本质上是一个数组,是用来存放函数地址的数组。

类比一下:

int Add(int x, int y)
{
	return x + y;
}

int main()
{
   	int (*p)(int,int) = &Add //函数指针
      
    //函数指针数组就是基于函数指针改造的
    //函数指针数组本质上是个数组
 
    int (*p[3])(int,int) = {&Add};
 
    //[]的优先级高于*,所以p先和[]结合,所以p是个数组
    //类型是  int (*)(int,int) -->函数指针类型
}

12.2 函数指针数组的用途

在讲函数指针数组用途之前,首先用代码为大家实现一个简单的计算器,能实现加、减、乘、除的功能。

#include <stdio.h>
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;
}
 
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 res = 0;
	do
	{
		//打印菜单
		menu(); 
		printf("请选择:");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			printf("请输入两个整数:");
			scanf("%d %d", &x, &y);
			res = Add(x, y);
			printf("相加的结果为:%d\n", res);
			break;
		
		case 2:
			printf("请输入两个整数:");
			scanf("%d %d", &x, &y);
			res = Sub(x, y);
			printf("相减的结果为:%d\n", res);
			break;
		
		case 3:
			printf("请输入两个整数:");
			scanf("%d %d", &x, &y);
			res = Mul(x, y);
			printf("相乘的结果为:%d\n", res);
			break;
		
		case 4:
			printf("请输入两个整数:");
			scanf("%d %d", &x, &y);
			res = Div(x, y);
			printf("相除的结果为:%d\n", res);
			break;
		
		case 0:
			printf("退出计算器");
			break;
		
		default:
			printf("选择错误\n");
			break;
		}
	} while (input);
}

【程序结果】

在这里插入图片描述

若此时计算器中还要加入按位与&、按位或|等操作符运算,虽然只要在代码内部多加几行函数,但这也会导致代码越来越来长。所以,可以使用函数指针数组来分别存放AddSub的地址

#include <stdio.h>
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;
}
 
void menu()
{
	printf("*******************************\n");
	printf("****    1.Add   2.Sub      ****\n");
	printf("****    3.Mul   4.Div      ****\n");
	printf("*****       0.Exit         ****\n");
	printf("*******************************\n");
}
//函数指针数组
int (*p[5])(int, int) = { 0,Add,Sub,Mul,Div };
//这里的0,是为了和菜单对应,当然也可以写NULL
 
int main()
{
	int input = 0;
	int x = 0;
	int y = 0;
	int res = 0;
	do
	{
		menu(); //打印菜单
		printf("请选择:");
		scanf("%d", &input);
 
		if (input == 0)
		{
			printf("退出计算器\n");
			break;
		}
		else if (input >= 1 && input <= 4)
		{
			printf("请输入两个整数:");
			scanf("%d %d", &x, &y);
 
			//当input为0则退出,
			//当input为1则调用Add函数
			//以此类推...
 
			int res = p[input](x, y);
			printf("结果为:%d\n", res);
		}
		else
		{
			printf("选择错误,请重新选择\n");
		}
		
	} while (input);
}

十三、指向函数指针数组的指针

指向函数指针数组的指针本质上是个指针,指针指向一个数组,数组每个元素的类型都是函数指针

13.1 定义

int Add(int x, int y)
{
	return x + y;
}
 
int main()
{
	//整型数组
	//arr是数组,&arr
	int arr[10] = { 0 };
	int(*p)[10] = &arr; //数组指针
 
	//函数指针数组
	//同样的,p也是个数组,也&p
	int (*p[3])(int, int);
	int (*(*pp)[3])(int, int) = &p; //这就是指向函数指针数组的指针
}

用概念再来分析一下:

int (*(*pp)[3])(int, int) = &p

pp先和*结合,说明pp是一个指针

②指向一个数组有3个元素

③每个元素的类型都是int (*)(int, int) --> 函数指针类型

Q:这么复杂类型怎么看?

int (*(*pp)[3] )(int, int),把(*pp)[3]去掉,剩下就是类型

十四、回调函数

回调函数就是一个通过函数指针调用的函数。

14.1 回调函数的使用

在【函数指针数组】部分,我们实现了一个简单的计算器:

在这里插入图片描述

就像上面的代码,方框部分是有大部分代码是重复的,我们可以利用回调函数的方式来把它变得更加简洁

#include <stdio.h>
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;
}
 
void calc(int (*p)(int ,int)) //函数传参,函数指针接收
{
	int x = 0;
	int y = 0;
	printf("请输入两个整数:");
	scanf("%d %d", &x, &y);
	int res = p(x, y);
	printf("计算结果为:%d\n",res);
}
 
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;
	do
	{
		menu(); //打印菜单
		printf("请选择:");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			calc(Add);
			break;
 
		case 2:
			calc(Sub);
			break;
 
		case 3:
			calc(Mul);
			break;
 
		case 4:
			calc(Div);
 
		case 0:
			printf("退出计算器");
			break;
 
		default:
			printf("选择错误\n");
			break;
		}
	} while (input);
}

【图解】

在这里插入图片描述

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

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

相关文章

HNU-计算机系统CS-学习感悟

本学期学的两门核心课&#xff0c;一个CS&#xff0c;一个OS。对我来说都有一定难度。 CS总评91/100。主要是期末考试没太发挥好&#xff0c; 主要原因是存储部分有个没有考虑写的情况&#xff0c;送了8分。 总领 CS的学习采用最经典的书——CSAPP&#xff0c;也被称为计算机…

vue3 ts vite electron开发桌面程序

1、搭建vuetsvite项目 # 创建Vue项目 npm init vue # 安装依赖 npm install # 一定要安装成开发依赖 npm install electron electron-builder -D 根目录创建plugins文件夹&#xff0c;文件夹中创建ts文件&#xff0c;vite.electron.build.ts是打包文件代码&#xff0c;v…

LeetCode面试题02.07.链表相交

面试题02.07.链表相交 两种解题思路 面试题02.07.链表相交一、双指针二、哈希集合 一、双指针 这道题简单来说&#xff0c;就是求两个链表交点节点的指针 这里注意&#xff1a;交点不是数值相等&#xff0c;而是指针相等 为了方便举例&#xff0c;假设节点元素数值相等&…

MySQL 坐标批量计算及优化

文章目录 1、坐标计算2、优化 现在有一个需求&#xff0c;就是找出距离某用户最近的一些点&#xff0c;一种实现方法就是调用地图的api来计算筛选&#xff0c;另外一种就是在数据库中计算&#xff0c;考虑到地图api有并发量限制&#xff0c;所以选用数据库计算的方式。 1、坐标…

Python实现HBA混合蝙蝠智能算法优化BP神经网络分类模型(BP神经网络分类算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 蝙蝠算法是2010年杨教授基于群体智能提出的启发式搜索算法&#xff0c;是一种搜索全局最优解的有效方法…

成功升级scikit-image的版本,从老版本0.13.0到0.17.2

成功升级scikit-image的版本&#xff0c;从老版本0.13.0到0.17.2 之前参考其他博客升级scikit-image的版本没有成功&#xff0c;这次参考scikit-image的github官网&#xff0c;顺利实现了升级。 scikit-image的github官网中关于安装的介绍页 https://github.com/scikit-imag…

Dubbo入门实战最全攻略(基于 Spring Boot 实现)

Dubbo应用 RPC通信 Apache Dubbo 3构建在 HTTP/2 协议之上&#xff0c;具有更好的穿透性与通用性 &#xff0c; 支持基于 IDL 的服务定义 集成了业界主流的大部分协议&#xff0c;使得用户可以在 Dubbo 框架范围内使用这些通信协议 &#xff0c; 这些协议包括 rest、hessian…

【动态规划】三步问题

&#x1f9c1;题目描述&#xff1a; 示例&#xff1a; &#x1f9c0;(1)题目解析&#xff1a; 小孩每一次可以走1,2,3步&#xff0c;那么形成不同的排列组合&#xff0c;会有很多种上楼梯方式。 &#x1f9c0;(2)算法原理&#xff1a; &#x1f951;[1]状态表示 根据题目要…

【C++杂货铺】构造函数和析构函数

文章目录 一、类的六个默认成员函数二、构造函数三、析构函数 一、类的六个默认成员函数 &#x1f4d6;默认成员函数 用户没有显式实现&#xff0c;编译器会自动生成的成员函数&#xff0c;称为默认成员函数。 构造函数&#xff1a;完成对象的初始化工作。析构函数&#xff…

容器化时代的领航者:Docker 和 Kubernetes 云原生时代的黄金搭档

一、Docker docker是一种开源的应用容器引擎&#xff0c;可以将应用程序和依赖打包成一个可移植的镜像&#xff0c;然后发布到任何支持docker的平台上&#xff0c;也可以实现虚拟化。docker的核心概念有三个&#xff1a;镜像&#xff08;image&#xff09;、容器&#xff08;co…

QT中QTimer的循环时间与槽函数执行时间以及在事件循环中触发,不同时间的结果分析

目录 当循环时间小于槽函数时间时&#xff1a; 当循环间隔时间大于槽函数时间时&#xff1a; 当存在两个定时器器&#xff0c;其中一个还是间隔100ms&#xff0c;另一个间隔1000ms&#xff1a; 当两个定时器的循环周期大于槽函数执行时间时 当在主程序中添加一个for循环…

怎么把视频转为gif动态图,3个方法轻松转换!

如何将视频转换为GIF动态图呢&#xff1f;相信许多人在日常聊天中喜欢使用各种有趣的表情包。每当互联网上出现一些有趣的热门视频时&#xff0c;我们也往往会看到许多相关的GIF表情包。那么我们应该如何将自己的视频或者一些有趣的视频制作成GIF动态图呢&#xff1f;下面我就为…

从C语言到C++_27(AVL树)概念+插入接口实现(四种旋转)

目录 1. AVL树的概念 2. AVL树结点和树的定义 3. AVL树的插入&#xff08;未包含旋转&#xff09; 4. AVL树的旋转 4.1 右右_左单旋 4.2 左左_右单旋 4.3 左右双旋 4.4 右左双旋 5. AVL树的验证 6. AVL树的删除(了解)和性能 7. AVL树插入验证完整代码 8. AVL树笔试…

下载pdm遇到的坑:Could not find a version that satisfies the requirement pdm

pip install pdm遇到的问题&#xff1a; Could not find a version that satisfies the requirement pdm (from versions: ) No matching distribution found for pdm检查了版本后发现&#xff0c;python版本在3.6&#xff0c;pdm不支持该版本 换成python3.7版本&#xff0c;则…

【力扣算法14】之 15. 三数之和 python

文章目录 问题描述示例1示例2示例 3提示 思路分析代码分析完整代码详细分析运行效果截图调用示例运行结果 完结 问题描述 给你一个整数数组 nums &#xff0c;判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i ! j、i ! k 且 j ! k &#xff0c;同时还满足 nums[i] num…

python编程语言之数据类型进阶操作

数值常用操作 python常用关于数值&#xff0c;数学常用的模块&#xff1a;math&#xff08;数学&#xff09;&#xff0c;random&#xff08;随机&#xff09;&#xff0c;numpy&#xff08;科学计算&#xff09;&#xff0c;pandas&#xff08;数据读写&#xff0c;数据分析&…

Yalmip入门教程(3)-约束条件的定义

博客中所有内容均来源于自己学习过程中积累的经验以及对yalmip官方文档的翻译&#xff1a;https://yalmip.github.io/tutorials/ 之前的博客简单介绍了约束条件的定义方法&#xff0c;接下来将对其进行详细介绍。 首先简单复习一下&#xff1a; 1.定义约束条件可以使用矩阵拼接…

如何通过nvm管理多个nodejs版本

随着前端项目的越来越多&#xff0c;不同项目使用的nodejs版本可能不一样&#xff0c;导致在切换不同项目时需要更换不同的nodejs版本&#xff0c;非常麻烦。本次推荐使用nvm进行多个nodejs版本的统一管理。 1、nvm的下载 nvm全称Node Version Manager&#xff0c;即Node版本管…

科技政策 | 2023年广东省省级企业技术中心(第22批)认定开始啦!

原创 | 文 BFT机器人 原文链接&#xff1a; http://gdii.gd.gov.cn/zwgk/tzgg1011/content/post_4218083.html 各企业请注意&#xff0c;2023年广东省省级企业技术中心&#xff08;第22批&#xff09;认定已经开始了&#xff0c;广东省工业和信息化厅接收资料截止时间为2023年…

【Java基础教程】Java学习路线攻略导图——史诗级别的细粒度归纳,持续更新中 ~

Java学习路线攻略导图 上篇 前言1、入门介绍篇2、程序基础概念篇3、包及访问权限篇4、异常处理篇5、特别篇6、面向对象篇7、新特性篇8、常用类库篇 前言 &#x1f37a;&#x1f37a; 各位读者朋友大家好&#xff01;得益于各位朋友的支持和关注&#xff0c;我的专栏《Java基础…