🤡博客主页:醉竺
🥰本文专栏:《C语言深度解剖》《精通C指针》
😻欢迎关注:感谢大家的点赞评论+关注,祝您学有所成!
✨✨💜💛想要学习更多C语言深度解剖点击专栏链接查看💛💜✨✨
目录
1.函数指针
2.函数指针数组
3.指向函数指针数组的指针
4.回调函数
4.1 void* 的使用
4.2 使用回调函数,模拟实现qsort
4.3 使用qsort排序结构体
5.指针和数组笔试题解析
1.一维数组
2.字符数组
3. 字符串数组
4.字符串指针
5.二维数组
6.指针笔试题(难点)
7.对数组和指针的一些思考
1.函数指针
首先看一段代码:
输出的是两个地址,这两个地址是 test 函数的地址。
对于函数加不加取地址符号” & “效果都一样,调用函数加不加解引用符号” * “也都一样。
那我们的函数的地址要想保存起来,怎么保存?(即函数指针的形式是怎样的?)
下面我们看代码:
首先,能给存储地址,就要求pfun1或者pfun2是指针,那哪个是指针?
答案是:
pfun1可以存放。pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void。
阅读两段有趣的代码:
分析上述代码1和代码2分别代表什么含义?
代码1解释:
代码1是 一次函数调用
调用0地址处的一个函数
首先代码中将0强制类型转换成类型为 void (*) ( ) 的函数指针
然后去调用0地址处的函数
第一个” * “号,可无可有,上面已讲过。
代码2解释:
代码2是 一次函数的声明
声明的函数名字为 signal
signal函数的参数有2个,第一个参数是int型,第二个参数类型为函数指针型void (*) (int),该函数指针指向的类型是 返回值为void,其中一个参数是int型的函数
signal函数的返回类型是一个函数指针,该函数指针指向的类型也是 返回值为void,其中一个参数是int型的函数
如果代码按照下面这样子写,大家可能就更容易理解了,不过这种语法是错误的(其实很多复杂指针之所以难学,跟C语言语法风格的设计有很大关系,设计的不够直观):
代码2太复杂,如何简化:
typedef void(*pfun_t)(int);pfun_t signal(int, pfun_t);
2.函数指针数组
数组是一个存放相同类型数据的存储空间,那我们已经学习了指针数组,比如:
int* arr[10];
//数组的每个元素是int*
如果一个数组中存放的都是函数的地址,那这个数组就叫函数指针数组,那函数指针数组如何定义呢?
例子:(计算器)
1.打印菜单及实现函数功能
void menu()
{printf("*********1.Add 2.Sub*********\n");printf("*********3.Mul 2.Div*********\n");printf("*********0.Exit*********\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;
}
方法1:分支循环实现计算器
int main()
{int ret = 0;int input = 0;do{menu();int x = 0, y = 0; // 参与计算的两个数字int ret = 0; // 保存计算结果printf("请选择计算方式->");scanf("%d", &input);switch (input){case 0:printf("退出游戏!\n");break;case 1:printf("请分别输入两个数字x和y:\n");scanf("%d%d", &x, &y);ret = Add(x, y);printf("运算结果为:%d\n", ret);break;case 2:printf("请分别输入两个数字x和y:\n");scanf("%d%d", &x, &y);ret = Sub(x, y);printf("运算结果为:%d\n", ret);break;case 3:printf("请分别输入两个数字x和y:\n");scanf("%d%d", &x, &y);ret = Mul(x, y);printf("运算结果为:%d\n", ret);break;case 4:printf("请分别输入两个数字x和y:\n");scanf("%d%d", &x, &y);ret = Div(x, y);printf("运算结果为:%d\n", ret);break;default:printf("输入不符合条件请重新输入!\n");break;}} while (input);return 0;
}
方法2:函数指针数组的应用->转移表,改善方式一的冗余
int main()
{int ret = 0;int input = 0;int (*pfArr[5])(int, int) = { 0,Add,Sub,Mul,Div }; // 转移表do{menu();int x = 0, y = 0; // 参与计算的两个数字int ret = 0; // 保存计算结果printf("请选择计算方式->");scanf("%d", &input);if (input >= 1 && input <= 4){printf("请分别输入两个数字x和y:\n");scanf("%d%d", &x, &y);ret = pfArr[input](x, y);printf("运算结果为:%d\n", ret);}else if (input == 0){printf("退出游戏!\n");break;}else{printf("输入不符合条件请重新输入!\n");}} while (input);return 0;
}
方式3:回调函数->实现计算器(第4节会学习回调函数)
void Cal(int (*pfun)(int, int))
{int x = 0, y = 0;printf("请分别输入两个数字x和y:\n");scanf("%d%d", &x, &y);int ret = pfun(x, y);printf("运算结果为:%d\n", ret);
}int main()
{int ret = 0;int input = 0;do{menu();int x = 0, y = 0; // 参与计算的两个数字int ret = 0; // 保存计算结果printf("请选择计算方式->");scanf("%d", &input);switch (input){case 0:printf("退出游戏!\n");break;case 1:Cal(Add);break;case 2:Cal(Sub);break;case 3:Cal(Mul);break;case 4:Cal(Div);break;default:printf("输入不符合条件请重新输入!\n");break;}} while (input);return 0;
}
3.指向函数指针数组的指针
指向函数指针数组的指针:首先是一个指针,该指针指向一个 数组,数组的元素都是 函数指针 ; 如何定义?
void test(const char* str)
{printf("%s\n", str);
}
int main()
{//函数指针pfunvoid (*pfun)(const char*) = test;//函数指针的数组pfunArrvoid (*pfunArr[5])(const char* str);pfunArr[0] = test;//指向函数指针数组pfunArr的指针ppfunArrvoid (*(*ppfunArr)[5])(const char*) = &pfunArr;return 0;
}
4.回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进 行响应。
首先演示一下qsort函数的使用:
4.1 void* 的使用
在C语言中,void*
是一种特殊的指针类型,它表示一个指向未知类型的指针。这种指针可以指向任何类型的数据,但它不知道所指向数据的具体类型。void*
主要用于以下几种情况:
-
函数指针参数或返回值:当函数需要处理多种类型的数据,但具体类型不确定时,可以使用
void*
作为参数类型或返回类型。例如,标准库函数memcpy
就使用了void*
作为参数。 -
动态内存分配:
malloc
函数返回void*
类型的指针,因为它可以分配任何类型的内存。在使用时,通常需要将void*
强制转换为实际需要的类型。 -
类型无关的代码:有时,我们希望编写可以处理任何类型的代码,例如通用数据结构或容器,这时可以使用
void*
来实现类型无关性。
示例1:作为函数参数
#include <stdio.h>void print_value(void* ptr) {// 强制类型转换int value = *(int*)ptr;printf("%d\n", value);
}int main() {int x = 10;print_value((void*)&x);return 0;
}
在这个例子中,print_value
函数接受一个 void*
类型的参数,并在函数内部将其强制转换为 int*
类型,然后打印出该指针指向的整数值。
示例2:动态内存分配
#include <stdio.h>
#include <stdlib.h>int main() {// 动态分配一个整型大小的内存void* ptr = malloc(sizeof(int));// 强制类型转换后使用*(int*)ptr = 20;printf("%d\n", *(int*)ptr);// 释放内存free(ptr);return 0;
}
在这个例子中,我们使用 malloc
分配内存,它返回一个 void*
类型的指针。我们将其强制转换为 int*
类型,以便能够存储一个整数值。
注意事项:
- 使用
void*
时,必须确保类型转换是正确的,否则可能会导致未定义行为。 void*
指针不能直接进行算术操作,例如自增或自减,因为编译器不知道指针指向的数据类型大小。- 在使用
void*
指针前,应确保它确实指向了正确的数据类型,否则在解除引用时可能会出现问题。
4.2 使用回调函数,模拟实现qsort
(这里内部结构采用冒泡的方式)
#include <stdio.h>
int int_cmp(const void* p1, const void* p2)
{return (*(int*)p1 - *(int*)p2);
}
void _swap(void* p1, void* p2, int size)
{int i = 0;for (i = 0; i < size; i++){char tmp = *((char*)p1 + i);*((char*)p1 + i) = *((char*)p2 + i);*((char*)p2 + i) = tmp;}
}
void bubble(void* base, int count, int size, int(*cmp)(void*, void*))
{int i = 0;int j = 0;for (i = 0; i < count - 1; i++){for (j = 0; j < count - i - 1; j++){if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0){_swap((char*)base + j * size, (char*)base + (j + 1) * size, size);}}}
}
int main()
{int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };//char *arr[] = {"aaaa","dddd","cccc","bbbb"};int i = 0;bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), int_cmp);for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++){printf("%d ", arr[i]);}printf("\n");return 0;
}
4.3 使用qsort排序结构体
5.指针和数组笔试题解析
注意一下代码测试环境为visual stdio 2022 x86环境下(地址/指针 大小为4字节)
1.一维数组
2.字符数组
下面一些代码存在一些问题:
-
strlen(*arr)
:这里*arr
是数组的第一个元素,是一个字符,不是字符串的地址,所以这是错误的。 -
strlen(arr[1])
:这里arr[1]
是数组的第二个元素,是一个字符,不是字符串的地址,所以这是错误的。 -
strlen(&arr)
:这里&arr
是整个数组的地址,但是strlen
需要一个以\0
结尾的字符串的地址,所以这是错误的。 -
strlen(&arr + 1)
:这里&arr + 1
是数组后面的内存地址,这并不是一个有效的字符串地址,所以这是错误的。 -
strlen(&arr[0] + 1)
:这里&arr[0] + 1
是数组的第二个元素的地址,但是数组没有以\0
结尾,所以这也是错误的。
- 使用
strlen
函数时,需要确保传递的参数是一个以\0
结尾的字符串的地址。 strlen(*arr)
、strlen(arr[1])
、strlen(&arr)
、strlen(&arr + 1)
和strlen(&arr[0] + 1)
都是错误的,因为它们传递的参数不是字符串的地址。- 由于数组
arr
没有以\0
结尾,所以strlen(arr)
和strlen(arr + 0)
也可能导致未定义行为。
为了避免这些问题,确保在使用 strlen
时传递的参数是一个以 \0
结尾的字符串的地址,并且在使用 sizeof
时理解你正在计算的是数组的大小还是指针的大小。
3. 字符串数组
下面一些代码存在一些问题:
-
strlen(*arr)
:这是错误的,*arr
是数组第一个元素,即字符'a'
,而不是一个字符串的地址。因此,strlen(*arr)
会导致未定义行为,因为strlen
预期的是一个字符串的地址。 -
strlen(arr[1])
:这也是错误的,arr[1]
是数组第二个元素,即字符'b'
,同样不是一个字符串的地址。因此,strlen(arr[1])
也会导致未定义行为。 -
strlen(&arr)
:这是错误的,&arr
是整个数组的地址,strlen
会尝试计算从该地址开始直到遇到 null 字符的字符数。但是,因为&arr
指向的是整个数组,而不是字符串的起始位置,所以这可能会导致计算出一个错误的结果,或者在某些情况下导致未定义行为。 -
strlen(&arr + 1)
:这是错误的,&arr + 1
是数组后面的内存地址,它不指向任何有效的字符串。因此,strlen(&arr + 1)
会导致未定义行为。
4.字符串指针
下面一些代码存在一些问题:
-
strlen(*p)
:这是错误的,*p
是字符串的第一个字符,不是一个字符串的地址,所以这是未定义行为。 -
strlen(p[0])
:这也是错误的,p[0]
是字符串的第一个字符,不是一个字符串的地址,所以这是未定义行为。 -
strlen(&p)
:这是错误的,&p
是指针p
的地址,不是一个字符串的地址,所以这是未定义行为。 -
strlen(&p + 1)
:这也是错误的,&p + 1
是指针p
后面的
5.二维数组
sizeof(a[3])
:这是错误的,因为 a
只有 3 行,a[3]
超出了数组的范围,这将导致未定义行为。正确的做法是确保索引在数组的范围内。
但是在Visua Stdio运行中,并没有报错,结果反而是16为什么呢?
- 在 C 语言中,
sizeof
运算符返回的是操作数的大小,以字节为单位。当你尝试计算sizeof(a[3])
时,实际上你在尝试获取数组的第四行(记住数组索引是从 0 开始的)的大小。然而,由于你的数组a
只有 3 行,a[3]
实际上是一个越界的访问。- 在 Visual Studio 中,当你尝试访问越界的数组行时,你可能会得到一个看似合理的值(比如 16 字节),这是因为
sizeof
运算符不会实际访问内存,它只是返回类型的大小。在这种情况下,a[3]
被当作一个指向int[4]
(即一个有 4 个整数的数组)的指针,因此sizeof(a[3])
返回的是int[4]
的大小,即 4 个整数乘以每个整数的大小(通常在 32 位系统中是 4 字节,在 64 位系统中是 8 字节,取决于int
的大小)。- 这就是为什么你得到了 16 字节的结果,因为它相当于
4 * sizeof(int)
。然而,这并不意味着a[3]
是一个有效的数组行,它只是sizeof
运算符根据a[3]
的类型推断出的结果。实际上,访问a[3]
是未定义行为,可能会导致程序崩溃或其他意外结果。
下面对二维数组的进行一些拓展:
总结:
6.指针笔试题(难点)
笔试题1:
笔试题2:
笔试题3
笔试题4:
笔试题5 :
笔试题6:
笔试题7:
笔试题8:
7.对数组和指针的一些思考
想继续深入学习指针,可以订阅下方”精通C指针“专栏 哦~
《精通C指针》http://t.csdnimg.cn/gbpQp