C语言进阶——指针修炼

本文详细介绍了C语言中与指针和数组相关的概念,包括字符指针、指针数组、数组指针、数组传参、指针传参的使用。强调了数组名是首元素地址,指针数组和数组指针的区别,并通过实例展示了如何处理二维数组和函数指针。此外,文章还探讨了回调函数的概念,以及如何通过函数指针数组实现简单的计算器功能。
摘要由CSDN通过智能技术生成

目录

一.字符指针

二.指针数组

三.数组指针

3.1 数组指针的定义

2.&数组名和数组名

3.数组指针的使用

四.数组传参

1.一维数组传参

2.二维数组传参

五.指针传参

1.一级指针传参

2.二级指针传参

六.函数指针

七.函数指针数组 

1.定义

 2.计算器

八.指向函数指针数组的指针

九.回调函数

1.冒泡排序

2.qsort函数

3.用qsort来改正冒泡排序

结束语


一.字符指针

在指针的类型中我们知道有一种指针类型为字符指针 char* ;

一般使用:

#include<stdio.h>
int main()
{
	char ch = 'w';
	char* pc = &ch;
	*pc = 'a';
	printf("%c", ch);
	return 0;
}

还可以把字符串放入字符指针

int main()
{
    const char* pstr = "hello bit.";//这里是把一个字符串放到pstr指针变量里了吗?
    printf("%s\n", pstr);
    return 0;
}

注:一个指针变量在32位机器下是4个字节,所以一个指针变量是存不下一个字符串的。
代码 const char* pstr = "hello bit."特别容易让同学以为是把字符串 hello bit 放到字符指针 pstr 里了,但是/本质是把字符串 hello bit. 首字符的地址放到了pstr中。
该指针变量存入的是该字符串首字符的地址。并且可以通过该地址打印出后面的字符,并且该指针变量加入const使代码更为严谨。

看一道题目

#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;
}

结果是

 注:str1和str2是否相等比较的不是该数组的内容,而是比较该数组名首元素的地址,而两个数组名的首元素的地址必然不相等。
这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针。指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4不同。

二.指针数组

在《指针》章节我们也学了指针数组,指针数组是一个存放指针的数组。

这里我们再复习一下,下面指针数组是什么意思?

整型数组 - 存放整型的数组

字符数组 - 存放字符的数组

指针数组 - 存放指针(地址)的数组

int* arr1[10]; //整型指针的数组
//描述:
//arr1数组有10个元素,且每个元素都为int*类型
 
char* arr2[4]; //一级字符指针的数组
//描述:
//arr2数组有4个元素,且每个元素都为char*类型
 
char** arr3[5];//二级字符指针的数组
//描述:
//arr3数组有5个元素,且每个元素都为char**类型
 

我们来看这段代码

#include<stdio.h>
int main()
{
	const char* arr[4] = { "abcdef","qsja","hello world","hehe" };
	int i = 0;
	for (i = 0; i < 4; i++)
	{
		printf("%s\n", arr[i]);
	}
	return 0;
}

 我们定义了一个字符指针数组,他是一个数组,数组中的每一个元素都是一个指针,虽然看上去是一个字符串,但是我们这是将一些字符串放入每一个元素中的,这每一个元素都是一个指针。就相当于将一个字符串赋值给一个指针,那我们就知道,这就是是相当于将字符串的首元素的地址赋值给指针了。而这些字符串都是只读的,所以要加上const。然后直接打印即可

 

举例指针数组——存放指针的数组

#include<stdio.h>
int main()
{
    int arr1[5] = { 1,2,3,4,5 };
    int arr2[5] = { 2,3,4,5,6 };
    int arr3[5] = { 3,4,5,6,7 };
    int arr4[5] = { 0,0,0,0,0 };
    //指针数组
    int* arr[4] = {arr1, arr2, arr3, arr4};
    int i = 0;
    for (i = 0; i < 4; i++)
    {
        int j = 0;
        for (j = 0; j < 5; j++)
        {
            printf("%d ", *(arr[i] + j));//arr[i][j]
        }
        printf("\n");
    }
 
    return 0;
}

