【编程之路(011)进阶指针】(C语言实现)

        之前已经介绍过指针的一点内容了,主要讲了关于指针是什么,指针类型与指针运算。如果没有看过的可以到 【编程之路(009)指针(初级)】(C语言实现)_p_fly的博客-优快云博客_c语言指针实现
看一下。
        今天将会继续深入了解指针,分别从以下几点:
1. 字符指针
2. 数组指针
3. 指针数组
4. 数组传参和指针传参
5. 函数指针
6. 函数指针数组
7. 指向函数指针数组的指针
8. 回调函数

字符指针

字符指针的使用一般有两种情况。
1. 针对一个字符
就像以下这段代码,这种情况没什么复杂之处。
int main()
{
	char ch = 'a';
	char* pc = &ch;
	*pc = 'a';
	return 0;
}

2. 针对一串字符
int main()
{
	char* ps = "hello world";
	printf("%s\n", ps);
	return 0;
}

要注意,这其实是把这一串字符的首个字符的地址存到ps中去,而不是一整个字符串存进去。

那么它和字符数组有什么关系吗?
#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;
}

//结果
//str1 and str2 are not same
//str3 and str4 are same

我们可以看出虽然都是相同的字符,但是存入字符数组中时,每次都会开辟不同的空间。

但是字符串常量却是在相同的空间。原因就是二者分别是在内存的栈区和常量区。每次有新的变量是,栈区都会开辟新的空间,不管内容是否相同。但是字符串到常量区后,其内容不能被改变,所以相同内容的就不会多分配空间。


指针数组

指针数组是一个存放指针的数组。
int* arr1 [ 3 ]; // 整形指针的数组
char * arr2 [ 4 ]; // 一级字符指针的数组
int ** arr3 [ 5 ]; // 二级整形指针的数组


数组指针

数组指针的定义

我们先分析以下两个代码分别代表什么意思。
int * p1 [ 10 ];
int ( * p2 )[10];
注: []的优先级要高于 * 号的
第一个p1先与[]结合构成数组,自然而然数组的类型便是int*
第二个有了p2有了(),先与*结合,此时便表明它是指针变量,在看剩余部分,是一个int型的数组。所以p2是一个指针,指向一个数组,叫数组指针。
总结:当看到一个名词时,看最后的名词是什么,它就是什么,前面的只是修饰它。
另外看到*和[]时,看中间有没有括号,若*与括号结合,就表明是指针。

&数组名与数组名

我们已经知道数组名是首元素的地址,那么&数组名又和它有什么区别呢?

#include <stdio.h>
int main()
{
	int arr[10] = { 0 };
	printf("%p\n", arr);
	printf("%p\n", &arr);
	return 0;
}

//结果
//0097F998
//0097F998

我们看到结果一样,但是再来看以下代码,结果便不一样了。

#include <stdio.h>
int main()
{
 int arr[10] = { 0 };
 printf("arr = %p\n", arr);
 printf("&arr= %p\n", &arr);
 printf("arr+1 = %p\n", arr+1);
 printf("&arr+1= %p\n", &arr+1);
 return 0; 
}

 数组名加一,移动了四个字节的长度

&数组名加一,移动了四十个字节的长度

我们的数组是int类型,大小为10,可以得出结论:

&arr 表示的是 数组的地址,arr只是 首元素的地址。

数组指针的使用

