C语言指针及内存原理

前言

c语言是一种比较底层的语言,为什么说他底层呢?主要原因就是C语言会直接对于内存中的地址进行操作,而C语言就是通过指针这个工具对于内存进行操作的。

指针是什么?

指针是什么?

指针理解的2个要点:

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

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

https://nicedream18.github.io/picx-images-hosting/20240118/image.1cz42rxiww9s.webp

指针变量

我们可以通过&(取地址操作符)取出变量的内存其实地址,把地址可以存放到一个变量中,这个 变量就是指针变量。

#include <stdio.h>
int main()
{
    int a = 10;//在内存中开辟一块空间
    int* p = &a;//这里我们对变量a,取出它的地址,可以使用&操作符。
       //a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量
    中,p就是一个之指针变量。
        return 0;
}

总结:

指针变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)。

那这里的问题是:

  • 一个小的单元到底是多大?(1个字节)

  • 如何编址?

经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。

对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0);

那么32根地址线产生的地址就会是:

00000000 00000000 00000000 00000000

00000000 00000000 00000000 00000001

...

11111111 11111111 11111111 11111111

这里就有2的32次方个地址。

每个地址标识一个字节,那我们就可以给 (2^32Byte == 2^32/1024KB ==2^32/1024/1024MB==2^32/1024/1024/1024GB == 4GB) 4G的空闲进行编址。

同样的方法,那64位机器,如果给64根地址线,那能编址多大空间,自己计算,计算方法如上所示,此处不再计算。

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

总结: 指针是用来存放地址的,地址是唯一标示一块地址空间的。 指针的大小在32位平台是4个字节,在64位平台是8个字节。

指针和指针类型

这里我们在讨论一下:指针的类型 我们都知道,变量有不同的类型,整形,浮点型等。

那指针有没有类型呢? 准确的说:有的。 当有这样的代码:

int num = 10;
p = &num;

要将&num(num的地址)保存到p中,我们知道p就是一个指针变量,那它的类型是怎样的呢? 我们给指针变量相应的类型。

char  *pc = NULL;
int   *pi = NULL;
short *ps = NULL;
long  *pl = NULL;
float *pf = NULL;
double *pd = NULL;

这里可以看到,指针的定义方式是: type + * 。

其实: char* 类型的指针是为了存放 char 类型变量的地址。

short* 类型的指针是为了存放 short 类型变量的地址。

int* 类型的指针是为了存放 int 类型变量的地址。 那指针类型的意义是什么?下面我们会进行讲解!

指针的不同引用

#include <stdio.h>
int main()
{
    int n = 0x11223344;
    char* pc = (char*)&n;
    int* pi = &n;
    *pc = 0;   //重点在调试的过程中观察内存的变化。
    *pi = 0;   //重点在调试的过程中观察内存的变化。
    return 0;
}

https://cdn.jsdelivr.net/gh/nicedream18/picx-images-hosting@master/20240118/image.4ain4cohm1m0.webp

上图中我们执行到了*pc=0这一句,此时因为是char类型的指针,所以我们的地址只有一个发生了变化

https://cdn.jsdelivr.net/gh/nicedream18/picx-images-hosting@master/20240118/image.1dg1nozisfl.webp

而在上面这张图中我们已经将走到了将int类型的指针为0,所以会发生四个字节的改变。

总结:

指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。

比如: char 的指针解引用就只能访问一个字节,而 int 的指针的解引用就能访问四个字节。

野指针

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

这种指针往往意味着程序的不可控制性,所以我们在编程的过程中要尽量减少野指针的产生。

怎样会产生和预防野指针

指针不进行初始化

#include <stdio.h>
int main()
{
    int* p;//局部变量指针未初始化,默认为随机值,我们在使用时,如果没有初始的地址,我可以初始为空值,如:int* p = NULL
    *p = 20;
    return 0;
}

指针越界访问

这种情况主要出现在我们对于数组进行访问时

#include <stdio.h>
int main()
{
    int a[10] = { 0 };
    int* p = a;
    int i = 0;
    for (i = 0; i <= 11; i++)
    {
        //当指针指向的范围超出数组的范围时,p就是野指针
        *(p++) = i;
    }
    return 0;
}

在使用指针前对于指针进行验证

#include <stdio.h>
int main()
{
    int* p=NULL;
    if(p!=NULL){//对于地址进行可访问性验证
      *p = 20;  
    }
    return 0;
}