这里是用一维数组来模拟二维数组,但是该一维数组不一定是连续的,因为严格意义上的二维数组的地址是连续的,所以该模拟的二维数组不是严格意义上的二维数组

当然指针中打印的方法很多

三.数组指针

3.1 数组指针的定义

字符指针 :存放字符地址的指针 ,指向字符的指针,char*

整型指针 :存放整型地址的指针 ,指向整型的指针,int*

浮点型的指针:存放浮点型地址的指针,指向浮点型的指针,float*,double*

下面俩个代码分别是什么 

int *p1[10];
int (*p2)[10];
//p1, p2分别是什么?

p1是指针数组                                                                  p2是数组指针
int (*p)[10];          p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫 数组指针。
这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。

2.&数组名和数组名

直接看代码分析 

#include<stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	
	printf("%p\n", arr);
	printf("%p\n", arr + 1);
 
	printf("%p\n", &arr[0]);
	printf("%p\n", &arr[0] + 1);
 
	printf("%p\n", &arr);
	printf("%p\n", &arr + 1);
 
	return 0;
}

我们看一下打印结果,发现

分析这三个的区别

arr - 数组名 - 数组首元素的地址,加一就是跳过一个字节

&arr[i] - &数组名[i] - 也是数组每个元素的地址,加一就是跳过一个字节

&arr - & 数组名 - 是数组的地址,加一就是跳过一个数组

当我们加一的时候,根据类型来看跳过的字节,最后一个跳过一个数组,所有是四十个字节

3.数组指针的使用

既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。

当我们打印数组的时候,根据我们以前学的内容

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	//最简单的方法,*(p + i),直接首元素地址去打印,然后加一,往后跳去打印
	int*p = arr;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}

	return 0;
}

当我们用数组指针来写

    //用数组指针, 这样不如下面用首元素的地址往后跳,找到数组的地址,然后用下标
	int (* p)[10] = &arr;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", (*p)[i]);
	}

但是有点大材小用了,我们再来看二维数组中的使用,会不会比以前的方法变的简单呢

void print1(int arr[3][4], int r, int c)
{
	int i = 0;
	for (i = 0; i < r; i++)
	{
		int j = 0;
		for (j = 0; j < c; j++)
		{
			printf("%d ", arr[i][j]);
			//i行j列去打印,二维数组的普通打印用下标
		}
		printf("\n");
	}
}

我们用数组指针来写

void print2(int(*p)[4], int r, int c)
{
	int i = 0;
	for (i = 0; i < r; i++)
	{
		int j = 0;
		for (j = 0; j < c; j++)
		{			
			printf("%d ", (*(p + i))[j]);
//先拿到数组指针找到这个数组,p是指针哪一行的,加i就是找到所有的行,然后再加上一个下标
		}
		printf("\n");
	}
}

打印的时候还有一种写法

printf("%d ", p[i][j]);
//这样写也是可以的,直接找到整个数组第一行地址,然后往后下一行,每一行的下标j,找到所有元素

需要注意的是:数组名arr,表示首元素的地址,但是二维数组的首元素是二维数组的第一行,所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址

我们二维数组名可以看成一个个指向一行数组的指针,先通过一次解引用,访问到这一行数组,然后再通过一个解引用,就可以很自然的访问到每一个元素了

如果不好理解,我们展开来讲

如图所示,我们给arr+1之后,它的地址跳过了一个一维数组的大小,也就是跳过了一行,所以我们可以通俗的把二维数组arr理解为,arr数组包含3个元素,每个元素都是元素个数为5、元素类型为int类型的数组

我们甚至可以换一种方式来定义二维数组:

int arr1[5]={1,2,3,4,5};
int arr2[5]={6,7,8,9,10};
int arr3[5]={0};

int  (  *  (arr[3])  )  [5] = { arr1,arr2,arr3 };

arr[3]//首先arr是一个数组,且包含3个元素,所以先和[3]结合
 
int (*) [5]//这是一个数组指针的类型,表明该指针指向一个数组
//且数组包含5个元素,每个元素是int类型
 
