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

C语言——深入理解指针(3)

文章目录

  • 字符指针变量
  • 数组指针变量
    • 数组指针变量是什么?
    • 数组指针变量怎么初始化
  • 二维数组传参的本质
  • 函数指针变量
    • 函数指针变量的创建
    • 函数指针变量的使用
    • 两段关于函数的有趣代码
      • typedef关键字
  • 函数指针数组
  • 转移表
    • 第一种写法:
    • 第二种写法(函数指针数组):
    • 第三种写法:

字符指针变量

我们知道有一种指针类型为字符指针:char*
我们原来对字符指针的用法:存放一个字符的地址
在这里插入图片描述
现在我们再学习一种,字符指针可以指向一个常量字符串
我们写段代码:

int main()
{//char ch = 'w';//char* pc = &ch;此时pc就是一个指针变量,可以存放一个字符的地址char arr[10] = "abcdef";char* p = arr;return 0;
}

我们将这段代码的图画出来:
在这里插入图片描述
我们再写一种代码:

int main()
{//char ch = 'w';//char* pc = &ch;此时pc就是一个指针变量,可以存放一个字符的地址//char arr[10] = "abcdef";//char* p = arr;char* p = "abcdef";return 0;
}

写出这段代码我们肯定会疑问:
这句代码是把这个字符串完整的赋值给p吗?

我们对这段代码进行编译,发现编译器并没有发出警告:
在这里插入图片描述
说明这种写法是正确的。
那么这种写法的意思是什么呢?
注意:这个字符串是常量字符串,上面那个是字符数组。在这里插入图片描述
我们敲出来的这个常量字符串在内存中也有属于它自己的空间。
char* p = "abcdef";这条代码是将它首字符的地址放进指针变量p里面去:在这里插入图片描述
因为常量字符串在内存中也是连续存放的,所以找到首字符的地址,也就可以找到其他字符了。

我们可以看到,这两种形式非常的像,那么字符数组常量字符串的区别是什么呢?
区别就在于:
第一个p指向的是数组,数组的内容是可以被改变的。
但是第二个p指向的是常量字符串,常量字符串的内容是不可以被改变的。
我们不妨试一下:

int main()
{char arr[10] = "abcdef";char* p1 = arr;*p1 = 'w';//试着将a改为wchar* p2 = "abcdef";*p2 = 'w';//也试着将a改为wreturn 0;
}

写这样一段代码进行调试:
首先查看arr数组里的内容:
在这里插入图片描述
确实放着10个元素,接着往下:
当我们执行完*p1 = 'w'后第一个元素确实被改变为w了:
在这里插入图片描述
接着往下调试:
在这里插入图片描述
接着往下调试:
执行到*p2 = 'w'时却报出警告:
在这里插入图片描述
这是因为abcdef是个常量字符串,常量字符串是不能被修改的,一旦修改它,就会报错。

所以:数组的内容是可以被修改的,而常量字符串是不能被修改的。

此时这段代码应该这样写:
在这里插入图片描述
*的左边加上const,这样就限制了p2指向的内容,一旦内容出现修改,编译器就会报错,可能出现的错误就直接被扼杀在编译期间了。
正确写法:

int main()
{//char arr[10] = "abcdef";//char* p1 = arr;//*p1 = 'w';//试着将a改为wconst char* p2 = "abcdef";//也试着将a改为wreturn 0;
}

所以const char* p2 = "abcdef"这种写法并不是将整个字符串都放进指针变量里,而是将首字符的地址放进指针变量里。

接下来我们将它们打印出来:
在这里插入图片描述
这里为什么写一个p1p2就能打印出来呢?
因为我们打印字符串的时候只提供一个起始地址就可以了,就像之前一直写的printf("%s\n",arr)在这里插入图片描述

当我们用%s打印字符串的时候,只需要提供起始地址就行了。
不需要解引用,用了解引用是错的。
因为*p1a,一个字符不能用%s打印。

我们接下来看《剑指offer》中收录的一道和字符串相关的笔试题:

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不相等呢?
在代码中,先后创建了两个数组str1 str2,然后再用"hello bit."初始化了这两个数组:在这里插入图片描述
因为两个数组是在不同的内存空间里,所以str1 所对应的h的地址与str2对应的h的地址不同,并不是同一个地址,所以str1 不等于str2,打印str1 and str2 are not same

那为什么str3 str4相等呢?
因为hello bit.在这里是个常量字符串:
在这里插入图片描述

常量字符串是不能被修改的,既然不能被修改,所以在内存空间里只有一个字符串。
因为这里str3str4指向的是一个同一个常量字符串:"hello bit."
在这里插入图片描述

既然是同一个常量字符串,那么str3str4中的地址都是同一块空间,所以str3==str4

那为什么指向的是同一个常量字符串呢?
因为常量字符串是不能被改变的,相同的常量字符串没必要保存两份(只有能被改变的才有必要存原本的字符串进行备份),所以在内存中只有一份这样的常量字符串,即str3str4指向的都是同一个常量字符串,所以str3str4里面存的地址相同。
我们可以进行调试看看str3str4存的地址是否像我们说的相同:
在这里插入图片描述


数组指针变量

数组指针变量是什么?

之前我们学习了指针数组,指针数组是一种数组,数组中存放的是地址(指针)。
那么数组指针变量是指针变量、还是数组呢?
答案是:指针变量。
我们已经熟悉:

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

所以,数组指针变量就应该是:存放的是数组的地址,能够指向数组的指针变量。
例如:
我们取出整个数组的地址,将数组的地址放进指针变量p中去,这个p就是数组指针变量
在这里插入图片描述

注意:我们要将数组指针与指针数组区分开
指针数组:是数组,是存放指针的数组
数组指针:指向数组的指针(变量)

那么这个p的类型是什么呢?
我们首先来分析一下这段代码,看p1是指针还是p2是指针呢?
在这里插入图片描述

  • p1的分析:
    我们可以看到,p1左边有个*,右边放着[10],它首先是与方块结合的。
    既然是与方块结合的,那么p1就是一个数组名,方块里表示的就是这个数组里有几个元素。
    最后前面的int*表示p1数组里面的10个元素都是int*类型的
    在这里插入图片描述
    结论:p1是个指针数组——存放指针的数组

  • p2的分析:
    我们可以看到p2与前面的*被括号括起来了,所以它没办法与后面的方块结合了。
    我们在深入理解指针(1)中学习过,变量左边放个*就表示这个变量是指针变量:在这里插入图片描述
    所以p2前面放个*就表示了这个p2是个指针变量。
    既然我们说p2是指针变量,那么它指向什么呢?
    我们看到括号的后面有个[10],就说明这个指针变量指向的应该是个数组,这个数组有10个元素,又因为最前面有个int,所以数组的每个元素都应该是int类型的。
    我们之前学过数组是有类型的,数组名去掉就是这个数组的类型:在这里插入图片描述
    所以p2是个指针变量,指向数组类型为int [10]的数组
    在这里插入图片描述
    结论:p2是个指针变量,指向的是数组
    在这里插入图片描述

