指针进阶(二)

指针进阶(二)

1.函数指针数组

之前已经讲解过了整型指针数组,用于存放整型指针。那么这里的函数指针数组就是用于存放函数指针的。写法是怎样的呢?

void test()
{
	printf("hehe\n");
}
int main()
{
	void(*pf[10])() = { test };
	return 0;
}

代码的第7行,pf先与[]结合,说明pf是个数组名,将 pf[10]去掉,剩下的部分就是数组的元素类型,即void(*)();。所以pf是个函数指针数组。

1.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;
}
int main()
{
	int x = 0;
	int y = 0;
	int input = 0;
	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("请输入操作数:\n");
			scanf("%d %d", &x, &y);
		    ret = Add(x, y);
			printf("%d\n", ret);
			break;
		case 2:
			printf("请输入操作数:\n");
			scanf("%d %d", &x, &y);
			ret = Sub(x, y);
			printf("%d\n", ret);
			break;
		case 3:
			printf("请输入操作数:\n");
			scanf("%d %d", &x, &y);
			ret = Mul(x, y);
			printf("%d\n", ret);
			break;
		case 4:
			printf("请输入操作数:\n");
			scanf("%d %d", &x, &y);
			ret = Div(x, y);
			printf("%d\n", ret);
			break;
		case 0:
			printf("退出程序!\n");
			break;
		default:
			printf("您的输入有误,请重新输入!\n");
		}

	} while (input);
	return 0;
}

但是这种写法会导致代码中重复的内容过多。那么能否改进改进呢?

我们观察这四个函数,可以发现他们的返回值和函数参数都是相同的。所以可以将这四个函数存放进一个函数指针数组中,这样在switch语句中调用对应的函数时,就可以使用下标进行快速访问。

 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 x = 0;
	int y = 0;
	int input = 0;
	int ret = 0;
	int(*pf[5])(int, int) = { 0,Add,Sub,Mul,Div };
	do
	{
		printf("*************************\n");
		printf(" 1:add 2:sub \n");
		printf(" 3:mul 4:div \n");
		printf("*************************\n");
		printf("请输入你想做的运算:");
		scanf("%d", &input);
		if (input >= 1 && input <= 4)
		{
			printf("请输入操作数:\n");
			scanf("%d %d", &x, &y);
			ret = pf[input](x, y);
			printf("运算结果为:%d\n", ret);
		}
		else if(input==0)
		{
			printf("退出程序!\n");
		}
		else
		{
			printf("你的输入有误,请重新输入!\n");
		}
	} while (input);
	return 0;
}

代码的第23行,这里使用0进行占位,将1,2,3,4这四个下标所对应的位置各自对应相应的函数。

1.2指向函数指针数组的指针

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

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

这里注意函数名代表的是函数的地址,&函数名代表的也是函数的地址

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 Calc(int(*pf)(int, int))
{
	int x = 0;
	int y = 0;
	printf("请输入两个操作数:\n");
	scanf("%d %d", &x, &y);
	int ret = pf(x, y);
	printf("%d\n", ret);
}
int main()
{
	int x = 0;
	int y = 0;
	int input = 0;
	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:
			Calc(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");
		}
	} while (input);
	return 0;
}

2.1 qsort函数

查看官方文档:

指针进阶(二)_第1张图片

qsort函数可以对任意数组进行排序,包括结构体数组也可以。qsort函数有四个参数,第一个参数是目标数组的起始位置。第二个参数是数组中元素的个数。第三个参数是数组中每个元素的大小。第四个参数是一个函数,这个函数能够用于比较数组中的两个元素。

注意:这里qsort函数中的最后一个参数是一个函数名,这个函数的两个类型必须是void*类型的。为什么必须是void*类型的呢?

这是因为void*类型指针变量可以接收任意类型的指针变量。因为对于这个函数的设计者而言,他也不知道用户要比较的什么类型的元素,所以就设计了void*这个可以接收任意类型指针的“万能指针变量”,但是void*类型的指针变量不能进行解引用操作也不能进行加减操作。例如:

