1.函数指针数组
之前已经讲解过了整型指针数组,用于存放整型指针。那么这里的函数指针数组就是用于存放函数指针的。写法是怎样的呢?
void test() { printf("hehe\n"); } int main() { void(*pf[10])() = { test }; return 0; }
代码的第7行,pf先与[]结合,说明pf是个数组名,将 pf[10]去掉,剩下的部分就是数组的元素类型,即void(*)();。所以pf是个函数指针数组。
1.1函数指针数组的应用实例
当我们想要实现一个对于整数的四则运算的计算器时,我们或许可以这样写:
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 x = 0; int y = 0; int input = 0; 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("请输入操作数:\n"); scanf("%d %d", &x, &y); ret = Add(x, y); printf("%d\n", ret); break; case 2: printf("请输入操作数:\n"); scanf("%d %d", &x, &y); ret = Sub(x, y); printf("%d\n", ret); break; case 3: printf("请输入操作数:\n"); scanf("%d %d", &x, &y); ret = Mul(x, y); printf("%d\n", ret); break; case 4: printf("请输入操作数:\n"); scanf("%d %d", &x, &y); ret = Div(x, y); printf("%d\n", ret); break; case 0: printf("退出程序!\n"); break; default: printf("您的输入有误,请重新输入!\n"); } } while (input); return 0; }
但是这种写法会导致代码中重复的内容过多。那么能否改进改进呢?
我们观察这四个函数,可以发现他们的返回值和函数参数都是相同的。所以可以将这四个函数存放进一个函数指针数组中,这样在switch语句中调用对应的函数时,就可以使用下标进行快速访问。
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 x = 0; int y = 0; int input = 0; int ret = 0; int(*pf[5])(int, int) = { 0,Add,Sub,Mul,Div }; do { printf("*************************\n"); printf(" 1:add 2:sub \n"); printf(" 3:mul 4:div \n"); printf("*************************\n"); printf("请输入你想做的运算:"); scanf("%d", &input); if (input >= 1 && input <= 4) { printf("请输入操作数:\n"); scanf("%d %d", &x, &y); ret = pf[input](x, y); printf("运算结果为:%d\n", ret); } else if(input==0) { printf("退出程序!\n"); } else { printf("你的输入有误,请重新输入!\n"); } } while (input); return 0; }
代码的第23行,这里使用0进行占位,将1,2,3,4这四个下标所对应的位置各自对应相应的函数。
1.2指向函数指针数组的指针
指向函数指针数组的指针是一个指针,其指向一个函数指针的数组。
void test(const char* str) { printf("%s\n", str); } int main() { //函数指针pfun void (*pfun)(const char*) = test; //函数指针的数组pfunArr void (*pfunArr[5])(const char* str); pfunArr[0] = test; //指向函数指针数组pfunArr的指针ppfunArr void (*(*ppfunArr)[5])(const char*) = &pfunArr; return 0; }
这里注意:函数名代表的是函数的地址,&函数名代表的也是函数的地址。
2.回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
我们可以通过回调函数的机制,可以将上面的计算器的代码再进行改进。
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 Calc(int(*pf)(int, int)) { int x = 0; int y = 0; printf("请输入两个操作数:\n"); scanf("%d %d", &x, &y); int ret = pf(x, y); printf("%d\n", ret); } int main() { int x = 0; int y = 0; int input = 0; 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"); } } while (input); return 0; }
2.1 qsort函数
查看官方文档:
qsort函数可以对任意数组进行排序,包括结构体数组也可以。qsort函数有四个参数,第一个参数是目标数组的起始位置。第二个参数是数组中元素的个数。第三个参数是数组中每个元素的大小。第四个参数是一个函数,这个函数能够用于比较数组中的两个元素。
注意:这里qsort函数中的最后一个参数是一个函数名,这个函数的两个类型必须是void*类型的。为什么必须是void*类型的呢?
这是因为void*类型指针变量可以接收任意类型的指针变量。因为对于这个函数的设计者而言,他也不知道用户要比较的什么类型的元素,所以就设计了void*这个可以接收任意类型指针的“万能指针变量”,但是void*类型的指针变量不能进行解引用操作,也不能进行加减操作。例如:
//这段代码不会有任何的警告和报错。 int main() { int a = 0; char c = 0; float f= 1.0; void* pv; pv = &a; pv = &c; pv = &f; return 0; }
qsort函数的底层是快速排序算法,假定我们使用qsort函数对整数组中的元素进行排序:(qsort函数默认是升序排列的)
int compare_int(void* e1, void* e2) { return (*(int*)e1 - *(int*)e2); } void print(int* p,int sz) { int i = 0; for (i = 0; i < sz; i++) { printf("%d ", p[i]); } } int main() { int arr[10] = { 1,2,4,1,6,2,21,4745,64 }; int sz = sizeof(arr) / sizeof(arr[0]); print(arr, sz); printf("\n"); qsort(arr,sz,sizeof(arr[0]),compare_int); print(arr, sz); return 0; }
这里使用冒泡排序模拟实现qsort函数,对乱序的整型数组进行排序。
void swap(char* e1,char*e2,int sz) { char tmp = 0; while (sz--) { tmp = *e1; *e1 = *e2; *e2 = tmp; e1++; e2++; } } int compare_int(const void* e1, const void* e2) { return (*(int*)e1 - *(int*)e2); } void myBubble(void* base, int num, int sz, int (*pf)(void*, void*)) { int i = 0; for (i = 0; i < num - 1; i++) { int j = 0; for (j = 0; j < num - 1 - i; j++) { if (pf((char*)base +j*sz, (char*)base + sz*(j + 1)) > 0) { swap((char*)base + j * sz,(char*)base+(j+1)*sz,sz); } } } } void print(int* p, int sz) { int i = 0; for (i = 0; i < sz; i++) { printf("%d ", p[i]); } printf("\n"); } int main() { int arr[10] = { 1,0,4,23,55,34,756,754,7,999 }; int sz = sizeof(arr) / sizeof(arr[0]); print(arr, sz); myBubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof(arr[0]), compare_int); print(arr, sz); return 0; }
这里bubble函数和库函数qsort的参数类型设计的是一样的,这里要特别注意,qsort函数的最后一个参数是一个函数名,这个函数的参数的是void*类型的,也就是说这个函数是用于接收要比较的两个元素的地址的,并不是接收两个函数本身,所以要如何找到这两个要比较的元素的地址呢?可以将base这个参数强制转换为char*类型的,这样子base每执行一次+1操作都会向后移动一个字节,要比较的元素的大小是已知的,为sz。假如要找到bubble函数中找到arr[4]这个元素的地址,就可(char*)base+4*sizeof(int);,base指针向后移动16个字节,就成功找到了arr[4]的地址。
在swap函数中,参数是两个元素的地址,第三个参数是每个元素的大小,要交换这两个元素,只需要交换这两个元素的每一个字节就可以了。
假如要比较结构体数组:(假设以姓名为标准进行比较:)
struct Student { char name[20]; int age; }; void swap(char* e1, char* e2, int sz) { char tmp = 0; while (sz--) { tmp = *e1; *e1 = *e2; *e2 = tmp; e1++; e2++; } } int compare_name(const void* e1, const void* e2) { return strcmp(((struct Student*)e1)->name, ((struct Student*)e2)->name); } void myBubble(void* base, int num, int sz, int (*pf)(const void*,const void*)) { int i = 0; for (i = 0; i < num - 1; i++) { int j = 0; for (j = 0; j < num - 1 - i; j++) { if (pf((char*)base + j * sz, (char*)base + sz * (j + 1)) > 0) { swap((char*)base + j * sz, (char*)base + (j + 1) * sz, sz); } } } } int main() { struct Student arr[3] = { {"zhangshan",12},{"lisi",20},{"wangwu",16} }; int sz = sizeof(arr) / sizeof(arr[0]); myBubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof(arr[0]), compare_name); return 0; }
注意:这里字符串的比较是使用的strcmp函数进行比较的。不能直接进行字符串之间的加减操作。
假定以年龄为标准进行比较:
struct Student { char name[20]; int age; }; void swap(char* e1, char* e2, int sz) { char tmp = 0; while (sz--) { tmp = *e1; *e1 = *e2; *e2 = tmp; e1++; e2++; } } int compare_int(const void* e1, const void* e2) { return ((struct Student*)e1)->age - ((struct Student*)e2)->age; } void myBubble(void* base, int num, int sz, int (*pf)(const void*,const void*)) { int i = 0; for (i = 0; i < num - 1; i++) { int j = 0; for (j = 0; j < num - 1 - i; j++) { if (pf((char*)base + j * sz, (char*)base + sz * (j + 1)) > 0) { swap((char*)base + j * sz, (char*)base + (j + 1) * sz, sz); } } } } int main() { struct Student arr[3] = { {"zhangshan",12},{"lisi",20},{"wangwu",16} }; int sz = sizeof(arr) / sizeof(arr[0]); myBubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof(arr[0]), compare_int); return 0; }
这里就可以直接对年龄进行相减的操作了。
3.指针和数组的习题组
请预测下面代码的运行结果:
前言:数组名只在两种情况下代表数组名整个数组:其他情况下,数组名代表的是数组首元素的地址。
- sizeof(数组名),当数组名单独放在sizeof内部,此时数组名代表的是整个数组。并且计算出的也是整个数组的大小。
- &数组名,当数组名被单独放在&符号之前时,此时的数组名代表的是整个数组的地址,数组名代表的是整个数组。
//一维数组 int a[] = {1,2,3,4}; printf("%d\n",sizeof(a));//16 //数组名被单独放在sizeof内部,代表的是整个数组,计算结果是整个数组的大小。 printf("%d\n",sizeof(a+0));//4/8 //数组名不符合前言中的任何一种情况,此时数组名代表数组首元素的地址,地址的大小为4/8个字节 printf("%d\n",sizeof(*a));//4 //数组名不符合前言中的任何一种情况,此时数组名代表数组首元素的地址,解引用操作,拿到了int类型的首元素,大小4字节。 printf("%d\n",sizeof(a+1));//4/8 //数组名不符合前言中的任何一种情况,此时数组名代表数组首元素的地址,+1是第二个元素的地址。为4/8个字节。 printf("%d\n",sizeof(a[1]));//4 //a[1]是数组的第二个元素,大小是4字节。 printf("%d\n",sizeof(&a));//4/8 //&a取出的是整个数组的地址,但也是地址,大小为4/8个字节 printf("%d\n",sizeof(*&a));//16 //对数组名进行&和*操作,得到的最终还是数组名,相当于是将数组名单独放在sizeof内部,计算的是整个数组的大小。 printf("%d\n",sizeof(&a+1));//4/8 //&a取出整个数组的地址,+1操作跳过整个数组,得到一个新地址,但只要是地址,就是4/8个字节。 printf("%d\n",sizeof(&a[0]));//4/8 //a[0]是数组首元素,&a[0]是数组首元素的地址,地址的大小是4/8个字节。 printf("%d\n",sizeof(&a[0]+1));//4/8 //&a[0]是数组首元素的地址,+1操作,得到了数组中第二个元素的地址,大小为4/8个字节。 //字符数组 char arr[] = {'a','b','c','d','e','f'}; printf("%d\n", sizeof(arr));//6 //sizeof(数组名)计算的是整个数组的大小。数组中有6个元素,大小就是6. printf("%d\n", sizeof(arr+0));//4/8 //数组名没有单独放在sizeof内部,所以代表的是首元素的地址,+0操作,还是首元素的地址,地址就是4/8字节 printf("%d\n", sizeof(*arr));//1 //*arr是数组首元素,数组中的元素都是char类型,所以计算结果为1字节 printf("%d\n", sizeof(arr[1]));//1 //arr[1]是数组中的第二个元素,大小是1个字节。 printf("%d\n", sizeof(&arr));//4/8 //&arr取出的是整个数组的地址,地址的大小是4/8. printf("%d\n", sizeof(&arr+1));//4/8 //&arr代表的是整个数组的地址,+1操作跳过整个数组,得到一个新的地址,还是4/8个字节。 printf("%d\n", sizeof(&arr[0]+1));//4/8 //&arr[0]是数组首元素的地址,+1操作,得到数组第二个元素的地址,为4/8字节。 //要注意strlen函数的机制,strlen函数会计算从当前地址开始,一直到'\0'之前的所有元素个数 printf("%d\n", strlen(arr));//随机值 //arr是数组的起始地址,但是arr数组中并没有'\0',所以strlen会一直向后计算,知道遇到了'\0'为止。所以是个随机值。 printf("%d\n", strlen(arr+0));//随机值 //也是随机值,和上面的原因一样的 printf("%d\n", strlen(*arr));//报错 //这里将数组的第一个元素的ascll码,即97传过去了,意味着strlen函数要从97地址处开始向后计算,但是该内存空间是不允许被访问的,所以会报错。 printf("%d\n", strlen(arr[1]));//报错 //这里的原因与上一个类似的 printf("%d\n", strlen(&arr));//随机值 //&arr取出整个数组的地址,传递给strlen,由于不知道'\0'的位置,所以会计算出一个随机值。 printf("%d\n", strlen(&arr+1));//随机值 //&arr+1得到的是跳过一个数组之后的地址,由于不知道'\0'的位置,所以计算结果是随机值。 printf("%d\n", strlen(&arr[0]+1));//随机值 //同样的道理,也会计算出一个随机值 char arr[] = "abcdef";//要注意:字符串的末尾会自动添加'\0'。所以这个数组中含有7个元素 printf("%d\n", sizeof(arr));//7 //sizeof(数组名)计算的是整个数组的大小,要注意:字符串的末尾会自动添加'\0',会被sizeof加入计算结果的。 printf("%d\n", sizeof(arr+0));//4 //arr没有单独放在sizeof内部,+0仍然代表的是首元素的地址,大小4/8字节 printf("%d\n", sizeof(*arr));//1 //*arr是数组的首元素,数组是char类型数组,元素的大小都为1字节 printf("%d\n", sizeof(arr[1]));//1 //arr[1]是数组的第二个元素,数组是char类型的数组,大小都是1字节 printf("%d\n", sizeof(&arr));//4/8 //&arr取出的是整个数组的地址,只要是地址,大小就是4/8字节 printf("%d\n", sizeof(&arr+1));//4/8 //&arr取出整个数组的地址,+1操作跳过一个数组得到一个新的地址,大小仍然为4/8 printf("%d\n", sizeof(&arr[0]+1));//4/8 //&arr[0]取出数组首元素的地址,+1操作得到数组第二个元素的地址,只要是地址,大小就是4/8字节 printf("%d\n", strlen(arr));//6 //数组中含有'\0',arr是数组首元素的地址,到'\0'这个字符之前共有6个元素 printf("%d\n", strlen(arr+0));//6 //相同的道理 printf("%d\n", strlen(*arr));//报错 //*arr是将a字符的ascll码值传递给了strlen函数,这块内存空间访问是非法的,报错。 printf("%d\n", strlen(arr[1]));//报错 //arr[1]是将b字符的ascll码值传入了,该空间访问也是非法的,报错 printf("%d\n", strlen(&arr));//6 //&arr是数组的首地址,传入strlen函数时,会被转化为char*类型。得出计算结果为6 printf("%d\n", strlen(&arr+1));//随机值 printf("%d\n", strlen(&arr[0]+1));//5 //&arr[0]+1是第二个元素的地址,向后计算时不会将第一个元素算入,结果为5 char *p = "abcdef"; printf("%d\n", sizeof(p));//4/8 //p是一个指针变量,指向字符串的第一个元素的地址,指针变量的大小是4/8字节 printf("%d\n", sizeof(p+1));//4/8 //p+1是第二个字符的地址,地址的大小是4/8个字节 printf("%d\n", sizeof(*p));//1 //*p是字符串的第一个元素,大小是1字节 printf("%d\n", sizeof(p[0]));//1 //p[0]是字符串的第一个元素,大小也是1字节 printf("%d\n", sizeof(&p));//4/8 //&p是指针变量的地址,&p是个二级指针,大小也是4/8字节 printf("%d\n", sizeof(&p+1));//4/8 //&p+1也是一个二级指针,大小是4/8字节 printf("%d\n", sizeof(&p[0]+1));//4/8 //&p[0]+1是数组第二个元素的地址,大小是4/8字节 printf("%d\n", strlen(p));//6 //p指向的是字符串的第一个元素的地址,strlen能计算出这个字符串中的元素个数,为6 printf("%d\n", strlen(p+1));//5 //p+1地址处开始算,元素个数就是5个 printf("%d\n", strlen(*p));//报错 //*p的值是97,该空间访问是非法的,会报错 printf("%d\n", strlen(p[0]));//报错 //p[0]的值是97,同理,该空间访问非法,会报错 printf("%d\n", strlen(&p));//随机值 //&p是p这个指针变量的地址,由于不知道'\0'的位置,所以计算出一个随机值 printf("%d\n", strlen(&p+1));//随机值 //&p是二级指针,+1操作之后,仍因为不知'\0'的位置,最终计算出一个随机值。 printf("%d\n", strlen(&p[0]+1));//5 //&p[0]+1是数组第二个元素的地址,第一个元素不会被算入,计算结果就是5了 //二维数组 int a[3][4] = {0}; printf("%d\n",sizeof(a));//48 //sizeof(数组名)计算的是整个数组的大小,为48字节 printf("%d\n",sizeof(a[0][0]));//4 //a[0][0]是数组中第一行第一列的元素0,大小是4字节 printf("%d\n",sizeof(a[0]));//16 //a[0]是二维数组中第一个一维数组的数组名,计算的是整个第一行的大小,为16个字节 printf("%d\n",sizeof(a[0]+1));//4/8 //a[0]并没有单独放在sizeof内部,所以代表的是二维数组第一个一维数组的第一个元素的地址,也就是int*类型的,再+1操作,代表的是二维数组的第一个一维数组的第二个元素地址,大小是4/8字节 printf("%d\n",sizeof(*(a[0]+1)));//4 //sizeof括号里的内容可以改写为:a[0][1],所以计算结果为4 printf("%d\n",sizeof(a+1));//4/8 //a这个数组名没有单独放在sizeof内部,所以a代表第一个一维数组的地址,+1代表第二个一维数组的地址,地址的大小是4/8字节 printf("%d\n",sizeof(*(a+1)));//16 //sizeof括号里的可改写做:a[1],a[1]是数组第二行元素数组名,被单独放在sizeof内部,计算的是第二行元素的大小。16 printf("%d\n",sizeof(&a[0]+1));//4/8 //&a[0]代表的是第一个一维数组的地址,+1代表的第二个一维数组的地址,地址的大小是4/8字节 printf("%d\n",sizeof(*(&a[0]+1)));//16 //sizeof括号里可以改写:a[1],是第二个一维数组的数组名,被单独放在sizeof的括号里,计算的是第二个一维数组的大小。16 printf("%d\n",sizeof(*a));//16 //*a与a[0]等价,被单独放在sizeof内部,计算的是第一行元素的大小。16 printf("%d\n",sizeof(a[3]));//16 虽然a[3]不存在,但是与sizeof的计算结果无关,sizeof只关心括号里的数据类型。最终将这个数据类型的大小算出来即可。 //a[3]也是一个一维数组的数组名,被单独放在sizeof内部,计算的是一行的元素的大小,为16.
结尾:指针进阶的内容先讲解到这里,后续会继续更新,如有不足,敬请指正!!