• 猿创征文丨赶紧进来!!! 详细介绍C语言指针(九千字完结篇)


    本文是指针的完结篇,介绍了函数指针和练习,函数指针数组和计算器的简易实现,认识函数指针数组指针,认识回调函数和qsort排序库函数以及回调函数的使用(模拟实现qsort库函数)

    指针初阶篇->指针进阶篇->指针完结篇

    请添加图片描述

    一.函数指针

    在c语言中函数又称子程序,是为了分模块实现各种不同功能而设计的
    在这篇博客中详细介绍到函数->c语言函数
    而声明定义的函数在调用时实际是在内存中申请的至少能容纳函数里所有变量大小的一块内存空间也称函数栈帧是内存空间就存在编号即指针–而调用该函数,为了提高计算机效率,实际上也是使用了该函数在内存里的编号即函数指针,快速在内存中找到这块函数进行调用

    1.认识函数指针

    既然函数有地址我们就可以通过%p将地址显示出来↓

    #include 
    void test()
    {
    printf("hehe\n");
    }
    int main()
    {
    printf("%p\n", test);
    printf("%p\n", &test);   //两个输出结果是什么
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这里插入图片描述

    我们可以看到 函数名和&数函数名的地址是一样的!!!
    它们两个除了样子不同表达的意义是完全等价的,但是数组名和&数组名地址相同但是类型是不同的

    输出的是两个地址,这两个地址是 test 函数的地址。
    那我们的函数的地址要想保存起来,怎么保存?

    函数指针要放在函数指针变量里
    而函数指针变量表示形式是 函数返回类型 +变量名(函数参数类型)

    
    void test()
    {
    printf("hehe\n");
    }
    //下面pfun1和pfun2哪个有能力存放test函数的地址?
    void (*pfun1)(); //1
    void *pfun2();//2
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    test函数 返回类型是void 形参列标为空
    代码1:表示的是一个指针变量pfun1 而这个指针变量而指针类型是一个返回类型为void 参数为空的函数指针,所以1是函数指针变量
    代码2: pfun2 表示是一个 函数名 而void*表示是函数的返回类型
    ()表示函数的参数 所以2表示的是函数pfun2的声明语句…

    2.函数指针练习

    阅读两段有趣的代码:

    //代码1
    (*(void (*)())0)();  //代码1 和代码2分别表示什么..?
    //代码2
    void (*signal(int , void(*)(int)))(int);
    
    • 1
    • 2
    • 3
    • 4

    一下子看到这么多括号肯定会懵.不妨把它们拆开看看

    代码1: (* ( void ( * ) () ) 0 ) ();
    代码1拆开后可以看到void(*)()是一个返回类型为void 参数为空的函数指针类型 然后被()包住表示它是一个强制类型转换符 ,将数字0强制类型转换为了函数指针类型,再看旁边有一个
    *表示将这个0地址作为的函数指针解引用 ,得到的也就是这个函数本身
    然后最右边的()表示函数调用操作符
    代码1表达的意思就是 将数字0强制类型转换为函数指针 并解引用得到这个函数然后调用这个函数…

    代码2:void ( * signal ( int , void() (int) ) ) (int)
    我们看到( int , void(
    ) (int) ) ) 表达的就是一个整形类型 和一个返回类型为void 函数参数为int类型的函数指针.而旁边signal即是一个函数名
    看到函数名旁边的不知道什么意思是不妨把signal ( int , void() (int) ) 这个看成一个整体a 此时得到void(a)(int)
    此时就明白了a以外的这部分表达的就是函数指针类型
    或者将 void(
    )(int) 类型重定义 ↓
    typedef void(* a )(int); 重定义的名字a只能写在括号的* 右边
    a signal(int, pfun_t); 此时a 表示是一个函数指针类型
    void ( * signal ( int , void(*) (int) ) ) (int)而这个最后表达的就是
    返回类型为一个函数指针,函数参数为一个int类型和一个函数指针类型函数名是signal的一个函数声明语句…

    函数指针类型有点绕,但是根据规则仔细看是有迹可循的,
    平时写代码中可以不写这种类型,但是得要学会分辨这种类型~

    二.函数指针数组

    上篇博客已经讲到了指针数组,表示数组每个元素都是一个指针如整形指针数组
    int arr[10];
    //数组的每个元素是int

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

    1.函数指针数组的定义

    int (*parr1[10])();  //  1这三个哪个是函数指针数组呢?
    int *parr2[10]();   //2
    int (*)() parr3[10]; // 3
    
    • 1
    • 2
    • 3

    1: 先看(parr1[10])表示是一个指针数组 而旁边分离出来的int()()表示是这个指针是一个返回类型为int 参数类型为空的函数指针类型 此时1表示的就是函数指针数组
    2:代码2属于错误写法,表示函数指针数组 需要用括号把指针和数组括起来 ,否则 []旁边的()会和数组先组合,是一种错误写法
    3:代码3也是一种错误写法,表达函数指针数组类型时,(
    ) 里 *旁边要放数组,()这个形参列表要放最右边,这样写法视觉上是像函数指针数组 但是语法上是错的,代码1 才是最标准的…

    2.函数指针数组的使用

    函数指针数组的用途:转移表
    当我们要调用多个函数实现不同功能式,代码会显得很冗余
    而使用函数指针数组可以使代码更简洁不冗余

    ①计算器简单实现(使用Switch单独调用函数)

    #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;
    	}
    	int main()
    	{
    		int x, y;
    		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:
    				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("输入两个操作数(用空格隔开):");
    				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

    上面代码是计算器的简单实现
    菜单函数里的1 add表示加法,2 sub表示减法,3 mul表示乘法,4 div表示除法.0表示退出程序
    x,y表示两个操作数
    根据菜单函数选择加减乘除 然后 输入两个操作数(整数)得到两个操作数运算后的结果…

    看上面的代码一个简单的加减乘除计数器实现用到了Switch case分支,这是正常的,
    但是每个分支里都重复着这两句:
    printf(“输入两个操作数(用空格隔开):”); scanf(“%d %d”, &x, &y);
    一看上去代码就特别繁琐,后面要新增计算器功能时,又少不了要加分支要加这两句
    而如何解决这个现象呢? 不妨试试函数指针数组

    ②计算器简单实现(使用if else 函数指针数组调用函数)

    #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;
    	}
    	int main()
    	{
    		int x, y;
    		int input = 1;
    		int ret = 0;
    		int (*counter[5]) (int, int) = { 0,add,sub,mul,div };// 五个元素为了调用四个函数 对应 数组下标 1 2 3 4
    
    		
    		do
    		{
    			printf("*************************\n");
    			printf(" 1:add 2:sub \n");
    			printf(" 3:mul 4:div 0:退出程序\n");
    			printf("*************************\n");
    			printf("请选择:");
    			scanf("%d", &input);
    			printf("请输入两个操作数:");
    			scanf("%d%d", &x, &y);
    			if (input >= 1 && input <= 4)  //当input在  1-4之间 进入分支
    			{
    				int ret=counter[input](x, y); // 调用函数指针数组 下标为input的元素 即对应菜单上的函数指针 然后把(x,y)两个操作数传参调用函数指针指向的函数 最后返回结果
    				printf("ret=%d\n", ret);
    			}
    			else if (input == 0)
    			{
    				printf("退出程序\n");
    			}
    			else
    			{
    				printf("非法输入\n");
    			}
    		} 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

    上面代码是采用函数指针数组,将add sub mul div函数名即函数指针 放在作为数组元素
    分别对应的input的值
    此时只要满足条件可以直接数组里对应的函数指针得到运算结果
    使用函数指针数组 构建转移表的方法 使得这代码比上面代码减少重复的代码,更加简洁
    这也是函数指针数组的一个用途之一~

    三.函数指针数组指针

    函数指针数组指针实际上是 指向函数指针数组的指针 是一个 指针
    表示指针指向一个 数组 ,数组的元素都是 函数指针
    这又如何定义呢?

    void test(const char* str)
    {
    printf("%s\n", str);
    }
    int main()
    {
    //函数指针pfun
    void (*pfun)(const char*) = test; //函数指针变量存放test函数指针
    //函数指针的数组pfunArr
    void (*pfunArr[5])(const char* str);  //函数指针数组
    pfunArr[0] = test;  //数组一个元素放test函数指针
    
    void (*(*ppfunArr)[5])(const char*) = &pfunArr;//指向函数指针数组pfunArr的指针ppfunArr 
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    上面代码ppfunArr就是函数指针数组指针

    刚接触会觉得很懵,顺着逻辑慢慢想,能够认识多一点类型,更能扩展知识面
    指针是c语言灵魂,而嵌套套娃是精髓~
    还是那句,可以不用这些奇奇怪怪的类型,但是可能别人会用,见到后你得认识…

    四.回调函数

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

    1.库函数->qsort排序函数介绍

    qsort是一个用来对一段物理地址连续的内存单元里的有效数据进行有序排序
    例如数组 int arr[5]={1,2,5,4,3};我们可以自己写一个排序函数也可以用qsort函数使得这个数组变成有序{1,2,5,4,3}->{1,2,3,4,5}
    当然qsort不仅仅作用一个整形类型任何类型的数组都可以使用qsort进行排序(只需要更改排序条件)

    在这里插入图片描述

    上面是官网对qsort库函数的描述:使用这个函数要传四个参数
    第一个:数组第一个元素的地址
    第二个:数组要排序的所有元素个数
    第三个:其中一个元素的大小
    第四个:一个自己设置的比较函数的函数指针
    注意:使用qsort要包含头文件#include

    2.qsort排序库函数的使用

    #include
    #include
    #include
    struct stu
    {
    	char name[20];
    	int age;
    };
    //
    //int arr_cmpage(const void* e1, const void* e2)
    //{
    //	return (((struct stu*)e1)->age - (((struct stu*)e2)->age));  //按年龄大小排序结构体成员   用整形减整形
    //}
    int arr_cmpname(const void* e1, const void* e2)
    {
    	return strcmp(((struct stu*)e1)->name, ((struct stu*)e2)->name); //按name 字符串大小排序结构体   用字符串比较函数
    }
    int main()
    {
    	/*int arr[] = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };*/
    	struct stu arr[3] = { {"李五",19},{"李四",18},{"李三",20} };
    	qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(arr[0]),arr_cmpname);   //第一个参数为数组首元素地址,第二个参数为数组元素个数,第三个为每个元素的大小,第四个为形参为两个void*指针作用比较两个元素大小返回整形的函数指针
    	int i = 0;
    	for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
    	{
    		printf("%s ", arr[i].name);
    		printf("%d ", arr[i].age);
    	}
    
    	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

    为了突出qsort可以对多种数据类型都能排序,上面代码实现对一个结构体数组进行排序(可以根据name成员变量进行排序或者age成员变量进行排序)
    初始化有3个成员:{ {“李五”,19},{“李四”,18},{“李三”,20} }
    可以看到名字和姓名都是无序排列的
    用qsort排序函数排序:第一个参数arr:为数组名 表示首元素地址
    第二个参数为:要排序的元素个数即sizeof(arr)表示整个数组大小即数组所有元素大小之和/sizeof(arr[0])除以第一个元素大小之和 得到->数组所有元素个数.
    第三个参数为:sizeof(arr[0])表示数组要排序的其中一个元素的大小
    第四个参数为arr_cmpname(用来比较数组里要排序的每两个元素的大小(通过这个方式让qsort知道每个元素比较的大小以及是升序要使降序)):是自己定义的函数 的函数名即函数指针 上面代码设计的是根据name进行排序结构体数组的元素,(上面代码也有被注释的根据age即整形类型作为排序依据的比较函数)

    函数参数是两个变量void * 类型的变量(因为排序的元素类型是不确定的,用空类型可以接收这些变量) 最后在函数实现里将其强制类型转换为struct stu* (对应的结构体stu指针类型)
    然后指向name变量 通过strcmp(字符串比较库函数)得出两个元素的之间的大小(第一个元素大于第二个元素返回大于0的数,反之返回小于0的数,相等则返回0)(默认是升序排序 如果 加上负号则相反变成降序排序)

    在这里插入图片描述

    最后实现了 按名字升序排序这个结构体数组里的元素

    3.qsort排序库函数的模拟实现

    qsort是事先被人封装写好的排序库函数,我们知道了具体实现原理,就照猫画虎可以自己模拟实现一个自己的qsort函数…

    首先qsort 底层排序的算法使用的是快速排序.本次模拟实现用的是冒泡排序算法

    #include
    void swap(void* e1, void* e2,size_t sz)        //不确定传过来的是什么类型 用void*接受  最后一个参数为传过来的元素的类型长度 即占多少个字节空间
    {
    	int i = 0;
    	for (i = 0; i < sz; i++)           //  交换采取一个字节一个字节内存空间元素互换 直到互换完sz个内存空间的元素 则为将这两个个元素互换
    	{
    		char tmp = *( (char*)e1+i );              // 转化为char*类型 +i解引用 一个字节一个字节互换,直到互换完所有字节空间的元素
    		*((char*)e1+i) = *((char*)e2+i);
    		*((char*)e2+i)=tmp;
    
    	}
    	
    }
    int arr_cmpint(const void* e1, const void* e2)
    {
    	return *(int*)e1 - *(int*)e2;  //取正是升序排序 取负是降序排序
    }
    void my_qsort(void* buf, size_t num, size_t sz, int (*cmp)(const void* e1, const void* e2))     //因为不确定类型,此时用void*接受  num为元素个数用 size_t无符号整形接受  sz接受的每个元素大小, *cmp为自己设置的返回类型为整形,函数参数为两个 void*类型的指针(用于后面自己强制类型转化为相应的类型)
    {
    	int i, j;
    	for (i = 0; i < num - 1; i++)                               //排序方法设计为冒泡排序 趟数为元素个数减1次
    	{
    		for (j = 0; j < num - 1 - i; j++)                     //第一趟排总元素减1次  后面每一趟减一次 
    		{
    			if (cmp( ((char*)buf) + j * sz, ((char*)buf) + (j + 1) * sz )>0)   // 用接受的自己设的函数指针 cmp 传参调用(回调函数) 最后返回的整形为大于0则说明前一个元素大于后一个元素要进行互换 这里j*sz   (j+1)*sz为比较完当前元素往后按字节形式跨越对应j*sz个字节一个整个元素
    			{
    				swap( ( (char*)buf ) + j * sz, ( (char*)buf ) + (j + 1) * sz ,sz);  //封装一个swap函数 使得 这两个元素互换位置,对应上面的参数. 加上了第三个参数为元素的类型大小 用于互换
    			}
    		}
    	}
    }
    
    int main()
    {
    	int arr[] = { 9,8,7,6,5,4,3,2,1 };
    	my_qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(arr[0]), arr_cmpint); //第一个为要排序的数组名首元素地址,第二个为数组总元素大小  .第三个为每个元素的大小,第四个为设置的比较数组里两个数的大小的函数
    	int i = 0;
    	for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)                   
    	{
    		printf("%d ", arr[i]);
    	}
    	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

    上面是qsort的模拟实现代码my_qsort函数,对一个整形数组进行升序排序
    函数第一个形参 是空类型指针变量接受因为不确定排序的数组首元素地址是什么类型
    第二个和第三个形参 是size_t表示无符号整形类型接受总元素个数和一个元素大小
    第四个形参为int (cmp)(const void e1, const void* e2 )是一个函数指针变量接受着对应自己自定一个比较函数arr_intcmp 即函数指针(对两个整形元素进行比较直接返回两数之差(如果e1>e2就是返回大于0,小于就是返回小于0,相等就返回0))

    函数实现是写一个冒泡排序算法在比较相邻两个元素大小是就用传过来的函数指针然后将这两个元素传参调用,即是回调函数的使用,然后当得到大小是多少后,
    再封装一个swap交换函数交换数组两个元素位置…
    swap交换的时候 首先是前两个参数是要void接受实参,因为不确定传过来的元素是什么类型,第三个参数则是接收的要交换的元素的大小
    然后将其强制类型转换为char
    然后通过一个字节一个字节的交换,直到交换完这整个元素的字节数后即为实现了这两个元素的交换

    qsort实现的难点就是要排序的数据类型是未知的,但这也是这个排序函数的特点:可以对任何类型的数据进行有序排序…

    请添加图片描述

    五.总结

    本文是指针的完结篇,介绍了函数指针和练习,函数指针数组和计算器的简易实现,认识函数指针数组指针,认识回调函数和qsort排序库函数以及回调函数的使用(模拟实现qsort库函数)
    到这里,指针的使用由初阶->进阶->完结就结束了,学会了指针也就大概了解了内存…
    在下篇会更新一些经典的指针笔试题,为指针学习画上完美的句号吧~

    请添加图片描述

    写文不易,给个一键三连支持下叭~~~

  • 相关阅读:
    云安全—Dashboard 攻击面
    在没有硬盘的情况下进行电脑数据迁移
    MongoDB CRUD操作:快照查询
    企业级自定义表单引擎解决方案(十二)--体验代码目录结构
    2022网络搭建国赛题交换机安全配置
    中间件漏洞 | Apache-路径穿越升级版
    DPDK中Ring的优先级反转问题
    小红书推广高客单产品的技巧与方法
    006:vue使用lottie-web实现web动画
    CentOS 7离线安装MySQL 5.6
  • 原文地址:https://blog.csdn.net/lch1552493370/article/details/126794302