C语言进阶——指针进阶(上)

目录

一,字符指针

二,数组指针

2.1 数组指针定义

2.2 &arr和arr的区别

2.3 数组指针应用

2.4 关于数组指针传参

三,函数指针

四,函数指针数组

六,回调函数

6.1 改造计算器

6.2 回顾冒泡排序

6.3 标准库函数qsort

6.3.1 qsort定义

6.3.2 qsort排序各种内置类型

6.3.3 qsort排序结构体类型

6.4 改造冒泡排序


指针回顾:

①内存会划分为一个个的内存单元,每个内存单元都有一个独立的编号,这个编号称为地址
②地址在 C语言中称为指针,指针需要存储在变量中,这个变量称为指针变量
③指针的大小固定为4/8个字节(32位/64位平台),地址是物理的电线上产生的,32位机器 - 32根地址线 - 1/0
④所谓地址是由32个0和1组成的二进制序列组成,称所以需要32个bit才能存储这个地址,也就是4个字节
⑤指针是有类型的,类型决定了指针+-整数的步长,以及解引用时的权限

一,字符指针

void main1()
{
	char ch = 'w';
	char* pc = &ch;//pc就是字符指针
	*pc = 'a';
}

 使用字符指针char*指针变量初始化字符串时,只是将字符串首字母地址放进指针变量里去了。

void main2()
{
	char arr[] = "abcdef";
	//[a b c d e f]
	const char* p = "abcdef";
	//这里不是把abcdef全部放进去了,而是只把首字母a的地址放到p里面去了
	//由于是常量字符串,建议在前面加上const
	printf("%s\n", p);  //p里存的是首字母地址,代表着整个字符串,打印abcdef
	printf("%c\n", *p); //只打印字符a
}

下面来看一道经典题目

void main3()
{
	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");
        //用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同

	if (str3 == str4)
		printf("str3 and str4 are same\n");
        //常量字符串在常量区里放着,所以str3和str4存的是一样的地址,都存了常量字符串的地址
	else
		printf("str3 and str4 are not same\n");
}

 

二,数组指针

2.1 数组指针定义

数组指针是指针 ,我们已经熟悉整形指针(int * pint)是能够指向整形数据的指针,字符指针(char* charp)是能够指向字符或字符串数据的指针,所以数组指针就是能够指向数组的指针

int *p1[10] ---- []的优先级高于*,所以这个式子可以理解为p1是个10个元素的数组,数组中每个元素类型为int*,所以这是一个指针数组

int (*p2)[10] ---- ()将*和p2结合,所以p2是个指针,这个指针指向一个数组,这个数组里有十个元素,所以我们称p2为数组指针,它指向一个数组

使用指针模拟实现二维数组 

void main4()
{
	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");
	}
}

2.2 &arr和arr的区别

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

void main5()
{
	int arr[10] = { 0 };
	printf("%p\n", arr);        //00CF6D8
	printf("%p\n", &arr[0]);    //00CF6D8
	//这两个值一模一样,证明数组名是数组首元素的地址

	printf("%p\n", arr);        //00CF6D8
	printf("%d\n", sizeof(arr));//40
	//1. sizeof(数组名),这里的数组名不是数组首元素的地址,数组名表示整个数组,sizeof(数组名)计算的是整个数组的大小,单位是字节

	printf("%p\n", arr);       //00CF6D8
	printf("%p\n", &arr[0]);   //00CF6D8
	printf("%p\n", &arr);      //00CF6D8
	//2. &数组名,这里的数组名表示整个数组, &数组名取出的是整个数组的地址
}

 上面代码后面三个虽然打印的地址一样,但是类型不一样

void main6()
{
	int arr[10] = { 0 };
	printf("%p\n", arr);       //0058FAD4
	printf("%p\n", arr+1);     //0058FAD8,为int*类型,+1就+4个字节

	printf("%p\n", &arr[0]);   //0058FAD4
	printf("%p\n", &arr[0]+1); //0058FAD8,为int*类型,+1就+4个字节

	printf("%p\n", &arr);      //0058FAD4  &arr代表整个数组,所以下面+1跳过sizeof(arr)个字节
	printf("%p\n", &arr+1);    //0058FAFC
	                           //这个是16进制,FC-D4=(F-D)+(C-4)=2+8=0x28=2*16^1+8*16^0=32+8=40
}

 对于数组名来说,&数组名得到的就是数组的地址

void main7()
{
	int arr[10] = {1,2,3,4,5,6,7,8,9,10};
	//数组的地址,存储到数组指针变量
	//int[10] *p = &arr;//error,数组指针不能这样定义
	int (* p)[10] = &arr;
	//p是指针,把p去掉,剩下的int (* )[10]就是&arr的类型
	//int* p2 = &arr;//error
}