int (*(arr[3]))[5] //表明arr数组包含3个元素,且每个元素的类型都是数组指针

 分析一下下面有意思的代码

int arr[5];                //一个整型数组,数组有五个元素,每个元素都是int类型
int *parr1[10];        //一个指针数组,数组有十个元素,每个元素都是int*类型
int (*parr2)[10];       //这是一个数组指针,这个指针指向一个有十个元素的,每个元素都是int类型的数组
int (*parr3[10])[5];//首先parr3先跟[]结合,所以他是一个数组,这个数组有十个元素,每个元素的类型我们只需要将parr3[10]抽出来就知道了,所以他的每个元素的类型是int (*)[5],也就是每个元素都是一个数组指针,而这每一个数组指针都指向一个数组,这个数组是由五个元素的,每个元素都是int类型的

四.数组传参

1.一维数组传参

#include<stdio.h>
 
void test1()//参数该如何设计?
 
void test2()//参数该如何设立?
 
int main()
{
	int arr1[10] = { 0 };
	int* arr2[20] = { 0 };
	test1(arr1);
	test2(arr2);
}

我们来看参数如何去写

void test(int arr[])
//函数中arr传来,数组传参,参数的部分写为数组
{}
void test(int arr[10])
//函数中arr传来,数组传参,参数的部分写为数组,定义个大小也是可以的
{}
void test(int* arr)
//函数中arr传来,是首元素地址,每个元素都整型,数组传参,也可以用指针接受,所有用整型指针来接收
{}
void test2(int* arr[20])
//函数中arr2传来,是首元素地址,arr2是数组里面放的指针,传来后,用指针数组来接受地址
{}
void test2(int** arr)
//函数中arr2传来,是首元素地址,arr2是数组里面放的指针,形参里面的*arr,就是数组首元素地址解引用,就是数组,所有**arr就可以接收传来的指针
{}
void test2(int* arr)
//这样是不行的,传来的数组名是首元素地址,都是指针,所有我们接收的时候也要二级指针
{}

对于arr1,是一个整型数组,元素类型为int ,所以用int*的指针来接收;

对于arr2,是一个指针数组,元素类型为int* ,所以用int**的二级指针来接收。

2.二维数组传参

#include<stdio.h>
int main()
{
    int arr[3][5] = {0};
    test(arr);
    return 0;
}

我们来看参数如何去写

void test(int arr[3][5])
//传来的三行五列,我也用数组接收,也是三行五列接收
{}
void test(int arr[][])
//传来的是三行五列,行可以省略,但是列不可以省略
{}
void test(int arr[][5])
//这样不省略列,就是可以,能省略行,不能省略列!!!
{}

//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。

void test(int* arr)
//这个是不行的,传来二维数组,arr是第一行的地址,是一维数组的地址,一维指针是不行的
{}
void test(int* arr[5])
//这个是数组,存放指针的数组,更不行了
{}
void test(int(*arr)[5])
//这个*arr是指针,是数组指针,第一行的地址传来,传来的是数组的地址,可以接收
{}
void test(int** arr)
//二级指针是接收一级指针的地址,这里是二维数组,更不行了
{}

二维数组的数组名就是首元素地址。而其实二维数组的每个元素又是一个数组。所以

我们可以用数组指针 int(*arr)[5] 或是 二级指针来接收。

五.指针传参

1.一级指针传参

#include <stdio.h>
//一级指针p传来,p里面是数组首元素地址,
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,p里面是数组首元素地址,传给函数
	print(p, sz);
	return 0;
}

我们思考一下,当一个函数的参数部分为一级指针的时候,函数能接收什么参数?

void test1(int* p)
{}
//test1函数能接收什么参数?

传一个整型变量的地址

int a;

test(&a);

传一个一级指针过去

int* p=&a;

test(p);

传一个数组名过去

int arr[5];

test(arr);

2.二级指针传参

#include <stdio.h>
//传来的接收二级指针,我就二级指针来接受
void test(int** ptr)
{
	printf("num = %d\n", **ptr);
}
int main()
{
	int n = 10;
	int* p = &n;
	int** pp = &p;
	//二级指针传参,pp是二级指针,我用二级指针接收就可以了
	test(pp);
	test(&p);
	//传过去一级指针的地址,相当于二级指针,我也用二级指针接收就可以了
	return 0;
}

