指针的进阶

指针的主题,我们在初级阶段的《指针》章节已经接触过了,我们知道了指针的概念:

  1. 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
  2. 指针的大小是固定的4/8个字节(32位平台/64位平台)。
  3. 指针是有类型,指针的类型决定了指针的+-整数的步长,指针解引用操作的时候的权限。
  1. 指针的运算。

字符 指针

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

一般使用

cpp 复制代码
int main()
{
    char ch = 'w';
    char *pc = &ch;
    *pc = 'w';
    return 0;
}

还有一种使用方式如下:

cpp 复制代码
int main()
{
    const char* pstr = "hello jzy.";//这里是把一个字符串放到pstr指针变量里了吗?不是的,是将字符串首元素地址传过去了,最好前边要加const 修饰常量字符串
    printf("%s\n", pstr);

    char arr[] = "abcdef";//[a b c d e f \0],这种初始化方式等价于char arr[]={'a','b','c','d','e','f','\0'};,因为字符串末尾自动跟一个\0
    return 0;
}

这个字符串是存在代码段的(内存的只读常量区)

"hello jzy."

面试题:

#include <stdio.h>
int main()
{
char str1[] = "hello jzy.";
char str2[] = "hello jzy.";
const char *str3 = "hello jzy.";
const char *str4 = "hello jzy.";
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[4]; //一级字符指针的数组
char **arr3[5];//二级字符指针的数组
我们看个例子
使用指针数组模拟实现二维数组

cpp 复制代码
int main()
{
	int arr1[] = { 1,2,3,4,5 };//arr1 - int*
	int arr2[] = { 2,3,4,5,6 };
	int arr3[] = { 3,4,5,6,7 };

	//指针数组
	int* arr[3] = { 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("\n");
	}
	return 0;
}

数组名是数组首元素的地址,arr[1]拿到的是arr1数组名,再使用下标就能访问数组的元素,很简单
补充一下数组名的理解 数组名是数组首元素的地址
有2个例外:

  1. sizeof(数组名),这里的数组名不是数组首元素的地址,数组名表示整个数组,sizeof(数组名)计算的是整个数组的大小,单位是字节
  2. &数组名,这里的数组名表示整个数组, &数组名取出的是整个数组的地址
    除此之外,所有的地方的数组名都是数组首元素的地址

    可以看到sizeof数组名并不是4或者8,而是整个数组的大小40,这是一个例外

    可以看到arr+1跳过4个字节1个整形,说明arr正常是首元素地址
    &arr+1,取出来整个数组的地址,所以+1会跳过整个数组40字节

数组指针(指向一个数组的指针)

数组指针的定义
数组指针是指针?还是数组?
答案是:指针。
我们已经熟悉:
整形指针: int * pint; 能够指向整形数据的指针。
浮点型指针: float * pf; 能够指向浮点型数据的指针。
那数组指针应该是:能够指向数组的指针。
下面代码哪个是数组指针?
int *p1[10];//是存放指针的数组,指针数组
int (*p2)[10];//是指向数组的指针,数组指针
//p1, p2分别是什么?
解释
int (*p)[10];
//解释:p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。
//这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。
数组指针的使用(遍历可以用到),p是一个数组指针变量,解引用p拿到数组名然后可以用方括号进行访问元素

cpp 复制代码
void Print(int arr[3][5], int r, int c)
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 5; 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;
}

//数组指针怎么使用呢?一般在二维数组上才方便

//

//1 2 3 4 5

//2 3 4 5 6

//3 4 5 6 7

//

//二维数组传参,形参是二维数组的形式
也可以换一种形式

//二维数组传参,形参是指针的形式

void Print(int (*p)[5], 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));

}

printf("\n");

}

}

int main()

{

int arr[3][5] = { 1,2,3,4,5, 2,3,4,5,6, 3,4,5,6,7 };

//arr 是数组名,数组名表示数组首元素的地址

Print(arr, 3, 5);

return 0;

}

cpp 复制代码
void test1(int arr[5], int sz)
{}
void test2(int* p, int sz)
{}

