目录
一. 字符指针
1.字符指针的定义
2.字符指针的用法
3.字符指针练习
二. 数组指针
1.指针数组的定义
2.指针数组的用法
三. 指针数组
1.数组指针的定义
2.数组名和&数组名的区别
3.数组指针的用法
4.练习
四. 数组传参和指针传参
1.一维数组传参
2.二维数组传参
3.一级指针传参
4.二级指针传参
五. 函数指针
1.函数指针的定义
2.取函数指针的地址
3.函数指针的用法
4.练习
六. 函数指针数组
1.函数指针数组的定义
2.函数指针数组的用法
七. 指向函数指针数组的指针
1.指向函数指针数组的指针的定义
2.指针总结
八. 回调函数
1.回调函数的概念
2.回调函数的例子
3.qsort函数
4.模拟实现冒泡排序版qsort函数
前言:
指针的主题,我们在初级阶段的《六.初阶指针_殿下p的博客-CSDN博客》章节已经接触过了,我们知道了指针的概念:
1. 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
2. 指针的大小是固定的4/8个字节(32位平台/64位平台)。
3. 指针是有类型,指针的类型决定了指针的+-整数的步长,指针解引用操作的时候的权限。
4. 指针的运算。
这个章节,我们继续探讨指针的高级主题。
定义:字符指针,常量字符串,存储时仅存储一份(为了节约内存)
char *pa="string";
用法:
int main()
{char ch = 'w';char *pc = &ch;*pc = 'w';return 0;
}
对于指向字符串的字符指针:
int main()
{char* pstr = "hello world";printf("%s\n", pstr);return 0;
}//打印结果为:hello world
上面代码 char* pstr = " hello world " 特别容易让人以为是把 hello world 放在字符指针 pstr 里了,但是本质上是把字符串 hello world 首字符的地址放到了 pstr 中。
下面代码将输出什么结果呢?
int main()
{char str1[] = "abcdef";char str2[] = "abcdef";const char* str3 = "abcdef";const char* str4 = "abcdef";if (str1 == str2)printf("str1 == str2\n");elseprintf("str1 != str2\n");if (str3 == str4)printf("str3 == str4\n");elseprintf("str3 != str4\n");return 0;
}
运行结果:
这是因为:
(1)str1和str2是数组,在内存中开辟两块内存空间,这两块内存空间的起始地址不相同,这两个值自然不一样。
(2)“abcdef”是常量字符串,本身不可以被修改,在内存中这个常量字符串只开辟一块内存空间,str3和str4是两个字符指针,这两个指针都指向该字符串的首字符地址,所以str3和str4相等。
定义:指针数组是数组,数组中存放的是指针(地址)
int arr1[10]; //整型数组
char arr2[5]; //字符数组
int *parr1[10]; //存放整形指针的数组--指针数组
char *parr2[5]; //存放字符指针的数组--指针数组
注:[] 优先级高于*,会先与 p 结合成为一个数组,再由 int* 说明这是一个整型指针数组。
#include int main()
{int arr1[] = {1, 2, 3, 4, 5};int arr2[] = {2, 3, 4, 5, 6};int arr3[] = {3, 4, 5, 6, 7};int* p[] = { arr1, arr2, arr3 }; // 首元素地址int i = 0;for(i=0; i<3; i++) {int j = 0;for(j=0; j<5; j++) {printf("%d ", *(p[i] + j)); // p[i]表示遍历指针数组内的每一个指针//*(p[i]+j)则表示每一个指针向后移动j个元素所指向的元素// == p[i][j] }printf("\n");}return 0;
}
如下图所示:
定义:数组指针是指针,是指向数组的指针。
整形指针 - 是指向整型的指针
字符指针 - 是指向字符的指针
数组指针 - 是指向数组的指针
int arr[n];
int (*p)[n]=&arr; //数组指针
前面提到过,[ ]的优先级高于*,所以加上括号,p先和*结合,说明p是一个指针变量,然后指向的是一个大小为n个整型的数组,所以p是一个指针,指向一个数组,叫数组指针。
先观察如下代码:
int main()
{int arr[10] = {0};printf("%p\n", arr);printf("%p\n", &arr);return 0;
}
发现它们的地址是一样的,但其实:
arr和&arr的值一样,但含义却不一样:
int main()
{int arr[10] = { 0 };int* p1 = arr;int(*p2)[10] = &arr;printf("%p\n", p1);printf("%p\n", p1 + 1);printf("%p\n", p2);printf("%p\n", p2 + 1);return 0;
}
我们发现arr+1跳过一个整形,而&arr+1跳过一个数组。
这是因为,arr表示数组首元素的地址,&arr表示整个数组的地址。
总结:
数组名是数组首元素的地址,但是有 2 个 例外:
① sizeof ( 数组名 ) - 数组名表示整个数组,计算的是整个数组的大小,单位是字节。
② &数组名 - 数组名表示整个数组,取出的是整个数组的地址。
二维数组以上常使用数组指针:
void print1 (int arr[3][5], int row, int col)
{int i = 0;int j = 0;for(i=0; i{1,2,3,4,5}, {2,3,4,5,6}, {3,4,5,6,7}};// print1(arr, 3, 5);print2(arr, 3, 5); // arr数组名,表示数组首元素的地址return 0;
}
分析下以下代码的含义:
int arr[5];
int* parr1[10];
int (*parr2)[10];
int (*parr3[10])[5];
解析:
//arr是一个有5个元素的整型数组
int arr[5]; //parr1是一个指针数组,数组有10个元素,每个元素都是int*的指针
int* parr1[10]; //parr2是一个数组指针,该指针指向一个数组,数组有10个元素,每个元素都是int型
int (*parr2)[10]; //parr3是一个数组指针数组,该数组存放10个数组指针,
//每个数组指针能够指向一个数组,数组有5个元素,每个元素的类型为int型
int (*parr3[10])[5];
判断下面函数的形参是否合理:
void test(int arr[]) //ok?
{}
void test(int arr[10]) //ok?
{}
void test(int *arr) //ok?
{}
void test(int *arr[]) //ok?
{}
void test2(int *arr[20]) //ok?
{}
void test2(int **arr) //ok?
{}int main()
{int arr[10] = {0};int* arr2[20] = {0};test(arr);test2(arr2);
}
答:以上都合理
判断以下函数的参数是否合理:
void test(int arr[3][5]) //0k?
{}
void test(int arr[][5]) //0k?
{}
void test(int arr[3][]) //ok?
{}
void test(int arr[][]) //ok?
{}void test(int* arr) //ok?
{}
void test(int* arr[5]) //ok?
{}
void test(int(*arr)[5]) //ok?
{}
void test(int** arr) //ok?
{}int main()
{int arr[3][5] = {0};test(arr); // 二维数组传参return 0;
}
答:只有第一,第二和第七个合理,其他都不行,理由如下:
//直接数组传参
void test(int arr[3][5])//可以
{}//数组传参,行可以省略
void test(int arr[][5]) //可以
{}//数组传参,列不可以省略
void test(int arr[3][]) //不可以
{}//同上
void test(int arr[][]) //不可以
{}//数组名表示数组首元素的地址,二维数组的首元素为第一行一维数组的地址,
//需要一个指向数组的数组指针来接受,一个一阶指针接受不下
void test(int* arr) //不可以
{}//参数部分为指针数组,不是指针,无法接受二维数组的首元素地址
void test(int* arr[5]) //不可以
{}//数组指针,该指针指向的数组有5个元素,可以接受二位数组的首地址
void test(int(*arr)[5]) //可以
{}//二维数组名表示第一行数组的地址,二级指针容纳不下
void test(int** arr) //不可以
{}int main()
{int arr[3][5] = {0};test(arr); // 二维数组传参return 0;
}
例:
void print(int* ptr, int sz) // 一级指针传参,用一级指针接收
{int i = 0;for(i=0; i
那么问题来了,当函数参数为一级指针时,可以接收什么样的参数呢?
如下所示:
void test1(int* p)
{}int main()
{int a = 10;int* pa = &a;test1(&a); test1(pa); return 0;
}
例:
void test(int** ptr)
{printf("num = %d\n", **ptr);
}int main()
{int n = 10;int* p = &n;int** pp = &p;test(pp);test(&p); // 取p指针的地址,依然是个二级指针return 0;
}
还是那个问题,当函数的参数为二级指针时,可以接收什么样的参数呢?
如下所示:
void test(int **p) // 二级指针
{;
}int main()
{int *ptr;int** pp = &ptr;test(&ptr); // 传一级指针变量的地址 test(pp); // 传二级指针变量 int* arr[10]; //指针数组test(arr); // 传存放一级指针的数组,因为arr是首元素地址,元素类型为int* return 0;
}
定义:指向函数的指针,存放函数地址的指针
int Add(int x, int y)
{return x + y;
}int main()
{int (*pf)(int, int) = &Add; //pf是一个函数指针return 0;
}
int ( * pf ) (int ,int ) = & Add;
解释:*先于pf结合,表示pf是一个指针,然后pf指向一个函数,括号内表示该函数有两个参数,参数类型都为int ,对于函数来说还有返回类型,最前面的int表示函数的返回类型
函数也是有地址的,取函数地址可以通过 &函数名 或者 函数名 实现。
但是,要注意:
如下所示:
int Add(int x, int y)
{return x + y;
}int main()
{// 函数指针 - 存放函数地址的指针// &函数名 - 取到的就是函数的地址printf("%p\n", &Add);printf("%p\n", Add);return 0;
}
地址是一样的:
例:
int Add(int x, int y)
{return x + y;
}int main()
{int (*pf)(int, int) = &Add; //创建函数指针,指向Add函数int ret = (*pf)(3, 5); // 对 pf 进行解引用操作,找到它所指向的函数,然后对其传参printf("%d\n", ret);return 0;
}
那么能不能把(*)pf(3,5)写成*pf(3,5)呢?
答案是不可以的,因为这么写会导致*对函数返回值进行解引用,所以星号一定要用括号括起来。
当然也可以选择不写*,因为前文提到过,函数名和&函数名都表示函数的地址:
int Add(int x, int y)
{return x + y;
}int main()
{int (*pf)(int, int) = &Add;// int ret = Add(3, 5);int ret = pf(3, 5);printf("%d\n", ret);return 0;
}
结果是一样的,说明(*pf)前的*加不加都可以。
(1)例一:
(*(void (*)())0)();
解析:这段代码的作用其实是调用 0 地址处的函数,该函数无参,返回类型是 void
如下图所示:
(2)例二:
void (*signal(int, void(*)(int)))(int);
解析:这段代码是对函数的声明
如下图所示:
1.signal先与()结合,说明signal是函数
2.signal函数的第一个参数类型是int,第二个参数类型是函数指针,该函数指针,指向一个参数为int,返回类型是void的函数。
3.signal函数的返回类型也是一个函数指针,该函数指针,指向一个参数为int,返回类型为void的函数。
上面的函数声明看上去过于冗杂,一眼让人难以察觉这段代码的真正含义,我们可以做如下简化:
int main()
{void (* signal(int, void(*)(int)) )(int);typedef void(*pfun_t)(int); // 对void(*)(int)的函数指针类型重命名为pfun_tpfun_t signal(int, pfun_t); // 和上面的写法完全等价return 0;
}
用typedef对重复出现的函数指针进行重命名,这样该函数声明就一目了然了。
定义:如果要把函数的地址存到一个数组中,那这个数组就叫函数指针数组。
int Add(int x, int y) {return x + y;
}int Sub(int x, int y) {return x - y;
}int main()
{int (*pf)(int, int) = Add;int (*pf2)(int, int) = Sub;int (*pfArr[2])(int, int) = {Add, Sub}; //函数指针数组,数组元素为函数的地址return 0;
}
引例:实现一个计算器,可以进行简单的加减乘除运算。
代码1:
include void menu()
{printf("*****************************\n");printf("** 1. add 2. sub **\n");printf("** 3. mul 4. div **\n");printf("** 0. exit **\n");printf("*****************************\n");
}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 input = 0;do {menu();int x = 0;int y = 0;int ret = 0;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 = Div(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;
}
运行结果:
当前代码看着主要功能都实现了,但是还有很多可以优化的地方:
这时候使用函数指针数组就会方便很多:
#include void menu()
{printf("*****************************\n");printf("** 1. add 2. sub **\n");printf("** 3. mul 4. div **\n");printf("** 0. exit **\n");printf("*****************************\n");
}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 input = 0;do {menu();// pfArr 就是函数指针数组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个操作数:> ");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;
}
这就是函数指针数组的应用。接收一个下标,通过下标找到数组里的某个元素,这个元素如果恰好是一个函数的地址,就会去调用那个函数。它做到了一个 "跳板" 的作用,所以我们通常称这种数组叫做 转移表 。
定义:指向函数指针数组的指针是一个指针,指针指向一个数组,数组的元素是函数指针。
int Add(int x, int y) {return x + y;
}int main()
{int arr[10] = {0};int (*p)[10] = &arr; // 取出数组的地址int (*pfArr[4])(int, int); // pfArr是一个数组 - 函数指针的数组int (* (*ppfArr)[4])(int, int) = &pfArr;// ppfArr 是一个指针,指针指向的数组有4个元素// 每个元素的类型是一个函数指针 int(*)(int, int)return 0;
}
void add(int,int); //函数int arr[10]; //数组int *prr[10]; //指针数组int (*pa)[10]; //数组指针int (*padd)(int,int)=add; //函数指针int (*parr[10])(int,int); //函数指针数组int (*(*pparr)[10])(int,int)=&parr; //指向函数指针的数组
回调函数是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时候,我们就称之为回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的时间或条件发生时由另外的一方调用的,用于该事件或条件进行响应。
用上面switch版本的计算器为例:
#include void menu()
{printf("*****************************\n");printf("** 1. add 2. sub **\n");printf("** 3. mul 4. div **\n");printf("** 0. exit **\n");printf("*****************************\n");
}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("请输入2个操作数:>");scanf("%d %d", &x, &y);printf("%d\n", pf(x, y));
}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;
}
解析:
定义:qsort 函数是C语言编译器函数库自带的排序函数( 需引入头文件 stdlib.h )
#include void qsort(void* base, //待排序数组的首元素size_t num, //待排序数组的元素个数size_t size, //待排序数组的每个元素的大小,单位是字节int (*compar)(const void*, const void*) //可以实现 比较待排序数据大小的 函数);
让我们回顾下冒泡排序:
#include void bubble_sort (int arr[], int sz)
{int i = 0;// 确认趟数for (i = 0; i < sz-1; i++) {// 一趟冒泡排序int j = 0;for (j = 0; j < sz-1-i; j++) {if(arr[j] > arr[j + 1]) {// 交换int tmp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = tmp;}}}
}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]);print_arr(arr, sz);bubble_sort(arr, sz);print_arr(arr, sz);return 0;
}
会发现,这个冒泡排序只能实现整形数据的排序,当我们想要对字符串或者结构体排序时,这个冒泡排序就显得有些寒酸了,而qsort函数可以帮助我们实现任意数据类型的排序:
qsort 整型数据排序(升序):
#include
#include /*
void qsort (void* base,size_t num,size_t size,int (*cmp_int)(const void* e1, const void* e2));
*/int cmp_int(const void* e1, const void* e2)
{// 升序: e1 - e2return *(int*)e1 - *(int*)e2;
}void print_arr(int arr[], int sz)
{int i = 0;for (i = 0; i < sz; i++) {printf("%d ", arr[i]);}printf("\n");
}void int_sort()
{int arr[] = {9,8,7,6,5,4,3,2,1,0};int sz = sizeof(arr) / sizeof(arr[0]);// 排序(分别填上四个参数)qsort(arr, sz, sizeof(arr[0]), cmp_int);// 打印print_arr(arr, sz);
}int main()
{int_sort();return 0;
}
运行结果为:0 1 2 3 4 5 6 7 8 9
qsort 对结构体排序:
#include
#include
#include struct Stu
{char name[20];int age;
};/*
void qsort (void* base,size_t num,size_t size,int (*cmp_int)(const void* e1, const void* e2));
*///按照年龄来排序
int cmp_struct_age(const void* e1, const void* e2)
{return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}//按照名字,也就是字符串来排序
int cmp_struct_name(const void* e1, const void* e2)
{return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}void struct_sort()
{// 使用qsort函数排序结构体数据struct Stu s[3] = { {"Ashe", 39},{"Hanzo", 38},{"Ana", 60}};int sz = sizeof(s) / sizeof(s[0]);// 按照年龄排序qsort(s, sz, sizeof(s[0]), cmp_struct_age);// 按照名字来排序qsort(s, sz, sizeof(s[0]), cmp_struct_name);
}int main()
{struct_sort();return 0;
}
运行结果:
按照年龄排序:
按照姓名(字符串)排序:
模拟qsort实现一个冒泡排序版本的通用排序算法:
#include
#include struct Stu
{char name[20];char age;
};// 模仿qsort实现一个冒泡排序的通用算法
void Swap(char*buf1, char*buf2, int width) {int i = 0;for(i=0; i 0) {//交换Swap((char*)base+j*width, (char*)base+(j+1)*width, width); }}}
}int cmp_struct_age(const void* e1, const void* e2) {return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
int cmp_struct_name(const void* e1, const void* e2) {return strcmp( ((struct Stu*)e1)->name, ((struct Stu*)e2)->name );
}
void struct_sort()
{// 使用qsort排序结构体数据struct Stu s[] = {"Ashe", 39, "Hanzo", 38, "Ana", 60};int sz = sizeof(s) / sizeof(s[0]);// 按照年龄排序bubble_sort_q(s, sz, sizeof(s[0]), cmp_struct_age);// 按照名字排序bubble_sort_q(s, sz, sizeof(s[0]), cmp_struct_name);
}void print_arr(int arr[], int sz)
{int i = 0;for(i=0; i
本篇到此结束,码文不易,还请多多支持哦!