//这段代码不会有任何的警告和报错。
int main()
{
	int a = 0;
	char c = 0;
	float f= 1.0;
	void* pv;
	pv = &a;
	pv = &c;
	pv = &f;
	return 0;
}

qsort函数的底层是快速排序算法,假定我们使用qsort函数对整数组中的元素进行排序:(qsort函数默认是升序排列的)

int compare_int(void* e1, void* e2)
{
	return (*(int*)e1 - *(int*)e2);
}
void print(int* p,int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", p[i]);
	}
}
int main()
{
	int arr[10] = { 1,2,4,1,6,2,21,4745,64 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	print(arr, sz);
	printf("\n");
	qsort(arr,sz,sizeof(arr[0]),compare_int);
	print(arr, sz);
	return 0;
}

这里使用冒泡排序模拟实现qsort函数,对乱序的整型数组进行排序。

void swap(char* e1,char*e2,int sz)
{
	char tmp = 0;
	while (sz--)
	{
		tmp = *e1;
		*e1 = *e2;
		*e2 = tmp;
		e1++;
		e2++;
	}
}
int compare_int(const void* e1, const void* e2)
{
	return (*(int*)e1 - *(int*)e2);
}
void myBubble(void* base, int num, int sz, int (*pf)(void*, void*))
{
	int i = 0;
	for (i = 0; i < num - 1; i++)
	{
		int j = 0;
		for (j = 0; j < num - 1 - i; j++)
		{
			if (pf((char*)base +j*sz, (char*)base + sz*(j + 1)) > 0)
			{
				swap((char*)base + j * sz,(char*)base+(j+1)*sz,sz);
			}
		}
	}
}
void print(int* p, int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", p[i]);
	}
	printf("\n");
}
int main()
{
	int arr[10] = { 1,0,4,23,55,34,756,754,7,999 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	print(arr, sz);
	myBubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof(arr[0]), compare_int);
	print(arr, sz);
	return 0;
}

这里bubble函数和库函数qsort的参数类型设计的是一样的,这里要特别注意,qsort函数的最后一个参数是一个函数名,这个函数的参数的是void*类型的,也就是说这个函数是用于接收要比较的两个元素的地址的,并不是接收两个函数本身,所以要如何找到这两个要比较的元素的地址呢?可以将base这个参数强制转换为char*类型的,这样子base每执行一次+1操作都会向后移动一个字节,要比较的元素的大小是已知的,为sz。假如要找到bubble函数中找到arr[4]这个元素的地址,就可(char*)base+4*sizeof(int);,base指针向后移动16个字节,就成功找到了arr[4]的地址。

在swap函数中,参数是两个元素的地址,第三个参数是每个元素的大小,要交换这两个元素,只需要交换这两个元素的每一个字节就可以了。

假如要比较结构体数组:(假设以姓名为标准进行比较:)

struct Student
{
	char name[20];
	int age;
};
void swap(char* e1, char* e2, int sz)
{
	char tmp = 0;
	while (sz--)
	{
		tmp = *e1;
		*e1 = *e2;
		*e2 = tmp;
		e1++;
		e2++;
	}
}
int compare_name(const void* e1, const void* e2)
{
	return strcmp(((struct Student*)e1)->name, ((struct Student*)e2)->name);
}
void myBubble(void* base, int num, int sz, int (*pf)(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++)
		{
			if (pf((char*)base + j * sz, (char*)base + sz * (j + 1)) > 0)
			{
				swap((char*)base + j * sz, (char*)base + (j + 1) * sz, sz);
			}
		}
	}
}
int main()
{
	struct Student arr[3] = { {"zhangshan",12},{"lisi",20},{"wangwu",16} };
	int sz = sizeof(arr) / sizeof(arr[0]);
	myBubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof(arr[0]), compare_name);
	return 0;
}

注意:这里字符串的比较是使用的strcmp函数进行比较的。不能直接进行字符串之间的加减操作。