int main()
{
	int arr[5] = { 0 };
	test1(arr, 5);
	test2(arr, 5);
	return 0;
}

//一维数组传参,形参的部分可以是数组,也可以是指针

cpp 复制代码
void test3(char arr[3][5], int r, int c)
{}

void test4(char (*p)[5], int r, int c)
{}
int main()
{
	char arr[3][5] = {0};
	test3(arr, 3, 5);
	test4(arr, 3, 5);

	return 0;
}

//二维数组传参,形参的部分可以是数组,也可以是指针
以上就是对数组指针的介绍

数组参数、指针参数

在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?
1 一维数组传参

#include <stdio.h>
void test(int arr[])//ok?ok数组名传参可以是指针可以是数组
{}
void test(int arr[10])//ok?//ok注意方块里的数字填不填都可以,因为本质这部分不会创建数组,本质是指针
{}
void test(int *arr)//ok?//包ok的
{}
void test2(int *arr[20])//ok?包ok的是一个指针数组
{}
void test2(int **arr)//ok?包的,二级指针,一级指针数组首元素的地址包是二级指针类型
{}
int main()
{
int arr[10] = {0};
int *arr2[20] = {0};
test(arr);
test2(arr2);
}
2 二维数组传参
void test(int arr[3][5])//ok?包的,类似一维数组
{}
void test(int arr[][])//ok?不可以之只能省略第一个数,第二个必须要有,因为要知道一行几个元素
{}
void test(int arr[][5])//ok?包可以的
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int *arr)//ok?包不行的,二维数组数组名是第一行数组的地址,这是整形地址
{}
void test(int* arr[5])//ok?不行这是指针数组
{}
void test(int (*arr)[5])//ok?包可以的,最规范的写法
{}
void test(int **arr)//ok?不行数组指针跟二级指针差距很大
{}
int main()
{
int arr[3][5] = {0};
test(arr);
}

函数指针(指向函数的指针)

看段代码

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


输出的是两个地址,这两个地址是 test 函数的地址。
那我们的函数的地址要想保存起来,怎么保存?
下面我们看代码:

cpp 复制代码
void test()
{
 printf("hehe\n");
}
//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (*pfun1)();//对
void *pfun2();//err

首先,能给存储地址,就要求pfun1或者pfun2是指针,那哪个是指针?
答案是:
pfun1可以存放。pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void。
我们来阅读两段有趣的代码

cpp 复制代码
//代码1
(*(void (*)())0)();//本质上是调用0地址处的函数,先将0强制类型转换为void (*)()  类型的函数指针,后调用0地址处的函数-》函数名()只是没有传参
//代码2
void (*signal(int , void(*)(int)))(int);

:推荐《C陷阱和缺陷》
这本书中提及这两个代码。
代码2太复杂,如何简化:
typedef void(*pfun_t)(int);//把类型重命名
pfun_t signal(int, pfun_t);
函数指针数组
数组是一个存放相同类型数据的存储空间,那我们已经学习了指针数组,
比如:
int *arr[10];
//数组的每个元素是int*
那要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
int (*parr1[10])();
int *parr2[10]();
int (*)() parr3[10];
答案是:parr1
parr1 先和 [] 结合,说明 parr1是数组,数组的内容是什么呢?
是 int (*)() 类型的函数指针。
用函数指针数组的方式实现一个转移表

cpp 复制代码
//函数指针数组的方式


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");
}
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_s("%d", &input);
		if (input >= 1 && input <= 4)
		{
			printf("请输入两个操作数:");
			scanf_s("%d %d", &x, &y);
			ret = pfArr[input](x, y);
			printf("ret = %d\n", ret);
		}
		else if(input == 0)
		{
			printf("退出计算器\n");
		}
		else
		{
			printf("选择错误,重新选择\n");
		}
	} while (input);

	return 0;
}

就可以玩了,原理代码就很清晰了
指向函数指针数组的指针(了解一下就行)

cpp 复制代码
void test(const char* str)
{
	printf("%s\n", str);
}

