当前位置:网站首页>深度理解指针(冒泡排序模拟实现qsort,函数指针实现回调函数)
深度理解指针(冒泡排序模拟实现qsort,函数指针实现回调函数)
2022-07-21 05:04:00 【liu_xuixui】
目录
前言
随着c语言的不断学习,初级的指针已无法满足更多的需求,为了深度理解指针在内存的使用方式,并为接下来数据结构等高阶知识的学习打下一个良好的基础,我们有必要更近一步的去学习指针.
初级指针概要
指针就是一个变量用来存放地址,而地址唯一标识一块内存空间.指针的大小是4/8个字节(32位或64位平台).指针有类型其基本类型决定了+-时的步长和解引用时的权限大小.指针-指针可以计算出之间相差元素的个数,而指针+指针无意义.由指针的基本数据类型我们可以归纳总结出全部的指针类型.
一 、字符指针
在指针类型中我们知道有一种指针类型为字符指针char*,通常其使用方式如下:
int main()
{
char ch = 'w';
char *pc = &ch;
*pc = 'w';
return 0;
}
还有一种使用方式:
int main()
{
const char* pstr = "hello world.";
printf("%s\n", pstr);
return 0;
}
很多初学者会认为"hello world",被存放在指针变量pstr中,其实不然上面代码仅仅是将字符串的首元素地址存放在指针变量中.
#include <stdio.h>
int main()
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char *str3 = "hello bit.";
const char *str4 = "hello bit.";
if(str1 ==str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if(str3 ==str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0
}
以上代码块的输出结果为 not same和same.有些同学会感到很疑惑,其实char* 类型的字符串一经赋值就被放进常量池中无法改变.所以str3与str4共用一个字符串常量指向同一处地址.而str1和str2表示的数组每次创建都会开辟新的空间,因此地址必然不同.
因此以后创建字符串指针时,不必修改的可以创建为char* a = ".....";后续需要修改的需要创建为char* a[] = "......";
二、数组名是什么?
在学习指针数组与数组指针之前我们有必要深入了解一下数组名的意义.
在一维数组中 int arr[10]={0}; arr为首元素地址,只有两种情况例外:1.&arr为整个数组的地址.2.sizeof(arr)计算整个数组的大小=4*10.值得注意的是以上两种情况都必须是arr单独出现,如果出现sizeof(arr+0),那么久表示计算数组第一个元素的大小=4*1.
在二维数组中 int arr[10][10]={0};arr为第一行元素的地址,arr[i](0<i<9)表示第i行地址.同样有两种情况例外:1.&arr为整个二维数组的地址.2.sizeof(arr)为整个二维数组的地址.
这时很多同学疑惑 arr的地址打印出来和&arr一模一样到底有什么意义呢?其实(arr+1)只能跳过一个元素,而(&arr+1)可以跳过整个数组.
三、指针数组
指针数组,顾名思义就是将指针放进数组里,其实就是普通数组存基本元素,而指针数组存地址.
例如:
int* arr1[10]; //整形指针的数组
char *arr2[4]; //一级字符指针的数组
char **arr3[5];//二级字符指针的数组
根据运算符的优先级我们发现指针数组的arr先与[]结合构成数组,其中存放的元素的int*或char*类型的.
四、数组指针
数组指针,顾名思义就是指向数组的指针,其实就是存放数组地址的指针.
我们可以做一个类比,存放整形地址的叫整形指针(int* p),存放字符地址的叫字符指针(char* p),那么存放数组地址的东西不言而喻就是数组指针 int(*parr)[].根据优先级的规则我们先让*与parr结合构成指针,再指向存放int类型元素的数组.综上数组指针的类型就是int(*)[].
那么数组指针有什么用呢?我们可以观察以下代码,数组名arr,表示首元素的地址但是二维数组的首元素是二维数组的第一行,所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址因此可以数组指针来接收.
void print_arr1(int(*arr)[5], int row, int col)
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { 1,2,3,4,5,6,7,8,9,10 };
print_arr1(arr, 3, 5);
return 0;
}
五、函数指针
首先我们来看一组代码,结果输出的test函数的两组地址.由此可见函数名本身就可以表示函数地址.
那么什么样的指针才能接收函数的地址呢?观察下面两组代码,我们根据函数指针的名称可以推测如果是指针那么*一定先与pfun1结合,然后再写明函数的函数类型与参数即可.
void test()
{
printf("hehe\n");
}
//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (*pfun1)();
void *pfun2();
给大家推荐两段有趣的代码(出自<<C陷阱与缺陷>>),接下来为大家画图分析这两段代码加深对函数指针的理解.
//代码1
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);
六、.函数指针数组
数组是一个存放相同类型数据的存储空间,我们已经学习了指针数组.那么将函数的地址放进一个数组中就叫做函数指针数组,那么如何定义呢?
首先主语为数组那么arr先与[]结合,再仿照函数指针的写法就是 void(*arr[])(int).很多初学者都很疑惑这么复杂拗口的写法有什么用呢?------转移表
我们先浅写一个计算器,观察代码可以发现内容十分的冗余.所以我们稍做改进.
#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("*************************\n");
printf(" 1:add 2:sub \n");
printf(" 3:mul 4:div \n");
printf("*************************\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;
}
观察这段代码我们使用函数指针数组实现,大大简化了代码的实现.将所有的函数地址放进一个函数指针数组中,只需对数组传参就可以很方便的调用所需操作啦.
#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 }; //转移表
while (input)
{
printf("*************************\n");
printf(" 1:add 2:sub \n");
printf(" 3:mul 4:div \n");
printf("*************************\n");
printf("请选择:");
scanf("%d", &input);
if ((input <= 4 && input >= 1))
{
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = (*p[input])(x, y);
}
else
printf("输入有误\n");
printf("ret = %d\n", ret);
}
return 0;
}
七、函数指针实现回调函数
回调函数就是通过函数指针调用的函数,把一个函数的地址(指针)作为参数传递给另一个函数时,当这个函数指针被用来调用其所指向函数时,我们称其为回调函数.回调函数不是由调用函数直接调用的而是在其特定条件或条件发生时由另一方调用.
在模拟实现qsort之前我们先理解如何使用qsort以及一些注意事项:
#include<stdio.h>
int main() {
void qsort( void* base, //待排序元素的首元素地址
size_t num,//待排序元素的数目
size_t width,//待排序元素的大小(字节)
int( * cmp)(const void* e1, const void* e2));//自己实现的比较函数
return 0;
}
参考官方的定义再经过我们的简写后可以看出qsort需要传四个参数,因为不知道排序什么类型的元素,所以我们用void*接收首元素地址(体现了void*的包容性和泛型的思想),即使不知道元素的类型通过传递元素的个数和大小,再将其强转成(char*)就可以一个字节一个字节的改变,*cmp是我们根据要排序的元素类型自己实现的比较函数的地址.由于数据类型较多我们举两个较为极端的例子:排序int型和结构体类型.
注意比较函数的返回类型:我们只需返回两数之差或调用strcmp即可.
首先是int类型,值得注意的是由于之前我们已经知道函数名与&函数名效果一样,所以传参时第四个参数直接传函数名即可.
#include<stdio.h>
#include<stdlib.h>
int cmp_int(const void* e1, const void* e2) {
return * (int*)e2 - *(int*)e1;//e2-e1降序,e1-e2升序
}
int main() {
int arr[] = { 1,2,3.4,5,6,7,8,9,10 };
int num = sizeof(arr) / sizeof(arr[0]);
int width = sizeof(arr[0]);
qsort(arr, //待排序元素的首元素地址
num,//待排序元素的数目
width,//待排序元素的大小(字节)
cmp_int);//自己实现的比较函数
for (int i = 0; i < num; i++) {
printf("%d ", arr[i]);
}
return 0;
}
最后是结构体类型,我们将其封装成一个函数.比较年龄与int型一致,所以我们比较name.在比较函数里面我们实现采用strcmp函数,恰好其返回值与我们的比较函数一致.注意!需先声明结构才能将e1和e2强转成结构体类型的指针.
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
struct Stu {
char name[20];
int age;
};
int cmp_struct(const void* e1, const void* e2) {
return strcmp(((struct Stu*)e1->name), ((struct Stu*)e2->name));
}
void test2() {
struct Stu s[] = { {"zhangsan",25},{"lisi",23},{"wangwu",27} };
int num = sizeof(s) / sizeof(s[0]);
int width = sizeof(s[0]);
qsort(s, //待排序元素的首元素地址
num,//待排序元素的数目
width,//待排序元素的大小(字节)
cmp_struct);//自己实现的比较函数
for (int i = 0; i < num; i++) {
printf("%s ", s[i].name);
}
}
int main() {
test2();
return 0;
}
在观察qsort过程中不难发现其中第四个参数类型就是回调函数的思想,.再次强调回调函数不是由调用函数直接调用的而是在其特定条件或条件发生时由另一方调用.
八、使用冒泡排序的思想模拟实现qsort
弄清楚qosrt的使用方法后,我们就可以尝试用冒泡排序的思想实现qsort.首先我们写出优化版的冒泡排序.
#include<stdio.h>
void bubble_sort(int arr[],int size) {
for (int i = 0; i < size-1; i++) {
int flag = 1;
for (int j = 0; j < size - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
flag = 0;
}
}
if (flag == 1) {
break;
}
}
}
int main() {
int arr[] = { 1,2,4,7,8,5,7,9 };
int size = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr,size);
for (int i = 0; i < 8; i++) {
printf("%d ", arr[i]);
}
return 0;
}
接着我们作出修改,在冒泡排序的基础上将其参数类型改写成和qsort一致.主要比较了int型和struct类型的变量(int型是注释部分),冒泡排序中大体框架不变,为了比较并交换未知类型的元素,我们得改变比较和交换的方法.因为不知道比较类型所以我们将传参内容统一强转成(char*)并乘以其宽度,交换也是一样的道理在不知道其类型的情况下一个字节一个字节的交换最安全.注意一个字节交换完后指针变量一定要++,否则就会犯和我一样的错误.
#include<stdio.h>
#include<string.h>
//int cmp_int(const void* e1, const void* e2) {
// return ( * (int*)e1 - *(int*)e2);//e2-e1降序,e1-e2升序
//}
struct Stu {
char name[20];
int age;
};
int cmp_struct_name(const void* e1, const void* e2) {
return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
void Swap(char* scr1,char* scr2, size_t width) {
for (int i = 0; i < width; i++) {
char tmp = *scr1;
*scr1 = *scr2;
*scr2 = tmp;
scr1++;
scr2++;
}
}
void bubble_sort(void* base,size_t num,size_t width,int(*cmp_struct_name)(const void* e1,const void* e2)) {
for (int i = 0; i < num-1; i++) {
int flag = 1;
for (int j = 0; j < num - 1 - i; j++) {
if (cmp_struct_name((char*)base+width*j,(char*)base+(j+1)*width)>0) {
//定义一个交换函数
Swap((char*)base+j*width,(char*)base+(j+1)*width,width);
flag = 0;
}
}
if (flag == 1) {
break;
}
}
}
void test2() {
struct Stu s[] = { {"zhangsan",25},{"lisi",23},{"wangwu",27} };
int num = sizeof(s) / sizeof(s[0]);
int width = sizeof(s[0]);
bubble_sort(s, //待排序元素的首元素地址
num,//待排序元素的数目
width,//待排序元素的大小(字节)
cmp_struct_name);//自己实现的比较函数
for (int i = 0; i < num; i++) {
printf("%s ", s[i].name);
}
}
int main() {
/*int arr[] = { 9,8,7,6,5,4,3,2,1 };
int num = sizeof(arr) / sizeof(arr[0]);
int width = sizeof(arr[0]);
bubble_sort(arr,num,width, cmp_int);
for (int i = 0; i < 8; i++) {
printf("%d ", arr[i]);
}*/
test2();
return 0;
}
总结
以上就是深度理解指针的全部内容,只要深刻的理解指针的含义和其使用方法在未来无论是学数据结构还是做一些复杂的项目才不至于手忙脚乱.由于不足之处欢迎指正.
边栏推荐
- 【C语言】一些常用的字符串函数--学习笔记
- Datalosserror: corrected record at XXXXXXX, Bert pre training error
- 2. Learn the vector calculation of paddlepaddle from scratch
- OneNote plug-in, cloud expansion
- Mask RCNN loading weight error
- Bert from introduction to practice notebook
- 深度剖析 —— 结构体
- MySQL installation failed
- 我的第一篇博客
- 桶排序,冒泡排序,快速排序
猜你喜欢
Hetai ht32--4spi drive 0.96 inch OLED display implementation
1027打印沙漏
[PCB] Based on Hetai ht32f52352 chip circuit board drawing experiment (WiFi and optical sensor module) - drawing board notes
Datalosserror: corrected record at XXXXXXX, Bert pre training error
pycharm专业版创建flask项目|下载flask包|以及一些例子
Examples of enumeration
OLED(经典0.96英寸)--4SPI--SSD1306控制原理(含常用芯片_oled例程)
合泰HT32 & 淘晶驰TJC--T0串口屏学习笔记
Pytorch installation
Amy-Tabb机器人世界手眼标定(2、实验结果)
随机推荐
做题引发对getchar()的理解
bert-serving
[record] the operation of optisystem is stuck, and it is unable to click to close, input variable values and other solutions
【PCB】基于合泰HT32F52352芯片电路板绘制实验(WiFi及光传感模块)-画板笔记
Hetai 32 onenet WiFi module - Hetai MCU data cloud through mqtt protocol (I)
【3D建模】Solidworks 3D建模及PrusaSlicer切片打印学习笔记
力扣中 26.删除有序数组中的重复项 88.合并两个有序数组 和189.旋转数组
(环境配置)TDD-net
基于OpenCV和Dlib+fr的人脸检测以及基于Dlib人脸对齐
Pytorch installation
3. Build the basic model of paddlepaddle from scratch (compare with keras and pytorch)
Examples of enumeration
吴恩达深度学习L4W4人脸识别
Checkpoint in the deep learning source code project
Onenote插件,云扩容
bert从入门到实践笔记本
Model definition of pytorch
Hetai ht32--4spi drive 0.96 inch OLED display implementation
C. Doremy‘s IQ
字符串逆序(详细)