假定以年龄为标准进行比较:

struct Student
{
	char name[20];
	int age;
};
void swap(char* e1, char* e2, int sz)
{
	char tmp = 0;
	while (sz--)
	{
		tmp = *e1;
		*e1 = *e2;
		*e2 = tmp;
		e1++;
		e2++;
	}
}
int compare_int(const void* e1, const void* e2)
{
	return ((struct Student*)e1)->age - ((struct Student*)e2)->age;
}
void myBubble(void* base, int num, int sz, int (*pf)(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++)
		{
			if (pf((char*)base + j * sz, (char*)base + sz * (j + 1)) > 0)
			{
				swap((char*)base + j * sz, (char*)base + (j + 1) * sz, sz);
			}
		}
	}
}
int main()
{
	struct Student arr[3] = { {"zhangshan",12},{"lisi",20},{"wangwu",16} };
	int sz = sizeof(arr) / sizeof(arr[0]);
	myBubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof(arr[0]), compare_int);
	return 0;
}

这里就可以直接对年龄进行相减的操作了。

3.指针和数组的习题组

请预测下面代码的运行结果:

前言:数组名只在两种情况下代表数组名整个数组:其他情况下,数组名代表的是数组首元素的地址。

  1. sizeof(数组名),当数组名单独放在sizeof内部,此时数组名代表的是整个数组。并且计算出的也是整个数组的大小。
  2. &数组名,当数组名被单独放在&符号之前时,此时的数组名代表的是整个数组的地址,数组名代表的是整个数组。
//一维数组
int a[] = {1,2,3,4};
printf("%d\n",sizeof(a));//16
//数组名被单独放在sizeof内部,代表的是整个数组,计算结果是整个数组的大小。
printf("%d\n",sizeof(a+0));//4/8
//数组名不符合前言中的任何一种情况,此时数组名代表数组首元素的地址,地址的大小为4/8个字节
printf("%d\n",sizeof(*a));//4
//数组名不符合前言中的任何一种情况,此时数组名代表数组首元素的地址,解引用操作,拿到了int类型的首元素,大小4字节。
printf("%d\n",sizeof(a+1));//4/8
//数组名不符合前言中的任何一种情况,此时数组名代表数组首元素的地址,+1是第二个元素的地址。为4/8个字节。
printf("%d\n",sizeof(a[1]));//4
//a[1]是数组的第二个元素,大小是4字节。
printf("%d\n",sizeof(&a));//4/8
//&a取出的是整个数组的地址,但也是地址,大小为4/8个字节
printf("%d\n",sizeof(*&a));//16
//对数组名进行&和*操作,得到的最终还是数组名,相当于是将数组名单独放在sizeof内部,计算的是整个数组的大小。
printf("%d\n",sizeof(&a+1));//4/8
//&a取出整个数组的地址,+1操作跳过整个数组,得到一个新地址,但只要是地址,就是4/8个字节。
printf("%d\n",sizeof(&a[0]));//4/8
//a[0]是数组首元素,&a[0]是数组首元素的地址,地址的大小是4/8个字节。
printf("%d\n",sizeof(&a[0]+1));//4/8
//&a[0]是数组首元素的地址,+1操作,得到了数组中第二个元素的地址,大小为4/8个字节。


//字符数组
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr));//6
//sizeof(数组名)计算的是整个数组的大小。数组中有6个元素,大小就是6.
printf("%d\n", sizeof(arr+0));//4/8
//数组名没有单独放在sizeof内部,所以代表的是首元素的地址,+0操作,还是首元素的地址,地址就是4/8字节
printf("%d\n", sizeof(*arr));//1
//*arr是数组首元素,数组中的元素都是char类型,所以计算结果为1字节
printf("%d\n", sizeof(arr[1]));//1
//arr[1]是数组中的第二个元素,大小是1个字节。
printf("%d\n", sizeof(&arr));//4/8
//&arr取出的是整个数组的地址,地址的大小是4/8.
printf("%d\n", sizeof(&arr+1));//4/8
//&arr代表的是整个数组的地址,+1操作跳过整个数组,得到一个新的地址,还是4/8个字节。
printf("%d\n", sizeof(&arr[0]+1));//4/8
//&arr[0]是数组首元素的地址,+1操作,得到数组第二个元素的地址,为4/8字节。


