【搞透C语言指针】那年我双手插兜, 不知道指针是我的对手
☃️内容专栏:【C语言】进阶部分
☃️本文概括: 征服C语言指针!一篇文章搞清楚指针的全部要点。
☃️本文作者:花香碟自来_
☃️发布时间:2023.3.3
目录
一、字符指针
二、指针数组
三、数组指针
1.数组指针的定义
2.数组名和&数组名
3.数组指针的使用
四. 数组传参和指针传参
1.一维数组传参
2.二维数组传参
3.一级指针传参
4.二级指针传参
五. 函数指针
1.函数指针
2.利用函数指针
3.阅读两段有趣的代码
六. 函数指针数组
1.函数指针的使用
2.函数指针数组的应用
七. 指向函数指针数组的指针
八. 回调函数
九、qsort函数的应用
1.利用qsort来排序整型数组
2 利用qsort来排序结构体数据
十、以冒泡排序的思想,模拟实现qsort函数
一、字符指针
在指针的类型中我们知道有一种指针类型为字符指针 char*
一般这么使用:
int main()
{char ch = 'w';char *pc = &ch;*pc = 'w';return 0;
}
但是还有一种方式
字符串常量
int main()
{const char* pstr = "hello world.";//这里是把一个字符串放到pstr指针变量里了吗?printf("%s\n", pstr);return 0;
}
以上特别容易以为是把字符串 hello world.放到字符指针 pstr 里了,其实是把字符串的首字符地址h的地址存放到字符指针变量 pstr 当中,如果当32位平台,4个字节大小的指针变量pstr能够装得下字符串吗?显然,不能。
需要注意的一点,观察以下图:
好了,小伙伴们理解了吗?下面来看一道来自于《剑指offer》当中的一道题:
#include<stdio.h>
int main()
{char str1[] = "hello world.";char str2[] = "hello world.";const char *str3 = "hello world.";const char *str4 = "hello world.";if(str1 ==str2)printf("str1 and str2 are same\n"); elseprintf("str1 and str2 are not same\n");if(str3 ==str4)printf("str3 and str4 are same\n");elseprintf("str3 and str4 are not same\n");return 0
}
这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4不同。
二、指针数组
指针数组就是存放指针的数组,int* arr1[10] 表示存放整型指针的数组,char* arr2[20] 表示存放字符指针的数组。
应用:
int main()
{/* char* arr[] = { "abcdef", "hehe", "qwer" };int i = 0;for (i = 0; i < 3; i++){printf("%s\n", arr[i]);} */int arr1[] = { 1,2,3,4,5 };int arr2[] = { 2,3,4,5,6 };int arr3[] = { 3,4,5,6,7 };//arr[i] == *(arr+i)//arr是一个存放整型指针的数组int* arr[] = { arr1, arr2, arr3 };int i = 0;for (i = 0; i < 3; i++){int j = 0;for (j = 0; j < 5; j++){//printf("%d ", arr[i][j]);printf("%d ", *(arr[i]+j));}printf("\n");}return 0;
}
三、数组指针
1.数组指针的定义
整形指针: int * pint; 能够指向整形数据的指针。
浮点型指针: float * pf; 能够指向浮点型数据的指针。
那数组指针应该是:能够指向数组的指针。
//下面代码哪个是数组指针?
int main()
{int *p1[10];int (*p2)[10];//p1, p2分别是什么?return 0;
}
解释:int (*p)[10]; //解释:p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个 指针,指向一个数组,叫数组指针。
//这里要注意:[ ]的优先级要高于*号的,所以必须加上()来保证p先和*结合。
2.数组名和&数组名
我们知道,数组名绝大部分情况下是数组首元素的地址。
但是有两种特殊情况:
1. sizeof(数组名) - sizeof内部单独放一个数组名的时候,数组名表示的整个数组,计算得到的是数组的总大小。
2. &arr - 这里的数组名表示整个数组,取出的是整个数组的地址,从地址值的角度来讲和数组首元素的地址是一样的,但是意义不一样。
观察以下代码
#include<stdio.h>
int main()
{int arr[10] = { 0 };//printf("%d\n", sizeof(arr));printf("%p\n", arr);//其类型是int * printf("%p\n", arr+1);//4printf("%p\n", &arr[0]);//int* printf("%p\n", &arr[0]+1);//4printf("%p\n", &arr);//其类型是int(*)[10]printf("%p\n", &arr+1);int (*p)[10] = &arr;//p是一个数组指针//int(*)[10] return 0;
}
解释:根据上面的代码我们发现,其实&arr和arr,虽然值是一样的,但是意义应该不一样的。 实际上: &arr 表示的是数组的地址,而不是数组首元素的地址。(细细体会一下)
本例中 arr 和 &arr[0] 表示的都是数组首元素的地址,arr + 1和&arr[0] +1 需要看数组首元素地址的类型是int *,所以加一跳过一个整型的大小。而&arr 的类型是: int(*)[10] ,是一种数组指针类型 数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40。
3.数组指针的使用
那数组指针是怎么使用的呢? 既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。
#include<stdio.h>
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int sz = sizeof(arr) / sizeof(arr[0]);int (* p)[10] = &arr;int i = 0;//p --- &arr//*p --- *&arr//*p --- arrfor (i = 0; i < sz; i++){printf("%d ", *((*p) + i));}//虽然对,但是有点别扭,不推荐 //一般还是将数组名存放到一个整型指针中再访问使用//int* p = arr;//for (i = 0; i < sz; i++)//{// printf("%d ", *(p + i));//}return 0;
}
一个数组指针的使用(二维数组传参):
#include<stdio.h>
void print(int(*arr)[5], int row, int col)
{int i = 0;for (i = 0; i < 3; i++){int j = 0;for (j = 0; j < 5; j++){//*(arr + 0) == arr[0] == &arr[0][0] 为第一行数组首元素的地址……以此类推printf("%d ", *(*(arr + i) + j));//printf("%d ", arr[i][j]);//和以上代码表示一样}printf("\n");}
}
int main()
{int arr[3][5] = {1,2,3,4,5, 2,3,4,5,6, 3,4,5,6,7};//二维数组的数组名,也表示首元素的地址//二维数组可以理解为起始是一维数组的数组//二维数组的首元素是第一行(一维数组)//首元素的地址就是第一行的地址,是一个一维数组的地址print(arr, 3, 5);return 0;
}
辨别一下以下代码
int arr[5]; //整型数组int *parr1[10]; //指针数组int (*parr2)[10]; //数组指针int (*parr3[10])[5]; //存放数组指针的数组
四. 数组传参和指针传参
在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?
1.一维数组传参
对于函数test(),一维整型数组传参,形参部分可以是数组的形式接收,也可以是指针的形式接收。
对于函数test2(),指针数组传参,形参部分可以是指针数组的形式接收,也可以是二级指针的形式接收。
#include <stdio.h>
void test(int arr[])// ture
{}
void test(int arr[10])// ture
{}
void test(int* arr)// true
{}
void test2(int* arr[20])// true
{}
void test2(int** arr)// true
{}
int main()
{int arr[10] = { 0 };int* arr2[20] = { 0 };test(arr);test2(arr2);
}
2.二维数组传参
如果是数组,行可以省略,但是列不能省略。如果是指针,传过去的是第一行的地址,形参部分接收的就应该是数组指针。
void test(int arr[3][5])//ture
{}
void test(int arr[][])//false
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int arr[][5])//ture
{}
void test(int *arr)//false
{}
void test(int* arr[5])//false
{}
void test(int (*arr)[5])//ture
{}
void test(int **arr)//false
{}
int main()
{int arr[3][5] = {0};test(arr);
}
3.一级指针传参
#include <stdio.h>
void print(int *p, int sz)
{int i = 0;for(i=0; i < sz; i++){printf("%d\n", *(p+i));}
}
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;
}
思考: 当一个函数的参数部分为一级指针的时候,函数能接收什么参数?
void test1(int *p)
{}
//test1函数能接收什么参数?
- 一级指针
- 整型变量的地址
- 整型的一维数组名
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 test2(int **p)
{}
//test2函数能接收什么参数?
- 二级指针变量
- 一级指针变量的地址
- 整型的指针数组的数组名
五. 函数指针
1.函数指针
首先看一段代码
#include <stdio.h>
void test()
{printf("hehe\n");
}
int main()
{printf("%p\n", test);printf("%p\n", &test);return 0;
}
输出的是两个地址,这两个地址是 test 函数的地址。 那我们的函数的地址要想保存起来,怎么保存?
void (*pfun)();
解释:pfun就是一个函数指针。pfun先和*结合,说明pfun是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void。
⚠️注意:函数名和&函数名是一样的,都表示的是函数的地址。
2.利用函数指针
int Add(int x, int y)
{return x + y;
}
#include<stdio.h>
int main()
{//函数指针变量int (*pfun)(int, int) = Add;//调用函数int ret1 = (*pfun)(3, 5); //函数的地址存放到函数指针当中printf("%d\n", ret1);//我们平常写的函数调用int ret2 = Add(4, 7);printf("%d\n", ret2);//那么就有以下表示方法int ret3 = pfun(8, 9);//所以我们知道 pfun和*pfun是一样的,这里的*只是帮助理解,加多少个*都只是"摆设"printf("%d\n", ret3);return 0;
}
那么假设一个函数是这么写的 char* test(int c,float* pf),那么这个函数指针该如何表示的呢?
char*(*pt)(int,float*) = test;
讲到这里,有人会质疑,以上的应用不相当于脱裤子放p,多此一举吗?那函数指针到底是如何应用的呢,答案当然是有的,在函数需要调用另一个函数的时候,那么就需要传入函数的地址了,在形参部分利用函数指针来接收,就需要具体用到函数指针了,这里就不多细讲,感兴趣的同学可以写一段代码,深切体会一下。
3.阅读两段有趣的代码
两段代码源于《C陷阱与缺陷》这本书
(*(void (*)())0)(); //code1void (*signal(int , void(*)(int)))(int); //code2
解释code1:对于code1代码,(*(void (*)())0)(),是将0强制类型转换为void(*)()类型的函数指针,这就意味着0地址处放置着一个函数,该函数没有参数,返回类型为void,然后调用0地址处的这个函数。
解释code2:对于code2代码,void (* signal(int , void(*)(int)) )(int),这个代码是一个函数的声明。
分析:首先我们可以确定signal这个符号按优先级来说,先和右边的括号结合,里面放了一个参数 int,另一个参数void(*)(int),由此我们可以判断,signal是一个函数,它的一个参数为整型变量,另一个参数为一个函数指针,该函数指针指向的是一个参数为int,返回类型为void的函数。那么signal函数还缺什么呢?缺返回类型,绿色的加粗部分的void(*)(int)表示的就是signal的返回类型。
其实大概可以理解为:// void (*) (int) signal(int , void(*)(int)),但是语法格式并不支持,于是signal只能和*结合在一起。
那么我们可以用typedef这个关键字来简化这段代码,使代码变得容易理解。
typedef int* ptr_t; typedef void(*pf_t)(int);//将void(*)(int) 类型重命名为pf_tpf_t signal(int, pf_t); //将 void(*signal(int,void(*)(int)))(int)的简化
六. 函数指针数组
要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
int (*parr[10])()
解释:parr1就是一个数组,数组的内容就是int (*)()类型的函数指针
1.函数指针的使用
写一个简单的计算器
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");
}
#include<stdio.h>
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("请输入两个操作数:>");scanf("%d %d", &x, &y);ret = Add(x, y);printf("%d\n", ret);break;case 2:printf("请输入两个操作数:>");scanf("%d %d", &x, &y);ret = Sub(x, y);printf("%d\n", ret);break;case 3:printf("请输入两个操作数:>");scanf("%d %d", &x, &y);ret = Mul(x, y);printf("%d\n", ret);break;case 4:printf("请输入两个操作数:>");scanf("%d %d", &x, &y);ret = Div(x, y);printf("%d\n", ret);break;case 0:printf("退出计算器\n");break;default:printf("选择错误\n");break;}} while (input);return 0;
}
以上代码有没有发现有些许冗余了,未来如果有需要添加整型的其他运算,如<< >> & | ^ ……等运算,代码太冗长,那有没有什么办法将代码给简化一下呢?直接上代码:
2.函数指针数组的应用
以下代码改为了转移表的形式,转移表主要用指针数组来实现,每个元素存一个函数指针,我们只要通过已定义好的编号来进行访问。
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");
}
#include<stdio.h>
int main()
{int input = 0;int x = 0;int y = 0;int ret = 0;//转移表int (*pfarr[5])(int,int) = { NULL,Add,Sub,Mul,Div };//0 1 2 3 4do{menu();printf("请选择:>");scanf("%d", &input);if (input >= 1 && input <= 4){printf("请输入两个操作数:>");scanf("%d %d", &x, &y);ret = pfarr[input](x, y);printf("%d \n", ret);}else if (input == 0){printf("退出计算器\n");}else{printf("输入有误\n");}} while (input);return 0;
}
七. 指向函数指针数组的指针
指向函数指针数组的指针是一个 指针,该指针指向一个 数组 ,数组的元素都是 函数指针。
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 (* pfArr[4])(int, int) = {Add, Sub};//int (*(* ppfArr)[4])(int, int) = &pfArr;//ppfArr是一个指向函数指针数组的指针变量return 0;
}
以上代码中的int (*(* ppfArr)[4])(int, int)就是一个指向函数指针数组的指针,(* ppfArr)可以看出ppfArr是一个指针变量,加上[4]说明是一个数组指针,指针指向的是一个数组,该数组的元素是什么呢?剩余部分就是数组的元素类型了(函数指针),由此,我们将其一步步拆开看, 其实就是一个指针,指向的是一个数组,数组的每个元素类型是函数指针。
八. 回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
什么意思呢?
我们将上面函数指针数组部分写的简单计算器,觉得冗余的代码,应用在此场景,作出修改编写如下。
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 i = 0, j = 0;printf("请输入两个整数:>");scanf("%d %d", &i, &j);printf("%d\n", pf(i, j));
}
#include<stdio.h>
int main()
{int input = 0;do{menu();printf("请选择:>");scanf("%d", &input);switch (input){case 1:Calc(Add);case 2:Calc(Sub);case 3:Calc(Mul);case 4:Calc(Div);case 0:printf("退出计算器\n");break;default:printf("输入有误\n");break;}} while (input);return 0;
}
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
九、qsort函数的应用
qsort 为quick sort的简写,意为快速排序,主要用于对各种类型的数组进行排序,需要引用头文件<stdlib.h>中
下面我们来看看它的用法
首先我们在 https://legacy.cplusplus.com/ 网站的旧版本,找到对qosrt函数具体规则的描述。
在搜索框搜索qsort,显示如下:
观察以上图的绿色字体部分,就是官方对于qsort函数的声明
void qsort(void* base, size_t num, size_t size, int (*compar)(const void*, const void*))解释:
- base -- 指向要排序的数组的第一个元素的指针。
- num -- 由 base 指向的数组中元素的个数。
- size -- 数组中每个元素的大小,以字节为单位。
- compar -- 用来比较两个元素的函数
qsort可以排序任意类型的数据
如果读者知道冒泡排序的算法,是如何实践的,我们可能就知道在比较两个整型的时候,可以用大于小于操作符来比较,在比较字符串的时候,我们可以使用strcmp函数,但是对于两个结构体数据,指定的比较的标准是什么呢?
所以我们不妨设计统一的标准,使各种类型数组元素都可以进行排序,具有通用性。
qsort在声明函数时的参数int (*compar)(const void*, const void*)就是让使用者,指定一个比较的方法(使用者写一个函数,对应此时的函数指针的标准),那么compar函数指针的具体规则是什么呢?
函数指针指向的函数,比较的是两个元素的大小,而p1和p2是待排序的两个元素的地址。
如果p1指向的元素小于p2指向的元素,则返回值是一个小于0的数值;
如果p1指向的元素等于p2指向的元素,则返回值是一个等于0的数值;
如果p1指向的元素大于p2指向的元素,则返回值是一个大于0的数值。
(0默认排序为升序,若想要排序为降序,将p1与p2调换即可)
话不多说,下面直接上代码。
1.利用qsort来排序整型数组
void print_arr(int arr[], int sz)
{int i = 0;for (i = 0; i < sz; i++){printf("%d ", arr[i]);}printf("\n");
}
//void表示无具体类型的指针,他可以接收任何类型的地址
int compar(const void* p1, const void* p2)
{//使用者在使用的时候需要访问几个字节,//就需要进行强制类型转换,为指向的元素类型是什么类型的指针//如元素地址是int*类型,就需要强制转换为int*return *(int*)p1 - *(int*)p2; //若将p1和p2调换,则打印的是降序顺序
}
#include<stdlib.h>
#include<stdio.h>
int main()
{int arr[] = { 9,8,7,6,5,4,3,2,1,0 };int sz = sizeof(arr) / sizeof(arr[0]);//qsort默认排成升序qsort(arr, sz, sizeof(arr[0]), compar);print_arr(arr,sz);return 0;
}
将结果打印:
2 利用qsort来排序结构体数据
首先编写一个简单的结构体
struct Stu
{char name[20];int age;
};int main()
{struct Stu s[] = { {"zhangsan",20},{"lisi",18},{"wangwu",30}};return 0;
}
按照年龄的大小来排序(升序)
struct Stu
{char name[20];int age;
};
//按照年龄来排序
int cmp_stu_by_age(const void* p1,const void* p2)
{return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age;
}
#include<stdio.h>
#include<stdlib.h>
int main()
{struct Stu s[] = { {"zhangsan",20},{"lisi",18},{"wangwu",30}};int sz = sizeof(s) / sizeof(s[0]);qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);return 0;
}
按F10调试代码,
排序后的结果正是我们想要的结果
数组从小到大排列,age分别是18,20,30
按照名字的大小来排序(升序)
字符串比较大小需要用到strcmp函数,在比较字符串时, 按照一位一位进行比较,本质比较的是其字符的ACSII码值,若字符串的第一位字符串的ASCII码值与另一个字符串的第一位字符的ASCII码值相等,继续向后比较两者的第二位ASCII码值、第三位……以此类推。
strcmp()函数的比较规则如下: 需要引用头文件<string.h>
- 如果字符串1的第n位的ASCII码值大于字符串2的第n位的ASCII码值 则输出结果:1,表示字符串1 > 字符串2;
- 如果字符串1与字符串2每一位的ASCII码值都相等,而且长度相同, 则输出结果:0 表示字符串1 == 字符串2;
- 字符串1的第n位的ASCII码值小于字符串2的第n位的ASCII码值,则输出结果:-1 表示字符串1 < 字符串2;
struct Stu
{char name[20];int age;
};
//按照名字来排序
int cmp_stu_by_name(const void* p1, const void* p2)
{return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
}
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{struct Stu s[] = { {"zhangsan",20},{"lisi",18},{"wangwu",30}};int sz = sizeof(s) / sizeof(s[0]);qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);return 0;
}
调试代码
排序后的结果正是我们想要的结果
字符串相比较,两两元素从第一个字符的ASCII码值进行比较,按照元素的大小,'l'<'w'<'z',第一个字符比较出了结果,就不需要进行比较第二个字符ASCII码值的大小了。
十、以冒泡排序的思想,模拟实现qsort函数
我们知道了qsort函数的用法,下面我们就使用冒泡排序的思想实现一个类似于qsort函数的适用于各种类型的冒泡排序。
先在main函数里,将排序的数组,和打印内容罗列下来。
#include<stdio.h> int main() {int arr[] = { 3,1,5,2,4,9,8,6,5,7 };int sz = sizeof(arr) / sizeof(*arr); //sizeof(*arr)表示首元素int i = 0;printf("排序前:>");for (i = 0; i < sz; i++){printf("%d ", arr[i]);}printf("\n");bubble_sort(arr, sz, sizeof(*arr), cmp_int);//实现通用的冒泡排序printf("排序后:>");for (i = 0; i < sz; i++){printf("%d ", arr[i]);}return 0;}
下面编写bubble_sort函数的内部逻辑:
//以冒泡排序思想模拟实现 //bubble_sort可以排序任意类型数组的数据 void bubble_sort(void* base,int num,int width,int (*cmp)(const void* p1, const void* p2)) {int i = 0;//趟数for (i = 0; i < num - 1; i++){int flag = 1;//假设该趟数有序//每趟冒泡排序的过程int j = 0;for (j = 0; j < num - 1 - i; j++){//两两元素进行比较//将base强转为char* + j乘以每个元素的大小, 相当于跳过了j*width个字节//将base强转为char* + (j + 1)乘以每个元素的大小, 相当于跳过了(j+1)*width 个字节if (cmp_int((char*)base + j * width, (char*)base + (j + 1) * width) > 0){flag = 0;//有交换就置为0//交换//封装一个Swap函数,用来交换元素Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);}}if (flag == 1)break;} }
将两个元素的交换部分,封装一个Swap()函数,专门在这个函数里面编写。
//传过来的是char*类型,如果要修改元素的大小,则需要将元素的每个字节进行交换 //所以这里也就是为什么要将width传递过来的原因了。 void Swap(char* buf1, char* buf2,int width) {int i = 0;for (i = 0; i < width; i++){char tmp = *buf1;*buf1 = *buf2;*buf2 = tmp;buf1++;buf2++;} }
最后,使用者自己编写一个比较元素的大小的函数。
int cmp_int(const void* p1,const void* p2) {return *(int*)p1 - *(int*)p2; }
最后将整体代码放在下面。
int cmp_int(const void* p1,const void* p2)
{return *(int*)p1 - *(int*)p2;
}
//传过来的是char*类型,如果要修改元素的大小,则需要将元素的每个字节进行交换
//所以这里也就是为什么要将width传递过来的原因了。
void Swap(char* buf1, char* buf2,int width)
{int i = 0;for (i = 0; i < width; i++){char tmp = *buf1;*buf1 = *buf2;*buf2 = tmp;buf1++;buf2++;}
}
//以冒泡排序思想模拟实现
//bubble_sort可以排序任意类型数组的数据
void bubble_sort(void* base,int num,int width,int (*cmp)(const void* p1, const void* p2))
{int i = 0;//趟数for (i = 0; i < num - 1; i++){int flag = 1;//假设该趟数有序//每趟冒泡排序的过程int j = 0;for (j = 0; j < num - 1 - i; j++){//两两元素进行比较//将base强转为char* + j乘以每个元素的大小, 相当于跳过了j*width个字节//将base强转为char* + (j + 1)乘以每个元素的大小, 相当于跳过了(j+1)*width 个字节if (cmp_int((char*)base + j * width, (char*)base + (j + 1) * width) > 0){flag = 0;//有交换就置为0//交换//封装一个Swap函数,用来交换元素Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);}}if (flag == 1)break;}
}
#include<stdio.h>
int main()
{int arr[] = { 3,1,5,2,4,9,8,6,5,7 };int sz = sizeof(arr) / sizeof(*arr); //sizeof(*arr)表示首元素int i = 0;printf("排序前:>");for (i = 0; i < sz; i++){printf("%d ", arr[i]);}printf("\n");bubble_sort(arr, sz, sizeof(*arr), cmp_int);//实现通用的冒泡排序printf("排序后:>");for (i = 0; i < sz; i++){printf("%d ", arr[i]);}return 0;}
最后,将代码打印达到了我们实现的效果。
🎐🎐🎐 创作不易,请给波支持点赞+关注再走吧,嘿~~~
相关文章:
【搞透C语言指针】那年我双手插兜, 不知道指针是我的对手
☃️内容专栏:【C语言】进阶部分 ☃️本文概括: 征服C语言指针!一篇文章搞清楚指针的全部要点。 ☃️本文作者:花香碟自来_ ☃️发布时间:2023.3.3 目录 一、字符指针 二、指针数组 三、数组指针 1.数组指针的定义…...
如何从 Android 手机上的 SD 卡恢复已删除的照片
为了扩展手机的存储空间,很多人都会在安卓手机上插入一张SD卡来存储一些大文件,比如电影、照片、视频等。虽然SD卡给我们带来了很大的方便,但我们还是避免不了数据丢失一些事故造成的。您是否正在为 SD 卡上的照片意外丢失而苦恼?…...
01-前端-htmlcss
文章目录HTML&CSS1,HTML1.1 介绍1.2 快速入门1.3 基础标签1.3.1 标题标签1.3.2 hr标签1.3.3 字体标签1.3.4 换行标签1.3.5 段落标签1.3.6 加粗、斜体、下划线标签1.3.7 居中标签1.3.8 案例1.4 图片、音频、视频标签1.5 超链接标签1.6 列表标签1.7 表格标签1.8 布…...
【YOLO系列】YOLOv5超详细解读(网络详解)
前言 吼吼!终于来到了YOLOv5啦! 首先,一个热知识:YOLOv5没有发表正式论文哦~ 为什么呢?可能YOLOv5项目的作者Glenn Jocher还在吃帽子吧,hh 目录 前言 一、YOLOv5的网络结构 二、输入端 (1…...
从 ChatGPT 爆火回溯 NLP 技术
ChatGPT 火遍了全网,多个话题频频登上热搜。见证了自然语言处理(NLP)技术的重大突破,体验到通用技术的无限魅力。GPT 模型是一种 NLP 模型,使用多层变换器(Transformer)来预测下一个单词的概率分…...
面了 6 家大厂,并拿下 5 家 offer,进大厂好像也没有那么困难吧....
前言 二月份的时候因为换工作的缘故,陆续参加了华为、蚂蚁、字节跳动、PDD、百度、Paypal 的社招面试,除了字节跳动流程较长,我主动结束面试以外,其他的都顺利拿到了 Offer。 最近时间稍微宽裕点了,写个面经…...
四、Spring对IoC的实现
1.IoC 控制反转 控制反转是一种思想。控制反转是为了降低程序耦合度,提高程序扩展力,达到OCP原则,达到DIP原则。控制反转,反转的是什么? 将对象的创建权利交出去,交给第三方容器负责。将对象和对象之间关系…...
Java语言如何求平方根
问题 在编程时,会遇到求平方根的问题,本次问题讲到如何使用Java来求解平方根。 方法 使用java.lang.Math类的sqrt(double)方法求平方根。Math是java.lang包中的类,所以就可以直接使用这个类。Double为对象中的基本类型。例如求正整数16的平方…...
C++20中的span容器
一.span容器 span 是 C20 中引入的一个新的标准容器,它用于表示连续的一段内存区间,类似于一个轻量级的只读数组容器。 span 是一个轻量级的非拥有式容器,它提供了对连续内存的引用。 span 的主要用途是作为函数参数,可以避免不…...
codeforces周赛div3#855记录
目录 总结 一,A. Is It a Cat? 二,B. Count the Number of Pairs 三,C1. Powering the Hero (easy version) 四,C2. Powering the Hero (hard version) 总结 真羡慕ACM校队的同学,能AC七八题,甚至ak …...
2022年考研结果已出,你上岸了吗?
官方公布:2022年考研人数为457万。 2月20号左右,全国考研分数已经陆续公布,现在已经过去一周左右的时间了,你上岸了吗,还是在等调剂,或者已经知道落榜不知道何去何从? 考研的热潮在近几年席卷…...
2023 工业互联网平台:智慧制硅厂 Web SCADA 生产线
我国目前是全球最大的工业硅生产国、消费国和贸易国,且未来该产业的主要增量也将来源于我国。绿色低碳发展已成为全球大趋势和国际社会的共识,随着我国“双碳”目标的推进,光伏产业链快速发展,在光伏装机需求的带动下,…...
6-2 SpringCloud快速开发入门:声明式服务消费 Feign实现消费者
声明式服务消费 Feign实现消费者 使用 Feign实现消费者,我们通过下面步骤进行: 第一步:创建普通 Spring Boot工程 第二步:添加依赖 <dependencies><!--SpringCloud 集成 eureka 客户端的起步依赖--><dependency>…...
Git-学习笔记01【Git简介及安装使用】
Java后端 学习路线 笔记汇总表【黑马-传智播客】Git-学习笔记01【Git简介及安装使用】Git-学习笔记02【Git连接远程仓库】Git-学习笔记03【Git分支】目录 01-git的历史 02-git和svn的对比 03-git的安装 04-向本地仓库中添加文件 05-修改文件内容并提交 06-删除本地仓库中…...
【Python】控制自己的手机拍照,并自动发送到邮箱
前言 嗨喽,大家好呀~这里是爱看美女的茜茜呐 今天这个案例,就是控制自己的摄像头拍照, 并且把拍下来的照片,通过邮件发到自己的邮箱里。 想完成今天的这个案例,只要记住一个重点:你需要一个摄像头 思路…...
八股文(二)
一、 实现深拷贝和浅拷贝 1.深拷贝 function checkType(any) {return Object.prototype.toString.call(any).slice(8, -1) }//判断拷贝的要进行深拷贝的是数组还是对象,是数组的话进行数组拷贝,对象的话进行对象拷贝 //如果获得的数据是可遍历的&#…...
在CANoe/CANalyzer中观察CAN Message报文的周期Cycle
案例背景: 该篇博文将告诉您,如何直观的,图示化的,查看CAN网络中各CAN Message报文的周期变化。 优质博文推荐阅读(单击下方链接,即可跳转): Vector工具链 CAN Matrix DBC CAN M…...
Linux命令·ls
ls命令是linux下最常用的命令。ls命令就是list的缩写缺省下ls用来打印出当前目录的清单如果ls指定其他目录那么就会显示指定目录里的文件及文件夹清单。 通过ls 命令不仅可以查看linux文件夹包含的文件而且可以查看文件权限(包括目录、文件夹、文件权限)查看目录信息…...
Mysql InnoDB 存储引擎笔记
1 存储引擎 简介 Mysql 存储引擎有多种:包括 MyISAM、InnoDB 和 Memory。 其中MyISAM 和 INNODB 的区别: 事务安全(MyISAM不支持事务,INNODB支持事务);外键 MyISAM 不支持外键, INNODB支持外…...
智慧工地AI视频分析系统 opencv
智慧工地AI视频分析系统通过pythonopencv网络模型图像识别技术,智慧工地AI视频分析算法自动识别现场人员穿戴是否合规。本算法模型中用到opencv技术,OpenCV基于C实现,同时提供python, Ruby, Matlab等语言的接口。OpenCV-Python是OpenCV的Pyth…...
小红书「高效达人筛选攻略」
三八女神节降临,诸多品牌纷纷开启铺垫预热,在各大平台借势宣传。而聚集庞大年轻女性消费群体的小红书,对“她营销”的重要性不言而喻。节点序幕拉开,面对海量达人信息,如何提前积草屯粮、高效备战? 本期千瓜…...
大话数据结构-线性表
1 定义 线性表是零个或多个数据元素的有限序列。 2 抽象数据类型 ADT 线性表(List)Data:线性表的数据对象集合为{al,a2,a3,....an},每个元素的类型均为DataType。其中,除第一个元素a1外,每一个元素有且只有一个直接前驱元素&…...
分布式缓存 Memcached Linux 系统安装
1.Memcached简介 Memcached是一个开源、高性能,将数据分布于内存中并使用key-value存储结构的缓存系统。它通过在内存中缓存数据来减少向数据库的频繁访问连接的次数,可以提高动态、数据库驱动之类网站的运行速度。 Memcached在使用是比较简单的&#…...
【数据结构】链表:看我如何顺藤摸瓜
👑专栏内容:数据结构⛪个人主页:子夜的星的主页💕座右铭:日拱一卒,功不唐捐 文章目录一、前言二、链表1、定义2、单链表Ⅰ、新建一个节点Ⅱ、内存泄漏Ⅲ、插入一个节点Ⅳ、销毁所有节点Ⅴ、反转一个链表3、…...
linux shell 入门学习笔记18 函数开发
概念 函数就是将你需要执行的shell命令组合起来,组成一个函数体。一个完整的函数包括函数头和函数体,其中函数名就是函数的名字。 优点 将相同的程序,定义,封装为一个函数,能减少程序的代码数量,提高开发…...
如何最巧妙回答HR面试“送命题”:你为什么离开上家公司?
一 HR面试存在“送命题”? 一个资深HR朋友聊到,他最近pass掉一个名校高材生。 其实洽谈过程还比较愉悦,小姑娘名校毕业,落落大方,薪酬要求比较合理,各方面都比较符合,最后就在决定要录用时,HR朋友随口问了句 “你为什么离开上家公司?”,小姑娘也是随口说了句“我不喜…...
注意力机制详解系列(五):分支与时间注意力机制
👨💻作者简介: 大数据专业硕士在读,CSDN人工智能领域博客专家,阿里云专家博主,专注大数据与人工智能知识分享,公众号:GoAI的学习小屋,免费分享书籍、简历、导图等资料&…...
创宇盾重保经验分享,看政府、央企如何防护?
三月重保已经迫近,留给我们的准备时间越来越少,综合近两年三月重保经验及数据总结,知道创宇用实际案例的防护效果说话,深入解析为何创宇盾可以在历次重保中保持“零事故”成绩,受到众多部委、政府、央企/国企客户的青睐…...
软件测试面试汇总
在浏览器中输入 URL,回车后发生了什么? 在浏览器中输入URL并按下回车键后,大致流程如下: 1、浏览器解析 URL,提取出协议(例如HTTP、HTTPS)、主机名和路径等信息。 2、浏览器查找该URL的缓存记录࿰…...
空指针,野指针
空指针在C/C中,空指针(null pointer)是指向内存地址0的指针变量。NULL在C/C中的定义为:#ifndef NULL#ifdef __cplusplus#define NULL 0#else#define NULL ((void *)0)#endif #endif从上面的代码定义中,我们可以发现在C…...
网站建设注意事情/电商平台运营
1.团队课程设计博客链接 https://www.cnblogs.com/hq9-/p/10278470.html 2.个人任务 1)权重功能实现 public static void accuAccordPercent(String tablename, String cname, Map<String, Double> per) {Connection conn null;PreparedStatement ps null;P…...
华为网站建设方案模板/宁德seo公司
将正则表达式直接写在mock里 Mock拦截Ajax的函数如下: Mock.mock(rurl, method, function) 已知 rurl 可以直接使用正则表达式,那么不妨碍直接把正则表达式写在 rurl 里,即如下: Mock.mock(/http:\/\/localhost:3000\/ywcklb\/g…...
网站开发需会的课程/青岛网站建设公司电话
4.1 开发完第一个鸿蒙应用后,下面在了解一下完整的鸿蒙应用打包发布后应该是什么样子:一个完整的打包后应用结构如下图所示,这里我们先了解结构,具体怎么打包很简单只要前提是要签名!1. HAP的分类HAP又可分为entry和feature两种模…...
南昌专业网站制作公司/百度sem
背景。元素的背景显示区域在内容区和内边距区,并且边框是画在背景之上的。这就是说如果边框的样式是dotted之类的,则边框空隙之间是可以看到背景的。 可以设置背景的声明有:background-color、background-image、background-position、backgr…...
网站建设APP的软件/趣丁号友情链接
3.6 大型企业中的云计算前两节已经介绍过,创业公司和中小型企业都在使用云获得巨大优势。很多情况下,这些小规模组织要么除了把应用部署到云中外没有其他可行方案,要么就是因为云解决方案明显的成本优势而显得采用其他部署方案没有意义。小规…...
做网站推广的话术/上海seo培训
12.1.委托概述12.1.2 委托的数据类型为了减少重复代码数量,可以将比较方法作为参数传递给 BubbleSort()方法。此外,为了将方法作为参数传递,必须有一个能够标识方法的数据类型——也就是委托。这里的委托类型是 Compar…...