既然p2是个指针变量,指向的是数组,所以p2就应该是个数组指针变量。
p也是数组指针变量,所以p类型的写法就与p2类型的写法相似:
p2指向10个元素都为int的数组,写成int (*p2)[10]
p也指向10个元素都为intarr数组,所以写成:int (*p)[10]=&arr;

拆解:
首先p得是个指针,所以要在p前面加个*说明:

*p=&arr;

下一步加括号来保证它俩先结合:

*p)=&arr;

强调:括号一定不要掉!掉了会与后面的方块结合,此时p就会变为数组名,成为指针数组,而不是指针变量

int* p[10]=&arr;
//err

最后:让它指向10个元素都为int的数组:

int*p)[10]=&arr;

解释::p先和*结合,说明p是一个指针变量,然后指针指向的是一个大小为10个整型的数组。

最后形态:
在这里插入图片描述
p是⼀个指针,指向⼀个数组,叫做数组指针


数组指针变量怎么初始化

数组指针变量是用来存放数组地址的,那怎么获得数组的地址呢?
这就是我们之前学习的:&数组名

int arr[10]={0};
&arr;//得到数组的地址

如果要存放数组的地址,就得存在数组指针变量中,数组指针的初始化:

int*p)[10]=&arr;

这里我们再梳理一些知识:
由这段代码:

int main()
{int arr[10]={0};int* p1=arr;int(*p2)[10] = &arr;return 0;
}

我们可知p1的类型为int*
那么数组指针变量p2的类型是什么呢?
去掉变量名就是该数组指针变量的类型了,p2的类型为:int(*)[10]

在这里插入图片描述
我们让这两个指针分别+1,再将地址打印出来观察:
在这里插入图片描述
可以观察到p1+1跳过了4个字节,p2+1跳过了40个字节
我们之前学过:指针类型决定了指针进行+1或-1操作的时候,一次跳过多少个字节。
例如:
字符指针+1跳过1个字节;整型指针+1跳过4个字节…这里的数组指针+1就跳过一整个数组的字节大小。
int arr[10]这个数组共有10个元素,每个元素4个字节,所以共跳过了40个字节。
这与深入理解指针(2)(数组与指针)中的这段笔记相似:
在这里插入图片描述
p2跳过一整个数组时指向哪里呢?
我们画个图:
在这里插入图片描述
p2跳过一个数组的大小时,我们并不知道这个指针最后指向哪里了(此时这个指针指向的位置是不可知的)这样会不会造成野指针呢?
答案是不会
在这里插入图片描述
总结:p2 + 1 本身不是野指针,因为它指向的是一个确定的内存地址(arr数组后的那个地址)。然而,对 p2 + 1 进行解引用操作是危险的,可能会引发程序异常,因为它超出了数组 arr 的有效范围。

那么数组指针到底有什么用呢?
它非常可能应用错场景。
例如,我们写段代码用指针遍历打印出数组内容:
在这里插入图片描述
那我们今天学过数组指针变量后,那我们能不能这样写呢?
我们将整个数组的地址取出来放进数组指针变量p中:

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

用数组指针变量打印出数组所有的元素:
在这里插入图片描述
(*p)[i]该如何理解呢?
我们在深入理解指针(1)中学习过,&*可以相互抵消:

*&arr=arr

这条语句就是取出arr的地址,再对这个地址进行解引用,其实最后找到的就是arr
例如:
在这里插入图片描述
所以在这段代码中,p就是&arr。再在p前面加上*后得到的就是个数组名

(*p)==(*&arr)==arr

此时要打印出所有元素就得使用下标打印

arr[i]

虽然这种写法最终也可以打印出正确结果,但是这是个不好的示范,不推荐这样使用数组指针。
数组指针可以使用在二维数组的传参上。


二维数组传参的本质

在过去,我们有一个二维数组需要传参给一个函数时,是这样写的:
我们创建个二维数组,再写个函数将这个数组打印出来
在这里插入图片描述
学一维数组时我们说:数组名是数组首元素的地址,那么对于二维数组来说是否也一样呢?
答案:对于二维数组来说,数组名也是数组首元素的地址。
那么对于这个二维数组来说,到底谁是首元素呢?真的是数字1吗?
我们之前学习二维数组的时候,说过:把一维数组做为元素,这个数组就是二维数组
在这里插入图片描述
所以,二维数组的每个元素是一个一维数组,二维数组的首元素就应该是第一行,首元素的地址就是第一行的地址。
而第一行的地址就是一个一维数组的地址,所以形参接收时的指针类型应该是数组指针类型
在这里插入图片描述
虽然二维数组在我们眼里是有行有列的,但是在内存中是连续存放的。
所以:arr是指向第一行,让它+1就会跳过整个一维数组来到第二行…在这里插入图片描述
在这里插入图片描述
完整代码:

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