忘记将用完的地址进行释放

这个就是忘记free掉用过的指针,我们在后面讲吧。

指针的运算

指针+-整数

#include <stdio.h>
int main()
{
 int n = 10;
 char *pc = (char*)&n;
 int *pi = &n;

 printf("%p\n", &n);
 printf("%p\n", pc);
 printf("%p\n", pc+1);
 printf("%p\n", pi);
 printf("%p\n", pi+1);
 return  0;
}

这里的char+1地址加了一个1,而int+1地址加了一个4

https://cdn.jsdelivr.net/gh/nicedream18/picx-images-hosting@master/20240118/image.2znd2jg15u20.webp

总结:指针的类型决定了指针向前或者向后走一步有多大(距离)。

指针-指针

#include<stdio.h>
int main()
{
    int a[5] = { 1,2,3,4,5 };
    int* p1 = &a[4];
    int* p2 = &a[0];
    printf("%d", p1 - p2);
    return 0;
}

https://jsd.cdn.zzko.cn/gh/nicedream18/picx-images-hosting@master/20240118/image.4roeles7ho20.webp

上面进行相减后得出的结果是4,为什么会得出4这个结果呢?因为p1和p2之间相差4个整型元素,这个地方为什么我们要强调是整形元素呢?因为我们在运算符两侧的指针类型均是整型指针,实际上指针类型的变量进行相减是指针所代表的地址进行相减,将得出的值除以指针所指向的空间所占据的字节数,这样将大家不是很容易理解,有一点点抽象,我给大家简单举一下例子,比如在上面这个例子中p1中所存储的地址值是16,而p2中所存储的值是0,那么我们将p2减去p1后得出的16除以p1和p2所指向的数据类型即整型类型,每个整型元素所占据的字节数为4,所以16除以4之后的结果为4,即最终在屏幕上的输出结果为4,那么很多小伙伴就问了,在上面这个例子中,我们将两个指针的类型进行强制转换为char类型后得出的结果是不是就会改变为16了呢,因为根据上面的我给出的推理方式确实应该是这样的,因为char类型占据的字节数为1,16除以1之后的结果仍为16,接下来我们代码展示一下是不是就像我们的推理一样,得出的结果是16.

https://jsd.cdn.zzko.cn/gh/nicedream18/picx-images-hosting@master/20240118/image.8p4rhut8nvk.webp

指针的关系运算

既然指针变量中所存储的是地址,地址从本质上来说也只是一串数据,那么也是可以进行比较大小的!此处不再进行举例给大家进行展示,因为不难理解,但是下面会给大家进行强调一个要点!

标准规定:

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

指针与数组

我们先看一段程序

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

这段代码的运算结果是这样的

https://jsd.cdn.zzko.cn/gh/nicedream18/picx-images-hosting@master/20240118/image.4myniw4belk0.webp

可见数组名和数组首元素的地址是一样的。

结论:数组名表示的是数组首元素的地址。

那么我们可以用指针访问数组

#include <stdio.h>
int main()
{
    int arr[] = { 1,2,3,4,5,6,7,8,9,0 };
    int* p = arr; //指针存放数组首元素的地址
    int sz = sizeof(arr) / sizeof(arr[0]);
    for (int i = 0; i < sz; i++)
    {
        printf("&arr[%d] = %p   <====> p+%d = %p\n", i, &arr[i], i, p + i);
    }
    return 0;
}

https://jsd.cdn.zzko.cn/gh/nicedream18/picx-images-hosting@master/20240118/image.1esoulp11olc.webp

所以 p+i 其实计算的是数组 arr 下标为i的地址。

那我们就可以直接通过指针来访问数组。

所以可以这么访问

int main()
{
    int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
    int* p = arr; //指针存放数组首元素的地址
    int sz = sizeof(arr) / sizeof(arr[0]);
    int i = 0;
    for (i = 0; i < sz; i++)
    {
        printf("%d ", *(p + i));
    }
    return 0;
}

二级指针

指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里? 这就是二级指针 。

https://jsd.cdn.zzko.cn/gh/nicedream18/picx-images-hosting@master/20240118/image.57ea0cb4rz00.webp

对于二级指针的运算有:

  • ppa 通过对ppa中的地址进行解引用,这样找到的是 pa , ppa 其实访问的就是 pa .
int b = 20;
*ppa = &b;//等价于 pa = &b;
  • *ppa 先通过 ppa 找到 pa ,然后对 pa 进行解引用操作: *pa ,那找到的是 a .
