C语言——指针进阶

此文章对于我之前的文章《C语言——指针-CSDN博客》进行补充,以及记录一些新知识。

一、字符指针

1、字符指针与字符串

#include <stdio.h>

int main()
{
	char* p = "abcdef";
	printf("%s", p);
	return 0;
}

对于字符指针,实际上可以这样赋值,这里p中存储的时字符串“abcdef“的首地址,这样与数组有些类似。

运行结果:

对于有些编译器,这个语句可能报错:

这是因为将这个常量字符串赋值给字符指针可能导致不安全,因为这时可以用字符指针对字符串进行操作,例如对字符串进行更改,这是不安全的。

实际上,字符串字面量(如 "abcdef")在内存中的表现形式是一个以 \0 终止的字符数组。编译器会将这样的字符串字面量存储在程序的只读数据段中,此处存储的字符序列被认为是不可变的,它是一个常量。

#include <stdio.h>

int main()
{
	const char* p = "abcdef";
	printf("%s\n", p);
	return 0;
}

这时可以在char前面加上const修饰,表示p指向的内容不可修改。这时就可以正常地运行了。

2、例子

#include <stdio.h>

int main()
{
	const char* p1 = "abcdef";
	const char* p2 = "abcdef";

	char arr1[] = "abcdef";
	char arr2[] = "abcdef";

	if (p1 == p2)
	{
		printf("p1 == p2\n");
	}
	else
	{
		printf("p1 != p2\n");
	}
	if (arr1 == arr2)
	{
		printf("arr1 == arr2\n");
	}
	else
	{
		printf("arr1 != arr2\n");
	}
	return 0;
}

abcdef作为字符串常量,并不是在写代码的时刻就被创建的,也不是一直存在内存中。实际上,字符串字面量是在您的源代码被编译和链接为可执行文件的过程中被创建并嵌入到程序中去的。编译器会将这些字面量转换成程序中的数据,然后这些数据会被存储在程序的可执行文件中的特定区域,比如只读数据段。

当你在程序中使用某些常量字符串时,这些字符串通常会被编译器存储在程序的内存空间中的特定区域。这个区域通常称为文本段(text segment)或只读数据段(read-only data segment),它是程序可执行文件的一部分。

在程序执行时,这些字符串会被加载到内存中,程序代码就可以引用这些字符串常量的内存地址来使用它们。换句话说,编译器确保每个常量字符串在程序的生命周期内都有一个固定的内存位置,这样你的程序就可以在需要的时候访问它们。

所以这种常量字符串的地址是确定的,所以p1和p2指针种存的地址是同一个,所以p1 == p2。

对于两个字符串数组,开辟的内存空间是不同的,实际上是不同的内存空间,所以首地址是必然不同的。所以arr1 != arr2。

所以程序运行结果是:

二、指针数组

1、指针数组模拟二维数组

1)介绍

#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* parr[3] = { arr1,arr2,arr3 };
	return 0;
}

这里数组名表示首元素地址,是int*类型,将三个一维数组的首元素地址存到一个指针数组中,就可以模拟二维数组。

2)如何访问元素

#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* parr[3] = { arr1,arr2,arr3 };
	int i = 0, j = 0;
	for (i = 0; i < 3; i++)// i表示行
	{
		for (j = 0; j < 5; j++)// j表示列
		{
			printf("%d ", *(parr[i] + j));// 利用指针的运算
		}
		printf("\n");
	}
	return 0;
}

运行结果:

由于parr中存储的是三个数组的数组名,则可以通过parr[索引]访问到三个数组名,又由于数组名是首元素地址,又由指针的运算可得,对数组首元素地址加n则是访问下标为n的元素,  再通过解引用操作符访问数组元素。

对于语句

*(parr[i] + j)

可以简化为

parr[i][j]

依旧可以正常访问,这与二维数组访问方式是一样的。

#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* parr[3] = { arr1,arr2,arr3 };
	int i = 0, j = 0;
	for (i = 0; i < 3; i++)// i表示行
	{
		for (j = 0; j < 5; j++)// j表示列
		{
			printf("%d ", parr[i][j]);// 利用指针的运算
		}
		printf("\n");
	}
	return 0;
}

运行结果:

这种伪二维数组与二维数组是有一定区别的,二维数组的内存是连续的,而这种伪二维数组的内存不一定连续,因为其中的若干个一维数组是分别创建的,内存不一定连续。

三、数组指针

1、数组名

一般情况下数组名代表首元素地址:

#include <stdio.h>

int main()
{
	int arr[10] = { 0 };
	printf("%p\n", arr);
	printf("%p\n", &arr[0]);

	return 0;
}

这里的运行结果:

两个结果是一样的。

但是有两个例外:

1)sizeof(数组名)

#include <stdio.h>

int main()
{
	int arr[10] = { 0 };
	printf("%zu\n", sizeof(arr));
	return 0;
}

运行结果:

这里返回的是整个数组的大小,而不是首元素地址的大小。

2)& 取地址

#include <stdio.h>

int main()
{
	int arr[10] = { 0 };
	printf("%p\n", arr);
	printf("%p\n", &arr);
	printf("%p\n", &arr + 1);
	return 0;
}

运行结果:

这里的&arr表示的是指向整个数组的指针,因为在进行加一操作时,这个指针跳的一个步长是整个数组的大小。

虽然数组指针与数组首元素地址打印结果是一样的,但实际性质是不一样的。

这里&arr中的arr就不能简单理解为数组首元地址。

2、数组指针

1)引入

我们知道&arr就是指向整个数组的数组指针,那对于这个数组指针我么要用什么类型的变量来存储呢。

对于整形指针我们使用整型指针变量来存储,例如这样:

int a = 0;
int *p = &a;

这里&a是指向整型的整型指针,对于p是一个整形指针变量,用来存储&a的值。

这里p的类型就是去掉变量名剩下的,就是 int * 类型

又如:

char c = '\0';
char *p = &c;

这里&c是指向字符型的字符型指针,对于p是一个字符型指针变量,用来存储&c的值。

同样的,这里p的类型也是去掉变量名剩下的,就是 char * 类型。

2)数组指针变量

定义数组指针的格式:

类型 (*指针变量名)[数组大小];

对于数组指针,我们要用数组指针变量来存储,那数组指针变量如何创建或者说定义呢,如下:

#include <stdio.h>

int main()
{
	int arr[10] = { 0 };
	int(*ptr)[10] = &arr;
	return 0;
}

这里的ptr就是数组指针变量,存储了&arr这个数组指针,它的类型是 int (*) [10] 。

这里 int(*ptr)[10] 的 () 是必须的,因为如果没有 () 的话,这个语句就变成了int *ptr[10],这样ptr就变成了指针数组了。

  • int *ptr[10]; 这里没有圆括号,根据C语言的运算符优先级规则,[] 的优先级高于 。因此,这个声明意味着 ptr 是一个包含10个元素的数组,每个元素都是指向 int 的指针。这里的指针数组的类型是去掉变量名 ptr ,即为 int * [10],对于数组指针的类型是少了一对圆括号的。

  • int (*ptr)[10]; 这里使用了圆括号,改变了运算符的默认优先级。现在,先于 [] 操作,这意味着 ptr 是一个指针,它指向一个包含10个整数的数组。这里的数组指针的类型是去掉变量名 ptr ,即为 int (*) [10],对于指针数组的类型是多了一对圆括号的。

所以,圆括号在这里是必须的,以便正确声明一个指向数组的指针,而不是声明一个指针数组。

3)例子

#include <stdio.h>

int main()
{
	char* arr[5] = { 0 };
	char* (*ptr)[5] = &arr;
	return 0;
}

这里的arr是指针数组,这里的ptr是数组指针变量,这里的数组指针变量ptr的类型就是char* (*)[5]。

4)注意

#include <stdio.h>

int main()
{
	int arr[] = { 1,2,3,4,5 };
	int(*ptr)[5] = &arr;
	return 0;
}

这里 int(*ptr)[5] 方括号中的5不能省略,如果写成 int(*ptr)[],编译器就会报错。对于这种未指明大小的数组,在定义数组指针时,要在方括号中指明数组的实际大小。

5)通过数组指针访问数组元素

#include <stdio.h>

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

运行结果:

可以这样理解,ptr 是一个数组指针,它指向一个具有10个整型的数组。你可以通过 (*ptr) 来获取这个数组,而 (*ptr + i) 会给你这个数组的第 i 个元素的地址。

也可以理解成:&arr 就是这个数组指针 ptr ,然后 ptr 解引用后就得到了数组名 arr ,数组名就是数组首元素指针,然后就可以访问数组的每个元素。

这里的

	printf("%d ", *(*ptr + i));

可以换成

	printf("%d ", (*ptr)[i]);

这里 printf("%d ", (*ptr)[i]); 中的圆括号是必须的,因为 []  的优先级高于 * 。

这样运行结果就是:

对于一维数组很少这样使用,这种用法常用在多维数组中。正常情况下一维数组访问直接用数组名来访问数组就行了。

3、数组指针在高维数组中的运用

1)数组指针作为函数参数

二维数组可以看成一维数组的数组,所以二维数组的首元素是第一个子数组,也就是二维数组的第一行。

对于二维数组

	int arr[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};

来说,arr是其数组名,数组名一般情况是首元素地址,所以这里的数组名是二维数组的首元素地址,而二维数组首元素是第一个子数组,所以二维数组数组名代表首个子数组的地址,也就是第一个子数组的指针。

总的来说就是,一般情况下二维数组数组名是其首个子数组的指针,也就是一个一维数组指针。

#include <stdio.h>

void print(int(*ptr)[4], int row, int col)
{
	int i = 0, j = 0;
	for (i = 0; i < 3; i++)
	{
		for (j = 0; j < 4; j++)
		{
			printf("%d ", *(*(ptr + i) + j));
		}
		printf("\n");
	}
}

int main()
{
	int arr[3][4] = {1,2,3,4,5,6,7,8,9,0,1,2};

	print(arr, 3, 4);

	return 0;
}

在传参时表现成的也是一维数组指针的形式,这样就可以对二维数组进行访问,运行结果:

这是怎么实现的呢?