2.3 数组指针应用

常规使用指针访问数组

void main8()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
}

使用数组指针访问数组

void main9()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int (*p)[10] = &arr;//*&arr --> arr
	int i = 0;
	for (i = 0; i < 10; ++i)
	{
		printf("%d ", *((*p) + i));//两个结果一样
		printf("%d ", (*p)[i]);
        //这时候p是&arr,然后又去对p进行解引用也就是*p,其实*&arr就是arr,所以可以直接用[]访问
	}
}

2.4 关于数组指针传参

①从上面的使用数组指针访问数组可以看出,数组指针不适用于 一维数组,所以一般在二维数组上使用才方便

②二维数组的每一行都可以理解为二维数组的一个元素怒,每一行又是又是一个单独的一维数组,所以二维数组可以理解为一维数组的数组

下面是普通方式二维数组和传参和数组指针传参访问二维数组

//普通方式打印二维数组
void Print1(int arr[3][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 ", arr[i][j]); //正常方式打印二维数组的值
		}
		printf("\n");
	}
}
//二维数组传参,形参是指针的形式
void Print2(int(*p)[5], int r, int c)
{              //此处p是数组指针,类型为int(*)[5],代表元素数为5的数组的首元素地址
	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再解引用找到那一行的首地址,然后再+j找对应下标的地址,然后再解引用拿出存的值
		}
		printf("\n");
	}
}
void main10()
{
	int arr[3][5] = { 1,2,3,4,5, 2,3,4,5,6, 3,4,5,6,7 };
    //1 2 3 4 5
    //2 3 4 5 6
    //3 4 5 6 7
	//二维数组arr 是数组名,表示数组首元素的地址,也就是第一行的地址,也是一维数组的地址
	Print1(arr, 3, 5);
	Print2(arr, 3, 5);
}

一二维数组传参

//一维数组传参,形参的部分可以是数组,也可以是指针
void Test1(int arr[5], int sz) //虽然显示的是arr,但本质还是指针,因为传的是数组名也就是首元素地址,所以用指针接收地址
{}
void Test2(int* p, int sz)
{}
//二维数组传参,形参的部分可以是数组,也可以是指针
void Test3(char arr[3][5], int r, int c)
{}

void Test4(char(*p)[5], int r, int c)
{}
void main11()
{
	int arr1[5] = { 0 };
	Test1(arr1, 5);
	Test2(arr1, 5);

	char arr2[3][5] = { 0 };
	Test3(arr2, 3, 5);
	Test4(arr2, 3, 5);
}

 二级指针传参

void Test(char** p)
{}
void main12()
{
	char n = 'a';
	char* p = &n;
	char** pp = &p;
	char* arr[5];
	Test(&p);
	Test(pp);
	Test(arr);
}

三,函数指针

如果说数组指针是指向数组的指针,那么函数指针就是指向函数的指针

函数名是函数的地址,&函数名也是函数的地址

int Add(int x, int y) { return x + y; }
void test(char* pc,int arr[10]) {}
void main13()
{
	int arr[10] = {0};
	int (*pa)[10] = &arr;
	printf("%p\n", &Add);
	printf("%p\n", Add);//打印结果一样
	//函数名是函数的地址,&函数名也是函数的地址

	int (*pf1)(int, int) = &Add;
	//pf是函数指针变量,存函数的地址,函数的参数类型是(int,int),函数的返回类型也是int
	//pf是函数指针变量,把pf去掉,剩下的int (*)(int, int) 就是函数Add()的指针类型

	void (*pf2)(char*, int[10]) = test;
}
void main14()
{
	int(*pf)(int, int) = Add;
	int r = Add(3, 5);
	printf("%d\n", r);

	//int m = (*pf)(4, 5); //和下面的语句效果一样
	int m = pf(4, 5);      //这样不加*也可以调用pf
	printf("%d\n", m);
}

 来看下面这两个”有趣的“代码

void main17()
{
	( *( void (*)() )0 )();
	void(*signal(int, void(*)(int) ) ) (int);
}

 第一眼看见可能会被这一大坨的括号给搞懵,但是不用担心,咱们来一点点拆解。

①先看第一个,发现里面有个数字0,所以我们可以从这个数字零下手。再看这个:void(*p)(),这个式子,p是函数 指针,这个函数参数为空参,返回类型为void,0默认为int类型,所以可以理解为将0强制转换为void (*)()函数指针类型,然后就解引用,然后最右边那个括号表示这个函数指针解引用之后进行调用。总结:将9强制转化为void(*)()函数指针类型,然后解引用调用这个函数