int main()
{
	void (*pf)(const char*) = test;//pf是函数指针变量
	void (*pfArr[10])(const char*);//pfArr是存放函数指针的数组
	void (* (*p) [10])(const char*) = &pfArr;//p指向函数指针数组的指针

	return 0;
}

回调函数

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



直接使用qsort

cpp 复制代码
#include <stdlib.h>

int cmp_int(const void* p1, const void* p2)
{
	return (*(int*)p1 - *(int*)p2);
}

void print(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
}

//测试qsort排序整型数据
void test1()
{
	int arr[10] = { 3,1,5,2,4,7,9,6,8,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	//默认是升序的
	qsort(arr, sz, sizeof(arr[0]),cmp_int);//
	print(arr, sz);
}

int main()
{
	test1();
	return 0;
}

这里前三个参数都很简单,第四个参数要自己写,通过自己比较的数据相邻数据的大小判断升序降序,左边数据大于右边数据,返回大于0的数字,默认是升序(这就是回调函数,我们把函数指针传递给参数,等我们真正需要用的时候会调用这个cmp_int函数)!!!注意,我们自己写的cmp_int函数一定要和库里的保持一致,参数是const void*,在cmp_int函数体内,我们想比较什么类型的数据,就强转为什么类型很方便)

测试结构体年龄排序

cpp 复制代码
#include <stdlib.h>
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;
}

void test2()
{
	struct Stu arr[] = { {"zhangsan", 20}, {"lisi", 50},{"wangwu", 15} };
	int sz = sizeof(arr) / sizeof(arr[0]); 
	qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_age);
}


int main()
{
	test2();
	return 0;
}

可以看到没排序之前是乱序

排序后是升序

测试结构体名字排序

cpp 复制代码
#include <stdlib.h>
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);
}

void test3()
{
	struct Stu arr[] = { {"zhangsan", 20}, {"lisi", 50},{"wangwu", 15} };
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);
}


int main()
{
	test3();
	return 0;
}

没排序之前是乱序

排序之后变成升序了
注意我们为什么要用void*作为参数接受呢

cpp 复制代码
int main()
{
	int a = 10;
	float f = 3.14f;
	//int* pa = &a;
	//char* pc = &a;//err会提示类型不兼容
	void* pv = &a;
	pv = &f;
	*pv;//err
	pv++;//err
	return 0;
}

void* 的指针 - 无具体类型的指针

void* 类型的指针可以接收任意类型的地址(可以看到pv可以接收int也可以接收float没有报错,不能进行解引用和++操作,因为void*类型不确定)
我们采用回调函数模拟实现qsort函数(用冒泡排序实现,但是底层是快排)

排序整形严格按照库里qsort函数的参数(注意:交换函数要实现为char* 因为这样可以一次拿到一个字节,进行交换,理解为两个整数的交换就行)

cpp 复制代码
void Swap(char* buf1, char* buf2, int size)//交换arr[j],arr[j+1]这两个元素
{
	int i = 0;
	char tmp = 0;
	for (i = 0; i < size; i++)
	{
		tmp = *buf1;
		*buf1 = *buf2;
		*buf2 = tmp;
		buf1++;
		buf2++;
	}
}

void bubble_sort(void* base, int num, int size, int (*cmp)(const void*, const void*))
{
	int  i = 0;
	//趟数
	for (i = 0; i < num - 1; i++)
	{
		int j = 0;
		//一趟内部比较的对数
		for (j = 0; j < num - 1 - i; j++)
		{
			//假设需要升序cmp返回>0,交换
			if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0)//两个元素比较,需要将arr[j],arr[j+1]的地址要传给cmp
			{
				//交换
				Swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
			}
		}
	}

}



int cmp_int(const void* p1, const void* p2)
{
	return (*(int*)p1 - *(int*)p2);
}


//测试bubble_sort 排序整型数据
void test1()
{
	int arr[10] = { 3,1,5,2,4,7,9,6,8,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);
}
int main()
{
	test1();
	
	return 0;
}