打印成功:
在这里插入图片描述
关键点分析,指针操作:

  • arr+i:表示指向第i行的指针(类型为int(*)[5]
  • *(arr+i):解引用得到第i行数组的首地址(类型为int*
  • *(arr+i)+j:指向第i行第j列元素的指针
  • *(*(arr+i)+j):获取第i行第j列的元素值

所以,二维数组传参的本质其实是传一个地址,并且是一个一维数组的地址。但是形参的部分既可以写成指针的形式,也可以写成数组的形式。


函数指针变量

函数指针变量的创建

什么是函数指针变量呢?
根据前面学习整型指针,数组指针的时候,我们类比关系,不难得出结论:
函数指针变量应该就是用来存放函数的地址的,通过这个地址能够调用这个函数。
首先,函数是否有地址呢?
我们写个函数,打印它的地址,看能不能打印出来:
在这里插入图片描述
发现确实打印出来一个数值。
我们之前学过:

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

两个地址的值是一样的!
那么对于函数来说呢?
我们再将函数名打印出来对比一下:
在这里插入图片描述
确实与&函数名一样。
那么我们能不能说&函数名是整个函数的地址,函数名是函数首元素的地址呢?
答案:不能。在函数里,&函数名与函数名都是函数的地址,它们是没有区别的(在数组里面,数组名与&数组名是有区别的)

所以,函数名就是函数的地址,当然也可以通过&函数名的方式来获得函数的地址。

如果我们要将函数的地址存放起来,就得创建函数指针变量,函数指针变量的写法其实与数组指针变量的写法非常相似。如下:
在这里插入图片描述
但是pf的类型该怎么写呢?
首先得给pf加个*表面它是个指针变量:

(*pf)=&Add;

又因为pf指向一个函数,函数后面是个(),所以也要在后面加上()
在这里插入图片描述

(*pf)()=&Add;

函数参数类型是int,int,所以括号里面写int,int;函数返回类型也是int,所以返回类型也是int(返回类型写在最前面)
在这里插入图片描述

int (*pf)(int,int)=&Add;

这就是pf的完整写法:
在这里插入图片描述

  • pf:函数指针变量名
  • (int,int)pf指向函数的参数类型和个数交代
  • intpf指向函数的返回类型

所以pf的类型为:

int (*)(int,int)
//pf函数指针变量的类型

我们再将数组指针写出来类比一下:
在这里插入图片描述
注意,括号绝对不能掉,掉了之后会变成什么样呢?
在这里插入图片描述
因为括号的优先级比*高,掉了之后pf会与括号相结合。
此时pf就变成函数名了,函数的参数是int,int,返回类型是int *
这是有问题的,所以pf外面的括号绝对不能掉!

检验一下学习成果:
写一个函数test,将这个函数的地址取出来放进函数指针变量p中,这个函数指针变量p该怎么写呢?

char* test(char c, int n)
{//...
}int main()
{p= &test;return 0;
}

答案:

char* (*p)(char, int) = &test;

还要说明一点:
函数指针也可以把形参名字c,n写出来:

char* (*p)(char c, int n) = &test;

类型为

char* (*)(char c, int n)

但其实这里的c,n加不加都,只需要将指向函数的 参数类型,返回类型 交代清楚就行了。(指向某个函数,参数的名字叫什么不重要,因为这里不会用c,n


函数指针变量的使用

原本我们是这样使用函数的:
在这里插入图片描述
接下里试试使用函数指针:
在这里插入图片描述
所以,把一个函数的地址存在函数指针变量里,未来就能通过这个函数指针变量调用这个函数。

注意:如果函数没有参数的话,我们解引用后面也就不需要传参了;如果函数返回值是void,我们也就不需要写变量来接收返回值…根据不同的情况而变化

在这里,我们通过对pf这个函数指针变量里的地址解引用找到了该函数,然后再进行了函数的调用。
我们又从上面的内容知道:函数名就是该函数的地址,
所以函数名调用就是通过函数的地址直接调用。
在这里插入图片描述

那当我们将这个地址放进pf中时,也通过函数的地址直接调用,不解引用呢?
写段代码试试:
在这里插入图片描述
所以,C语言规定,函数指针的解引用操作(*)是可选的。既可以省略不写,也可以写多个:
在这里插入图片描述


两段关于函数的有趣代码

代码1:

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

我们首先得认出:
在这里插入图片描述

void (*)()

这是个函数指针类型。
那么括号里放着类型是干什么的呢?
答案:强制类型转换。
在这里插入图片描述

( void (*)() )0

所以,这一步就是将整型0强制类型转换成函数指针类型void (*)()

  • void (*)():表示一个指向无参数、无返回值函数的指针
  • 转换后,0的类型变为void (*)()(函数指针类型),但它的值仍然是0(即内存地址0)

此时的0被看作为一个无参数、无返回值函数的地址。
对这个0(地址)解引用,找到这个函数。

 *( void (*)() )0;

调用这个函数:

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

在这里插入图片描述

总结:这段代码的作用是将地址为0处的内存当作一个无参数、无返回值的函数来调用。

代码2:

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

我们看到signal后面有括号,通常会猜测signal会不会是个函数名呢?
在这里插入图片描述
再往后看,我们可以清楚的看到:
在这里插入图片描述
参数1可以传个整型值,参数2可以传个函数的地址。
函数名、参数都有了,对于函数来说来缺少什么呢?
在这里插入图片描述
可以看到,还缺少返回类型。

我们再将已经分析过的内容省略掉:
在这里插入图片描述
剩下的void (*)(int)就是返回类型
那么这整段代码在表达什么呢?
在这里插入图片描述
注意,这种写法是错误的,这样写只是方便表达意思:

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

*必须在函数名的旁边,所以写成:

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

这也是一种函数声明:
在这里插入图片描述

但是这种写法不容易难理解,所以我们可以对它进行简化。
怎么简化呢?
这里就要学习一个关键字typedef


typedef关键字

typedef是用来类型重命名的,可以将复杂的类型简单化。
比如,当觉得unsigned int写起来不方便,如果能写成u_int就方便多了,那么我们可以使用:

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

由此,下面两种写法,a1a2都为无符号整型:

int main()
{unsigned int a1;u_int a2;return 0;
}

如果是指针类型,能否重命名呢?
其实也是可以的,比如,将int*重命名为ptr_t

typedef int* ptr_t;

类型重命名对于数组指针和函数指针稍微有点区别:
例如我们有数组指针类型int(*)[5],需重命名为parr_t
不能像上面那样写:

typedef int(*)[5] parr_t;

需要这样写:

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

这里的parr_t就是int(*)[5],已经将int(*)[5]这个数组指针类型重命名了。

int main()
{int(*pa1)[5];//这里的pa1是个数组指针变量,指向数组类型为int [5]的数组parr_t pa2;//这里的pa2也是个数组指针变量,指向数组类型为int [5]的数组return 0;
}

这两种写法表达的意思也是一模一样的。

函数指针类型的重命名也是:
比如,将void(*)(int)类型重命名为pf_t
不能这样写:

typedef void(*)(int) pf_t;

要写成

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

此时pf_t就是void(*)(int)(这种函数指针类型)。

学了简化之后,这段代码就可以简化了:

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

这个返回类型不就是函数指针类型吗?
我们将这个函数类型void(*)(int)重命名为pf_t

typedef void(*pf_t)(int);
//pf_t就是void(*)(int)

简化之后这段代码为:

pf_t signal(int,pf_t)

所以部分1与部分2的作用是一样的:
在这里插入图片描述


函数指针数组

我们前面已经学了指针数组:

int* arr[10];
//指针数组
//数组的每个元素是int*(整型指针)
//可以存放5个整型的地址

我们知道,函数指针是函数的地址;指针数组可以存放多个地址。
如果要把多个函数的地址存到一个数组里面去,这个数组就是函数指针数组了。
所以,函数指针数组就是一个数组,数组里面的元素都是函数指针(函数的地址)。
那么我们该怎么定义函数指针数组呢?
我们首先写出四个函数:

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 (*pf1)(int, int) = &Add;//pf1是函数指针变量return 0;
}

这4个函数的参数与返回类型一模一样,所以这4个函数地址的类型一模一样,都是这样一种类型:int (*)(int,int)(函数指针类型)
既然这4个函数地址的类型都是一样的,若想把这4个地址存在pfarr这个数组里面去,该怎么写呢?
首先将这4个地址写进数组里面(函数名就是函数地址,所以直接将4个函数名写进数组就行了)

pfarr={Add,Sub,Mul,Div};

那么这个函数指针数组的类型该怎么写呢?
在函数指针变量的基础上去改造:

int main()
{int (*pf1)(int, int) = &Add;//pf1是函数指针变量pfarr={Add,Sub,Mul,Div};return 0;
}

使pfarr根据上面的函数指针变量的写法先变为:

int main()
{int (*pf1)(int, int) = &Add;//pf1是函数指针变量int (*pfarr)(int, int) ={Add,Sub,Mul,Div};return 0;
}

再在pfarr后面加上数组的方括号和元素个数:[4]

int main()
{int (*pf1)(int, int) = &Add;//pf1是函数指针变量int (*pfarr[4])(int, int) ={Add,Sub,Mul,Div};return 0;
}

我们对比一下:
在这里插入图片描述
我们将数组名与数组大小去掉之后就是数组的元素类型了:

int (*pfarr[4])(int, int) = { Add,Sub,Mul,Div };
int (*)(int, int) = { Add,Sub,Mul,Div };
//数组名、数组大小去掉
//此时就是这个数组的元素类型了
//为函数指针类型:int (*)(int, int)

此时这个pfarr是函数指针数组(它是个数组,里面存放的都是函数指针)

在函数指针的基础上,只要在后面加上[常量],就变成数组了

int (*pf1)(int, int) = &Add;//pf1是函数指针变量
int (*pfarr[4])(int, int) = { Add,Sub,Mul,Div };
//pfarr是函数指针数组

注意,在已经初始化的情况下,方块里的4是可以省略的(数组方块里的大小是否可以省略取决于是否初始化了)

int (*pfarr[])(int, int) = { Add,Sub,Mul,Div };
//后面花括号已经初始化了,所以4可以省略

函数指针数组里存放函数地址,得保证它们的类型都是同一种函数指针类型(参数、返回类型都是一样的)

那么我们该怎么用函数指针数组呢?
我们写段代码,让这段代码依次完成8与4的“加法、减法、乘法、除法”:
因为pfarr是个数组,所以可以使用下标访问到具体的元素:
在这里插入图片描述
函数名就是函数的地址,拿到地址后就可以直接调用():传参8,4
在这里插入图片描述
再用一个变量r接收函数返回的值,将这个值打印出来:
在这里插入图片描述


转移表

函数指针数组的用途:转移表
举例:计算器的一般实现,完成整数的加、减、乘、除。
还可以选择:
1——加法、2——减法、3——乘法、4——除法、0——退出。

第一种写法:

  • 先打印出一个简易的菜单在这里插入图片描述

  • 再将要用的“加法、减法、乘法、除法”的函数写出来在这里插入图片描述

  • 函数写完后,根据菜单,提示要“选择哪种运算”
    在这里插入图片描述

  • 接下来就要将输入的值放进一个变量里了,所以写scanf函数,创建变量input
    在这里插入图片描述

  • 接下来就是对input值的判断了
    在这里插入图片描述
    如果选的是0,就退出计算器
    在这里插入图片描述
    若选的是除“0、1、2、3、4”之外的值,就提示选择错误
    在这里插入图片描述

  • 我们在while表达式里写input。因为当input0的时候,直接停止了循环,也就对应了上面的“退出计算器”。当input不为0的时候,就继续循环,选择进行加法、减法…
    在这里插入图片描述

  • 如果选的是1,就得提醒输入两个操作数,并创建两个变量存放输入的数
    在这里插入图片描述
    然后就得调用Add函数了,将x,y两个操作数传过去
    在这里插入图片描述
    算出的结果也要用变量接收,所以再创建一个变量z接收结果,再将结果z打印出来
    在这里插入图片描述

  • 同理,如果选2的话也得输入两个操作数…区别就是每次调用的函数不同,选择2实现的是减法
    在这里插入图片描述

  • 3、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;
}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 z = 0;do{menu();printf("请选择:");scanf("%d",&input);switch (input){case 1:printf("请输入两个操作数:");scanf("%d %d",&x,&y);z=Add(x, y);printf("%d\n",z);break;case 2:printf("请输入两个操作数:");scanf("%d %d", &x, &y);z = Sub(x, y);printf("%d\n", z);break;case 3:printf("请输入两个操作数:");scanf("%d %d", &x, &y);z = Mul(x, y);printf("%d\n", z);break;case 4:printf("请输入两个操作数:");scanf("%d %d", &x, &y);z = Div(x, y);printf("%d\n", z);break;case 0:printf("退出计算器\n");break;default:printf("选择错误\n");break;}} while (input);return 0;
}