**ppa = 30;
//等价于*pa = 30;
//等价于a = 30;

指针数组

指针数组是指针还是数组?

答案:是数组。是存放指针的数组。

那指针数组是怎样的?

int* arr3[5];//是什么?

arr3是一个数组,有五个元素,每个元素是一个整形指针。

https://jsd.cdn.zzko.cn/gh/nicedream18/picx-images-hosting@master/20240118/image.281nwzhce7k0.webp

字符指针

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

一般使用:

int main()
{
    char ch = 'w';
    char* pc = &ch;
    *pc = 'w';
    return 0;
}

将字符串指针进行使用如下:

int main()
{
    const char* pstr = "hello bit.";//这里是把一个字符串放到pstr指针变量里了吗?
    printf("%s\n", pstr);
    return 0;
}

但这段代码真的是将字符串直接存在一个地址中了吗,当然不是,他存储方式更加像是数组,他将开头的字符进行了存储。通过开头字母进行索引这个字符串。

上面代码的意思是把一个常量字符串的首字符 h 的地址存放到指针变量 pstr 中。

那就有可这样的面试题:

#include <stdio.h>
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");
    else
        printf("str1 and str2 are not same\n");

    if (str3 == str4)
        printf("str3 and str4 are same\n");
    else
        printf("str3 and str4 are not same\n");

    return 0;
}

运行结果如下

https://jsd.cdn.zzko.cn/gh/nicedream18/picx-images-hosting@master/20240118/image.2ujj3vuokko0.webp

这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当 几个指针。也就是指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化 不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4相同。

数组指针

数组指针的定义

数组指针是指针?还是数组?

答案是:指针。

我们已经熟悉:

整形指针: int * pint; 能够指向整形数据的指针。

浮点型指针: float * pf; 能够指向浮点型数据的指针。

那数组指针应该是:能够指向数组的指针。 下面代码哪个是数组指针?

int *p1[10];
int (*p2)[10];
//p1, p2分别是什么?

解释一下:

int *p1[10];//此处就不再过多解释了,因为这就是我们上面刚才讲过的指针数组
int (*p)[10];
//解释:p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个
指针,指向一个数组,叫数组指针。
//这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。

&数组名 与 数组名

arr 和 &arr 分别是啥?

我们知道arr是数组名,数组名表示数组首元素的地址。 那&arr数组名到底是啥?

我们看一段代码:

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

https://jsd.cdn.zzko.cn/gh/nicedream18/picx-images-hosting@master/20240118/image.1sa48p1ab3j4.webp

额好像两个结果是一样的,那是不是说明这两个其实是一样的呢?

当然不是,让我们看下面这个实例

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

https://jsd.cdn.zzko.cn/gh/nicedream18/picx-images-hosting@master/20240118/image.10ynqih3nzo0.webp

通过这个实例其实就已经明了了

其实&arr和arr,虽然值是一样的,但是意义应该不一样的。

实际上: &arr 表示的是数组的地址,而不是数组首元素的地址。

本例中 &arr 的类型是: int(*)[10] ,是一种数组指针类型 数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40

数组指针的使用

那数组指针是怎么使用的呢?

既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。

看代码:

#include<stdio.h>
int main()
{
    int arr[10] = { 1,2,3,4,5 };
    int(*ptr)[10] = &arr;
    for (int i = 0; i < 5; i++)
    {
        printf("%d ", *((*ptr) + i));
        /*printf("%d ",(*ptr)[i]);*/
        //上面的代码可以由下面的一行代码代替,因为(*ptr)就代表数组名,同时也代表着首元素的地址
    }
    return 0;
}

在来一个二维数组的演示吧