我们思考一下,当一个函数的参数部分为二级指针的时候,函数能接收什么参数?

void test1(int ** p)
{ }
//test1函数能接收什么参数?

二级指针

int** p;

test(p);

一级指针的地址

int* pp;

test(pp);

arr数组名,首元素地址,元素里面是地址,也就是指针数组

int* arr[10];

test(arr);

void test2(char ** p)
{ }
//test2函数能接收什么参数?

char c = 'b';

这个不行,是一级指针

char* pc = &c;

test(&pc);

这个可以,传的是二级指针,字符类型

char** ppc = &pc;

test(ppc);

这个也可以,传的是数组首元素地址,元素里面是地址,也就是指针数组

char* arr[10];

test(arr);

六.函数指针

数组指针——指向数组的指针
int arr[10];
int(*p)[10] = &arr;取出数组的地址放到p
&arr取出函数的地址

函数指针——指向函数的指针
int Add(int x, int y)
{}
int (*pf)(int, int) = &Add;取出函数的地址放到pf
&Add取出函数的地址

函数指针呢,其实就是指向函数的指针,我们可以类比的写出他的类型

#include<stdio.h>
int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int (*pA)(int, int) = &Add;
	return 0;
}

再来看一个代码

#include <stdio.h>
void test()
{
	printf("hehe\n");
}
int main()
{
	printf("%p\n", test);
	printf("%p\n", &test);
	return 0;
}

 我们发现,不仅&+函数名可以得到函数的地址,就连函数名本身也是函数的地址。

学会了定义和取函数地址,再来看怎么调用,调用的时候用定义的指针也可以,也可以不带*

int Add(int x, int y)
{
	return x + y;
}
int main()
{
	printf("%p\n", &Add);//直接可以打出函数的地址,把函数的地址存起来就是函数指针
	int (*pf)(int, int) = &Add;//pf就是一个存放函数地址的指针

	int ret = Add(2, 3);//现在调用函数,用函数名来使用
	int ret = (*pf)(2, 3);//用pf也可以进入到函数Add里面
	int ret = pf(2, 3);//因为Add和&Add是一样的,直接用Add,我们Add是给pf的,是一回事,所有pf也可以直接调用,
	// pf 和 *pf 是一样的,都是指针指向函数,*更好理解,pf是指针解引用去调用,但是这里函数没有区别,都可以调用

	printf("%d\n", ret);

	return 0;
}

我们最后来看两个代码,分析他们

#include<stdio.h>
int main()
{
	//代码1
	(*(void (*)())0)();
	//代码2
	void (*signal(int, void(*)(int)))(int);
	return 0;
}

首先是代码1:

这段代码可以一看的话,就懵逼了,其实大可不必,这段代码的意思是是将0强制类型转化为void (*)()类型的函数指针,然后再去调用0地址处的这个函数

我们在来分析代码2:

这是一次函数声明

声明的函数名字是signal

signal有两个参数,一个是int,一个是函数指针类型:void(*)(int),

signal的返回类型是也是一个函数指针:类型是void(*)(int)

当然这样我们会发现还是比较难以理解,我们可以使用一个typedef来简化代码

void (*signal(int, void(*)(int) )  )(int);
该代码是一次函数的声明,声明的函数名字叫signal
	signal函数的参数有2个,第一个是int类型,第二个是函数指针类型
	该函数指针能够指向的那个函数的参数是int,返回类型是void

	signal函数的返回类型是一个函数指针
	该函数指针能够指向的那个函数的参数是int, 返回类型是void
	相当于三个套娃函数指针

简化一下(typedef)类型简化,typedef unsigned int uint;(简化方法)

	typedef void(*pf_t)(int);//pfun_t就是函数指针的类型
	pf_t signal(int, pf_t);//这就是简化完,

七.函数指针数组 

1.定义

函数指针数组——存放函数指针的数组(存放函数的地址)

int my_strlen(const char* str)
{
	return 0;
}

