四、数组传参和指针传参
❓❔ 写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?
1、一维数组传参
#include <stdio.h> void test(int arr[])//ok {} void test(int arr[10])//ok {} void test(int* arr)//ok {} void test2(int *arr[20])//ok,arr2是指针数组,这里使用指针数组来接收可以匹配 {} void test2(int **arr)//ok,arr2传的是首元素地址 ———— int*,这里使用二级指针来接收一级指针没有问题 {} int main() { int arr[10] = {0}; int* arr2[20] = {0}; test(arr); test2(arr2); return 0; }
2、二维数组的传参
#include <stdio.h> void test(int arr[3][5])//ok {} void test(int arr[][])//err,二维数组传参,参数可以写成数组,但列不能省略 {} void test(int arr[][5])//ok {} void test(int *arr)//err,二维数组传参,传过来的是数组首地址 ———— 一维数组,不能匹配 {} void test(int* arr[5])//err,这里是存放指针的数组不能匹配 {} void test(int (*arr)[5])//ok,这里使用数组指针,它刚好指向二维数组的首地址 {} void test(int **arr)//err,二级指针不能匹配一维数组的地址 {} int main() { int arr[3][5] = {0}; test(arr); }
3、一级指针传参
#include<stdio.h> void print(int* ptr, int sz)//一级指针变量传参用一级指针接收 { int i = 0; for(i = 0; i < sz; i++) { printf("%d ", *(ptr + i)); } } int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; int* p = arr; int sz = sizeof(arr)/sizeof(arr[0]); print(p, sz); return 0; }
❓❔ 思考:函数参数部分为一级指针的时候,函数能接收什么参数?
当参数部分为一级指针的时候,函数能接收变量的地址或者一级指针变量
#include<stdio.h> void test(char* p) {} int main() { char ch = 'w'; char* p1 = &ch; test(&ch);//ok test(p1);//ok return 0; }
4、二级指针传参
#include<stdio.h> void test(int** p2)//二级指针变量传参使用二级指针来接收 { **p2 = 20; } int main() { int a = 10; int* pa = &a; int** ppa = &pa; //把二级指针ppa传参 test(ppa); printf("%d\n", a); return 0; }
❓❔ 思考:当函数的参数为二级指针的时候,可以接收什么参数?
当参数部分为二级指针的时候,函数可以接收一级指针变量的地址或者二级指针变量或者指针数组的数组名
#include<stdio.h> void test(int** p2) { } int main() { int a = 10; int* pa = &a; int** ppa = &pa; int* arr[10] = {0}; test(&pa); test(ppa); test(arr); return 0; }
五、函数指针
1、什么是函数指针
💨 函数指针顾名思义是指向函数的指针
#include<stdio.h> int Add(int x, int y) { return x + y; } int main() { //对于变量或数组的地址使用 &,当然对于一个函数的地址也可以使用 & printf("%p\n", &Add); printf("%p\n", Add); //这里有同学就有疑问了,对于函数名同数组名是一样的概念吗? //&数组名 != 数组名 //&函数名 == 函数名 //----------------------------------------------------- //如何存储函数的地址 //这里pf就是一个函数指针变量,它指向2个int类型参数,返回类型是int的函数 int (*pf)(int, int) = &Add; return 0; }
2、小例1:
💨 现有一个函数,void test (char* str),将test函数存储
void (*pt) (char) = &test;
3、函数指针的使用
❓❔ 指针指向函数,但是我们如何通过指针调用函数
#include<stdio.h> int Add(int x, int y) { return x + y; } int main() { int (*pf)(int, int) = &Add; //把指向函数的指针解引用找到函数,传参即可 int ret = (*pf)(3, 5); //int ret = *pf(3, 5);//err,这里相当于是把3和5传参后的返回值再解引用 printf("%d\n", ret); return 0; }
❓❔ 思考 :指向函数的指针必须需要解引用吗
#include<stdio.h> int Add(int x, int y) { return x + y; } int main() { //int (*pf)(int, int) = &Add; //&Add == Add,所以: int (*pf)(int, int) = Add;//这里就说明了pf == Add //int ret = (*pf)(3, 5); //int ret = Add(3, 5);//函数名调用函数 int ret = pf(3, 5);//说明那颗星是个摆设,无意义(写几颗星都没问题)。实际上只上为了方便理解 printf("%d\n", ret); return 0; }
4、函数指针实例1(出自《C陷阱和缺陷》):
🍳 有一次,一个程序员与我交谈一个问题。他当时正在编写一个独立运行于某种微处理器上的C程序,当计算机启动时,硬件将调用首地址为0位置的子例程为了
为了模拟开机启动时的情形,我们必须设计一个C语句,以显示调用该子例程。经过一段时间的思考,我们最后得到的语句如下:
分析:
1、void () () -> 函数指针类型
2、(void () () ) 0 -> 对0进行强制类型转换,被解释为一个函数地址3、* (void () () ) 0 -> 对地址进行解引用操作
4、 (void (*) () ) 0 () -> 调用0地址处的函数
总结:
这是一次函数调用
5、函数指针实例2(出自《C陷阱和缺陷》):
分析:
1、signal和()先结合,说明signal是函数名
2、signal函数的第1个参数的类型是int,第2个参数的类型是函数指针
3、signal的返回类型也是一个函数指针
其实这样更容易理解,但是语法不是这样规定的
总结:
signal是一个函数的声明
6、使用typedef对实例2中的代码进行简化
💨 typedef可以对类型进行重命名
#include<stdio.h> struct Student { int age; char name[20]; char sex[10]; }; int main() { //struct Student s = { 0 }; //=================================================== typedef struct Student s_Stu; s_Stu s = { 0 };//同上注释 return 0; }
💨 所以这里就使用typedef将实例2中的代码简化
int main() { void (*signal (int , void(*) (int) ) ) (int); //typedef void(*)(int) pfun_t;//err,这里又有点特殊,pfun_t不能写在后面 typedef void(*pfun_t)(int);//而应该写在里面。这里就将void(*)(int)类型重命名为pfun_t //所以,简化后: pfun_t signal(int, pfun_t); }
六、函数指针数组
1、什么是函数指针数组
💨 整型指针 -> int*
💨 整型指针数组 -> 存放整型指针的数组 -> int* arr[5]
💨 函数指针数组 -> 存放函数指针的数组 -> ???
int Add(int x, int y) { return x + y; } int Sub(int x, int y) { return x - y; } int main() { //将函数Add和Sub存储于指针pf1和pf2。发现pf1和pf2的类型是一样的 int (*pf1)(int, int) = Add; int (*pf2)(int, int) = ⋐ //数组是存放相同类型元素的集合,那当然能定义一个数组来存储Add和Sub //这里pfArr就是一个函数指针数组,而对于数组而言去掉数组名和[]剩下的就是类型int (*)(int, int) -> 每个元素就是函数指针类型 int (*pfArr[2])(int, int) = {Add, Sub}; return 0; }
2、实例1(转移表):实现计算器
1、错误示范:
#define _CRT_SECURE_NO_WARNINGS #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; do { menu(); int x = 0; int y = 0; int ret = 0; printf("请选择:>"); scanf("%d", &input); printf("请输入2个操作数:>\n"); scanf("%d %d", &x, &y); switch(input) { case 1: ret = Add(x, y); break; case 2: ret = Sub(x, y); break; case 3: ret = Mul(x, y); break; case 4: ret = Div(x, y); break; case 0: printf("退出程序\n"); break; default: printf("输入错误,请重新输入!\n"); break; } printf("ret = %d\n", ret); }while(input); return 0; }
💢 这里是有问题的:其1当这里输入的数字不符合要求或者为0时,它必不可少的都要先执行"printf(“请输入2个操作数:>\n”); scanf("%d %d", &x, &y);"这两条语句。其2是当这里输入的数字没有任何返回结果时,它也会输出ret = 0。所以这段代码是有bug的
2、纠正1
💨 在知识储备不够完善的情况下大多数人都会这样去改
//... ...以上省略 int main() { int input = 0; do { menu(); int x = 0; int y = 0; int ret = 0; printf("请选择:>"); scanf("%d", &input); switch(input) { case 1: printf("请输入2个操作数:>\n"); scanf("%d %d", &x, &y); ret = Add(x, y); printf("ret = %d\n", ret); break; case 2: printf("请输入2个操作数:>\n"); scanf("%d %d", &x, &y); ret = Sub(x, y); printf("ret = %d\n", ret); break; case 3: printf("请输入2个操作数:>\n"); scanf("%d %d", &x, &y); ret = Mul(x, y); printf("ret = %d\n", ret); break; case 4: printf("请输入2个操作数:>\n"); 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; }
💨 结果:这2处bug都解决了
💨 缺陷:
▶1、代码冗余(大量重复的代码)
▶2、可维护性低(后期需要增加其它功能时,不方便)
▶3、可读性差(对于程序员来说,在读代码的过程中,要不断的向上翻和向下翻)
3、纠正3(转移表)
💨 转移表是通过数组的下标访问具体的函数
▶ 使用函数指针数组
#define _CRT_SECURE_NO_WARNINGS #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; do { menu(); //pfArr就是函数指针数组,通过下标可以访问函数。但前提是函数的参数,返回类型都是统一的才行 //这里就好像一个跳板,所以经常把这样一个数组叫转移表,在《C和指针》中有提到 int(*pfArr[5])(int, int) = {NULL, Add, Sub, Mul, Div}; int x = 0; int y = 0; int ret = 0; printf("请选择:>"); scanf("%d", &input); if(input >= 1 && input <= 4) { printf("请输入2个操作数:>\n"); scanf("%d %d", &x, &y); ret = (pfArr[input])(x, y); printf("ret = %d\n", ret); } else if(input == 0) { printf("退出程序\n"); break; } else { printf("选择错误\n"); } }while(input); return 0; }
七、指向函数指针数组的指针
1、什么是指向函数指针数组的指针
💨 函数指针数组本质上是数组,取出函数指针数组的地址给指针就是指向函数指针数组的指针
对于整型数组,要存储arr1数组
int arr1 [5];
int (p1) [5] = &arr;
------------------------------------分割线------------------------------------
对于整型指针数组,要存储arr2数组
int arr2 [5];int* (*p2) [5] = &arr2;
------------------------------------分割线------------------------------------
对于函数指针数组,要存储arr3数组
这里p3就是指向函数指针数组的指针,这个数组有5个元素,每个元素是函数指针类型
int (*arr3 [5]) (int, int)
int ( * (*p3) [5]) (int, int) = &arr3;
2、小例1:
#include<stdio.h> void test(const char* str) { ; } int main() { //函数指针pfun void (*pfun)(const char*) = test; //函数指针的数组pfunArr void (*pfunArr[5])(const char* str); //指向函数指针数组pfunArr的指针ppfunArr void (*(*ppfunArr)[5])(const char*) = &pfunArr; return 0; }
九、回调函数
1、什么是回调函数
🍳 回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应
简单来说:就是有一个A函数,这里不是直接去调用A函数,而是把A函数的地址传给B函数(这里的B函数的参数就是一个函数指针),这时通过B函数去调用A函数时就称为回调函数
2、实例:
/***********************************************************************
目的:继续使用switch语句来实现并简化【实例1(实现计算器) - 纠正2】中的代码
想法:这里将会把红色标识区域封装成一个函数,调用即可。但这里有个问题是printf和scanf两条语句是相同的,但是每次调用这个函数时传的参数可不一样
分析:这里将封装一个函数Calc来帮我们分别计算加减乘除,而这个函数的参数将是一个函数指针pf,来接收不同模块的代码。然后再通过pf所指向的这个函数的地址去间接调用那个函数。这里就用到了回调函数
平台:Visual studio 2017 && windows
*************************************************************************/
#define _CRT_SECURE_NO_WARNINGS #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 Calc(int (*pf)(int, int))//这里使用函数指针来做为Calc的参数 { int x = 0; int y = 0; printf("请输入2个操作数\n"); scanf("%d %d", &x, &y); return pf(x, y);//这里通过pf指针来调用所对应传过来的函数 } int main() { int input = 0; do { menu(); int ret = 0; printf("请选择:>"); scanf("%d", &input); switch(input) { case 1: ret = Calc(Add);//这里应把Add函数的地址传给Calc,且在Calc中通过指针调用Add printf("ret = %d\n", ret); break; case 2: ret = Calc(Sub); printf("ret = %d\n", ret); break; case 3: ret = Calc(Mul); printf("ret = %d\n", ret); break; case 4: ret = Calc(Div); printf("ret = %d\n", ret); break; case 0: printf("退出程序\n"); break; default: printf("输入错误,请重新输入!\n"); break; } }while(input); return 0; }
3、qsort函数
1、bubble_sort
💨 之前有学习过冒泡排序,再来回顾一下
#include<stdio.h> void bubble_sort(int arr[], int sz) { int i = 0; int j = 0; for(i = 0; i < sz - 1; i++) { for( j = 0; j < sz - 1 - i; j++) { if(arr[j] > arr[j + 1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } } void print_arr(int arr[], int sz) { int i = 0; for(i = 0; i < sz; i++) { printf("%d ", arr[i]); } printf("\n"); } int main() { int arr[10] = {9,8,7,6,5,4,3,2,1,0}; int sz = sizeof(arr)/sizeof(arr[0]); //升序 bubble_sort(arr, sz); print_arr(arr, sz); return 0; }
2、解读qsort函数
💨 bulbble_sort是我们自己实现的函数,缺点是只能排序一种类型,如果还想排序其它类型的数组,就需要自己设计。
qsort ():快速排序,可以排序整型、字符型、结构体等等。接下来我们就来了解一下qsort函数:
qsort函数头:<stdlib.h>
qsort函数原型:
函数参数(我们来看一下文档):
全英文的文档可能对大家还是有点不友好,这里把大概意思翻译一下
void* base:base中存放的是待排序数据中第一个对象的地址
size_t num:num是排序数组元素的个数
size_t size:size是排序数据中一个元素的大小,单位是字节
int (* cmp) (const void**, const void*) :cmp是用来比较待排序数据中的2个元素的函数。
返回的是>0的数字:第1个元素 > 第2个元素
返回的是=0的数字:第1个元素 = 第2个元素
返回的是<0的数字:第1个元素 < 第2个元素
注:我们不防可以推理一下,当开发人员在设计这个函数时可能会想到的场景
1、这个函数是实现不同类型的数组排序,所以base必需有能力指向不同的数据类型,所以base这个指针的类型就是void*类型
2、同样的这个函数必需知道你待排序的数组里有几个元素,所以这里又设计了num来接收
3、如果void* base指向数组时,它并不知道一个元素有多大(因为这是void*类型),所以这里还需要一个参数size来接收一个数组元素有多大
4、因为不同的类型比较,比较的方法也有所差异(比如int类型使用>、<比较;而char类型使用strcmp比较),所以这里就使用函数指针来接收,且所指向的参数部分也是void*类型, 所以开发人员就把两个元素比较的这个函数交给使用者来确定
💨 使用qsort函数来排序整型数组
#include<stdlib.h> #include<stdio.h> int cmp_int(const void* e1, const void* e2) { //return *(int*)e1 - *(int*)e2;//升序 return *(int*)e2 - *(int*)e1;//降序 } void print_arr(int arr[], int sz) { int i = 0; for(i = 0; i < sz; i++) { printf("%d ", arr[i]); } printf("\n"); } int main() { int arr[10] = {8,9,0,6,5,1,3,2,4,7}; int sz = sizeof(arr)/sizeof(arr[0]); qsort(arr, sz, sizeof(arr[0]), cmp_int); print_arr(arr, sz); return 0; }
💨 使用qsort函数来排序结构体
```c #include<stdlib.h> #include<string.h> #include<stdio.h> struct Stu { char name[20]; int age; }; int sort_by_age(const void* e1, const void* e2) { //return ((struct Stu*)e1) -> age - ((struct Stu*)e2) -> age;//升序 return ((struct Stu*)e2) -> age - ((struct Stu*)e1) -> age;//降序 } int sort_by_name(const void* e1, const void* e2) { //return strcmp(((struct Stu*)e1) -> name, ((struct Stu*)e2) -> name);//升序 return strcmp(((struct Stu*)e2) -> name, ((struct Stu*)e1) -> name); } int main() { struct Stu s[] = {{"zhangshan", 30}, {"lisi", 34}, {"wangwu", 20}}; //按照年龄来升序 qsort(s, sizeof(s)/sizeof(s[0]), sizeof(s[0]), sort_by_age); //按照名字来排序 qsort(s, sizeof(s)/sizeof(s[0]), sizeof(s[0]), sort_by_name); return 0; }
3、模仿qsort函数实现冒泡排序的通用算法
#include<stdio.h> void Swap(char* buf1, char* buf2, int width) { int i = 0; for(i = 0; i < width; i++) { char temp = *buf1; *buf1 = *buf2; *buf2 = temp; 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 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); } } } } void print(int arr[], int sz) { int i = 0; for(i = 0; i < sz; i++) { printf("%d ", arr[i]); } } int cmp_int(const void* e1, const void* e2) { //return *(int*)e1 - *(int*)e2;//升序 return *(int*)e2 - *(int*)e1;//降序 } int main() { int arr[] = {8,9,0,6,5,1,3,2,4,7}; int sz = sizeof(arr)/sizeof(arr[0]); bubble_sort(arr, sz, sizeof(arr[0]), cmp_int); print(arr, sz); return 0; }
分析:
bubble_sort这个函数要模仿qsort函数,这是待排序的数组:
1、调用bubble_sort函数:首先参数部分一定是按照qsort函数参数来模拟的
(数组首元素,数组元素个数,数组元素大小,自定义的比较函数)
2、定义cmp_int函数
这里e1和e2接收了2个要比较的参数,如果要把它们的结果返回去,就需要将void* 类型强转为int* 类型才可以
3、定义bubble_sort函数:
3.1、确定好循环次数
3.2、接着就是应用回调用函数机制(通过bubble_sort函数的参数 -> cmp函数指针来调用cmp_int函数并把两两元素传参)
确定要传参的元素:
base + j, base + j + 1 -> 逻辑上是可行的,但是base的类型是void*,语法不支持
所以:
这里不管是什么类型都将base转换为char*类型(因为它是类型大小的最小单位),再加上 j 乘上1个元素的大小就可以确定元素地址
确定函数的返回值:
如果cmp这个函数的返回值>0则交换,刚好对应qsort函数
3.3、再定义并调用一个交换的函数
把两元素的地址传给Swap函数,因为类型的缘故,我们并不知道交换多大空间,所以这里把wide也传参的原因是让这个数据一个字节一个字节的进行交换