#include<stdio.h>
void print(int(*p)[5], int x, int y)
{
    for (int i = 0; i < x; i++)
    {
        for (int j = 0; j < y; j++)
        {
            printf("%d ", *(*(p + i) + j));
            //printf("%d ", (*(p + i))[j]);
            //printf("%d ", p[i][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} };
    print(arr, 3, 5);
    return 0;
}

问题1:为什么在print函数那的形参会是数组指针的形式?

答:因为数组名代表首元素的地址,我们都知道,整型的一维数组的元素是整型,实际上我们在求数组元素类型时常常会发生类似降维一样的情况,即此处由一维降到了点,那么我们来进行类推,二维数组也应该降成一维数组,而数组名我们在前面已经了解到了就是首元素的地址,而数组的地址的类型就是数组指针!

总结:我们在看待二维数组的时候,要把它看成是由一维数组组成的,即二维数组的每个元素就是一维数组,我们在将这个结论扩展到多维时也同样适用,比如三维数组的元素就是二维数组,四维数组的元素就是三维数组,那么其数组名的意义我们就相应的能够了解到了,

问题2:为什么上面的四种形式能够进行互换?

答:首先大家先看第一个为什么行,p代表的是第一行,p+i就是第i行,我们对其进行解引用,就是拿到的是这一行的元素,实际上在此处就是代表的是一维数组的数组名,而数组名又是数组的首地址,将其加上i后就是一维数组第i个元素的地址,再对其进行解引用,我们就得到了第i行第j列的元素。

为了帮助大家理解上面的等价替换,下面会给大家举个例子!

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

p[i] == arr[i] == (p+i) == (arr + i)

上面的四种形式其实是等效的!

下面我们进行类比一下,其实我们就能明白上面的四种形式为什么会相同!

((p + i) + j)) == ((p + i))[j]) == p[i][j]) == (p[i]+j))

其实这个地方也不难,只是由一维扩展到了二维!

指针传参

一维传参

首先是一维数组传参

#include<stdio.h>
void print(int arr[5])//方法一
//void print(int *arr)方法二
//void print(int arr[])方法三
{

}
int main()
{
    int arr[5] = { 1,2,3,4,5 };
    print(arr);
    return 0;
}

注意:其实这三种传参方式本身并没有什么区别,其实无论是上面的哪一种方式进行传参,其本质上都是通过指针的方法进行传参,就像方法二一样,所以在方法一中的数字,写什么都是可以的,并没有任何的问题,在后续的使用上也并没有任何的区别。

接下来是一维指针数组的传参

#include<stdio.h>
void print(int* arr[5])//方法一(当然,括号中的5也可以不写,也可以随便写一个数)
//void print(int **arr)方法二(因为数组的元素是指针,而数组名代表首元素的地址,指针的地址就是二级指针)
{

}
int main()
{
    int* arr[5];
    print(arr);
    return 0;
}

需要注意的点同上

二维传参

#include<stdio.h>
void print(int arr[2][3])//方法一(中规中矩的二维数组传参,此处一定要注意,行可以省略掉,但是列一定不能省略,同时需要注意,行可以随便写,但是列一定要与原来的数组保持一致,至于为什么,看下一种方法,即本质就能明白,因为列就是数组类型的一部分)
//void print(int (*arr)[3])方法二(也就是二维数组传参的本质所在)传的其实是一个一维的指针数组,每个元素保存这每一行的地址
{

}
int main()
{
    int arr[2][3] = { {1,2,3},{4,5,6} };
    print(arr);
    return 0;
}

一级指针传参

#include <stdio.h>
void print(int* p, int sz)
{
    int i = 0;
    for (i = 0; i < sz; i++)
    {
        printf("%d\n", *(p + i));
    }
}
int main()
{
    int arr[10] = { 1,2,3,4,5,6,7,8,9 };
    int* p = arr;
    int sz = sizeof(arr) / sizeof(arr[0]);
    //一级指针p,传给函数
    print(p, sz);
    return 0;
}

思考:

当一个函数的参数部分为一级指针的时候,函数能接收什么参数?

void test1(int* p)
{}
//test1函数能接收什么参数?
/*
    int a = 1;
    int *p = &a;
    int arr[] = {1,2,3,4,5};
    test1(&a);//可以
    test1(p);//可以
    test1(arr);//可以
*/
void test2(char* p)
{}
//test2函数能接收什么参数?
/*
    char ch = 'w';
    char *p = &ch;
    char arr[] = "abcde";
    test2(&ch);//可以
    test2(p);//可以
    test2(arr);//可以
*/

二级指针传参

#include<stdio.h>
void print(int** pp)
{

}
int main()
{
    int a = 10;
    int* p = &a;
    int** pp = &p;
    print(&p);
    print(pp);
    return 0;
}

思考:

当函数的参数为二级指针的时候,可以接收什么参数?

首先可以比较清楚的了解到上面的这两种传参方式肯定是没有问题的,但是除了上面这两种之外还有别的传参方式!