//要注意strlen函数的机制,strlen函数会计算从当前地址开始,一直到'\0'之前的所有元素个数
printf("%d\n", strlen(arr));//随机值
//arr是数组的起始地址,但是arr数组中并没有'\0',所以strlen会一直向后计算,知道遇到了'\0'为止。所以是个随机值。
printf("%d\n", strlen(arr+0));//随机值
//也是随机值,和上面的原因一样的
printf("%d\n", strlen(*arr));//报错
//这里将数组的第一个元素的ascll码,即97传过去了,意味着strlen函数要从97地址处开始向后计算,但是该内存空间是不允许被访问的,所以会报错。
printf("%d\n", strlen(arr[1]));//报错
//这里的原因与上一个类似的
printf("%d\n", strlen(&arr));//随机值
//&arr取出整个数组的地址,传递给strlen,由于不知道'\0'的位置,所以会计算出一个随机值。
printf("%d\n", strlen(&arr+1));//随机值
//&arr+1得到的是跳过一个数组之后的地址,由于不知道'\0'的位置,所以计算结果是随机值。
printf("%d\n", strlen(&arr[0]+1));//随机值
//同样的道理,也会计算出一个随机值


char arr[] = "abcdef";//要注意:字符串的末尾会自动添加'\0'。所以这个数组中含有7个元素
printf("%d\n", sizeof(arr));//7
//sizeof(数组名)计算的是整个数组的大小,要注意:字符串的末尾会自动添加'\0',会被sizeof加入计算结果的。
printf("%d\n", sizeof(arr+0));//4
//arr没有单独放在sizeof内部,+0仍然代表的是首元素的地址,大小4/8字节
printf("%d\n", sizeof(*arr));//1
//*arr是数组的首元素,数组是char类型数组,元素的大小都为1字节
printf("%d\n", sizeof(arr[1]));//1
//arr[1]是数组的第二个元素,数组是char类型的数组,大小都是1字节
printf("%d\n", sizeof(&arr));//4/8
//&arr取出的是整个数组的地址,只要是地址,大小就是4/8字节
printf("%d\n", sizeof(&arr+1));//4/8
//&arr取出整个数组的地址,+1操作跳过一个数组得到一个新的地址,大小仍然为4/8
printf("%d\n", sizeof(&arr[0]+1));//4/8
//&arr[0]取出数组首元素的地址,+1操作得到数组第二个元素的地址,只要是地址,大小就是4/8字节

printf("%d\n", strlen(arr));//6
//数组中含有'\0',arr是数组首元素的地址,到'\0'这个字符之前共有6个元素
printf("%d\n", strlen(arr+0));//6
//相同的道理
printf("%d\n", strlen(*arr));//报错
//*arr是将a字符的ascll码值传递给了strlen函数,这块内存空间访问是非法的,报错。
printf("%d\n", strlen(arr[1]));//报错
//arr[1]是将b字符的ascll码值传入了,该空间访问也是非法的,报错
printf("%d\n", strlen(&arr));//6
//&arr是数组的首地址,传入strlen函数时,会被转化为char*类型。得出计算结果为6
printf("%d\n", strlen(&arr+1));//随机值
printf("%d\n", strlen(&arr[0]+1));//5
//&arr[0]+1是第二个元素的地址,向后计算时不会将第一个元素算入,结果为5


