引言
在前面的两篇文章中,我们一起探索了C语言指针的一些知识,今天我们将继续学习指针,相信在学习完这节内容后,我们会对指针有更加深刻的理解
函数指针变量
1.函数指针变量的创建
函数指针是一种指向函数的指针,其本质是一个指针变量,存储的值是函数的入口地址。
可能会有友友感到疑惑:函数也有地址?
我们可以写个代码来测试一下:
#include<stdio.h>
void test()
{
printf("hello\n");
}
int main()
{
printf("test: %p\n", test);
printf("&tets %p\n", &test);
return 0;
}
运行结果如下:
因此我们得知:函数确实是有地址的,函数名就是函数的地址,当然也能通过 &函数名 的方式获得函数的地址。
2.函数指针变量的使用
如果我们想要将函数的地址存放起来,就需要创建函数指针变量,函数指针变量的写法和数组指针十分相似。
先看看函数指针的一般形式:
return_type (*pointer_name)(parameter list);
其中:
return_type:函数返回值的类型
pointer_name:函数指针变量的名称
parameter list:函数参数列表,包括参数类型和参数名(参数名可以省略)
来看个具体的例子:假设我们有一个函数 int add(int x, int y),它接受两个整数参数并返回它们的和。我们可以创建一个指向这个函数的函数指针变量
代码如下:
#include<stdio.h>
int add(int x, int y)
{
return x + y;
}
int main()
{
int (*func_ptr)(int, int); // 创建函数指针变量
func_ptr = add; // 将函数add的地址赋给函数指针变量
// 或者直接初始化
// int (*func_ptr)(int, int) = add;
int sum = func_ptr(2, 3); // 通过函数指针调用函数
printf("The sum is: %d\n", sum);
return 0;
}
阅读两段有趣的代码
在阅读这两段代码前,我们先来了解一下typedef
3.typedef
typedef 关键字在C语言中用于为已有的类型创建别名,使用 typedef 可以使代码更简洁、更易读。
举个例子:如果我们觉得 unsigned int 写起来十分不方便,那我们就可以使用 typedef 这个关键字
typedef unsigned int uint;
//将unsigned int重命名为uint
同样的,我们也可以对指针类型重命名,例如,把 int* 重命名为ptr_t,可以这样子写:
typedef int* ptr_t;
我们也能对数组指针和函数指针重命名,不过与以上两种略有区别
比如,我们有数组指针类型int(*)[5],需要重命名为 parr_t ,那可以这么写:
typedef int(*parr_t)[5]; //新的类型名需要放在*的右边
函数指针类型的重命名也是如此,如将void(*)(int)类型重命名为pfun_t,就可以这么写:
typedef void(*pfun_t)(int); //新的类型名需要放在*的右边
4.两段有趣的代码
注:两段代码均出自《C陷阱和缺陷》
代码1:
(*(void(*)())0)();
要如何解释这段代码呢?
我们分析一下这段代码:
(*(void(*)())0)();
//void(*)()--函数指针类型
//(void(*)())--强制类型转换
//(void(*)())0--将0强制类型转换为void(*)()的函数指针类型
//这意味着我们假设0地址处放着无参,返回值为void的函数
//最终是调用0地址处放着的这个函数
我们从里往外拆分,最里面void(*)()是一个函数指针类型,它的返回类型是空,参数也为空,我们可以将其简化为pf 。
那么我们就可以把代码改写成这样:
(*(void (*)())0)();
// 定义一个指向不接受参数且返回void的函数的指针类型
typedef void(*pf)();
// 将整数0强制转换为pf类型的指针,并调用该地址处的函数
(*(pf)0)();
这段代码是先将0强制类型转换为函数指针类型,然后对其解引用。
解引用之后相当于调用在0地址的函数,因为其参数为空所以只有一个单独的()。
代码2:
void (*signal(int , void(*)(int)))(int);
我们同样来分析一下这段代码
void (*signal(int, void(*)(int)))(int);
//signal 是一个函数
//这个函数接受两个参数:
//一个 int 类型的信号编号和一个 void(*)(int) 类型的函数指针
//这个函数返回一个 void(*)(int) 类型的函数指针
通过typedef进行化简:
// 定义一个新的类型别名 pfun_t,它是指向接受 int 参数并返回 void 的函数的指针
typedef void (*pfun_t)(int);
// 使用新的类型别名 pfun_t 来简化 signal 函数的声明
pfun_t signal(int, pfun_t);
计算器的实现
如果我们想要实现计算器的部分功能,我们可以这样:
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
do//简单计算机的模拟实现
{
printf(" 1:add 2:sub \n");
printf(" 3:mul 4:div \n");
printf(" 0:exit \n");
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = add(x, y);
printf("ret = %d\n", ret);
break;
case 2:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = sub(x, y);
printf("ret = %d\n", ret);
break;
case 3:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = mul(x, y);
printf("ret = %d\n", ret);
break;
case 4:
printf("输入操作数:");
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;
}
我们观察到:这个代码十分的冗杂,我们可以使用以下两种方法进行优化:
5.函数指针数组
函数指针数组是一个数组,其每个元素都是一个函数指针,即这个数组用于存放指向函数的指针。
这些函数指针指向的函数具有相同的返回类型和参数列表,以确保类型匹配和正确使用。
那么函数指针数组要如何使用呢?
语法如下:
return_type (*pointer_name[num])(parameter list);
其中:
return_type 是函数返回的类型
pointer_name 是函数指针数组的名称
num 是数组中函数指针的数量
parameter list 是函数接受的参数列表
这表示 pointer_name 是一个数组,它包含 num 个元素,每个元素都是一个指向接受特定 parameter list 并返回 return_type 类型结果的函数的指针。
函数指针数组的一重要用途就是转移表
接下来我们来修改优化一下计算器的代码:
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
int(*p[5])(int x, int y) = { 0, add, sub, mul, div };
do
{
printf(" 1:add 2:sub \n");
printf(" 3:mul 4:div \n");
printf(" 0:exit \n");
printf("请选择:");
scanf("%d", &input);
if ((input <= 4 && input >= 1))
{
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = (*p[input])(x, y);
printf("ret = %d\n", ret);
}
else if (input == 0)
{
printf("退出计算器\n");
}
else
{
printf("输⼊有误\n");
}
} while (input);
return 0;
}
6.回调函数
回调函数是一种通过函数指针调用的函数。在编程中,我们通常把一个函数的指针(即地址)作为参数传递给另一个函数,当这个指针被用来调用它所指向的函数时,我们就称之为回调函数。
回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
简单来说就是通过函数来调用函数
计算器的优化,代码如下:
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
void calc(int(*pf)(int, int))
//用函数指针来接收函数地址
{
int ret = 0;
int x, y;
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = pf(x, y);
printf("ret = %d\n", ret);
}
int main()
{
int input = 1;
do
{
printf(" 1:add 2:sub \n");
printf(" 3:mul 4:div \n");
printf(" 0:exit \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");
break;
}
} while (input);
return 0;
}
qsort函数
7.qsort的语法
qsort函数是C语言标准库中的一个函数,用于对数组进行快速排序。
其函数原型如下:
void qsort (void* base, size_t num, size_t size,int (*compar)(const void*,const void*));
下面是对qsort函数参数的详细解释:
void *base:指向要排序的数组的第一个元素的指针。由于qsort函数用于对任何类型的数组进行排序,因此使用void指针作为参数,以便能够接受任何类型的数组。
size_t nitems:数组中元素的个数。
size_t size:数组中每个元素的大小(以字节为单位)。
int (*compar)(const void *, const void *):一个指向比较函数的指针,该函数用于确定数组中元素的排序顺序。比较函数应该接受两个指向要比较的元素的指针,并返回一个整数来指示它们的相对顺序。如果第一个元素小于第二个元素,则返回负数;如果两个元素相等,则返回零;如果第一个元素大于第二个元素,则返回正数。
8.qsort的使用
8.1 使用qsort排列整型数据
代码如下:
#include<stdio.h>
#include<stdlib.h>
//void* 类型的指针是无具体类型的指针,这种类型的指针不能直接解引用,也不能进行整数的运算
//需要强制类型转换
int compare(const void* p1, const void* p2)
{
return (*(int*)p1 - *(int*)p2);
//(*(int*)p1 - *(int*)p2)>0 返回1
//(*(int*)p1 - *(int*)p2)=0 返回0、
//(*(int*)p1 - *(int*)p2)<0 返回-1
}
int main()
{
int arr[10] = { 3,2,4,5,1,7,8,9,0,6 };
int n = sizeof(arr) / sizeof(arr[0]);
qsort(arr, n, sizeof(int), compare);
//arr:指向数组的地址
//n:数组的大小
//sizeof(int):数组元素的大小
//compare:指向函数
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
8.2 使用qsort排列结构数据
我们还能借助qsort实现排列结构数据
代码如下:
struct Stu {
char name[10];
int age;
};
//结构体成员的间接访问
//使用方式:结构体指针->成员名
int cmp_stu_by_name(const void* p1, const void* p2)
{
return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
}
int cmp_stu_by_age(const void* p1, const void* p2)
{
return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age;
}
//结构体成员的直接访问是通过点操作符(.)访问的
//使用方式:结构体变量.成员名
int main()
{
struct Stu arr[3] = { {"zhangsan",20},{"lisi",18},{"wangwu",16} };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);
printf("按照名字首字母排列:\n");
for (int i = 0; i < sz; i++)
{
printf("%s %d\n", arr[i].name, arr[i].age);
}
qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_age);
printf("按照年龄排列:\n");
for (int i = 0; i < sz; i++)
{
printf("%s %d\n", arr[i].name, arr[i].age);
}
return 0;
}
8.3 冒泡排序
冒泡排序是一种计算机科学领域的较简单的排序算法。它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果它们的顺序错误就把它们交换过来。这个走访元素的工作会重复进行,直到没有相邻元素需要交换,也就是说该元素列已经排序完成。
冒泡排序的基本原理是两两比较待排序数据的大小,当两个数据的次序不满足顺序条件时即进行交换,反之则保持不变。这样每次最小(或最大)的结点就像气泡一样浮到序列的最前位置。因此,冒泡排序也因此得名。
代码的实现:
void bubble_sort(int arr[], int sz)
{
for (int i = 0; i < sz - 1; i++)
{
int flag = 0;
for (int j = 0; j < sz - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
flag = 1;
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
if (flag == 0)
{
break;
}
}
}
int main()
{
int arr[10] = { 3,4,2,1,0,9,8,6,7,5 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz);
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
8.4 qsort的模拟实现
使用回调函数,模拟实现qsort(使用冒泡排序的方式)
参数部分:
void bubble(void *base, int count , int size, int(*cmp )(void *, void *))
为了方便我们对不同的数据类型进行比较,我们可以用char*类型进行比较
像这样实现内容的交换:
void bubble_sort(void* arr,size_t sz,size_t width,int (*cmp)(const void*,const void*))
{
for (int i = 0; i < sz - 1; i++)
{
for (int j = 0; j < sz - i - 1; j++)
{
if (cmp((char*)arr + j * width, (char*)arr + (j + 1) * width) > 0)
{
// 交换两个元素
Swap((char*)arr + j * width, (char*)arr + (j + 1) * width, width);
}
}
}
}
我们也能用这种方式实现对结构体数据的排列
完整代码如下:
struct Stu {
char name[10];
int age;
};
int cmp_int(const void*p1,const void* p2)
{
return *(int*)p1 - *(int*)p2;
}
int cmp_int_age(const void* p1, const void* p2)
{
return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age;
}
int cmp_char_name(const void* p1, const void* p2)
{
return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
}
void Swap(char* s1, char* s2, size_t width)
{
for (int i = 0; i < width; i++)
{
char tmp = *s1;
*s1 = *s2;
*s2 = tmp;
s1++;
s2++;
}
}
void bubble_sort(void* arr,size_t sz,size_t width,int (*cmp)(const void*,const void*))
{
for (int i = 0; i < sz - 1; i++)
{
for (int j = 0; j < sz - i - 1; j++)
{
if (cmp((char*)arr + j * width, (char*)arr + (j + 1) * width) > 0)
{
// 交换两个元素
Swap((char*)arr + j * width, (char*)arr + (j + 1) * width, width);
}
}
}
}
int test1()
{
int arr[10] = { 0 };
int sz = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < sz; i++)
{
scanf("%d", &arr[i]);
}
bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
int test2()
{
struct Stu arr[3] = { {"zhangsan",17},{"lisi",18},{"wangwu",15} };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz, sizeof(arr[0]), cmp_int_age);
for (int i = 0; i < sz; i++)
{
printf("%s %d\n", arr[i].name, arr[i].age);
}
}
int test3()
{
struct Stu arr[3] = { {"zhangsan",17},{"lisi",18},{"wangwu",15} };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz, sizeof(arr[0]), cmp_char_name);
for (int i = 0; i < sz; i++)
{
printf("%s %d\n", arr[i].name, arr[i].age);
}
}
int main()
{
//对整型数据进行排列
test1();
//对结构体进行排列
//年龄
//test2();
//对结构体进行排序
//姓名首字母
//test3();
return 0;
}
结束语
花了一段时间,终于是将指针的内容简单写了一下。
希望看到的友友们能点赞收藏加关注!!!
十分感谢!!!