#include<stdio.h>
void print(int** pp)
{

}
int main()
{
    int* p[5];
    print(p);
    //p是指针数组,数组的每一个元素都是指针,而我们传的是指针数组的数组名,即指针数组的首元素的地址
    //指针数组的数组的首元素的地址即指针的地址,其类型就是二级指针
    return 0;
}

函数指针

先看这段代码

#include <stdio.h>
void test()
{
    printf("hello world\n");
}
int main()
{
    printf("%p\n", test);
    printf("%p\n", &test);
    return 0;
}

下面是程序的运行结果:

https://jsd.cdn.zzko.cn/gh/nicedream18/picx-images-hosting@master/20240118/image.2vpvbwl0l940.webp

输出的是两个地址,这两个地址是 test 函数的地址。

这个地方相信大家就会想问了,那么这两个代表的意义时候完全相同呢?还是说像数组一样,数组名和&数组名代表不同的含义呢,这个地方就给大家说明白,函数名和&函数名代表着相同的含义,表示的都是函数的地址,其数值表现形式也都是函数的地址,两者没有任何的区别,在使用上也没有任何的区别!

那我们的函数的地址要想保存起来,怎么保存?

下面我们看代码:

void test()
{
    printf("hello world\n");
}
//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (*pfun1)();
void* pfun2();

首先,能给存储地址,就要求pfun1或者pfun2是指针,那哪个是指针?

答案是:

pfun1可以存放。pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void。

在这个地方相信大家还是不怎么理解,这里给大家进行解释一下,我们知道,对于数组来说,我们把在函数定义时的语句的变量名去掉就能得到定义的变量的类型,对于pfun1来说,我们将变量名去掉后,剩余的部分是void ()(),如果我们将pfun2去掉之后,剩余的部分是 void pfun()。

好像这两个乍一看并没有太大的区别,在它们进行定义时的唯一的区别就是pfun1比pfun2多了一个(),我们清楚,()的优先级是大于的,那么在pfun1左右加上括号之后,*就将与pfun1变量名进行结合,这就说明了pfun1是一个指针变量而去掉变量名之后,就是一个函数,这就说明了pfun1是一个指向函数的指针变量,所以能够存储函数的地址。

接下来带大家来看一下,pfun2到底是一个什么!因为pfun2的左侧的操作符是,而右侧的操作符是(),很明显,()的优先级比要高,所以pfun2先与()进行结合,构成函数,而没有形成指针变量。void是函数pfun2的返回类型,此处我们就可以进行下结论了,即pfun2是一个函数名,函数的返回类型是void 类型。

这个地方相信已经给大家讲明白了!其实这些在清楚的了解了那些操作符的优先级和结合性顺序之后也并不难理解!

既然我们已经理解了函数指针的相关知识,我们就先简单的运用一下吧!

#include<stdio.h>
int add(int a, int b)
{
    return a + b;
}
int main()
{
    int (*padd)(int a, int b) = add;
    int sum = (*padd)(3, 5);//方法一
    //int sum = padd(3, 5);方法二
    printf("%d ", sum);
    return 0;
}

那么我们该如何取理解上面的两种调用方法呢?其实也不难,我们取理解一下函数名的概念就能比较轻松的理解上面的两种调用方法,函数名和&函数名代表的含义是相同的,都是表示的是函数的地址,下面我会再给出一段代码来帮助大家进行理解!

#include<stdio.h>
int add(int x,int y)
{
    return x + y;
}
int main()
{
    int (*p)(int x, int y);
    p = add;//当然,此处也可以写成p = &add
    //从上面这段代码中其实就可以明白,其实p和add是几乎完全相同的,所以它们的用法也是差不多完全相同的
    int sum = p(3, 4);
    //上面这一行可以用下面的三种形式进行代替
    //int sum = (*p)(3, 4);方法一
    //int sum = add(3, 4);方法二
    //int sum = (*(&add))(3, 4);方法三
    return 0;
}

回调函数

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

给一段简单的实例应该就能明白

#include <stdio.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 Calc(int (*ptrf)())//这个地方就是回调函数
{
    int x = 0;
    int y = 0;
    printf("输入操作数:");
    scanf("%d %d", &x, &y);
    printf("ret = %d\n", ptrf(x,y));
}
int main()
{
    int input = 1;
    int ret = 0;
    do
    {
        printf("*************************\n");
        printf(" 1:add           2:sub \n");
        printf(" 3:mul           4:div \n");
        printf("*************************\n");
        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;
}