实际上,ptr作为一个一维数组指针,通过 i 进行偏移,实现得到二维数组的每一个子数组的指针,就是 (ptr + i) 这一系列一维数组指针,对这一系列一维数组指针解引用,就像这样 *(ptr + i) ,就可以获取这个数组,然后通过 (*(ptr + i) + j) 获得第 i 个子数组的第 j 个元素的地址。(这一步骤是不是很像上面第五点我们提到的用数组指针来访问数组元素,那一个方法的应用就是在这然后再进行解引用就可以访问到每个子数组的每个元素了,就像这样 *(*(ptr + i) + j) 。


对于高维数组,例如二维数组 int arr[3][4],它的子数组的数组名是除去列数的部分,即 arr[i] i可取 0 ~ 2 ,就是它的3个子数组的数组名,而数组名又是首元素地址,所以我们可以通过这个来验证 arr[i] 是其子数组的数组名:

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

	printf("%d\n", *arr[0]);

运行结果:

既然是这样,那我们知道,用取地址数组名就可以获得数组的指针,就像这样:&arr[0] 就可以获得二维数组首个子数组的指针,也就是二维数组的首元素地址。
这样我们在传参时就可以将 &arr[0] 作为参数代替二维数组数组名,就像这样:

#include <stdio.h>

void print(int(*ptr)[4], int row, int col)
{
	int i = 0, j = 0;
	for (i = 0; i < 3; i++)
	{
		for (j = 0; j < 4; j++)
		{
			printf("%d ", *(*(ptr + i) + j));
		}
		printf("\n");
	}
}

int main()
{
	int arr[3][4] = {1,2,3,4,5,6,7,8,9,0,1,2};

	print(&arr[0], 3, 4);

	return 0;
}

运行结果:

这个也是用刚刚我们提到的使用数组指针来访问数组元素的方式。


我们还知道,对于一个一维数组 int arr[10] ,我们要访问它的每一个元素,通常有两种常见方式:

(i) 直接使用下标引用操作符 [] 

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

(ii) 使用数组名加偏移量再解引用

由于数组名是首元素地址,用偏移量可以访问首元素和首元素后面的全部元素。

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

这两种访问方式是等价的,准确地说,对于 *(arr + i) 的形式,是可以转换为 arr[i] 的形式的。

知道了这两种访问方式是等价的可以相互转换的后,我们可以对上面的使用数组指针对高维数组元素访问的代码进行一些修改:

#include <stdio.h>

void print(int(*ptr)[4], int row, int col)
{
	int i = 0, j = 0;
	for (i = 0; i < 3; i++)
	{
		for (j = 0; j < 4; j++)
		{
			printf("%d ", ptr[i][j]);
		}
		printf("\n");
	}
}

int main()
{
	int arr[3][4] = {1,2,3,4,5,6,7,8,9,0,1,2};

	print(arr, 3, 4);

	return 0;
}

将 *(*(ptr + i) + j) 换成了 ptr[i][j] 。

四、数组参数和指针参数

1、一维数组和二维数组传参

1)一维数组传参的几种方式

对于数组arr作为参数传给test函数,test函数的形参可以是什么形式?

	int arr[10] = { 0 };

	test(arr);

其实这里可以有多种形式:

void test(int arr[])
{
	
}
void test(int arr[10])
{

}
void test(int *arr)
{
    //传首元素的地址,首元素是整型,所以是整型指针
}

对于数组arr2作为参数传给test2函数,test2函数的形参可以是什么形式?

	int* arr2[10] = { 0 };

	test2(arr2);

其实这里可以有多种形式:

void test2(int* arr2[])
{
	
}
void test2(int* arr2[10])
{

}
void test2(int** arr2)
{
    //传首元素地址,首元素是整型指针,所以首元素的指针是二级指针
}

2)二维数组传参的几种方式

对于二维数组arr作为参数传给test函数,test函数的形参可以是什么形式?

	int arr[3][4] = { 0 };

	test(arr);

其实这里可以有多种形式:

void test(int arr[][4])
{

}
void test(int arr[3][4])
{

}
void test(int (*arr)[4])
{

}

3、指针传参

1)一级指针传参

#include <stdio.h>

void test(int *ptr)
{
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(ptr + i));
	}
	printf("\n");
}

int main()
{
	int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
	int* ptr = arr;
	test(ptr);
	return 0;
}

如果函数的形参是一级指针,那实参可以传什么参数呢?

#include <stdio.h>

void test(int *ptr)
{
	
}

int main()
{
	int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
	int* ptr = arr;

	int a = 0;
	int* p = &a;

	test(arr);
	test(ptr);
	test(&arr[0]);
	test(&a);
	test(p);

	return 0;
}

可以看到这里有很多种实参,实际上不止这些,它们都有一个共同点,就是它们的本质都是一级整形指针。

2)二级指针传参

如果函数的形参是二级指针,那实参可以传什么参数呢?

#include <stdio.h>

void test(int **ptr)
{
	
}

int main()
{
	int a = 0;
	int* p = &a;
	int** pp = &p;

	int* arr[10] = { 0 };
	
	test(&p);
	test(pp);
	test(arr);
	test(&arr[0]);

	return 0;
}

可以看到这里有很多种实参,实际上不止这些,它们都有一个共同点,就是它们的本质都是二级整形指针。