int main()
{
	//指针数组
	char* ch[5];

	int arr[10] = {0};
	//pa是数组指针
	int (*pa)[10] = &arr;
	
	//pf是函数指针
	int (*pf)(const char*) = &my_strlen;

	//函数指针数组
	int (*pfA[5])(const char*) = { &my_strlen };
	return 0;

定义:int (*parr1[10])();      parr1 先和 [] 结合,说明 parr1是数组,数组的内容是什么呢?
是 int (*)() 类型的函数指针。

//首先,函数指针数组是数组
 
parr[];
 
//数组的每个元素是函数指针,以无参函数,且无返回值的函数为例
 
void (*) ();
 
//结合后
 
void (*) () parr[];
 
//规范的写法
 
void (*parr[])()

多举几个例子

int (*parr1[5]) (int a,int b);
 
char (*parr2[10]) (int* a,int* b);
 
double (*parr3[10]) (double a.double* b);
 
int* (*parr4[10]) (int (*pfun) (int a,int b).int* a);
 
//....

 2.计算器

用我们以前学的写出加减乘除计算器

#include <stdio.h>

void menu()
{
	printf("*******************************\n");
	printf("****** 1. add   2. sub    *****\n");
	printf("****** 3. mul   4. div    *****\n");
	printf("****** 0. exit            *****\n");
	printf("*******************************\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;
}

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;
}

但是switch语句很长,我们用函数指针数组来优化,实现多种运算都可以

void menu()
{
	printf("*******************************\n");
	printf("****** 1. add   2. sub    *****\n");
	printf("****** 3. mul   4. div    *****\n");
	printf("****** 0. exit            *****\n");
	printf("*******************************\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;
}


int (*pf[5])(int, int) = { NULL, Add, Sub, Mul, Div };
//函数指针数组存放上述函数的地址
//但是也限定死了,只能是两个整型的操作数才能
//需要注意的是,我设置的计算是1,2,3,4,进入计算,所以第一个函数指针设置为0,后面对应1,2,3,4
int main()
{
	int input = 0;
	int x = 0;
	int y = 0;
	int ret = 0;
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		//也需要判断我们输入的数进行选择
		if (input == 0)
		{
			printf("退出计算器\n");
			break;
		}
		else if (input>=1 &&input<=4)
		{
			//直接用函数指针数组,用函数指针,直接进入到函数里,用不用*都一样
			printf("请输入两个操作数:>");
			scanf("%d %d", &x, &y);
			ret = pf[input](x, y);
			printf("%d\n", ret);
		}
		else
		{
			printf("选择错误\n");
		}
	} while (input);

	return 0;
}

思路就是:创建一个函数指针数组,去存放我的功能函数,加法函数,减法函数等等,里面设计的指针pf,在主函数中,先去判断你选择的数是否正确,是否是退出,然后就是用pf去进入函数去计算,小细节,我的计算功能函数是1,2,3,4...,但是我函数指针数组下标从0开始,就需要NULL

八.指向函数指针数组的指针

首先,指向函数指针数组的指针是一个指针。

指针指向一个数组,数组的每个元素都是函数指针。

int main()
{
	int arr[10];
	int (*pA)[10] = &arr;

	函数指针数组
	int (* pf[5])(int, int);

	ppf是指向函数指针数组的指针
	int (*(* ppf)[5])(int, int) = &pf;
	函数指针类型
	int (*)(int, int) = &pf;
	在这个基础上加上指针,ppf就是指向的这个函数指针数组
	ppf存的是函数指针数组的地址
	pf是存放函数指针的数组,ppf是指向这些放函数指针的数组的指针
	return 0;
}

我们来看怎么定义指向函数指针数组的指针

#include<stdio.h>
void test(const char* str)
{
	printf("%s\n", str);
}
int main()
{
	//函数指针pfun
	void (*pfun)(const char*) = test;
	//函数指针的数组pfunArr
	void (*pfunArr[5])(const char* str);
	pfunArr[0] = test;
	//指向函数指针数组pfunArr的指针ppfunArr
	void (*(*ppfunArr)[5])(const char*) = &pfunArr;
	return 0;
}

我们把这些展开分析

//指向函数指针数组pfunArr的指针ppfunArr
 
void (*(*ppfunArr)[5])(const char*) = &pfunArr;
 
//首先我们看到*与ppfunArr结合
 
//所以ppfunArr是一个指针,我们将*ppfunArr记作a
 
void (*a[])(const char*)
 
//现在我们看到,a与[]先结合
//说明指针a指向的是一个数组
//接下来将a[]移除
 
void (*) (const char*)
 
//剩下的部分我们已经学过,是一个函数指针
//说明数组的元素类型是函数指针
 
//总结,ppfunArr是一个指针,指向一个数组,数组的每个元素都是函数指针
//所以,ppfunArr称为指向函数指针数组的指针

九.回调函数

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个
函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数
的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进
行响应。
 

如果我们用回调函数来优化上面我们写的计算器

void menu()
{
	printf("*******************************\n");
	printf("****** 1. add   2. sub    *****\n");
	printf("****** 3. mul   4. div    *****\n");
	printf("****** 0. exit            *****\n");
	printf("*******************************\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;
}
//回调函数,写一个函数指针指向函数,传进来都是计算用的函数指针
//把我计算用的这些Add,Sub等等这些函数,传给calc这个函数,然后用clac函数参数的部分是函数指针
//这个clac函数通用了,可以加减乘除,就看传谁的地址,通过传来的函数地址,去找到这个函数
void calc(int (*pf)(int, int))
{
	int x = 0;
	int y = 0;
	int ret = 0;
	printf("请输入两个操作数:>");
	scanf("%d %d", &x, &y);
	ret = pf(x, y);
	printf("%d\n", ret);
}

int main()
{
	int input = 0;
	
	do 
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			calc(Add);
			//把Add这个函数地址传给clac里面,就是pf这个函数指针,这个pf函数指针指向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;
}

思路就是:我们先写一个calc函数,参数用函数指针来接受,我们前面写的有功能函数,这时候我们主函数中,调用calc函数,把功能函数的地址传到calc函数中,calc函数正好用函数指针来接受,calc函数中指针pf通过传来的功能函数地址,找到这些函数,去进行计算,然后返回值再打印,相当于calc就是一个中介

1.冒泡排序

两两相邻函数进行比较

弊端:只能排序整数

void bubble_sort(int arr[], int sz)
{
	//趟数
	int i = 0;
	for (i = 0; i < sz - 1; i++)
	{
		//一趟冒泡排序的过程
		int j = 0;
		for (j = 0; j < sz-1-i; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
	}
}
int main()
{

	int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr, sz);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
}

2.qsort函数

void qsort(void* base, //待排序的数组的起始地址
	size_t num,    //元素个数
	size_t width,  //一个元素的大小
	int (*cmp)(const void* e1, const void* e2)//两个元素的比较函数
);

 首先我们先使用一下qsort函数,来对整型进行排序

int cmp_int(const void* e1, const void* e2)
{
	return *(int*)e1 - *(int*)e2;
	//参数是无类型的指针来接受,所以这里要强制类型转化为int,然后再解引用
}
int main()
{

	int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, sz, sizeof(arr[0]), cmp_int);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
}

qsort就是这样实现的,把你提供的函数 cmp_int 地址传到qsort,qsort里用函数指针去接收,然后去指向这个函数,也就是一个回调函数,e1,e2,是要比较的这两个元素的地址,e1<e2返回小于零的数

3.用qsort来改正冒泡排序

冒泡排序可以排任何类型的,进行改造,怎么改

第一层循环是不变的,内部两两比较也不变,变的是判断,整型大小比较才能用大小号,字符或者浮点型比较方法不一样,当我们想让这个冒泡排序任何地方使用,是不是就是把比较的部分抽出来,你想排序整型,你自己提供一个排序整型的方法,想排序啥样的,就提供什么样子的

我们进入正题

函数1是用库函数qsort排序整型

函数2是用库函数qsort排序结构体

函数3是用冒泡排序模拟实现库函数qsort排序整型

函数4是用冒泡排序模拟实现库函数qsort排序结构体

int cmp_int(const void* e1, const void* e2)
{
	return *(int*)e1 - *(int*)e2;
	//参数是无类型的指针来接受,所以这里要强制类型转化为int,然后再解引用
}
//对于整型的排序
void test1()
{

	int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, sz, sizeof(arr[0]), cmp_int);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
}
//先定义一个结构体
struct Stu
{
	char name[20];
	int age;
};