我们运行代码:

  • 首先出现让我们选择哪种运算
    在这里插入图片描述
  • 假设我们选择6
    在这里插入图片描述
    再选择1
    在这里插入图片描述
    输入2、3
    在这里插入图片描述
  • 如果我们想要算乘法,选择3
    在这里插入图片描述
    输入3、3
    在这里插入图片描述
  • 如果不想算了,就选择0,退出计算器
    在这里插入图片描述

这就是我们写的简易计算器。
这段代码只是实现了两个整数的加法、减法、乘法、除法的运算,那万一我们还要算两个数的右移(>>)、左移(<<)、按位与(&)、按位或(|)…
这样的话switchcase就会无限多,所以这种写法不行。


第二种写法(函数指针数组):

我们将这段代码简化:

  • 我们发现这些函数的参数、返回类型都一模一样
    在这里插入图片描述
    因为它们的参数、返回类型都一模一样,所以我们可以将这些函数的地址都存到函数指针数组pfArr
    在这里插入图片描述

  • 将这个函数指针数组pfArr补充完整
    先写成函数指针:
    因为这个函数指针数组里的元素(函数)都是int (*)(int,int)类型的,所以写成int (*pfArr)(int,int)
    在这里插入图片描述
    再加方块[],写上数组大小(只要初始化了数组,大小也可以省略不写):
    在这里插入图片描述

  • 注意,当这个数组元素仅仅是{Add,Sub,Mul,Div}时与菜单上对应的数字不匹配。
    Add下标是0,而菜单里Add是1;
    Sub下标是1,而菜单里Sub是2…
    在这里插入图片描述
    所以,我们给数组多加一个0,这样下标与菜单选项就对应了在这里插入图片描述

  • 那么这个函数指针数组有什么用呢?
    在这里插入图片描述
    所以这样写:
    在这里插入图片描述
    在调用函数实现计算之前先提示输入两个操作数,所以将这两部分移上来:
    在这里插入图片描述
    在这里插入图片描述
    将函数的参数写上:
    在这里插入图片描述
    计算完的结果再赋值给变量z,并把z打印出来
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