只要实参的类型和形参的类型对应上就可以传参。

那这里为什么不可以传&&a呢?

主要是因为 &&a 在C语言中并不是合法的语法。在C语言中,并没有直接获取变量地址的地址(也就是二级地址)的简单方法,因为这种操作没有实际意义。每次使用 & 操作符时,它取的是它右侧变量或者表达式的地址。但是,&a 是一个右值(rvalue),它没有固定的内存地址(临时的,不可再次取址),因此你不能对它再次使用&操作符。

五、函数指针

函数指针,顾名思义是指向函数的指针。

1、函数指针

在C语言中,函数指针是一种特殊类型的指针,它指向函数而不是变量或数据。通过函数指针,可以将函数作为参数传递给其他函数,或者将其存储在数组中等,从而提供程序设计的灵活性和复用性。

1)函数的地址

在C语言中,函数的地址是该函数代码在内存中的起始位置。这是一个运行时的概念,因为当程序被加载到内存中执行时,操作系统为程序分配内存空间,各个函数的代码就会被加载到这个空间的特定位置。函数的地址就是程序被加载到内存后,该函数代码块的起始位置。这个地址可以用函数指针来存储和引用。

每个函数在内存中都有一个唯一的地址,这使得我们可以通过函数指针来调用函数。当你声明一个函数指针并将一个函数的名称赋值给它时,实际上是将那个函数的地址赋给了指针。在C语言中,函数名本身就代表该函数的地址,因此不需要使用取地址符&(尽管使用它也不会出错)。

在C语言中,函数名本身代表函数的地址,而使用&操作符加上函数名同样得到的是函数的地址。这意味着,在该上下文中&操作符是可选的。

#include <stdio.h>

int Add(int a, int b)
{
	return a + b;
}

int main()
{
	printf("%p\n", Add);
	printf("%p\n", &Add);

	return 0;
}

运行结果:

可以看到结果是一样的,这两个是没有什么区别的,所以对于函数的地址,可以直接用函数名,也可以用&函数名。这个特性简化了函数指针的使用,使得代码更加直观。同时,它也体现了C语言设计的一致性和简洁性。

2)函数指针的定义

函数指针的定义需要指定它将指向的函数的返回类型和参数类型。定义函数指针的基本语法如下:

返回类型 (*指针变量名)(参数类型列表);

实例:

#include <stdio.h>

int Add(int a, int b)
{
	return a + b;
}

int main()
{
	int (*funcPtr)(int, int) = Add;

	return 0;
}

对于函数指针中的参数名是否要像下面这样显式标出:

	int (*funcPtr)(int x, int y) = Add;

实际上没有必要,只要将类型标出就行了。

2、函数指针的使用

现在我们知道如何定义和初始化函数指针了,那函数指针该怎么用呢,又有什么用呢?

1)用函数指针对函数进行调用

既然是函数指针,那对它解引用不就可以访问函数了吗,就可以调用函数了。就像下面这样:

#include <stdio.h>

int Add(int a, int b)
{
	return a + b;
}

int main()
{
	int (*funcPtr)(int, int) = Add;
	int result = (*funcPtr)(1, 2);
	printf("%d\n", result);
	return 0;
}

运行结果:

实际上不止可以用这种方法,还可以不用解引用操作符,直接使用函数指针对函数调用。就像下面这样:

#include <stdio.h>

int Add(int a, int b)
{
	return a + b;
}

int main()
{
	int (*funcPtr)(int, int) = &Add;
	int result = funcPtr(1, 2);
	printf("%d\n", result);
	return 0;
}

运行结果:

这样也是没有任何问题的。

对于正常函数调用是直接用函数名,由于函数名就是函数地址,所以也可以像下面这里的第二句代码这样调用函数,效果是一样的:

	int result = Add(1, 2);
    int result = (*Add)(1, 2);

但是必须提醒的一点是,只要使用了*对函数指针进行解引用,就要加上(),这里的圆括号是必须的,因为函数调用操作符()的优先级大于解引用操作符。

2)小特性

对于下面的代码段是可以正常运行的:

#include <stdio.h>

int Add(int a, int b)
{
	return a + b;
}

int main()
{
	int result = (**********Add)(1, 2);
	printf("%d\n", result);
	return 0;
}

为什么可以加这么多解引用操作符呢?

实际上这是一个有趣的C语言特性,即对函数指针的多次解引用。在C语言中,函数名被视为指向函数的指针。正常情况下,如果你有一个指向函数的指针,你可以通过使用一个*(解引用操作符)来调用它,正如你将这样做对任何指针那样,也可以不使用解引用操作符,直接用函数指针对函数进行调用。然而,特别的是,当你对一个函数指针进行多次解引用时,你仍然得到对同一个函数的引用。

在给出的代码中,Add是一个函数,因此Add实际上是一个指向该函数的指针。当你写(**********Add)时,无论你添加多少个*,都是在多次解引用一个指向同一个函数的指针。每次解引用都返回指向同一个函数的指针,因此这样写虽然看起来很不寻常,但在C语言中是合法的,并且能够正常工作。这种编写方式纯粹是语法上允许的,并没有实际的用途,通常不推荐这样做,因为它会使代码难以阅读和理解。

