⭐前言⭐
※※※大家好!我是同学〖森〗,一名计算机爱好者,今天让我们进入学习模式。若有错误,请多多指教。
👍 点赞 ⭐ 收藏 📝留言 都是我创作的最大的动力!
⭐往期真集⭐
【C进阶】 | 【C进阶】 one -> 数据的存储 |
【C进阶】 | 【C进阶】two -> 指针进阶 |
⭐思维导图⭐
目录
※※※大家好!我是同学〖森〗,一名计算机爱好者,今天让我们进入学习模式。若有错误,请多多指教。
先复习下上节课讲的内容
聪明的你能不能分清楚上面的数组(指针)是那种类型?
arr先与[10]结合,是个数组,每个元素为int型,共有10个元素。
parr1先与[10]结合,是个数组,每个元素为int *型,共有10个元素。
parr2先与 * 结合,是个指针,指向的是是一个有10个元素的数组,数组的每个元素为int型。
parr3先于[10]结合,是个数组,共有10个元素,每个元素的类型是int(*)[5],即为数组指针。
parr3是一个存放数组指针的数组。
四、数组参数和指针参数
1、一维数组传参
#include<stdio.h>
int mian()
{
int arr1[10];
int *arr2[10];
test1(arr1);
test2(arr2);
return 0;
}
如以上代码,函数test1的形参该怎么写呢?用来打印一维数组。
提示:函数传参即test1、test2传的是数组首元素的地址。
void test1(int arr[10]) //1
{}
void test1(int arr[]) //2
{}
void test1(int arr[100]) //3
{}
void test1(int *pa) //4
{}
1、2、3是用数组来作为形参的,但形参定义的数组大小可以省略,类型不能省略。3这种方式会让人以为要传的数组有100个元素,不错但是不推荐这样写。
4是用指针来做形参,用来接收传过来的地址。没有问题。
这只是形参的两种表达方式。都对。在编译时,形参为数组的也会转换为指针。
举一反三
那么test2的形参的两种表达方式你会了吗?
void test2 (int *arr2[20]) //1
{}
void test2 (int *(*pb)) //2
{}
和你想的一样吗?
再多说一句:2的(*pb)表示pb是一个指针。int *表示指针指向的内容是int *型的。 就是二级指针。
现在的你能不能区分数组的地址和数组搜元素的地址的区别呢?
不会的话就看看小主以前的内容复习回顾下吧!3.2、&数组名VS数组名
2、二维数组传参
#include<stdio.h>
int main()
{
int arr[3][5];
test(arr);
return 0;
}
如上所示,test函数的形参该怎么写?test函数用来打印一维数组。
void test(int arr1[3][5]) //1
{}
void test(int arr1[][5]) //2
{}
void test(int arr1[3][]) //3
{}
void test(int arr1[][]) //4
{}
以上1、2、3、4 四种情况是用数组作为形参。其中1和2表示正确,而3和4是错误的表达。
// 总结:二维数组传参,函数形参的设计只能省略第一个 [] 的数字。 行能省略,但列不行// 因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。// 这样才方便运算。
void test(int *arr5)
{}
void test(int* arr6[5])
{}
void test(int (*arr7)[5])
{}
void test(int **arr8)
{}
5、6、7、8是用指针作为形参。先看我们的实参是二维数组的函数名,传过来的是二维数组中第一行的地址(可看作是一个一维数组的地址)。所以应该用数组指针来接收。二位数组的列为5所以指针为int (*arr)[5]; arr代表的意义是:因为arr与*先结合是个指针。指针指向的类型是int [5]。即指向一个整形数组,数组的有5个元素。
arr+1表示跳过一行即第二行的地址。(*arr)+ 1代表跳过一个即表示第一行第二个元素的地址。
arr5(整形指针)是个指向int类型的指针不同存放数组的指针。错误
arr6是个指针数组(存放指针的数组,即每个元素都是一个指针),不是指针不能存放地址。错误
arr7是数组指针(存放数组的指针),是一个类型是int [5]的指针。正确
arr8是二级指针(存放一级指针的指针)而传过来的是个数组的指针。错误
那如果一个二维数组 int arr[3][5]; 实参是arr和&arr有什么区别呢?
#include<stdio.h>
void test1(int(*pa)[5])
{
}
void test2(int(*pb)[3][5])
{
}
int main()
{
int arr[3][5];
test1(arr); //传的是第一行的地址。即行地址。
test2(&arr);//传的是整个二维数组的地址。即面地址。
return 0;
}
arr表示的是一维的地址,就是第一行的地址,+1跳过一行,一行5个元素。
&arr表示的是二维的地址, 就是整个二维数组的地址,+1跳过整个二维数组,共3行,15个元素。
3、一级指针传参
#include<stdio.h>
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;
}
print函数怎么写?打印数组。
void print(int *p, int sz)
{
int i = 0;
for(i=0; i<sz; i++)
{
printf("%d\n", *(p+i));
}
}
和你写的代码一样吗?十分简单,小主就不在多唠叨了。
那我们反过来想的话。如果函数的形参有了,那么都能传什么 呢?
void test1(int *p)
{}
//test1函数能接收什么参数?
首先是指针,
即
int a = 10;
int *pa = &a;
test(pa);
或者
test(&a);
其次我会想到整形一维数组
即
int arr[10] ;
test(arr);
你还会想到什么情况呢?
4、二级指针传参
#include <stdio.h>
void test(int** ptr)
{
printf("num = %d\n", **ptr);
}
int main()
{
int n = 10;
int*p = &n;
int **pp = &p;
test(pp);
test(&p);
return 0;
}
那当函数的参数为二级指针的时候,可以接收什么参数?
void test(int **p)
{}
首先是一级指针的地址或者二级指针
int a = 10;
int *b = &a;
int **c = &b;
test (&b);
test(c);
还有就是指针数组
int * arr[10]; 指针数组
test (arr);
五、函数指针
有多少小伙伴会一头雾水地问道:什么?函数还有指针
由数组指针我们可以推测出是指向函数的指针
#include <stdio.h>
void test()
{
printf("你好,中国\n");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
输出的结果是
既然我们知道了函数指针,那么怎么定义函数指针变量呢?
例如:函数 int Add(int m, int n);
我们先看数组指针变量怎么定义。
int arr[10];
int (*p)[10] = &arr;
我们先创建一个指针(*p),指针指向函数的参数是(int, int),返回类型是int型;
int(*p)(int , int) = &Add;
p就是Add类型的指针变量。
那函数int test (char * str);的指针变量你会了吗?
int (*p)(char*) = & test;
函数指针的使用:
#include<stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int m = 0, n = 0;
int(*p)(int, int) = Add; //定义并赋值函数指针变量
printf("请输入两个整数:");
scanf("%d%d", &m, &n);
int ret = (*p)(m, n); //函数指针变量的使用
printf("%d", ret);
return 0;
}
欣赏下列两个代码
//代码1
( * (void (*) () ) 0)();
//代码2
void (*signal(int, void(*)(int)))(int);
代码1
void (*)() 是函数指针类型
void (*p)();p是一个函数指针变量,无形参,返回类型是void
( void (*)() ) 强制类型转换,转换成函数指针类型
( void (*)() )0 对0进行强制类型的转换
( *( void (*)() )0 )();
1. 首先是把0强制类型转换为一个函数指针类型,这就意味着0地址处放一个返回类型是void,无参的一个函数
2. 调用0地址处的这个函数
代码2
函数声明
int Add(int, int);
代码2
void (* signal(int, void(*)(int)) )(int);//函数声明signal是一个函数的声明
signal( int, void(*)(int) )
signal函数的参数,第一个是int类型的,第二个是void(*)(int)的函数指针类型
signal函数的返回值类型也是:void(*)(int)的函数指针简化:
typedef void(* pf_t)(int) ;//给函数指针类型void(*)(int)重新起名叫:pf_t
pf_t signal(int, pf_t);
六、函数指针数组
我们来看我们熟悉的
字符指针数组
char * pa [5];
整形指针数组
int * pb [5];
那么函数指针数组呢?让我们拭目以待。
#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;
}
int main()
{
int m = 0, n = 0;
printf("请输入两个整数:");
scanf("%d%d", &m, &n);
若用函数指针变量
//int(*pa)(int, int) = Add;
//int(*pb)(int, int) = Sub;
//int(*pc)(int, int) = Mul;
//int(*pd)(int, int) = Div;
//那我们用函数指针数组呢?
int(*p[4])(int, int) = { Add,Sub,Mul,Div };
for (int i = 0; i < 4; i++)
{
int ret = p[i](m, n); //p[i]相当于是函数名
printf("%d\n", ret);
}
return 0;
}
运行结果
由以上代码可知,函数指针数组为
int (*p[4])(int ,int );
函数指针数组的应用(计算器)
#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;
int x = 0;
int y = 0;
int ret = 0;
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
printf("请输入2个操作数:>");
scanf("%d%d", &x, &y);
ret = Add(x, y);
printf("ret = %d\n", ret);
break;
case 2:
printf("请输入2个操作数:>");
scanf("%d%d", &x, &y);
ret = Sub(x, y);
printf("ret = %d\n", ret);
break;
case 3:
printf("请输入2个操作数:>");
scanf("%d%d", &x, &y);
ret = Mul(x, y);
printf("ret = %d\n", ret);
break;
case 4:
printf("请输入2个操作数:>");
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;
}
运行结果
我们会发现这个程序代码冗余。让我们来用函数指针数组简化下它
int main()
{
int input = 0;
int x = 0;
int y = 0;
int ret = 0;
//转移表
int (*pfArr[])(int, int) = {0, Add, Sub, Mul, Div};
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
if (input == 0)
{
printf("退出计算器\n");
}
else if(input >= 1 && input<=4)
{
printf("请输入2个操作数:>");
scanf("%d%d", &x, &y);
ret = pfArr[input](x, y);
printf("ret = %d\n", ret);
}
else
{
printf("选择错误\n");
}
} while (input);
return 0;
}
我们可以发现实现相同的功能,用转移表就会简化很多。
就是把用户的选择数字和函数指针数组的下标相结合。
七、指向函数指针数组的指针
指向函数指针数组的指针是一个 指针
指针指向一个 数组 ,数组的元素都是 函数指针 ;
怎么定义?
只要把函数指针数组的数组名变成(*p2)就好了。
注意:p3的大小也是4/8字节。
八、回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个 函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
如上:print_hehe 就是回调函数。
1、用回调函数简化计算器代码冗余
#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");
}
void calc(int (*pf)(int,int))
{
int x = 0;
int y = 0;
int ret = 0;
printf("请输入2个操作数:>");
scanf("%d%d", &x, &y);
ret = pf(x, y);
printf("ret = %d\n", ret);
}
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;
}
相比于原始版计算器,每个case语句用一个calc函数来调用Add等函数。显得更加简洁。
展现了回调函数的优势
2、qsort函数
void qsort (void* base, size_t num, size_t size, int (*compar)(const void*,const void*));对数组的元素进行排序对 数组中所指向的元素进行排序,每个元素的字节长度都使用该函数来确定顺序。
此函数使用的排序算法通过调用指定的函数并将它们作为参数的指针来比较元素对。
该函数不返回任何值,但通过重新排序其元素(如 定义)来修改所指向的数组的内容。
等效元素的顺序未定义。
qsort函数的应用
#include<stdio.h>
//要和qsort要求的类型相合
//int (*compar)(const void*,const void*)
int C_int(const void* e1, const void* e2)
{
//void*类型的指针不能解引用,也不能+1;
if (*(int*)e1 > *(int*)e2)
return 1;
else if (*(int*)e1 < *(int*)e2)
return -1;
if (*(int*)e1 == *(int*)e2)
return 0;
}
void test()
{
int arr[] = { 5,4,3,2,1, };
int sz = sizeof(arr) / sizeof(arr[0]); //求数组元素的多少
qsort(arr, sz, sizeof(arr[0]), C_int);
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
int main()
{
test();
return 0;
}
运行结果:
排序结构体
#include<stdio.h>
struct Stu
{
char name[20];
int age;
double score;
};
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 test1()
{
struct Stu arr[3] = { {"zhangsan", 20, 55.5},{"lisi", 30, 88.0},
{"wangwu", 10, 90.0} };
int sz = sizeof(arr) / sizeof(arr[0]);
//qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_age);
qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);
}
int main()
{
test1();
return 0;
}
3、克隆qsort函数
void Swap(char* p1, char* p2, size_t width)
{
size_t i = 0;
for (i = 0; i < width; i++) //逐个字符地替换
{
char tem = *p1;
*p1 = *p2;
*p2 = tem;
p1++;
p2++;
}
}
void qsort_1(void* base, size_t num, size_t width, int(*cmp)(const void* e1, const void* e2))
{
size_t i = 0, j = 0;
for (i = 0; i < num - 1; i++) //冒泡排序
{
j = 0;
//升序排列
for (j = 0; j < num - 1 - i; j++)
{
//因为base是void类型指针,要强制类型转换为char型
if (cmp((char*)base + j * width,
(char*)base + (j + 1) * width) > 0) //比较大小
{
//交换函数
Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
}
}
}
}
因为你不知道用户需要排序的是什么类型的数据,你只能用void类型指针来接收。而替换时用char类型,也是因为你不知道用户需要排序的是什么类型的数据,只能一个字节一个字节的替换。
4、完整代码
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
struct Stu
{
char name[20];
int age;
double score;
};
//打印整数数组
void print_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
//打印结构体数组
void Print(struct Stu arr[], int sz)
{
for (int i = 0; i < sz; i++)
{
printf("%s %d %.2lf\n", arr[i].name, arr[i].age, arr[i].score);
}
}
//qsort函数的比较大小函数,第四个函数参数
int cmp_int(const void* e1, const void* e2)
{
return (*(int*)e1 - *(int*)e2);
}
//结构体按年龄比较大小
int cmp_stu_by_age(const void* e1, const void* e2)
{
return ((struct Stu*)e1)->age - ((struct Stu*)e2)->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* p1, char* p2, size_t width)
{
size_t i = 0;
for (i = 0; i < width; i++) //逐个字符地替换
{
char tem = *p1;
*p1 = *p2;
*p2 = tem;
p1++;
p2++;
}
}
//克隆qsort
void qsort_1(void* base, size_t num, size_t width, int(*cmp)(const void* e1, const void* e2))
{
size_t i = 0, j = 0;
for (i = 0; i < num - 1; i++) //冒泡排序
{
j = 0;
for (j = 0; j < num - 1 - i; j++)
{
//因为base是void类型指针,要强制类型转换为char型
if (cmp((char*)base + j * width,
(char*)base + (j + 1) * width) > 0) //比较大小
{
//交换函数
Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
}
}
}
}
//整数排序测试
void test1()
{
int arr1[] = { 9,8,7,6,5,4,3,2,1,0 };
int arr2[] = { 9,8,7,6,5,4,3,2,1,0 };
//排序为升序
int sz1 = sizeof(arr1) / sizeof(arr1[0]);
int sz2 = sizeof(arr2) / sizeof(arr2[0]);
printf("整数排序测试:\n原数组排列顺序:\n");
print_arr(arr1, sz1);
printf("qsort函数升序排列\n");
qsort(arr1, sz1, sizeof(arr1[0]), cmp_int);
print_arr(arr1, sz1);
printf("克隆qsort函数升序排列\n");
qsort_1(arr2, sz2, sizeof(arr2[0]), cmp_int);
print_arr(arr2, sz2);
printf("\n");
}
//结构体排序测试
void test2()
{
struct Stu arr1[3] = { {"zhangsan", 20, 55.5},{"lisi", 30, 88.0},{"wangwu", 10, 90.0} };
struct Stu arr2[3] = { {"zhangsan", 20, 55.5},{"lisi", 30, 88.0},{"wangwu", 10, 90.0} };
int sz1 = sizeof(arr1) / sizeof(arr1[0]);
int sz2 = sizeof(arr2) / sizeof(arr2[0]);
printf("结构体排序测试:\n原数组排列顺序:\n");
Print(arr1, sz1);
printf("\nqsort函数按年龄升序排列\n");
qsort(arr1, sz1, sizeof(arr1[0]), cmp_stu_by_age);
Print(arr1, sz1);
printf("\n克隆qsort函数按年龄升序排列\n");
qsort_1(arr2, sz2, sizeof(arr1[0]), cmp_stu_by_age);
Print(arr2, sz2);
printf("\nqsort函数按名字升序排列\n");
qsort(arr1, sz1, sizeof(arr1[0]), cmp_stu_by_name);
Print(arr1, sz1);
printf("\n克隆qsort函数按名字升序排列\n");
qsort(arr2, sz2, sizeof(arr2[0]), cmp_stu_by_name);
Print(arr2, sz2);
}
int main()
{
test1();
test2();
return 0;
}
测试结果