②再看第二个,可以发现signal是一个函数,然后有两个参数,一个int,一个是void(*)(int),第二个参数是一个函数指针,这个函数指针指向的函数有一个int参数,返回类型为void;去掉signal函数主体后可以发现只剩下void(*  )(int),也是一个函数指针,参数为int,返回类型为void,所以这个函数里面的signal只是一个函数声明 。太复杂了,所以我们可以进行简化,如下代码

void (* signal(int, void(*)(int) ) )(int);
typedef void(*pf_t)(int);
pf_t signal(int, pf_t);//就可以简化成这样子了

四,函数指针数组

函数指针数组,顾名思义就是一个数组的每个元素都是函数指针类型。

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 main18()
{
	int (*pf1)(int, int) = Add;
	int (*pf2)(int, int) = Sub;
	int (*pf3)(int, int) = Mul;
	int (*pf4)(int, int) = Div;
	//一个一个定义太麻烦了,类型一样,所以尝试用一个数组存起来
	//函数指针数组
	int (*pfArr[4])(int, int) = {Add, Sub, Mul, Div};
}

 下面我们通过实现一个简单的计算器来快速上手函数指针数组

void menu()
{
	printf("***************************\n");
	printf("*****  1.add  2.sub  ******\n");
	printf("*****  3.mul  4.div  ******\n");
	printf("*****  0.exit        ******\n");
	printf("***************************\n");
}

void main19()
{
	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("ret = %d\n", ret);
		}
		else if(input == 0) { printf("退出计算器\n"); }
		else { printf("选择错误,重新选择\n"); }
	} while (input);
}

数组:数组名是数组首元素的地址,&数组名是整个数组的地址

函数:函数名是函数的地址,&函数名也是函数的地址

void tesT(const char* str) { printf("%s\n", str); }
void main20()
{
	void (*pf)(const char*) = tesT;        //pf是函数指针变量
	void (*pfArr[10])(const char*);        //pfArr是存放函数指针的数组
	void (*(*p)[10])(const char*) = &pfArr;//p指向函数指针数组的指针
}

六,回调函数

6.1 改造计算器

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数,简单来说就是在一个函数内通过函数指针去调用另一个函数。
所以我们直接改造一下前面的计算器,用上回调函数,如下代码
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); //通过函数指针去调用函数
	//ret = (*pf)(x, y); 对于函数指针来说这里加不加'*'都无所谓
	//ret = (************pf)(x, y);在这里 " * "就是个摆设
	printf("ret = %d\n", ret);
}

void main21()
{
	int input = 0;
	int x = 0;
	int y = 0;
	int ret = 0;
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case 1: Calc(Add); break;将Add作为地址传给Calc的第一个函数指针参数,然后通过那个函数指针参数调用Add,这就是回调函数,下面的都一样
		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);
}

6.2 回顾冒泡排序

void bubble_sort1(int arr[], int sz)
{
	int i = 0;
	//趟数,n个数据需要n-1趟
	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;
			}
		}
	}
}

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

6.3 标准库函数qsort

6.3.1 qsort定义

标准库中的库函数qsort可以实现排序,使用快速排序算法,而且适用于任意类型数据的排序

qsort在库里的定义如下:

void qsort(void* base,    //指向了需要排序的数组的第一个元素
           size_t num,    //排序的元素个数
           1size_t size,  //一个元素的大小,单位是字节
           int (*cmp)(const void*, const void*)
           //函数指针类型 - 这个函数指针指向的函数,能够比较base指向数组中的两个元素
          );

6.3.2 qsort排序各种内置类型

int cmp_int(const void* p1, const void* p2)
{
	//void* 的指针 - 无具体类型的指针
	//void* 类型的指针可以接收任意类型的地址
	//这种类型的指针是不能直接解引用操作的
	//也不能直接进行指针运算的
	return (*(int*)p1 - *(int*)p2);//强转然后采用相减的方法判断是否小于0然后返回
}

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

void testA()
{
	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);
}

6.3.3 qsort排序结构体类型

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

int cmp_stu_by_name(const void* p1, const void* p2)
{
	return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
}

void testB()
{
	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);
}

void testC()
{
	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);
}

6.4 改造冒泡排序

接下来使用冒泡排序的思想,实现一个功能类型qsort的函数bubble_sort,并且使其适用于任意类型的数据,并且加上回调函数。

void Swap(char* buf1, char* buf2, int size) 
{
	int i = 0;
	char tmp = 0;
	for (i = 0; i < size; i++)//size是字节数,因为是char,需要用循环一个一个字节换
	{
		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,cmp再传给对应的回调函数
			{
				//交换
				Swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
			}
		}
	}
}

int cmp_stu_by_age(const void* p1, const void* p2)
{
	return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age;
}

int cmp_stu_by_name(const void* p1, const void* p2)
{
	return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
}

//测试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);
}
//测试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);
}
//测试bubble_sort 排序结构体数据
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);
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值