所以,result = (**********Add)(1, 2);实际上和result = Add(1, 2);做的是同样的事情,只是使用了一个非常不寻常的方式来调用Add函数。

3)函数指针的用途

例1

编写一个计算函数,使它可以进行两个数的加减:

#include <stdio.h>

int Add(int a, int b)
{
	return a + b;
}

int calculate(int (*funcPtr)(int, int),int x,int y)
{
	return (*funcPtr)(x, y);
}

int main()
{
	int a = 1, b = 9;
	printf("%d\n", calculate(Add, a, b));
	return 0;
}

这样使用函数指针可以让calculate函数变得极其灵活。通过传入不同的函数指针,可以在运行时决定calculate函数执行什么样的操作,这样一来,同一个calculate函数就能实现加法、减法、乘法、除法等多种操作。通过这种方式,calculate函数的行为可以通过传给它的函数指针来动态改变,使得你的代码更加灵活和可重用。

如果你想要通过calculate函数实现减法,就可以像这样编写一个减法函数:

int Subtract(int a, int b)
{
    return a - b;
}

然后,你可以像这样调用calculate函数来执行加法和减法操作:

int main()
{
    int a = 10, b = 5;
    printf("加法结果:%d\n", calculate(Add, a, b));  // 使用Add函数指针进行加法
    printf("减法结果:%d\n", calculate(Subtract, a, b));  // 使用Subtract函数指针进行减法
    return 0;
}

这样可以使得calculate函数能够以一种更灵活的方式工作:它可以对任何具有相同签名(即接受两个int类型参数并返回一个int类型结果)的函数进行操作。这种方式是动态的,因为最终调用的函数可以在运行时决定。

例2

写一个计算器,可以实现简单的四则运算:

#include <stdio.h>

//打印菜单函数
void printmenu()
{
	printf("---------------------------\n");
	printf("---------- 1加法 ----------\n");
	printf("---------- 2减法 ----------\n");
	printf("---------- 3乘法 ----------\n");
	printf("---------- 4除法 ----------\n");
	printf("---------- 0退出 ----------\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;
}

//计算函数
void calculate(int(*funcPtr)(int ,int ))
{
	int x = 0, y = 0;
	printf("请输入表达式:>");
	scanf("%d%*c%d", &x, &y);
	printf("%d\n", (*funcPtr)(x, y));
}

int main()
{
	printmenu();//打印菜单
	int i = 0;//i接受输入,x和y作为操作数
	do
	{
		printf("请选择:>");

		while (scanf("%d", &i) != 1)//检测输入
		{
			printf("输入错误!\n");
			while (getchar() != '\n');//输入错误后,清空输入缓冲区
		}

		switch (i)//选择使用哪一个功能
		{
		case 0:
			printf("已退出!\n");
			break;
		case 1:
			calculate(&Add);
			break;
		case 2:
			calculate(&Sub);
			break;
		case 3:
			calculate(&Mul);
			break;
		case 4:
			calculate(&Div);
			break;
		default :
			printf("error!\n");
			break;
		}
	} while (i);//输入0则退出循环
	return 0;
}

这里就是使用了C语言中的回调函数。

这个例子充分展示了回调函数在C语言中的实际应用,通过将函数作为参数传递给其他函数,从而实现了高度的模块化和灵活性。

  4)一些有趣的例子

例1
	( *(void (*)())0 )();

这行C语言代码( *(void(*)())0 )();是一种特定的指针和函数调用写法,它尝试执行位于地址0的函数。详细解释如下:

  • (void(*)()):这是一个函数指针的强制类型转换。它指明了一个指向函数的指针,该函数没有参数(括号内为空)并返回void类型,即不返回任何值。这个代码对0进行强制类型转换,将0强制类型转换为一个函数指针。
  • (*(void(*)())0):将地址0转换为上述指定的函数指针类型,即这部分表示“一个位于地址0的、返回类型为void且不带参数的函数”。解引用在这里是可省略的,都可以对位于地址0处的函数进行调用。
  • (*(void(*)())0)();:在最后加上一对括号表示尝试调用这个函数。

简单地说,这行代码尝试调用起始地址在内存地址0处的函数。在大多数现代操作系统和硬件平台上,地址0是保留地址,通常用于表示空指针,且不允许访问。尝试执行这行代码很可能导致程序崩溃,引发一个访问违规错误(例如,段错误或访问违规异常),因为程序试图执行一个非法的内存地址。

这类代码可能出现在试图故意引发错误的场景中,或者在需要直接与硬件通信的嵌入式系统开发中,其中某些特定地址可能被映射到硬件功能。然而,在标准的应用程序开发中,这行代码通常被视为危险的,应该避免使用。

例2
	void (* signal( int, void(*)(int) ) )(int);

这段代码是C语言中的函数声明语句,用于声明signal函数。

signal函数的声明可以分解来理解:

  • signal:这是函数的名称。
  • int:这是函数的第一个参数,是一个整型(int)。
  • void (*)(int):这是函数的第二个参数,它是一个函数指针,指向的函数接收一个int类型的参数并返回void
  • void ( *signal( int, void (*)(int) ) )(int):整体来看,这表明signal函数接收一个整数和一个函数指针作为参数,并返回一个函数指针。返回的函数指针指向的函数接收一个int类型的参数并返回void