//先对结构体的年龄比较
int cmp_stu_by_age(const void* e1, const void* e2)
{
	return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
	//参数是无类型的指针来接受,所以这里要强制类型转化为struct Stu*,相当于拿到了结构体指针,然后再箭头指向年龄
}
//再对结构体的名字比较
int cmp_stu_by_name(const void* e1, const void* e2)
{
	return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
	//参数是无类型的指针来接受,所以这里要强制类型转化为struct Stu*,相当于拿到了结构体指针,然后再箭头指向名字
	//名字比是字符串比较不能用减号了,用函数strcmp,比较对应位置上的字符,strcmp返回值跟qsort返回值一样的
}
//对于结构体的排序
void test2()
{
	//结构体的描述
	struct Stu s[3] = { {"zhangsan",20}, {"lisi", 50}, {"wangwu", 33} };
	int sz = sizeof(s) / sizeof(s[0]);
	qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
	qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
}

上面test1和test2是用库函数qsort来排序整型和结构体
上面test3和test4是用冒泡排序的思想模拟实现qsort来排序整型和结构体

//改造冒泡排序函数,使得这个函数可以排序任意指定的数组
void bubble_sort(void* base, size_t sz, size_t width, int (*cmp)(const void* e1, const void* e2))
//为了能接收各种类型所以void* base,sz就是元素的个数, width宽度你只知道个数,宽度为了知道几个字节,最后用函数指针用来,在这个函数中指针接收要比较的地址
{
	size_t i = 0;
	for (i = 0; i < sz - 1; i++)
	{
		size_t j = 0;
		for (j = 0; j < sz - 1 - i; j++)
		{
			if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
			//这里是找我们要比较的那俩地址,最开始是base,我们用width去找这一对,往后跳对应字节去找,用j可以找后面所有的要比较的数了
			//然后这俩地址传给e1,e2,就是通过指针找到比较函数
			{
				Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
				 //交换,交换函数,先把这俩地址给我,再把宽度给我
			}
		}
	}
}
//交换函数,用char接收,然后也接收宽度,交换的细节,是一个字节的交换,不是整个的交换,然后往后跳
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 test3()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);
	//我们arr传给base,最开始的地址是base,第二个是加上一个,但是是无类型的,要给她类型
	//但是给他什么类型,也不能写死,所以我们用我们写的宽度,单位是字节,所以我们转化为char的指针
	//然后我找base后面的地址,就可以用字节(宽度)跳过
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
}

