• 指针进阶(二)


    5.函数指针

    我们来学习一个高级语法,函数指针,意为指向函数的指针,存放函数地址的指针。

    首先看一段代码:

    #include 
    int Add(int x,int y)
    {
        return x + y;
    }
    int main()
    {
        printf("%p\n", Add);
        printf("%p\n", &Add);
        //&函数名 - 取到的就是函数的地址
        //数组名!=&数组名
        //函数名==&数组名
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    输出的结果:image-20220619155414959

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

    那我们的函数的地址要想保存起来,怎么保存?下面我们看代码:

    int Add(int x,int y)
    {
        return x + y;
    }
    //下面pfun1和pfun2哪个有能力存放Add函数的地址?
    int (*pfun1)();
    int  *pfun2();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

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

    答案是:

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

    int (*pf)(int,int) = &Add;
    //pf就是一个函数指针变量
    
    • 1
    • 2

    pf先和*结合,说明pf是一个指针,指向函数Add, (int ,int)说明pf指向的函数参数有两个,均是int类型。最前面的int 说明函数返回值类型为int。

    int Add(int x,int y)
    {
    	return x + y;
    }
    int main()
    {
    	int a = 1;
        int b = 2;
        int (*pa)(int, int) = &Add;
        //int (*pa)(int a,int b) = Add;
        //a,b数值可带可不带
        int ret = (*pa)(a,b);//这里的*可有可无,甚至带多个*也可以
        int ret = pa(a,b);//函数指针pa和Add函数是一样的
        //*可带可不带,但是如果带*就一定要加上()
        printf("%d",ret);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    函数名和&函数名,他们的地址是一样的,且本质上没有任何区别。

    使用函数指针调用函数,可以把pa当成函数名Add调用,解引用没有意义。Add(a,b)等价于pa(a,b)。

    这里的 *可带可不带,但是如果带 *就一定要加上(),这是因为 * pa(a,b)意为对函数调用的结果进行解引用,带上括号可避免此问题。

    阅读两段有趣的代码:

    这两段代码均来自一本书《C陷阱与缺陷》,有意者可去学习。

    //代码1
    (*(void (*)())0)();
    
    //代码2
    void (*signal(int, void(*)(int)))(int);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    image-20220621000041666

    代码1:以上代码是一次函数调用,调用的是0作为地址处的这个函数

    1.把0强制类型转换为:无参,返回类型是void的函数的地址 2.调用0地址处的函数

    代码2:以上代码是一次函数声明,声明signal函数。

    1.声明的signal函数的第一个参数的类型是int,第二个参数的类型是函数指针,该函数指针指向的函数参数是int,返回类型是void

    2.signal函数的返回类型也是一个函数指针,该函数指针指向的函数参数是int,返回类型是void

    6.函数指针数组

    数组是一个存放相同类型数据的存储空间,那我们已经学习了指针数组,比如:

    int *arr[10];
    //数组的每个元素是int*
    
    • 1
    • 2

    那要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?

    int (*parr1[10])();
    int *parr2[10]();
    int (*)() parr3[10];
    
    • 1
    • 2
    • 3

    答案是:parr1

    parr1先和 []结合,说明 parr1是数组,数组的内容是什么呢?

    是 int (*)()类型的函数指针。

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

    可能大家会很疑惑,为什么我们要费劲周章去用函数指针?我直接用函数名调用函数不就行了,就像上面的add,我直接用函数名调用函数不就行了,为什么要使用函数指针pa呢?这是因为我们平时很少接触高级的代码,所以对函数指针的使用不常见,但是函数指针在C语言中算是一个非常高级的语法,而且在复杂的代码中经常用到。

    例子:(计算器)

    #include 
    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("*****************************\n");
    	printf("****   1. add   2. sub  *****\n");
    	printf("****   3. mul   4. div  *****\n");
    	printf("****   0. exit          *****\n");
    	printf("*****************************\n");
    }
    int main()
    {
         int x, y;
         int input = 1;
         int ret = 0;
       	 do
       		{
                meun();
                printf("请选择:");
                scanf( "%d", &input);
                switch (input)
                {
                case 1:
                      printf("输入操作数:");
                      scanf( "%d %d", &x, &y);
                      ret = add(x, y);
                      printf( "ret = %d\n", ret);
                      break;
                case 2:
                      printf( "输入操作数:");
                      scanf( "%d %d", &x, &y);
                      ret = sub(x, y);
                      printf("ret = %d\n", ret);
                      break;
                case 3:
                      printf("输入操作数:");
                      scanf( "%d %d", &x, &y);
                      ret = mul(x, y);
                      printf( "ret = %d\n", ret);
                      break;
                case 4:
                      printf("输入操作数:");
                      scanf( "%d %d", &x, &y);
                      ret = div(x, y);
                      printf("ret = %d\n", ret);
                      break;
                case 0:
                      printf("退出程序\n");
                      break;
               default:
                      printf( "选择错误\n" );
                      break;
                }
            } while (input);   
            return 0; 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71

    我们发现这样的代码非常冗余,switch下面的case语句重复度太高,我们可以把他封装成一个函数。我们实现一个calc函数,根据不同的运算法则,传不同的函数地址。

    Calc函数:

    void calc(int (*pf)(int, int))
    {
        int x = 0;
        int y = 0;
        int ret = 0;
        printf("请输入操作数:")scanf("%d %d",&x,&y);
        ret = pf(x, y);
        printf("%d\n",ret);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    其他部分:

    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;
     
    	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;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    里用函数指针代码已经简化很多了,但是这还不是最简的,利用函数指针数组我们可以是代码更加简单!

    使用函数指针数组的实现:

    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 menu()
    {
    	printf("*****************************\n");
    	printf("****   1. add   2. sub  *****\n");
    	printf("****   3. mul   4. div  *****\n");
    	printf("****   0. exit          *****\n");
    	printf("*****************************\n");
    }
    int main()
    {
         int x, y;
         int input = 1;
         int ret = 0;
         int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
         while (input)
         {
              menu();
              printf( "请选择:" );
              scanf("%d", &input);
              if(input == 0)
              {
                  printf("退出程序\n");
                  break;
              }
              else if(input <= 4 && input >= 1)
              {
                  printf( "输入操作数:" );
                  scanf( "%d %d", &x, &y);
                  ret = (*p[input])(x, y);
                  printf("ret = %d\n",ret);
              }
              else
              {
                   printf( "输入有误,请重新输入\n" );
                   continue;   
              }   
         }
          return 0; 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    7.指向函数指针数组的指针

    指向函数指针数组的指针是一个指针

    指针指向一个数组 ,数组的元素都是函数指针 ;

    如何定义?

    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; 
    }
    int main()
    {
    	int (*p)(int, int) = add;//p是函数指针
        int (*arr[4])(int, int) = {add,sub,mul,div};//arr是函数指针数组
        int (*(*parr[4]))(int, int) = &arr;//parr是函数指针数组指针
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    image-20220621152612743

    8.回调函数

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

    上述计算器代码的实现就使用了回调函数。

    8.1库函数qsort介绍

    我们初步认识一下qsort库函数:(打开MSDN)

    image-20220621010620538

    通过MSDN的介绍,我们知道qsort的作用是一次快速排序,返回类型为void, 参数4个,分别为:

    void *base:返回类型为无类型指针,参数base中存放的是待排序数据的起始位置

    size_t num:返回类型为无符号整形,参数num中存放的是待排序数据元素的个数

    size_t width:返回类型为无符号整形,参数width中存放的是待排序数据元素的大小(单位是字节)

    int (_cdecl *compare)(const void *elem1,const void *elem2):参数compare一个是返回类型为函数指针类型,指向函数的参数为两个无类型指针,返回类型为int的比较函数。caelc是函数调用约定。elem1,elem2是要比较的两个元素的地址。返回值如下:

    image-20220626233012328

    8.2 qsort函数排序整形数组

    我们已经基本了解了qsort的基本知识,下面我们使用qsort排序整形数组int arr[] = {1,3,5,2,4,6,7,9,8,0};

    这里我们只需要考虑qsort的参数问题,传参需要考虑传数组名(数组首元素的地址,即待排序数据的起始位置),数组元素个数,每个元素的大小(字节),比较函数,但是这个比较函数必须我们自己来实现。

    int num = sizeof(arr)/sizeof(arr[0]);//数组元素个数
    int width = sizeof(arr[0]);//每个元素的大小
    int int_cmp(const void* e1, const void* e2)//比较函数
    {
        return (*(int*)e1 - (*(int*)e2);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    注意:void* 是无类型的指针,可以接受任意类型的地址,因为e1,e2是无类型指针,不能解引用和做加减整数,所以使用时需要先将无类型指针强制类型转化为int *,再进行解引用操作。

    #include 
    #include 
    int int_cmp(const void* e1, const void* e2)
    {
    	return (*(int*)e1 - *(int*)e2);//升序排序
    }
    int main()
    {
    	int arr[] = {1,3,5,2,4,6,7,9,8,0};
    	int num = sizeof(arr) / sizeof(arr[0]);
    	qsort(arr, num, sizeof(arr[0]), int_cmp);
    	for (int i = 0; i < num; i++)
    	{
    		printf("%d ", arr[i]);//打印0 1 2 3 4 5 6 7 8 9
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    8.3 qsort函数排序结构体

    struct Stu
    {
        char name[20];
        int name;
    };
    int cmp_stu_by_name(const void* e1, const void* e2)
    {
        return strcmp((struct Stu*)e1->name,(struct Stu*)e2->name);
    }
    int cmp_stu_by_age(const void* e1, const void* e2)
    {
        return ((struct Stu*)e1).age - ((struct Stu*)e2).age;
    }
    void test()
    {
    	struct Stu s[3] = {{"zhangsan",18},{"lisi",25},{"wangwu",22}};
        int sz = sizeof(s)/sizeof(s[0]);
        qsort(s,sz,sizeof(s[0]),cmp_stu_by_name);
        for(int i = 0; i < sz; i++)
        {
            printf("%s ",s[i].name);//输出lisi wangwu zhangsan
        }
        qsort(s,sz,sizeof(s[0]),cmp_stu_by_age);
        for(int j = 0; j < sz; j++)
        {
            printf("%d ",s[j].age);//输出18 22 25
        }
    }
    int main()
    {
        test();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    注意:strcmp函数的返回值刚好契合int_cmp_by_name函数的返回值,可以直接使用return返回返回值。比较字符本质是比较ASCII码值,比如:abfgmc和abgmfnpxz比较,先比较a, 相同,再比较b,相同,再比较f和g, f的ASCII码值大于g, 返回一个大于0的数。

    8.4 基于qsort函数实现改写bubble_sort函数

    8.4.1 排序整形数组

    #include 
    #include 
    int int_cmp(const void* e1, const void* e2)
    {
    	return (*(int*)e1 - *(int*)e2);//升序排序
    }
    void swap(char* buf1, char* buf2, int width)
    {
        for(int i = 0; i < width; i++)
        {
            char tmp = *buf1;
            *buf1 = *buf2;
            *buf2 = tmp;
            buf1++;
            buf2++;       
        }
    }
    void bubble_sort(void* base, int sz, int width, int(*cmp)(const void* e1,const void* e2))
    {
        int i = 0;
        for(i = 0; i < sz - 1; i++)
        {
            int flag = 1;//假设数组是排好序的
            int j = 0;
            for(j = 0; j < sz - 1 - i;j++)
            {
                if(cmp((char*)base + j*width, (char*)base + (j+1)*width)>0)
                {
                    swap((char*)base + j*width, (char*)base + (j+1)*width, width);
                    flag = 0;
                }
            }
            if(flag == 1)
            {
                break;
            }
        }
    }
    int main()
    {
    	int arr[] = {1,3,5,2,4,6,7,9,8,0};
    	int sz = sizeof(arr) / sizeof(arr[0]);
    	bubble_sort(arr, sz, sizeof(arr[0]), int_cmp);
    	for (int i = 0; i < sz; i++)
    	{
    		printf("%d ", arr[i]);//打印0 1 2 3 4 5 6 7 8 9
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    8.4.2 排序结构体

    #include 
    #include 
    #include 
    struct Stu
    {
        char name[20];
        int age;
    };
    int cmp_stu_by_name(const void* e1, const void* e2)
    {
        return strcmp((struct Stu*)e1->name,(struct Stu*)e2->name);
    }
    void swap(char* buf1, char* buf2, int width)
    {
        for(int i = 0; i < width; i++)
        {
            char tmp = *buf1;
            *buf1 = *buf2;
            *buf2 = tmp;
            buf1++;
            buf2++;       
        }
    }
    void bubble_sort(void* base, int sz, int width, int(*cmp)(const void* e1,const void* e2))
    {
        int i = 0;
        for(i = 0; i < sz - 1; i++)
        {
            int flag = 1;//假设数组是排好序的
            int j = 0;
            for(j = 0; j < sz - 1 - i;j++)
            {
                if(cmp((char*)base + j*width, (char*)base + (j+1)*width)>0)
                {
                    swap((char*)base + j*width, (char*)base + (j+1)*width, width);
                    flag = 0;
                }
            }
            if(flag == 1)
            {
                break;
            }
        }
    }
    int main()
    {
    	struct Stu s[3] = {{"zhangsan",18},{"lisi",25},{"wangwu",22}};
        int sz = sizeof(s)/sizeof(s[0]);
        bubble_sort(s,sz,sizeof(s[0]),cmp_stu_by_name);
        for(int i = 0; i < sz; i++)
        {
            printf("%s ",s[i].name);//输出lisi wangwu zhangsan
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
  • 相关阅读:
    统信UOS Linux操作系统下怎么删除某个程序在开始菜单或桌面的快捷方式
    网安学习-应急响应1
    wordpress获取当前主题文件夹所在的路径
    计算机毕业设计SSM_社区无接触快递栈【附源码数据库】
    Vue3响应系统的实现(二)
    DRF-接口文档-(三方drf-yasg)-(简单使用): 使用自定义的认证类,使用jwt做token时,想要使用自动生成文档
    flutter 本地存储数据(shared_preferences)
    港陆证券:电子竞技传来重磅消息!概念股上半年业绩普增
    力扣376. 摆动序列错误用例
    遥感图像地物覆盖分类,数据集制作-分类模型对比-分类保姆级教程
  • 原文地址:https://blog.csdn.net/m0_64224788/article/details/126092663