我们可以发现这两个例子是代码是比较不利于理解的,那如何改善这种代码呢?

那就要引入我们下面的方法了:

5)使用typedef简化

我们知道使用typedef可以对某个类型进行重命名,例如:

typedef unsigned int uint;

这里是将 unsigned int 类型重命名为 uint 。

当然,我们也可以将某个函数指针类型重命名,例如:

typedef void(*funcPtr_v_i)(int);

这里是将函数指针类型 void(*)(int) 重命名为funcPtr_v_i。在后面使用时就可以直接使用funcPtr_v_i这个新名字。

这样就可以对上面的语句进行简化了,对于上面的例2,可以简化为:

typedef void(*funcPtr_v_i)(int);
funcPtr_v_i signal(int, funcPtr_v_i);

这样更利于理解代码。

六、函数指针数组

在 C 语言中,函数指针数组是一种数据结构,它可以存储多个指向函数的指针。这些函数有着相同的返回类型和参数列表。数组中的每个元素都是一个函数指针,可以用来调用它指向的函数。这样的结构特别有用,在需要根据不同情况调用不同函数时,可以简化代码并提高灵活性。

1、函数指针数组

1)引入

我们知道整形指针数组和字符型指针数组:

int* arr1[10];
char* arr2[10];

对于函数指针,也是一种指针,那就也可以放到数组中。这样就的到了函数指针数组:

2)函数指针数组的定义

函数指针数组的定义格式:

返回类型 (*数组名[])(参数类型列表);

当然,这里也可以用我么之前说到的简化方式:

typedef 返回参数(*funcPtr)(参数列表);
funcPtr arr[10];

3)函数指针的初始化

#include <stdio.h>

//加法函数
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(*fparr[4])(int, int) = { Add,Sub,Mul,Div };
	return 0;
}

这里的fparr是函数指针数组,我么将Add,Sub,Mul,Div这四个函数指针初始化给了fparr数组。

2、函数指针数组的使用

1)使用方法

使用函数指针数组时,可以通过数组索引来调用不同的函数。例如:

#include <stdio.h>

//加法函数
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(*fparr[4])(int, int) = { Add,Sub,Mul,Div };
	int i = 0;
	for (i = 0; i < 4; i++)
	{
		printf("%d\n", fparr[i](6, 6));
	}
	return 0;
}

不同的索引对应不同的函数,这样就可以在一个语句中调用不同的函数。

2)用途

函数指针数组可以用于实现类似于面向对象编程中多态的功能。例如,你可以根据运行时的条件来决定调用哪个函数。

上面和下面的例子就是这样。

#include <stdio.h>
#define FUNC 4