这就是我们修改后的代码:

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 z = 0;//函数指针数组int (*pfArr[5])(int, int) = { 0,Add,Sub,Mul,Div };//                            0  1   2   3   4do{menu();printf("请选择:");scanf("%d",&input);//3printf("请输入两个操作数:");scanf("%d %d", &x, &y);z=pfArr[input](x,y);printf("%d\n", z);} while (input);return 0;
}

但是这段代码还是有问题的,当我们输入1时,程序可以正常运行
在这里插入图片描述
但当我们输入菜单以外的数字7时,还会让我们继续输入两个操作数
在这里插入图片描述
所以我们还得给这段代码加一些判断
在这里插入图片描述
代码修改后再运行程序
在这里插入图片描述
输入1就可以实现加法
在这里插入图片描述
不想计算了,输入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;
}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 z = 0;//函数指针数组——转移表int (*pfArr[5])(int, int) = { 0,Add,Sub,Mul,Div };//                            0  1   2   3   4do{menu();printf("请选择:");scanf("%d",&input);//3if (input >= 1 && input <= 4){printf("请输入两个操作数:");scanf("%d %d", &x, &y);z = pfArr[input](x, y);printf("%d\n", z);}else if (input == 0){printf("退出计算器\n");}else{printf("输入错误,重新输入\n");}} while (input);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;
}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 z = 0;do{menu();printf("请选择:");scanf("%d",&input);switch (input){case 1:printf("请输入两个操作数:");scanf("%d %d",&x,&y);z=Add(x, y);printf("%d\n",z);break;case 2:printf("请输入两个操作数:");scanf("%d %d", &x, &y);z = Sub(x, y);printf("%d\n", z);break;case 3:printf("请输入两个操作数:");scanf("%d %d", &x, &y);z = Mul(x, y);printf("%d\n", z);break;case 4:printf("请输入两个操作数:");scanf("%d %d", &x, &y);z = Div(x, y);printf("%d\n", z);break;case 0:printf("退出计算器\n");break;default:printf("选择错误\n");break;}} while (input);return 0;
}

我们观察这段代码,会发现这些部分非常冗余
在这里插入图片描述
这些部分除了调用的函数不同,其余一模一样。
那么我们试着将这些重复的部分封装成一个函数

cal()
{printf("请输入两个操作数:");scanf("%d %d", &x, &y);z = Add(x, y);printf("%d\n", z);
}

但是这样以来就把函数写死了,只能完成加法的运算。

可我们得让这个函数能完成不同的运算。
这部分中就调用的函数是不同的,所以我们就把调用函数的部分抽出来。并且,函数的调用只需要知道函数的地址就可以了。
所以就有了一个想法:
把函数的地址以参数的形式传给calc函数,再让函数的地址去调用对应的函数。这样就可以实现完成不同的运算,冗余的代码也只用写一次。
我们现在将calc函数写出来:

  • 传的的是函数的地址,所以写一个函数指针变量来接收这个函数的地址
    在这里插入图片描述
    再将冗余的代码加上:
    在这里插入图片描述
    接下来要修改冗余的代码,如果这个地方继续写Add就会将代码写死
    在这里插入图片描述
    我们将Add改为函数指针变量pf。这样以来,实参传的是哪个函数的地址,就调用哪个函数
    在这里插入图片描述
    因为x,y,z的创建原本是在主函数中,所以在calc函数中会报警告
    在这里插入图片描述
    现在我们将x,y,z的创建移到calc函数中就好了
    在这里插入图片描述
  • 若想选1想实现加法运算,就将Add函数名(地址)传过去
    在这里插入图片描述
  • 同理,想实现减法运算就把Sub函数的地址传过去…
    在这里插入图片描述
    完整的代码为
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");}void calc(int (*pf)(int, int))
{int x = 0;int y = 0;int z = 0;printf("请输入两个操作数:");scanf("%d %d", &x, &y);z = pf(x, y);printf("%d\n", z);
}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);break;case 0:printf("退出计算器\n");break;default:printf("选择错误\n");break;}} while (input);return 0;
}

这段代码中,如果给calc函数传的是Add函数的地址,那么pf就指向Add函数,实现加法运算;如果给calc函数传的是Sub函数的地址,那么pf就指向Sub函数,实现减法运算…

运行代码:

  • 选1实现加法运算,输入1、2
    在这里插入图片描述
    结果正确,为3
  • 再输入7
    在这里插入图片描述
    提示选择错误
  • 选择0
    在这里插入图片描述
    此时就退出了计算器

此时就利用了calc函数,既避免了冗余的代码,又可以进行加法、减法、乘法…的运算

我们再将整体的逻辑理一下:

  • 在这段代码中,先选择进行哪种运算,若选了1,就进入case1中,执行calc(Sub),进入calc函数中
    在这里插入图片描述

  • calc函数中依次往下执行语句。创建完变量后输入两个操作数,然后调用Add函数,实现加法运算
    在这里插入图片描述

  • 调用完Add函数后结果返回到z中,再将z打印出来
    在这里插入图片描述

  • 在这一过程中main函数可以被称为主调函数
    在这里插入图片描述
    这个主调函数没有直接去调用Add Sub Mul...等函数
    而是将地址传给calc函数,再在calc函数内部通过指针的方式去调用Add Sub Mul...等函数

  • Add/Sub/Mul...被调用的函数就被称为回调函数
    在这里插入图片描述
    该怎么理解这里的回调函数呢
    答:不会直接调用Add/Sub/Mul...这些函数,而是把它们的信息传递给一个函数(calc函数),然后在这个函数(calc函数)内部,通过函数指针来调用它们。
    所以,回调函数就是一个通过函数指针调用的函数。

函数指针的用法也在这里体现了:
主调函数将函数地址传给calc函数,calc函数用函数指针变量接收,后面再用函数指针变量调用指向的函数。
这里主调函数与calc函数之间就是通过函数指针来沟通的,用函数指针实现了回调函数的机制。

回调函数在深入理解指针(4)中详细讲解。

相关文章:

C语言——深入理解指针(3)

文章目录 字符指针变量数组指针变量数组指针变量是什么&#xff1f;数组指针变量怎么初始化 二维数组传参的本质函数指针变量函数指针变量的创建函数指针变量的使用两段关于函数的有趣代码typedef关键字 函数指针数组转移表第一种写法&#xff1a;第二种写法&#xff08;函数指…...

CentOS 7 企业级Redis 7部署指南