没排序之前

排序之后

测试结构体age

cpp 复制代码
void Swap(char* buf1, char* buf2, int size)//交换arr[j],arr[j+1]这两个元素
{
	int i = 0;
	char tmp = 0;
	for (i = 0; i < size; i++)
	{
		tmp = *buf1;
		*buf1 = *buf2;
		*buf2 = tmp;
		buf1++;
		buf2++;
	}
}

void bubble_sort(void* base, int num, int size, int (*cmp)(const void*, const void*))
{
	int  i = 0;
	//趟数
	for (i = 0; i < num - 1; i++)
	{
		int j = 0;
		//一趟内部比较的对数
		for (j = 0; j < num - 1 - i; j++)
		{
			//假设需要升序cmp返回>0,交换
			if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0)//两个元素比较,需要将arr[j],arr[j+1]的地址要传给cmp
			{
				//交换
				Swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
			}
		}
	}

}

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

//测试bubble_sort 排序结构体数据
void test2()
{
	struct Stu arr[] = { {"zhangsan", 20}, {"lisi", 50},{"wangwu", 15} };
	int sz = sizeof(arr) / sizeof(arr[0]); 
	bubble_sort(arr, sz, sizeof(arr[0]), cmp_stu_by_age);
}



int main()
{
	test2();
	//test3();

	return 0;
}

没排序之前

排序之后可以看到以年龄排序成功

测试结构体name

cpp 复制代码
void Swap(char* buf1, char* buf2, int size)//交换arr[j],arr[j+1]这两个元素
{
	int i = 0;
	char tmp = 0;
	for (i = 0; i < size; i++)
	{
		tmp = *buf1;
		*buf1 = *buf2;
		*buf2 = tmp;
		buf1++;
		buf2++;
	}
}

void bubble_sort(void* base, int num, int size, int (*cmp)(const void*, const void*))
{
	int  i = 0;
	//趟数
	for (i = 0; i < num - 1; i++)
	{
		int j = 0;
		//一趟内部比较的对数
		for (j = 0; j < num - 1 - i; j++)
		{
			//假设需要升序cmp返回>0,交换
			if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0)//两个元素比较,需要将arr[j],arr[j+1]的地址要传给cmp
			{
				//交换
				Swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
			}
		}
	}

}

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


void test3()
{
	struct Stu arr[] = { {"zhangsan", 20}, {"lisi", 50},{"wangwu", 15} };
	int sz = sizeof(arr) / sizeof(arr[0]);
	printf("%d\n", sizeof(struct Stu));
	bubble_sort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);
}


int main()
{
	test3();

	return 0;
}

排序之前

排序之后


以上就是我对回调函数的认识和讲解,感谢支持!!!以后会创作更多有用文章

相关推荐
Good Note11 分钟前
Golang笔记——包的循环引用问题(import cycle not allowed)和匿名导入
java·开发语言·笔记·golang·秋招·校招·春招
蹦蹦跳跳真可爱58927 分钟前
Python----Python高级(面向对象:封装、继承、多态,方法,属性,拷贝,组合,单例)
开发语言·python
xcLeigh27 分钟前
JAVA实现五子棋小游戏(附源码)
java·开发语言
怀旧66641 分钟前
IDEA 中配置启动Tomcat
java·开发语言·后端·tomcat·个人开发
m0_748255261 小时前
CC++链接数据库(MySQL)超级详细指南
c语言·数据库·c++
带多刺的玫瑰1 小时前
Leecode刷题C语言之或值至少K的最短子数组①
java·c语言·算法
cdut_suye1 小时前
冯·诺依曼体系结构:计算机科学的奠基石
java·linux·c++·人工智能·python·ubuntu·机器学习
froginwe111 小时前
Python 正则表达式
开发语言
小堃学编程1 小时前
QT跨平台应用程序开发框架(3)—— 信号和槽
开发语言·qt
大风起兮122 小时前
C语言中NUL和NULL、‘\0‘之间的关系
c语言·开发语言·算法