//打印菜单函数
void printmenu()
{
	printf("---------------------------\n");
	printf("---------- 1加法 ----------\n");
	printf("---------- 2减法 ----------\n");
	printf("---------- 3乘法 ----------\n");
	printf("---------- 4除法 ----------\n");
	printf("---------- 0退出 ----------\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(*fparr[FUNC])(int, int) = { Add,Sub,Mul,Div };
	int i = 0,x = 0,y = 0;
	do
	{
		printmenu();
		printf("请选择:>");

		while (scanf("%d", &i) != 1)//检测输入
		{
			printf("输入错误!\n");
			while (getchar() != '\n');//输入错误后,清空输入缓冲区
		}

		if (i >= 1 && i <= 4)
		{
		    printf("请输入表达式:>");
		    scanf("%d%*c%d", &x, &y);
			printf("%d\n", fparr[i - 1](x, y));
		}
		else if(i == 0)
		{
			printf("已退出!\n");
		}
		else
		{
			printf("错误!\n");
		}
	} while (i);
	return 0;
}

这样也可以实现简单的四则运算。

假如以后增加更多功能,就可以将增加的函数指针放到函数指针数组中,就可以添加更多功能了:

例如要增加按位与、按位或、按位异或功能:

#include <stdio.h>
#define FUNC 7

//打印菜单函数
void printmenu()
{
	printf("---------------------------\n");
	printf("---------- 1加法 ----------\n");
	printf("---------- 2减法 ----------\n");
	printf("---------- 3乘法 ----------\n");
	printf("---------- 4除法 ----------\n");
	printf("---------- 5按位与 --------\n");
	printf("---------- 6按位或 --------\n");
	printf("---------- 7按位异或 ------\n");
	printf("---------- 0退出 ----------\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 AndFunc(int x, int y)
{
	return x & y;
}

//按位或函数
int OrFunc (int x, int y)
{
	return x | y;
}

//按位异或函数
int XorFunc(int x, int y)
{
	return x ^ y;
}

int main()
{
	int(*fparr[FUNC])(int, int) = { Add,Sub,Mul,Div,AndFunc,OrFunc,XorFunc };
	int i = 0,x = 0,y = 0;
	do
	{
		printmenu();

		printf("请选择:>");
		while (scanf("%d", &i) != 1)//检测输入
		{
			printf("输入错误!\n");
			while (getchar() != '\n');//输入错误后,清空输入缓冲区
		}

		if (i >= 1 && i <= FUNC)
		{
			printf("请输入表达式:>");
			scanf("%d%*c%d", &x, &y);
			printf("%d\n", fparr[i - 1](x, y));
		}
		else if(i == 0)
		{
			printf("已退出!\n");
		}
		else
		{
			printf("错误!\n");
		}
	} while (i);
	return 0;
}

这样就可以添加新的功能了。

3)回调函数

函数指针数组也可以用来存储回调函数,使得程序可以动态地根据需要回调不同的函数。

#include <stdio.h>

//加法函数
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 calculate(int n,int(*fparr[4])(int, int),int x,int y)
{
	printf("%d\n", fparr[n](x, y));
}

int main()
{
	int(*fparr[4])(int, int) = { Add,Sub,Mul,Div };
	int i = 0;
	for (i = 0; i < 4; i++)
	{
		calculate(i, fparr, 6, 6);//通过函数指针数组实现回调函数
	}
	return 0;
}

七、回调函数

在C语言中,回调函数是一种通过函数指针传递给其他函数的函数。简而言之,它是一个可以在将来的某个时间点被调用的函数。这种机制允许第二个函数在适当的时候调用回调函数。

回调函数在C语言中是一个非常有用的概念,它允许某个函数在完成其任务后调用另一个函数。这种机制使得程序可以在运行时动态地决定需要执行的代码,提高了程序的灵活性和可扩展性。

1、回调函数使用的实例

1)qsort

这里使用一个C语言的库函数作为例子:

C语言库函数中提供的快速排序函数,我们分析这个函数的参数:

void qsort(void* base, size_t num, size_t width, int(__cdecl* compare)(const void* elem1, const void* elem2));

我们可以发现这个函数的最后一个参数是一个函数指针,这里的__cdecl是函数调用约定,去掉这个,我们可以看的更清楚:

int(* compare)(const void* elem1, const void* elem2)

这明显是一个函数指针,接受两个const void*类型的参数,返回一个int类型的参数。

对于这个函数的参数的解释:

  • void *base:指向待排序数组的第一个对象的指针。
  • size_t num:待排序数组中的元素数量。
  • size_t size:数组中每个元素的大小,以字节为单位,可以通过sizeof运算符获得。
  • int (*compare)(const void*, const void*):比较函数的指针,用于确定排序的顺序。该函数接受两个指向要比较的元素的指针。返回值的要求:这里的意思就是这个compare函数需要用户自己实现,对用户需要比较的数据类型进行适配。比较函数的一般形式如下:
    int compare(const void *a, const void *b)
    {
        // 类型转换,然后执行比较逻辑
    }

    需要注意的是,比较函数应该根据实际情况进行定义,以确保排序行为符合预期。

对于qsort函数,它可以对数组进行排序,不仅限于整数类型,还可以对任何类型的对象数组进行排序,因为它通过指针操作和回调函数来实现排序逻辑,非常灵活。

2)使用示例

对整数数组排序:
#include <stdio.h>
#include <stdlib.h>

int compare(const void* a, const void* b)//根据需要来编写比较函数
{
	return (*(int*)a - *(int*)b);//这里我们想将数据由小到大排列,就可以在返回值上实现,a大于b返回值就大于零,a等于b返回值就等于零,a小于b返回值就小于零,就可以实现由小到大排序,返回值反过来就可以实现由大到小排序
}

int main()
{
	int arr[10] = { 9,67,7,23,5,33,3,44,1,45 };
	int i = 0;

	qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(arr[0]), compare);

	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

这里就能实现从小到大排序。

运行结果:

这里可以通过改变比较函数来实现从大到小的排序:

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

int compare(const void* a, const void* b)//根据需要来编写比较函数
{
	return (*(int*)b - *(int*)a);
}

int main()
{
	int arr[10] = { 9,67,7,23,5,33,3,44,1,45 };
	int i = 0;

	qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(arr[0]), compare);

	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

运行结果:

对于这里的compare函数,就是一个回调函数,qsort函数会在合适的时间对compare进行调用。

对于compare函数的参数,为什么是const void*?

const这个关键字表示指针指向的数据不能被修改。在 compare 函数的上下文中,这意味着你不能通过这个指针改变数组中的元素。这是一个好的实践,因为比较函数的目的仅仅是为了比较元素,而不是去修改它们。void*是泛型指针,这里可以传入任意类型的指针,在C语言中,void* 被用来表示一个通用的指针类型,任何类型的指针都可以被转换为 void* 类型并且反之亦然,而且这种转换不需要显示地强制类型转换。这使得 void* 成为在处理不确定类型数据时的一个有用工具。

对于void的详细解释在我之前的文章《C语言——数据类型-CSDN博客》中。

对于泛型指针,我们不能直接对其进行操作,要先将其转换为其他类型后,再对其操作。

对结构体类型排序:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

//定义结构体类型
typedef struct Student
{
	char name[20];
	int age;
};

int compare_by_name(const void* a, const void* b)
{
	const Student* Studenta = (const Student*)a;
	const Student* Studentb = (const Student*)b;
	return strcmp(Studenta->name, Studentb->name);
}

int compare_by_age(const void* a, const void* b)
{
	const Student* Studenta = (const Student*)a;
	const Student* Studentb = (const Student*)b;
	return Studenta->age - Studentb->age;
}