CentOS 7 企业级Redis 7部署指南 目录导航 一、环境准备 1.1 依赖管理 二、离线安装 2.1 源码编译安装2.2 目录结构规范 三、生产配置 3.1 主配置文件3.2 配置生成脚本 四、系统集成 4.1 Systemd服务文件4.2 服务管理命令 五、安全加固 5.1 网络安全配置5.2 审计配置 六、性能…...

Linux-C/C++《C++/1、C++基础》(C++语言特性、面向对象等)

这里主要介绍概念为主&#xff0c;主要介绍 C与 C 语言中常用的不同点&#xff0c;和一些新的变化。其中不会去说指针、数据类型、变量类型、判断和循环等这些知识&#xff0c;这些和C 语言基本是一样使用的。我们主要学习 C的面向对象编程&#xff0c;对学习 Qt 有很大的帮助。…...

Java-11

淘天集团2025届春季校园招聘在线笔试-研发 1。设有一个顺序共享栈storageArray[70]&#xff0c;其中栈X的栈顶指针top1的初值为-1&#xff0c;栈Y的栈顶指针top2的初值为70&#xff0c;通过不断进行入栈操作&#xff0c;直到storageArray数组已满&#xff0c;此时top1 top2 …...

第4章 信息系统架构(四)

4.6 网络架构 网络是信息技术架构中的基础&#xff0c;不仅是用户请求和获取IT信息资源服务的通道&#xff0c;同时也是 信息系统架构中各类资源融合和调度的枢纽。特别是云计算、大数据和移动互联网技术飞速发 展的今天&#xff0c;网络更加成为实现这些技术跨越的重要环节。…...

用C++ Qt实现安卓电池充电动效 | 打造工业级电量控件

一、为什么需要自定义电池控件&#xff1f; 在工业控制、车机系统、智能硬件等领域的UI开发中&#xff0c;电池状态显示是高频出现的UI组件。通过实现一个支持颜色渐变、动态充电动画、警戒阈值提示的电池控件&#xff0c;开发者可以系统掌握以下核心能力&#xff1a; Qt绘图…...

【第二节】C++设计模式(创建型模式)-抽象工厂模式

目录 引言 一、抽象工厂模式概述 二、抽象工厂模式的应用 三、抽象工厂模式的适用场景 四、抽象工厂模式的优缺点 五、总结 引言 抽象工厂设计模式是一种创建型设计模式&#xff0c;旨在解决一系列相互依赖对象的创建问题。它与工厂方法模式密切相关&#xff0c;但在应用…...

自用题库---面试使用

1、css中如何实现水平垂直居中 方法一&#xff1a;flex&#xff1a; display: flex; justify-content: center; align-item: center;方法二&#xff1a;绝对定位margin:auto: position: absolute; left: 0; right: 0; top: 0; bottom: 0; margin:auto;方法三&#xff1a;已…...

【Mysql】我在广州学Mysql 系列—— 有关日志管理的示例

ℹ️大家好&#xff0c;我是练小杰&#xff0c;今天星期四了&#xff0c;明天周五&#xff0c;美好的周末又要到了&#xff01;&#xff01;&#x1f606; 本文是对MySQL日志管理内容进行练习&#xff0c;后续将添加更多相关知识噢&#xff0c;谢谢各位的支持&#x1f64f; 复习…...

dify如何升级到0.15.3(目前最新版本)

Docker Compose 部署 警告 docker-legacy 目录中的文件将很快停止维护&#xff0c;并将从存储库中删除。如果您仍在使用它们&#xff0c;请尽快切换到新版本。 备份自定义的 docker-compose YAML 文件&#xff08;可选&#xff09; cd docker cp docker-compose.yaml docker-c…...

UNIAPP开发之利用阿里RTC服务实现音视频通话后端THINKPHP5

下面是一个使用ThinkPHP 5实现后端逻辑的示例。我们将创建一个简单的ThinkPHP 5项目来处理生成推流和播流地址的请求。 后端部分&#xff08;ThinkPHP 5&#xff09; 1. 初始化ThinkPHP 5项目 首先&#xff0c;确保你已经安装了Composer。然后使用Composer创建一个新的Think…...

使用JWT实现微服务鉴权

目录 一、微服务鉴权 1、思路分析 2、系统微服务签发token 3、网关过滤器验证token 4、测试鉴权功能 前言&#xff1a; 随着微服务架构的广泛应用&#xff0c;服务间的鉴权与安全通信成为系统设计的核心挑战之一。传统的集中式会话管理在分布式场景下面临性能瓶颈和扩展性…...

基于WOA鲸鱼优化的WSN网络最优节点部署算法matlab仿真

目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.本算法原理 5.完整程序 1.程序功能描述 鲸鱼优化算法&#xff08;WOA&#xff09;是一种模拟座头鲸捕食行为的元启发式优化算法。其主要原理基于座头鲸独特的 “气泡网” 捕食策略&#xff0c;通过数学模…...

TikTok账户安全指南:如何取消两步验证?

TikTok账户安全指南&#xff1a;如何取消两步验证&#xff1f; 在这个数字化的时代&#xff0c;保护我们的在线账户安全变得尤为重要。TikTok&#xff0c;作为全球流行的社交媒体平台&#xff0c;其账户安全更是不容忽视。两步验证作为一种增强账户安全性的措施&#xff0c;虽…...

将 Type Code 替换为 State/Strategy 模式

Replace Type Code with State/Strategy 要将 Type Code 替换为 State/Strategy 模式&#xff0c;你的目的是通过将行为分离到独立的类中来消除使用类型代码&#xff08;如整数、字符串或枚举类型&#xff09;来决定行为的需要。这种做法能够让系统变得更加灵活和易于维护。 …...

c#爬取数据并解析json

安装 Newtonsoft.Json Install-Package Newtonsoft.Json代码 HttpClient client new HttpClient();// 获取网页内容HttpResponseMessage response client.GetAsync("https://opentdb.com/api.php?amount10&category18&difficultyeasy&typemultiple"…...

Spring Boot 内置工具类

ObjectUtils // 获取对象的类名。参数为 null 时&#xff0c;返回字符串&#xff1a;"null" String nullSafeClassName(Object obj) // 参数为 null 时&#xff0c;返回 0 int nullSafeHashCode(Object object) // 参数为 null 时&#xff0c;返回字符串&#xff1…...

蓝桥杯15 填空题

1.握手问题&#xff1a; 思路&#xff1a;首先当所有人都握过手&#xff0c;由于一次握手相当于两个人都握手过&#xff0c;所以容易发现这是一个组合问题&#xff0c;为&#xff08;50*49&#xff09;/2&#xff0c;而其中有7个人没有相互握过手&#xff0c;那么减去&#xff…...