//使用我们自己写的bubble_sort函数排序结构体数组
//结构体的时候跟上面的是一样的
void test4()
{
	struct Stu s[3] = { {"zhangsan",20}, {"lisi", 50}, {"wangwu", 33} };
	int sz = sizeof(s) / sizeof(s[0]);
	bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_age);
	bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_name);
}

int main()
{
	//用库函数qsort实现排序
	test1();//整型
	test2();//结构体
	//自己用冒泡排序的思想模拟实现qsort
	test3();//整型
	test4();//结构体
	return 0;
}

函数1函数2,就是我们上面用库函数来进行排序,很方便,只要我们创建一个类型函数,是要去排序整型还是结构体还是其他的,我们去创建一个,然后用函数指针去接收

函数3函数4,就是我们用冒牌排序的思想模拟实现这个库函数,我们单独拿出来分析

我们先看这两个主体函数,是我们自己写的bubble_sort函数,我们思考要给他接收什么样的参数

void test3()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);
	//我们arr传给base,最开始的地址是base,第二个是加上一个,但是是无类型的,要给她类型
	//但是给他什么类型,也不能写死,所以我们用我们写的宽度,单位是字节,所以我们转化为char的指针
	//然后我找base后面的地址,就可以用字节(宽度)跳过
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
}

