1 数组参数、指针参数
在实际应用中,经常会遇到把数组或指针传给函数的情况,那函数的参数应该如何设计呢?
1.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,大小为二十个元素,参数类型为int *(20同样可以省略)
void test(int **arr)//ok?
{}//可以,二级指针用来存放一级指针的地址,这里的二级指针存放了arr2的首元素地址
int main()
{
int arr[10]={0};
int *arr2[20]={0};
test(arr);
test(arr2);
}
一维数组传参,形参可以是数组,也可以是指针
当形参是指针的时候,要注意类型相匹配
1.2 二维数组传参
void test(int arr[3][5])//ok?
{}//可以,道理同一维数组
void test(int arr[][])//ok?
{}
//不可以,原因如下
void test(int arr[][5])//ok?
{}//可以,原因如下
//总结:二维数组传参时,形参设计只能省略第一个[]中的数字
//即可以不知道该二维数组有多少行,但一定要知道该二维数组有多少列,这样才方便运算
void test(int *arr)//ok?
{}//不可以,二维数组的首元素地址应该是第一行的地址,与形参类型不匹配,在这里强行使用该写法编译器会报警告
void test(int * arr[5])//ok?
{}//不可以,形参部分是指针数组形式,与实参类型不匹配
void test(int (*arr)[5])//ok?
{}
//可以,二维数组首元素地址是第一行的地址,该二维数组每一行有五个元素,
//形参是一个指向拥有五个元素的数组的数组指针,在这里指向arr的第一行
void test(int **arr)//ok?
{}//不可以,二级指针接受的是一级指针的地址,但实参传过去的并不是一级指针
int main()
{
int arr[3][5] = { 0 };
test(arr);
}
二级数组传参
参数可以是指针,也可以是数组
如果是数组,行可以省略,列不可以省略
如果是指针,传过去的是第一行的地址,而第一行相当于一个一维数组,形参应该是一个数组指针
1.3 一级指针传参
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;
//test1(&a);
//int *p=&a;
//test1(p);
//int arr[10];
//test1(arr);
1.4 二级指针传参
当函数的参数为二级指针的时候,可以接收什么参数?
void test(char **p)
{}
int main()
{
char c = 'b';
char* pc = &c;
char** ppc = &pc;
char* arr[10];
test(&pc);
test(ppc);
test(arr);
return 0;
}
2 函数指针
函数指针,顾名思义就是存放函数地址的指针变量。
而函数指针跟其他指针有一个非常重要的区别。先来看如下代码:
void test()
{}
int main()
{
printf("%p\n", &test);
printf("%p\n", test);
}
x64下运行结果:
x86下运行结果:
可以发现,输出的两个地址,都是函数的地址,而&函数名跟函数名,都是函数的地址。因此在通过函数指针指向函数的过程中,可以不用解引用操作。
函数指针的存储方式:
void test()
{}
int main()
{
void (*pf)();
//pf先和*结合,说明pf是指针。最后一个括号内应写的是指向函数的参数类型。在本例为空
}
3 函数指针数组
将函数的地址存放在一个数组中,这个数组就叫做函数指针数组。函数指针数组应该如何定义呢?
以下哪一条关于函数指针数组的定义是正确的?
int (*pf1[10])();
int *pf2[10]();
int (*)()pf3[10];
答案是第一条
pf先跟[]结合,表明pf是一个数组
将pf[10]除去,剩下部分为int (*)(),表明该数组中的数据是int (*)()类型的函数指针
以下是函数指针的一个用与简单计算器的例子:
#include <stdio.h>
void menu()
{
printf("***************************\n");
printf("**** 1.加法 2.减法 ****\n");
printf("**** 3.乘法 4.除法 ****\n");
printf("******** 0.退出 ******** \n");
printf("请选择:");
}
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, y;
int input = 1;
int ret = 0;
int (*p[5])(int, int) = { 0,Add,Sub,Mul,Div };
//该函数指针数组存放了5个函数指针
while (input)
{
menu();
scanf("%d", &input);
if (input <= 4 && input >= 1)
{
printf("请输入两个操作数");
scanf("%d %d", &x, &y);
ret = (p[input])(x, y);
}
else if (input == 0)
break;
else
printf("输入有误\n");
printf("结果是:%d\n", ret);
}
return 0;
}
函数指针数组在解决一些选择性问题的时候可以极大简化代码,同时也提高了代码的可维护性。
4 指向函数指针数组的指针
指向函数指针数组的指针是一个指针
该指针指向了一个数组,数组的每个元素是函数指针
应该如何定义呢?
void test(const char* str)
{}
int main()
{
//函数指针pf
void (*pf)(const char* str) = test;
//函数指针数组
void (*pfArr[5])(const char* str);
pfArr[0] = test;
//指向函数指针数组的指针
void (*(*pf2)[5])(const char* str)=&pfArr;
return 0;
}
如果继续往下延伸,还可以延伸出指向函数指针数组的指针数组,指向指向函数指针函数数组的指针数组的指针... ...理论上来说可以持续下套
但是在实际应用中,过多次的“套娃”并没有实际意义,所以关于这一块知识点到为止即可,过度深究反而走了弯路
5 回调函数
回调函数,是指一个通过函数指针调用的函数。如果将一个函数的指针(地址)作为参数传递给另一个函数,当这个指针用来调用其所指向的函数时,我们就说这是回调函数。
回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另一方(被调函数)所调用的,用于对该事件或条件进行相应。
5.1 qsort函数
被调函数一个很典型的例子就是qsort函数:
qsort函数是用来 对数组的元素进行排序的。使用时需要引用头文件<stdlib.h>
对数组中由指向的元素进行排序,每个元素字节长,使用该函数确定顺序。
此函数使用的排序算法通过调用指定的函数来比较元素对,并将指向它们的指针作为参数。
该函数不返回任何值,但通过重新排序数组的元素(如 所定义)来修改指向的数组的内容。
qsort函数的底层采用的是 快速排序算法
这是cplusplus上对qsort的注释:
以下是对该注释的具体介绍:
void qsort(void* base,
//指向待排序数组的第一个元素
size_t count,
// 待排序的元素个数
size_t size,
//每个元素的大小,单位是字节
int(*cmp)(const void*, const void*));
//指向比较元素大小的函数(比较函数),告诉qsort函数比较的规则,由使用者提供
为什么这里使用了void*的指针?
是因为设计者在设计qsort函数时,并不知道在将来使用者会用qsort排序哪种类型的数据。为了实现库函数的通用性,将参数设置为void*类型(这里写成const void*类型是为了使代码更加规范)
因此,使用者在设计比较函数时也要将比较函数的形参部分设计成void*(const void*)类型,在根据所比较数据的类型,在函数内部将指针 强制类型转换来实现对不同数据类型的比较
下面是具体使用:
#include <stdio.h>
#include <stdlib.h>
int int_cmp(const void* p1, const void* p2)
{
//这里实现的是升序排序
//如果要改成降序,只需要将语句改为return *(int*)p2 - *(int*)p1;
return *(int*)p1 - *(int*)p2;
//返回值>0,p1指向的对象大于p2指向的对象
//返回值=0,p1指向的对象等于p2指向的对象
//返回值<0,p1指向的对象小于p2指向的对象
}
int main()
{
int arr[] = { 7,5,8,4,6,2,1,3,9,0 };
qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(arr[0]), int_cmp);
int i = 0;
for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
printf("%d ", arr[i]);
}
return 0;
}
5.2 qsort函数的模拟实现
在这里,我们基于冒泡排序,来模拟实现qsort
在这里采用模块化开发的思想,将项目拆分为cmp.h, cmp.c, test.c三个部分:
cmp.h用来包含项目中涉及到的头文件;
cmp.c是函数的具体实现过程;
test.c用来测试
cmp.h:
#pragma once
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
struct Stu
{
char name[20];
int age;
};
//整形升序排序(函数使用者提供)
int int_cmp(const void* p1, const void* p2);
//字符升序排序
int cmp_strcmp(const void* p1, const void* p2);
//结构体成员排序(按照年龄)
int cmp_stu_by_age(const void* p1, const void* p2);
//结构体成员排序(按照姓名)
int cmp_stu_by_name(const void* p1, const void* p2);
//换位
void Swap(char* p1, char* p2, size_t size);
//模拟实现qsort(采用冒泡方式)
void bubble_sort(void* base, size_t count, size_t size, int(*cmp)(const void*, const void*));
void test1();
void test2();
void test3();
void test4();
cmp.c:
#include"cmp.h"
int int_cmp(const void* p1, const void* p2)
{
//这里实现的是升序排序,以下同理
//如果要改成降序,只需要将语句改为return *(int*)p2 - *(int*)p1;
return *(int*)p1 - *(int*)p2;
}
int cmp_strcmp(const void* p1, const void* p2)
{
return *(char*)p1 - *(char*)p2;
}
int cmp_stu_by_age(const void* p1, const void* p2)
{
return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age;
}
int cmp_stu_by_name(const void* p1, const void* p2)
{
return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
}
void Swap(char* p1, char* p2, size_t size)
{
//将指针强制类型转换为char *类型,这是因为站在设计者的角度,并不知道将来使用者会使用该函数排序怎样的数据类型
//将指针强制类型转化为char *,同时传递目标数据所占内存的大小,就可以逐个字节地交换数据
int i = 0;
for (i = 0; i < size; i++)
{
char tmp = *(p1 + i);
*(p1 + i) = *(p2 + i);
*(p2 + i) = tmp;
}
}
void bubble_sort(void* base, size_t count, size_t size, int(*cmp)(const void*, const void*))
//bubble_sort就是我们自己编写的“qsort”,形参类型应该与qsort一致
{
size_t i, j = 0;
for (i = 0; i < count - 1; i++)
{
for (j = 0; j < count - 1 - i; j++)
{
if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0)
{
//每多加一个size,就跳过大小为size的字节
Swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
}
}
}
}
void test1()
{
int arr1[] = { 8,7,6,5,2,4,1,3,9,0 };
int i = 0;
bubble_sort(arr1, sizeof(arr1) / sizeof(arr1[0]), sizeof(arr1[0]), int_cmp);
for (i = 0; i < sizeof(arr1) / sizeof(arr1[0]); i++)
{
printf("%d ", arr1[i]);
}
printf("\n\n");
}
void test2()
{
//按年龄排序
struct Stu s[] = { {"zhangsan",20},{"lisi",15},{"wangwu",40} };
bubble_sort(s, sizeof(s) / sizeof(s[0]), sizeof(s[0]), cmp_stu_by_age);
}
void test3()
{
//按名字排序(一般按照首字符排序,如果首字符一样再根据第二个字符,以此类推)
struct Stu s[] = { {"zhangsan",20},{"lisi",15},{"wangwu",40} };
bubble_sort(s, sizeof(s) / sizeof(s[0]), sizeof(s[0]), cmp_stu_by_name);
}
void test4()
{
char arr3[] = "zxbgadefasw";
bubble_sort(arr3, sizeof(arr3) / sizeof(arr3[0]), sizeof(arr3[0]), cmp_strcmp);
int i = 0;
for (i = 0; i < sizeof(arr3) / sizeof(arr3[0]); i++)
{
printf("%c ", arr3[i]);
}
printf("\n");
}
test.c:
#include "cmp.h"
int main()
{
test1();
test2();
test3();
test4();
return 0;
}
运行结果:
test2运行结果(在监视中显示):
test3运行结果(在监视中显示):
下次将为大家带来指针和数组典型笔试题解析
(本篇完)