分布式光纤声波振动技术在钻井泄漏检测中的应用

在石油天然气的钻井作业中&#xff0c;及时发现并定位泄漏点对于保障开采安全、降低环境污染以及避免经济损失至关重要。传统的泄漏检测方法往往存在局限性&#xff0c;而分布式光纤声波振动技术凭借其独特的优势&#xff0c;正逐渐成为钻井过程中寻找泄漏的有力工具。 技术原理…...

0081.基于springboot+uni-app的垃圾分类小程序+论文

一、系统说明 基于springbootuni-app的垃圾分类小程序,系统功能齐全, 代码简洁易懂&#xff0c;适合小白学编程。 现代经济快节奏发展以及不断完善升级的信息化技术&#xff0c;让传统数据信息的管理升级为软件存储&#xff0c;归纳&#xff0c;集中处理数据信息的管理方式。本…...

基于STM32+ESP8266+手机 APP+阿里云的智能小车设计与实现(系统资料)

基于STM32+ESP8266+手机APP+阿里云的智能小车设计与实现 摘要:本研究旨在设计一款高度智能化的智能小车系统,通过集成 STM32 微控制器、ESP8266 无线模块、手机 APP 以及阿里云平台,实现环境数据实时采集、远程精准控制和高效的数据交互。详细阐述系统各部分的设计原理、实…...

分布式 IO 模块:水力发电设备高效控制的关键

在能源领域不断追求高效与可持续发展的今天&#xff0c;水力发电作为一种清洁、可再生的能源形式&#xff0c;备受关注。而要实现水力发电设备的高效运行&#xff0c;精准的控制技术至关重要。分布式 IO 模块&#xff0c;正悄然成为水力发电设备高效控制的核心力量。 传统挑战 …...

Spring Boot +SQL项目优化策略,GraphQL和SQL 区别,Spring JDBC 等原理辨析(万字长文+代码)

WebFlux 定义&#xff1a;Spring 5引入的响应式Web框架&#xff0c;基于Reactor库实现非阻塞、异步编程模型。通常用途&#xff1a;构建实时通信、流数据处理等高并发场景。使用异步事件驱动模型&#xff0c;单线程可处理数千并发连接。用途&#xff1a;处理高并发网络请求&am…...

QSNCTF-WEB做题记录

第一题&#xff0c;文章管理系统 来自 <天狩CTF竞赛平台> 描述&#xff1a;这是我们的文章管理系统&#xff0c;快来看看有什么漏洞可以拿到FLAG吧&#xff1f;注意&#xff1a;可能有个假FLAG哦 1&#xff0c;首先观察题目网站的结构和特征 这个一个文件管理系统&#x…...

使用Dify将AI机器人嵌入到你的前端页面中及chrome的扩展应用

目录 1 博主有话说2 前提环境3 Dify创建个聊天助手应用4 将AI聊天机器人嵌入到html中5 将AI聊天机器人设置为chrome的扩展应用6 博主增语 1 博主有话说 那博主话不多说&#xff0c;先展示一下成果&#xff01; 这个界面是使用dify配置的一个“聊天助手”的应用&#xff0c;助…...

接口测试-API测试中常用的协议(下)

一、RPC RPC&#xff08;Remote Procedure Call&#xff09;即远程过程调用协议&#xff0c;它允许程序调用位于其他计算机上的程序中的过程或函数&#xff0c;就像调用本地程序中的过程一样。下面从其概念、工作原理、特点、应用场景等方面详细介绍&#xff1a; 概念起源与核…...

【git】提交修改、回撤、回滚、Tag 操作讲解,与reset (--soft、--mixed、--hard) 的区别

Git 提交修改、回撤、回滚、Tag 操作详解 1. git commit --amend -m "message" 作用&#xff1a;修改最近一次提交的信息或内容。 适用场景&#xff1a; 提交后发现 commit message 写错了。提交后发现 少 add 了文件&#xff0c;想直接加进上一次提交。 示例 1&…...

【设计模式】【创建型模式】抽象工厂模式(Abstract Factory)

&#x1f44b;hi&#xff0c;我不是一名外包公司的员工&#xff0c;也不会偷吃茶水间的零食&#xff0c;我的梦想是能写高端CRUD &#x1f525; 2025本人正在沉淀中… 博客更新速度 &#x1f44d; 欢迎点赞、收藏、关注&#xff0c;跟上我的更新节奏 &#x1f3b5; 当你的天空突…...

Embedding模型介绍部署部署

概述 是一种将高维数据&#xff08;如‌文本、图像、音频&#xff09;映射到低维空间的技术&#xff0c;其中每个数据点由实数构成的向量表示&#xff0c;这些向量在向量空间中的位置反映了数据的语义特征。这种技术广泛应用于‌自然语言处理和‌机器学习中&#xff0c;使得计…...

web网络安全:跨站脚本攻击(XSS)

跨站脚本攻击&#xff08;XSS&#xff09;概述 跨站脚本攻击&#xff08;XSS&#xff0c;Cross-Site Scripting&#xff09; 是一种常见的 Web 安全漏洞&#xff0c;攻击者通过向受信任的网站注入恶意脚本&#xff08;通常是 JavaScript&#xff09;&#xff0c;诱使其他用户在…...

【C++】 Flow of Control

《C程序设计基础教程》——刘厚泉&#xff0c;李政伟&#xff0c;二零一三年九月版&#xff0c;学习笔记 文章目录 1、选择结构1.1、if 语句1.2、嵌套的 if 语句1.3、条件运算符 ?:1.4、switch 语句 2、循环结构2.1、while 语句2.2、do-while 语句2.3、 for 循环2.4、循环嵌套…...

Kafka中commitAsync的使用与实例解析

在使用Apache Kafka进行消息处理时&#xff0c;正确管理偏移量&#xff08;offset&#xff09;是确保数据一致性和可靠性的重要环节。Kafka提供了多种方式来提交偏移量&#xff0c;其中commitAsync()方法是一种高效且灵活的选择。本文将通过一个完整的实例&#xff0c;详细介绍…...

上海餐饮冷藏配送全方案 一站式服务助企提速发展

在上海这座国际化大都市中&#xff0c;餐饮行业以其繁荣与多元著称。消费者对食品安全和品质要求的日益提高&#xff0c;餐饮冷链运输成为了保障餐饮供应链稳定、高效的关键环节。近日&#xff0c;华鼎冷链科技凭借其创新的一站式服务方案&#xff0c;在上海餐饮冷链领域脱颖而…...

基于微信小程序的电影院订票选座系统的设计与实现,SSM+Vue+毕业论文+开题报告+任务书+指导搭建视频

