【搞透C语言指针】那年我双手插兜, 不知道指针是我的对手

news2024/11/26 18:32:23

 

☃️内容专栏:【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");    
    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
}

这里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);//4

	printf("%p\n", &arr[0]);//int* 
	printf("%p\n", &arr[0]+1);//4

	printf("%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 --- arr
	for (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函数能接收什么参数?

  1.  一级指针
  2. 整型变量的地址
  3. 整型的一维数组名

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. 二级指针变量
  2. 一级指针变量的地址
  3. 整型的指针数组的数组名

五. 函数指针

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)(intfloat*) = test;

讲到这里,有人会质疑,以上的应用不相当于脱裤子放p,多此一举吗?那函数指针到底是如何应用的呢,答案当然是有的,在函数需要调用另一个函数的时候,那么就需要传入函数的地址了,在形参部分利用函数指针来接收,就需要具体用到函数指针了,这里就不多细讲,感兴趣的同学可以写一段代码,深切体会一下。

3.阅读两段有趣的代码

两段代码源于《C陷阱与缺陷》这本书

  (*(void (*)())0)(); //code1

  void (*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_t

pf_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   4
	do
	{
		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. 如果字符串1的第n位的ASCII码值大于字符串2的第n位的ASCII码值 则输出结果:1,表示字符串1 > 字符串2;
  2. 如果字符串1字符串2每一位的ASCII码值都相等,而且长度相同, 则输出结果:0 表示字符串1 == 字符串2;
  3. 字符串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;

}

最后,将代码打印达到了我们实现的效果。


🎐🎐🎐 创作不易,请给波支持点赞+关注再走吧,嘿~~~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/384822.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

UML常见图的总结

一、概述 UML&#xff1a;Unified Modeling Language&#xff0c;统一建模语言&#xff0c;支持从需求分析开始的软件开发的全过程。是一个支持模型化和软件系统开发的图形化语言、为软件开发的所有阶段提供模型化和可视化支持&#xff0c;包括由需求分析到规格&#xff0c;到…

ESP32设备驱动-SHT11温度湿度传感器驱动

SHT11温度湿度传感器驱动 SHT1x 数字湿度传感器是一种可回流焊接的传感器。 SHT1x 系列包括带有 SHT10 湿度传感器的低成本版本、带有 SHT11 湿度传感器的标准版本和带有 SHT15 湿度传感器的高端版本。 与 SHTxx 湿度传感器系列中的所有其他 Sensirion 传感器类型一样,它们经…

H2数据库连接时用户密码错误:Wrong user name or password [28000-214] 28000/28000 (Help)

H2数据库连接时用户密码错误: 2023-03-03 08:25:07 database: wrong user or password; user: "SA" org.h2.message.DbException: Wrong user name or password [28000-214]出现的问题配置信息原因解决办法org.h2.message.DbException: Wrong user name or password …

Python从入门到转行,看完这一篇就够了

Python零基础入门 零基础入门就得从最基本的变量&#xff0c;语法格式&#xff0c;数据类型&#xff0c;函数&#xff0c;作用域&#xff0c;模块等基本知识入手&#xff08;有编程基础基础也得这么来&#xff09; 和篮球一样&#xff0c;先来三步上篮&#xff1a; Python基…

Spring Boot 分片上传、断点续传、大文件上传、秒传

文件上传是一个老生常谈的话题了&#xff0c;在文件相对比较小的情况下&#xff0c;可以直接把文件转化为字节流上传到服务器&#xff0c;但在文件比较大的情况下&#xff0c;用普通的方式进行上传&#xff0c;这可不是一个好的办法&#xff0c;毕竟很少有人会忍受&#xff0c;…

MySQL面试题-基础篇

目录 前言 数据库基础 1.什么是关系型数据库和非关系型数据库&#xff1f; 2.什么是 SQL&#xff1f; 3.MySQL 有什么优点&#xff1f; 4.MySQL 的基础架构? 存储引擎 1.MySQL 支持哪些存储引擎&#xff1f;默认使用哪个&#xff1f; 2.MySQL 存储引擎架构了解吗&…

C# IoC控制反转学习笔记

一、什么是IOC IoC-Invertion of Control&#xff0c;即控制反转&#xff0c;是一种程序设计思想。 先初步了解几个概念&#xff1a; 依赖&#xff08;Dependency&#xff09;&#xff1a;就是有联系&#xff0c;表示一个类依赖于另一个类。 依赖倒置原则&#xff08;DIP&a…

苹果ipa软件下载网站和软件的汇总

随着时间的流逝&#xff0c;做苹果版软件安装包下载网站和软件的渐渐多了起来。 当然&#xff0c;已经关站、停运、下架、倒闭的苹果软件下载网站和软件我就不说了&#xff0c;也不必多说那些关站停运下架倒闭的网站和软件了。 下面我统计介绍的就是苹果软件安装包下载网站和软…

系列十二、MySQL管理

一、系统数据库 Mysql数据库安装完成后&#xff0c;自带了一下四个数据库&#xff0c;具体作用如下&#xff1a;二、常用工具 2.1、mysql 2.1.1、概述 该mysql不是指mysql服务&#xff0c;而是指mysql的客户端工具。 2.1.2、语法 # 语法 &#xff1a; mysql [options] [dat…

分库分表、分库分表带来的问题

文章目录1.分库垂直分库水平分库2.分表垂直分表水平分表3.水平分表的路由方式&#xff08;1&#xff09;范围路由&#xff08;2&#xff09;Hash 路由4.分库分表带来的问题分库&#xff08;1&#xff09;事务的问题注意&#xff08;2&#xff09;跨库 JOIN 问题分表&#xff08…

k8s-故障转移 livenessProbe readinessProbe 学习总结

k8s-故障转移学习总结 大纲 概念K8S中Pod健康检查livenessProbe & readinessProbelivenessProbe 存活探测实例readinessProbe 就绪探测实例 概念 故障转移有哪些情况 物理机故障程序故障 在计算机术语中&#xff0c;故障转移&#xff08;英语&#xff1a;failover&am…

深入底层源码的Listener内存马(内存马系列篇三)

写在前面 继前面的FilterServlet内存马技术&#xff0c;这是系列文章的第三篇了&#xff0c;这篇将给大家带来的是Listener内存马技术。 前置 什么是Listener&#xff1f; 监听器 Listener 是一个实现特定接口的 Java 程序&#xff0c;这个程序专门用于监听另一个 Java 对象…

Pag的2D渲染执行流程

Pag的渲染 背景 根据Pag文章里面说的&#xff0c;Pag之前长时间使用的Skia库作为底层渲染引擎。但由于Skia库体积过大&#xff0c;为了保证通用型&#xff08;比如兼容CPU渲染&#xff09;做了很多额外的事情。所以Pag的工程师们自己实现了一套2D图形框架替换掉Skia&#xff…

GC Garbage Collectors

本质一、算法1、哪些是垃圾&#xff1f;引用计数法&#xff1a;reference countPython中使用了。个对象如果没有任何与之关联的引用&#xff0c;即他们的引用计数都不为 0&#xff0c;则说明对象不太可能再被用到&#xff0c;那么这个对象就是可回收对象。漏洞&#xff1a;循环…

C/C++每日一练(20230303)

目录 1. 字符串相乘 2. 单词拆分 II 3. 串联所有单词的子串 1. 字符串相乘 给定两个以字符串形式表示的非负整数 num1 和 num2&#xff0c;返回 num1 和 num2 的乘积&#xff0c;它们的乘积也表示为字符串形式。 示例 1: 输入: num1 "2", num2 "3"…

【Linux】PXE+Kickstart无人值守安装系统

文章目录前言一、简介二、配置DHCP三、TFTP四、SYSLinux服务程序五、vsftpd服务六、Kickstart应答文件七、自动安装系统八、总结前言 本文来记录下PXEKickstart无人值守安装系统。 当需要安装OS的主机数量较多时&#xff0c;我们不可能通过U盘或光盘给这一台台主机去安装操作系…

SpringCloud项目报错和解决方法记录

1、项目在IDEA中正常启动&#xff0c;打包后报错 背景 项目本地是没有 application.yml 配置文件的&#xff0c;而是把配置文件放在nacos上&#xff0c;本地只有一个 bootstrap.yml 来绑定nacos上的配置文件。 项目在IDEA上是可以正常启动运行的&#xff0c;然后我就准备打包…

Hadoop之hdfs查看fsimage和edits

/opt/module/hadoop-3.1.3/data/dfs/name/current (1) Fsimage文件: HDFS文件系统元数据的一个永久性的检查点&#xff0c;其中包含HDFS文件系统的所有目录和文件inode的序列化信息。 (2) Edits文件:存放HDFS文件系统的所有更新操作的路径&#xff0c;文件系统客户端执行的所有…

10个优质的基于Node.js的CMS 内容管理平台

冬尽今宵长❝hi, 大家好, 我是徐小夕,之前和大家分享了很多「低代码可视化」和「前端工程化」相关的话题, 今天继续和大家聊聊「CMS」系统.❞内容管理系统 (「CMS」) 使没有强大技术背景的人也能够轻松发布内容。我们可以使用 「CMS」 来管理我们的内容和交付。市面上有不同类型…

触摸屏如何远距离无线采集各从站的模拟量信号?

本方案是昆仑通态触摸屏与4台DTD433FC无线模拟量信号测试终端进行无线 MODBUS 通信的实现方法。本方案中昆仑通态触摸屏作为主站显示各从站的模拟量信号&#xff0c;传感器、DCS、PLC、智能仪表等4个设备作为Modbus从站输出模拟量信号。方案中采用无线模拟量信号测控终端DTD433…