//使用我们自己写的bubble_sort函数排序结构体数组
void test4()
{
	struct Stu s[3] = { {"zhangsan",20}, {"lisi", 50}, {"wangwu", 33} };
	int sz = sizeof(s) / sizeof(s[0]);
	bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_age);
	bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_name);
}

bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);

传要排序的数组,大小,字节,还有我们提供的函数地址

bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_age);

bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_name);

结构体也是一样的,要传过去什么,最重要的是我们提供的函数

下面这些就是我们的提供的函数

int cmp_int(const void* e1, const void* e2)
{
	return *(int*)e1 - *(int*)e2;
	//参数是无类型的指针来接受,所以这里要强制类型转化为int,然后再解引用
}
//先定义一个结构体
struct Stu
{
	char name[20];
	int age;
};

//先对结构体的年龄比较
int cmp_stu_by_age(const void* e1, const void* e2)
{
	return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
	//参数是无类型的指针来接受,所以这里要强制类型转化为struct Stu*,相当于拿到了结构体指针,然后再箭头指向年龄
}
//再对结构体的名字比较
int cmp_stu_by_name(const void* e1, const void* e2)
{
	return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
	//参数是无类型的指针来接受,所以这里要强制类型转化为struct Stu*,相当于拿到了结构体指针,然后再箭头指向名字
	//名字比是字符串比较不能用减号了,用函数strcmp,比较对应位置上的字符,strcmp返回值跟qsort返回值一样的
}

下面就是主题,我们设计的排序函数

//改造冒泡排序函数,使得这个函数可以排序任意指定的数组
void bubble_sort(void* base, size_t sz, size_t width, int (*cmp)(const void* e1, const void* e2))
//为了能接收各种类型所以void* base,sz就是元素的个数, width宽度你只知道个数,宽度为了知道几个字节,最后用函数指针用来,在这个函数中指针接收要比较的地址
{
	size_t i = 0;
	for (i = 0; i < sz - 1; i++)
	{
		size_t j = 0;
		for (j = 0; j < sz - 1 - i; j++)
		{
			if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
			//这里是找我们要比较的那俩地址,最开始是base,我们用width去找这一对,往后跳对应字节去找,用j可以找后面所有的要比较的数了
			//然后这俩地址传给e1,e2,就是通过指针找到比较函数
			{
				Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
				 //交换,交换函数,先把这俩地址给我,再把宽度给我
			}
		}
	}
}

首先就是用什么去接收 

void bubble_sort(void* base, size_t sz, size_t width, int (*cmp)(const void* e1, const void* e2))
为了能接收各种类型所以void* base,sz就是元素的个数, width宽度你只知道个数,宽度为了知道几个字节,最后用函数指针用来,在这个函数中指针接收要比较的地址

然后就是去判断两个比较数

 if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
这里是找我们要比较的那俩地址,最开始是base,我们用width去找这一对,往后跳对应字节去找,用j可以找后面所有的要比较的数了,然后这俩地址传给e1,e2,就是通过指针找到比较函数

判断完去交换位置

Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);

交换,交换函数,先把这俩地址给我,再把宽度给我

交换函数

//交换函数,用char接收,然后也接收宽度,交换的细节,是一个字节的交换,不是整个的交换,然后往后跳
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++;
	}
}

思路:

当我们整体看的时候,我们先进入到test3函数中,到我们的冒泡排序bubble_sort中传入数据

传到cmp_int时候,是把cmp_int函数的地址传到bubble_sort函数中的参数int(*cmp)(const void* e1, const void* e2)函数指针中,也就是cmp

所以我bubble_sort函数中的cmp指针指向的就是cmp_int函数,然后我们开始进入循环然后到判断,利用base和wide宽度找到要比较的两个数的地址

由cmp指针指向到cmp_int函数(把这俩比较数的地址传来,给e1,e2)然后算出返回值,然后看是否进行交换,进入到swap交换函数中去交换

结束语

指针有很多深奥的地方,希望大家好好消化,还会出后记一些小练习增加印象

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值