int main()
{
	int i = 0;
	Student students[3] = {{"bob",18},{"cindy",16},{"alice",17}};//初始化结构体数组

	//按名字排序
	qsort(students, sizeof(students) / sizeof(students[0]), sizeof(students[0]), compare_by_name);

	printf("按名字排序结果:\n");
	for (i = 0; i < 3; i++)
	{
		printf("%s %d\n", students[i].name, students[i].age);
	}

	//按年龄排序
	qsort(students, sizeof(students) / sizeof(students[0]), sizeof(students[0]), compare_by_age);

	printf("按年龄排序结果:\n");
	for (i = 0; i < 3; i++)
	{
		printf("%s %d\n", students[i].name, students[i].age);
	}

	return 0;
}

通过对compare函数的自定义,可以实现不同规则的排序,这里实现了按名字排序和按年龄排序。

运行结果:

3)仿制

仿照这里的库函数qsort,设计一个冒泡排序函数,通过不同的比较函数来排不同的数据。

排整型数据
#include <stdio.h>

//交换函数
//将两个元素的数据分为一字节一字节的,一字节一字节地交换,因为不确定要排什么数据类型,这里的数据交换用这种方式
void Swap(char* elem1, char* elem2)
{
	int width = (int)(elem2 - elem1);//计算宽度,一个元素的大小,用来调整下面的循环次数
	int i = 0;
	char temp = 0;//临时变量
	for (i = 0; i < width; i++)//数据宽度有多大,就循环几次
	{
		//将两个元素的数据分为一字节一字节的,一字节一字节地交换
		temp = *(elem1 + i);
		*(elem1 + i) = *(elem2 + i);
		*(elem2 + i) = temp;
	}
}

//冒泡排序
void bubble_sort(void* base, int sz, int width, int (*compare)(const void* elem1, const void* elem2))
{
	int i = 0, j = 0;
	for (i = 0; i < sz - 1; i++)
	{
		for (j = 0; j < sz - i - 1; j++)
		{
			if (compare((char*)base + j * width, (char*)base + (j + 1) * width ) > 0)//通过compare函数来判断是否要交换,使函数更灵活
			{
				Swap((char*)base + j * width, (char*)base + (j + 1) * width);//交换数据
			}
		}
	}
}

//整数比较函数
int compare_int(const void* elem1, const void* elem2)
{
	return *(int*)elem1 - *(int*)elem2;//通过改变这两个元素的顺序,来改变排序逆序顺序
}

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

这里是用来排整型,这里的compare函数是针对int类型写的。运行结果:

这里的使用char*指针来对内存中的数据一个字节一个字节地交换示意图:

排结构体类型数据
#include <stdio.h>
#include <string.h>

//交换函数
//将两个元素的数据分为一字节一字节的,一字节一字节地交换,因为不确定要排什么数据类型,这里的数据交换用这种方式
void Swap(char* elem1, char* elem2)
{
	int width = (int)(elem2 - elem1);//计算宽度,一个元素的大小,用来调整下面的循环次数
	int i = 0;
	char temp = 0;//临时变量
	for (i = 0; i < width; i++)//数据宽度有多大,就循环几次
	{
		//将两个元素的数据分为一字节一字节的,一字节一字节地交换
		temp = *(elem1 + i);
		*(elem1 + i) = *(elem2 + i);
		*(elem2 + i) = temp;
	}
}

//冒泡排序
void bubble_sort(void* base, int sz, int width, int (*compare)(const void* elem1, const void* elem2))
{
	int i = 0, j = 0;
	for (i = 0; i < sz - 1; i++)
	{
		for (j = 0; j < sz - i - 1; j++)
		{
			if (compare((char*)base + j * width, (char*)base + (j + 1) * width ) > 0)//通过compare函数来判断是否要交换,使函数更灵活
			{
				Swap((char*)base + j * width, (char*)base + (j + 1) * width);//交换数据
			}
		}
	}
}

//定义结构体类型
typedef struct Student
{
	char name[20];
	int age;
};

int compare_by_name(const void* a, const void* b)
{
	const Student* Studenta = (const Student*)a;
	const Student* Studentb = (const Student*)b;
	return strcmp(Studenta->name, Studentb->name);
}

int compare_by_age(const void* a, const void* b)
{
	const Student* Studenta = (const Student*)a;
	const Student* Studentb = (const Student*)b;
	return Studenta->age - Studentb->age;
}

int main()
{
	int i = 0;
	Student students[3] = { {"bob",18},{"cindy",16},{"alice",17} };//初始化结构体数组

	//按名字排序
	bubble_sort(students, sizeof(students) / sizeof(students[0]), sizeof(students[0]), compare_by_name);

	printf("按名字排序结果:\n");
	for (i = 0; i < 3; i++)
	{
		printf("%s %d\n", students[i].name, students[i].age);
	}

	//按年龄排序
	bubble_sort(students, sizeof(students) / sizeof(students[0]), sizeof(students[0]), compare_by_age);

	printf("按年龄排序结果:\n");
	for (i = 0; i < 3; i++)
	{
		printf("%s %d\n", students[i].name, students[i].age);
	}
	return 0;
}

运行结果:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值