#include <stdio.h>
void print_arr1(int arr[3][5], int row, int col) 
{
	int i = 0, j = 0;
	for (i = 0; i < row; i++)
	{
		for (j = 0; j < col; j++)
		{
			printf("%d ", arr[i][j]);
		}
        printf("\n");
   }
}
void print_arr2(int(*arr)[5], int row, int col) 
{
	int i = 0, j = 0;
	for (i = 0; i < row; i++)
	{
		for (j = 0; j < col; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
}
int main()
{
	int arr[3][5] = { 1,2,3,4,5,6,7,8,9,10 };
	print_arr1(arr, 3, 5);
	//数组名arr,表示首元素的地址
	//但是二维数组的首元素是二维数组的第一行
	//所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
	//可以数组指针来接收
	printf("\n");
	print_arr2(arr, 3, 5);
	return 0;
}


数组参数、指针参数

一维数组传参

#include <stdio.h>

//我们知道数组的传参有两种形式,一是数组的形式,二是指针的形式
//但其本质上都是以指针的形式传参的

void test(int arr[])
{}
void test(int arr[10])
{}
//上面两种是以数组的形式传参的,前者省略了数组大小,后者没有省略,二者都是可以的
void test(int* arr)
{}
void test2(int* arr[20])
{}
//上面两种是以指针的形式传参的,同上,都是可以的
void test2(int** arr)
{}
//arr2是指针数组,存放的类型是int*,所以传参时就要用二级指针来接收
int main()
{
	int arr[10] = { 0 };
	int* arr2[20] = { 0 };
	test(arr);
	test2(arr2);
}

二维数组传参

void test(int arr[3][5])//正确
{}
void test(int arr[][])//错误
{}
void test(int arr[][5])//正确
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。

void test(int* arr)//错误
{}
void test(int* arr[5])//错误
{}
void test(int(*arr)[5])//正确
{}
void test(int** arr)//错误
{}

//二维数组的数组名,是首元素的地址,也就是一维数组的地址
//所以只有第三个是对的,arr是数组指针
int main()
{
	int arr[3][5] = { 0 };
	test(arr);
}

以上是数组传参,接下来是指针传参。

一级指针传参

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

二级指针传参

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

函数指针

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

通过这段代码及其结果显示,我们可以发现函数也是有地址的。既然有地址,那么我们就可以把地址保存起来。那么如何保存呢?

void test()
{
 printf("hehe\n");
}
void (*pfun)();
//这里的pfun便把test函数的地址保存了
//因为有括号,我们可以知道pfun先与*结合,此时就是指着变量
//在看类型,去掉名字pfun,void(*)()  就是其类型——函数指针,这里的返回类型是void,参数也是void

接下来让我们阅读两个代码,加深一下理解

(*(void (*)())0)();
//看到括号这么多的代码,我们首先把括号分开,方便我们看
//( * ( void(*)() ) 0 ) ()
//分开后就很好看了
//从内向外看,void(*)()不正是我们刚才所讲的函数指针类型吗?
//继续向外看,函数指针类型在整形0前面,那不就是把0的类型强制转换了吗?
//强制转换后,0从整形就变成了函数指针类型
//0此时已经不再是整数0了,就变成了指向一个函数的指针,也就是一个地址
//继续向外看,前面有*,这就是把0这个地址解引用,就是找到了这个函数
//最后,函数名(),这不就是函数调用吗?
//总结,以上代码为函数调用,调用的是0作为地址处的函数


void (*signal(int , void(*)(int)))(int);
//分开括号 void( * signal ( int , void(*)(int) ) )(int);
//最内部int与void(*)(int)这两个类型在一个括号中,我们很容易就想到函数函数声明,signal便是函数名
//往外看,函数名与*没有括号结合,这就排除了函数指针
//于是我们将 signal ( int , void(*)(int) )这函数声明取出,便剩下了void (*)(int) 这部分
//函数的声明既要有参数类型,还要有返回类型,那么返回类型只能是void (*)(int)
//总结,以上代码为函数声明,函数返回类型是void (*)(int),参数类型和int和void (*)(int)

//但是这样写是很复杂的,类型可以用typedef重命名,我们就可以简化以上代码
//typedef int MyType;
//正常情况就是把  int 重新命名成MyType这样的形式
//但是函数指针类型冲命名时,不能简单类比,它这么高级,肯定有特殊之处
//typedef void (* MyType)(int);
//这就是把void (*)(int)重新命名成MyType
//简化以上代码为 MyType signal(int ,MyType);




函数指针数组

根据我们的经验,这一眼看过去就是数组,在里面放函数指针的数组,书写应该也大致是长这个样子  int (*parr1[10])();   但是这有什么具体的用处呢?

函数指针数组的用途:转移表。这是个很重要的概念,我们通过代码介绍一下。

//该计算机就是为了举例子,不考虑浮点数的计算等
#include <stdio.h>
int add(int a, int b) 
{
	return a + b;
}
int sub(int a, int b) 
{
	return a - b;
}
int mul(int a, int b) 
{
	return a * b;
}
int div(int a, int b) 
{
	return a / b;
}
int main()
{
	int x, y;
	int input = 1;
	int ret = 0;
	do
	{
		printf("*************************\n");
		printf(" 1:add           2:sub \n");
		printf(" 3:mul           4:div \n");
		printf("*************************\n");
		printf("请选择:");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			ret = add(x, y);
			printf("ret = %d\n", ret);
			break;
		case 2:
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			ret = sub(x, y);
			printf("ret = %d\n", ret);
			break;
		case 3:
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			ret = mul(x, y);
			printf("ret = %d\n", ret);
			break;
		case 4:
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			ret = div(x, y);
			printf("ret = %d\n", ret);
			break;
		case 0:
			printf("退出程序\n");
			break;
		default:
			printf("选择错误\n");
			break;
		}
	} while (input);

	return 0;
}

我们可以看出,在case语句中有大量的代码冗余——代码基本一样,但多次重复出现。

通过使用函数指针数组优化代码:

#include <stdio.h>
int add(int a, int b) 
{
	return a + b;
}
int sub(int a, int b) 
{
	return a - b;
}
int mul(int a, int b) 
{
	return a * b;
}
int div(int a, int b) 
{
	return a / b;
}
int main()
{
	int x, y;
	int input = 1;
	int ret = 0;
	//数组中第一个0是为了占位,因为计算器功能从1开始的
	int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
	while (input)
	{
		printf("*************************\n");
		printf(" 1:add           2:sub \n");
		printf(" 3:mul           4:div \n");
		printf("*************************\n");
		printf("请选择:");
		scanf("%d", &input);
		if ((input <= 4 && input >= 1))
		{
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			//先通过数组的下标找到了功能函数地址
			//找到函数地址解引用就可以使用函数(不用解引用也可以使用)
			ret = (*p[input])(x, y);
		}
		else
			printf("输入有误\n");
		printf("ret = %d\n", ret);
	}
	return 0;
}


指向函数指针数组的指针

指向函数指针数组的指针是一个 指针
指针指向一个 数组 ,数组的元素都是 函数指针(套娃就完事了)
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; 
}

回调函数

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

另外,需要补充的一点,qsort是可以对所有数据可以排序的,不管是整形数据,浮点型数据,字符串 还是结构体,只要比较函数可以写出来,那么qsort就都可以排序。

正是由于这个特性,我们可以观察到起始位置的指针和比较函数内元素的指针都是void*类型。这样的意义就是什么类型的数据都可以,可以理解为包容性。但是使用的时候不能直接使用,需要先强制类型转换成你使用的类型。

#include <stdio.h>
#include <stdlib.h>

//使用qosrt函数,得自己实现一个比较函数
int cmp(const void* p1, const void* p2) 
{
            //因为数组的类型是int型,所以先强制类型转换成int*
	return (*(int*)p1 - *(int*)p2);
}
int main()
{
	int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
	int i = 0;

	qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), cmp);
	for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值