char *p = "abcdef";
printf("%d\n", sizeof(p));//4/8
//p是一个指针变量,指向字符串的第一个元素的地址,指针变量的大小是4/8字节
printf("%d\n", sizeof(p+1));//4/8
//p+1是第二个字符的地址,地址的大小是4/8个字节
printf("%d\n", sizeof(*p));//1
//*p是字符串的第一个元素,大小是1字节
printf("%d\n", sizeof(p[0]));//1
//p[0]是字符串的第一个元素,大小也是1字节
printf("%d\n", sizeof(&p));//4/8
//&p是指针变量的地址,&p是个二级指针,大小也是4/8字节
printf("%d\n", sizeof(&p+1));//4/8
//&p+1也是一个二级指针,大小是4/8字节
printf("%d\n", sizeof(&p[0]+1));//4/8
//&p[0]+1是数组第二个元素的地址,大小是4/8字节


printf("%d\n", strlen(p));//6
//p指向的是字符串的第一个元素的地址,strlen能计算出这个字符串中的元素个数,为6
printf("%d\n", strlen(p+1));//5
//p+1地址处开始算,元素个数就是5个
printf("%d\n", strlen(*p));//报错
//*p的值是97,该空间访问是非法的,会报错
printf("%d\n", strlen(p[0]));//报错
//p[0]的值是97,同理,该空间访问非法,会报错
printf("%d\n", strlen(&p));//随机值
//&p是p这个指针变量的地址,由于不知道'\0'的位置,所以计算出一个随机值
printf("%d\n", strlen(&p+1));//随机值
//&p是二级指针,+1操作之后,仍因为不知'\0'的位置,最终计算出一个随机值。
printf("%d\n", strlen(&p[0]+1));//5
//&p[0]+1是数组第二个元素的地址,第一个元素不会被算入,计算结果就是5了


//二维数组
int a[3][4] = {0};
printf("%d\n",sizeof(a));//48
//sizeof(数组名)计算的是整个数组的大小,为48字节
printf("%d\n",sizeof(a[0][0]));//4
//a[0][0]是数组中第一行第一列的元素0,大小是4字节
printf("%d\n",sizeof(a[0]));//16
//a[0]是二维数组中第一个一维数组的数组名,计算的是整个第一行的大小,为16个字节
printf("%d\n",sizeof(a[0]+1));//4/8
//a[0]并没有单独放在sizeof内部,所以代表的是二维数组第一个一维数组的第一个元素的地址,也就是int*类型的,再+1操作,代表的是二维数组的第一个一维数组的第二个元素地址,大小是4/8字节


printf("%d\n",sizeof(*(a[0]+1)));//4
//sizeof括号里的内容可以改写为:a[0][1],所以计算结果为4
printf("%d\n",sizeof(a+1));//4/8
//a这个数组名没有单独放在sizeof内部,所以a代表第一个一维数组的地址,+1代表第二个一维数组的地址,地址的大小是4/8字节
printf("%d\n",sizeof(*(a+1)));//16
//sizeof括号里的可改写做:a[1],a[1]是数组第二行元素数组名,被单独放在sizeof内部,计算的是第二行元素的大小。16
printf("%d\n",sizeof(&a[0]+1));//4/8
//&a[0]代表的是第一个一维数组的地址,+1代表的第二个一维数组的地址,地址的大小是4/8字节
printf("%d\n",sizeof(*(&a[0]+1)));//16
//sizeof括号里可以改写:a[1],是第二个一维数组的数组名,被单独放在sizeof的括号里,计算的是第二个一维数组的大小。16
printf("%d\n",sizeof(*a));//16
//*a与a[0]等价,被单独放在sizeof内部,计算的是第一行元素的大小。16
printf("%d\n",sizeof(a[3]));//16
虽然a[3]不存在,但是与sizeof的计算结果无关,sizeof只关心括号里的数据类型。最终将这个数据类型的大小算出来即可。
//a[3]也是一个一维数组的数组名,被单独放在sizeof内部,计算的是一行的元素的大小,为16.

结尾指针进阶的内容先讲解到这里后续会继续更新如有不足敬请指正!!

你可能感兴趣的:(零基础C语言之路,数据结构,算法,图论,c语言,c++)