本系统包含用户、管理员两个角色。 用户角色&#xff1a;注册登录、查看首页电影信息推荐、查看电影详情并进行收藏预定、查看电影资讯、在线客服、管理个人订单等。 管理员角色&#xff1a;登录后台、管理电影类型、管理放映厅信息、管理电影信息、管理用户信息、管理订单等。…...

Java 面试笔记 - Java基础

1 、JDK、JRE 和 JVM 是 Java 开发与运行环境中的三个核心组件&#xff0c;它们之间的关系和区别如下&#xff1a; 1. JDK (Java Development Kit) 定义&#xff1a;JDK 是 Java 开发工具包&#xff0c;包含了开发 Java 应用程序所需的所有工具和库。包含内容&#xff1a; 编…...

基于MFC实现的键盘电子乐器演奏程序

基于MFC实现的键盘电子乐器演奏程序设计 1.项目简介 需要连接西电微机原理实验室提供的 QTH9054 微机试验箱&#xff0c;使用其蜂鸣器发声&#xff0c;若不连接&#xff0c;程序会直接播放 mp3 文件模拟钢琴声。 请在 release 处下载编译好的 exe 文件运行&#xff0c;如需计…...

支持批量导出的软件,效率拉满!

今天给大家分享一款超实用的软件&#xff0c;它能帮你批量导出PPT里的图片&#xff0c;简直是提升工作效率的神器&#xff01; PPT转jpg PPT逐页导出为图片 这款软件超级简单易用&#xff0c;打开就能直接上手&#xff0c;不需要复杂的设置。 这个软件有三种功能&#xff0c; …...

AI前端开发:职业晋升的加速器

在数字经济时代&#xff0c;前端开发领域正在经历一场由人工智能 (AI) 技术驱动的深刻变革。掌握AI技能&#xff0c;已不再是锦上添花&#xff0c;而是成为前端工程师职业晋升的关键竞争力。本文将探讨AI写代码工具等AI技术如何重塑前端开发职业路径&#xff0c;以及如何通过学…...

在mfc中使用自定义三维向量类和计算多个三维向量的平均值

先添加一个普通类, Vector3.h, // Vector3.h: interface for the Vector3 class. // //#if !defined(AFX_VECTOR3_H__53D34D26_95FF_4377_BD54_57F4271918A4__INCLUDED_) #define AFX_VECTOR3_H__53D34D26_95FF_4377_BD54_57F4271918A4__INCLUDED_#if _MSC_VER > 1000 #p…...

RK3588配置成为路由器

文章目录 前言一、配置netplan二、安装hostapd1.创建hostapd.conf文件2.安装软件3.修改启动文件4.修改/etc/default/hostapd 文件 三、安装dnsmasq服务四、配置NET及重启验证五、常见问题总结 前言 RK3588开发板有两个网口&#xff0c;一个无线网卡。我需要配置为家用路由器模…...

力扣-回溯-40 组合总和Ⅱ

思路 需要实现树枝层面的去重&#xff0c;利用use数组来判别&#xff0c;如果前一个节点已经使用了&#xff0c;说明这是在往深处遍历了&#xff0c;允许重复&#xff0c;如果前一个节点没有使用且值相同的话&#xff0c;说明是在树枝上重复了 代码 class Solution { public…...

深入理解 Rust 中的 `Box<T>`:堆上的数据与递归类型

1. Box<T> 的基础知识 1.1 堆与栈的分工 在默认情况下&#xff0c;Rust 会将变量存储在栈上。然而&#xff0c;栈的空间有限&#xff0c;且对于大小未知或极大的数据来说&#xff0c;栈并不适用。使用 Box<T>&#xff0c;我们可以将数据存放在堆上&#xff0c;而…...

js原型和原型链,到底咋回事?

js原型和原型链&#xff0c;到底咋回事&#xff1f; js原型的原理 在 JavaScript 里&#xff0c;每个对象都有一个“隐藏的小伙伴”&#xff0c;这个“小伙伴”就是原型。可以把原型想象成一个模板或者一个仓库&#xff0c;对象能从它这个“小伙伴”那里借用一些属性和方法。…...

深入理解IP地址与端口:网络通信的基石

深入理解IP地址与端口&#xff1a;网络通信的基石 文章大纲 网络通信基础概念 1.1 什么是网络通信1.2 OSI模型与TCP/IP模型1.3 数据包传输原理 IP地址深度解析 2.1 IP地址定义与作用2.2 IPv4与IPv6对比2.3 子网划分与CIDR表示法2.4 公网IP与私网IP2.5 特殊IP地址详解 端口机…...

ubuntu系统本地部署deepseek

1、安装ollama 用Ollama部署deepseek模型&#xff0c;官网地址&#xff1a;https://ollama.com/download 根据官网给出的命令直接安装&#xff08;安装linux版本&#xff09; curl -fsSL https://ollama.com/install.sh | sh2、安装deepseek-r1模型 我这里用了8b&#xff0c…...

nginx ngx_http_module(10) 指令详解

nginx ngx_http_module(10) 指令详解 nginx 模块目录 nginx 全指令目录 一、目录 1.1 模块简介 ngx_http_v2_module&#xff1a;HTTP/2支持模块&#xff0c;允许Nginx通过HTTP/2协议与客户端进行通信。HTTP/2带来了许多性能优化&#xff0c;如多路复用、头部压缩和服务器推…...

rman 备份恢复1

前提&#xff1a; rman用户必须具有sysdba权限 使用常用连接方式如下&#xff1a; rman target / rman target sys/oracle rman target sys/oracleprod1 catalog dav/oracledav_db 一个rman连接会产生两个进程&#xff0c;action字段为空的就是rman的监控进程&#xff0c;另…...

BeautifulSoup、lxml/XPath和正则表达式在数据爬取中的适用场景

在数据爬取中&#xff0c;BeautifulSoup、lxml/XPath和正则表达式的适用场景各有侧重&#xff0c;具体选择需根据数据特征和需求权衡&#xff1a; 1. BeautifulSoup&#xff08;结合CSS选择器&#xff09; 适用场景 简单结构页面&#xff1a;标签层级清晰、属性固定的HTML页面…...

Python装饰器本质250220

定义一个函数&#xff0c;在不修改这个函数的代码的情况下&#xff0c;让函数执行前后会有新的内容加入 def func():print("func")return def outer():def inner():print("new code before")func()print("new code after")returnreturn inner f…...

实时、分时、半实时半分时操作系统

在操作系统中&#xff0c;实时、分时、半实时半分时是三种不同的调度策略&#xff0c;它们决定了系统如何分配和管理CPU资源。以下是它们的定义和举例&#xff1a; 1. 实时操作系统&#xff08;RTOS, Real-Time Operating System&#xff09; 定义&#xff1a;实时操作系统是…...