【C语言】指针篇 | 万字笔记

写在前面

在学习C语言过程,总有一个要点难点离不开,那就是大名鼎鼎的C语言指针,也是应为有指针的存在,使得C语言一直长盛不衰。因此不才把指针所学的所有功力都转换成这个笔记。希望对您有帮助🥰🥰

学习指针之前我们必须先精通数组,因为在数组中可以学到C语言的精髓:当前变量的数据类型是什么。这里给大家推荐一下不才写的数组笔记:【C语言】数组icon-default.png?t=O83Ahttps://blog.csdn.net/m0_71580879/article/details/138375047


目录

写在前面

一、指针的创建

二、指针是什么

三、指针变量是什么

四、指针类型

五、指针的解引用

六、野指针

6.1. 指针未初始化

6.2.指针越界访问

6.3.指针指向的空间释放

七、指针运算

7.1.指针+- 整数

7.2.指针-指针

7.3.指针的关系运算

八、指针和数组

九、二级指针

9.1二级指针的运算

十、字符指针

十一、指针与数组的各种关系

11.1.指针数组

11.2数组指针

11.3.数组参数、指针参数

十二、函数指针

十三、函数指针数组

十四、指向函数指针数组的指针

十五、回调函数

十六、void*类型


一、指针的创建

指针的定义方式是: type + *

在深入学习指针之前,我们需要知道如何创建一个指针,指针的关键字是什么。

举个栗子

#include <stdio.h>

int main() {
	int a = 1;

	int* p = &a;

	return 0;
}

解析:

  • 在上面代码中,我们首先定义了一个 整形变量a
  • 之后我们就定义了一个 指针变量 并且把 a的地址赋值给了 整形指针变量p

解析 int * p = &a :在不才之前的笔记中就有写道,' * ' 是解引用操作符(【C语言】基础操作符大全),但是在此时,' * '变成了一个标识符,标识了变量p 是一个指针变量。前面的 int 标志着 赋值的变量 类型是int整形(这个知识极其重要)。因为指针变量只用来存放地址的变量。(存放在指针变量中的值都被当成地址处理),所以必须把a的地址取出来赋值给指针变量。


二、指针是什么

在计算机科学中,指针(Pointer)是编程语言中的一个对象,利用地址它的值直接指向 (points to)存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址 的内存单元。 那我们就可以这样理解:指针 就是 内存,内存 就是 指针。


三、指针变量是什么

指针变量是个变量,用来存放内存单元的地址(存放在指针中的值都被当成地址处理)

既然我们知道了指针变量是变量,那么指针变量大小是多少?

如何计算我们的指针变量大小,关键在于我们机器上的地址线。因为在 二、指针是什么 中我们了解到地址就是指针,那么地址线的大小就关系着我们指针的大小。我们在2024年了,现在的机器绝大部分都是32位 / 64位机器了,32位机器就对于着32个地址线,我们以32位机器为例:

在32个地址中,我们就有32根地址线,那么假设每根地址线在寻址的是产生一个电信号正电/负电(1或 者0)。

那么32根地址线产生的地址就会是:

00000000 00000000 00000000 00000000

00000000 00000000 00000000 00000001

......

11111111 11111111 11111111 11111111

一个地址线占用空间大小是一个比特,那么32个地址线就占用了32个比特。我们知道一个字节就是 八个比特,那么32机器的指针变量占用空间就是 4个字节

通过上述的步骤我们不难计算出 在64位机器或环境中 指针变量 的大小是 8个字节 


四、指针类型

指针中也有类型,不过指针的具体类型需要根据 赋值变量 来决定,如在 一、指针的创建 中讲诉的一样

//列举部分

char  *pc = NULL;

int   *pi = NULL;

short *ps = NULL;

long  *pl = NULL;

float *pf = NULL;

double *pd = NULL;

 指针类型的具体作用在于可以让我们知道 指针 + 1 是跳过多少个字节,也可以知道对指针解引用的时候有多大的权限(能操作几个字节),这点是指针的精髓处之一。


五、指针的解引用

我们已经知道了指针变量保存的是指针指向变量的地址。那么我们可以通过解引用来访问到变量的内容

int main() {
	int i = 100;
	int* a = &i;

	*a = 50;
	printf("%d\n", *a);
	printf("%d\n", i);

	return 0;
}

  • 在上面截图中我们不难发现,i变量的值进行了改变,那么如果想通过指针变量改变指向的变量的值,我们只需要使用解引用操作符就可以完成相对于的操作。
  • *a就等于i

结合四、五的复合例子(设为小端字节序):

#include <stdio.h>
int main()
{
	int n = 0x11223344;
	char* pc = (char*)&n;
	int* pi = &n;
	*pc = 0;
	*pi = 0;
	return 0;
}

解析:

  • 在小端字节序中,我们n变量在内存中的存储为:

 在上图中我们可以看到我们n变量的值低位存放在低地址处高位存放在高地址处

  • 接着我们把n变量强制类型转换成 char* 后赋值给指针变量pc ,此时我们变量pc就指向了n变量的地址。也把n变量的指针赋值给了int* pi。

我们解引用pc把0赋值给*pc,即把0赋值给整形变量n,我们知道char类型只能访问一个字节,那么在内内存中,我们指针变量pc也只能访问到一个字节,并把0赋值给第一个字节

在调试内存中我们也清晰看见,整形变量 n 四个字节中第一个字节被修改成00,其他字节不改变,这说明了指针变量解引用后也收到我们类型的约束类型可以访问多少字节类型指针也只能访问多少字节

然后程序接着进行把整形指针变量解引用(*pi)并且把0赋值给*pi。

整形在内存中可以访问4个字节整形指针也同理解引用后也只能访问四个字节,我们把0赋值给了*pi,那再内存中,我们就把n变量的四个字节全部赋值为0


六、野指针

概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)指针变量 在定义时如果未初始化,其值是随机的,指针变量的值是别的变量的地址,意味着指针指向了一 个地址是不确定的变量,此时去解引用就是去访问了一个不确定的地址,所以结果是不可知的。

野指针成因 :

  • 1. 指针未初始化
  • 2. 指针越界访问 
  • 3. 指针指向的空间释放

6.1. 指针未初始化

#include <stdio.h>
int main()
{
	int* p;//局部变量指针未初始化,默认为随机值
	*p = 20;
	return 0;
}

6.2.指针越界访问

#include <stdio.h>

int main()
{
    int arr[10] = { 0 };
    int* p = arr;
    int i = 0;
    for (i = 0; i <= 11; i++)
    {
        //当指针指向的范围超出数组arr的范围时,p就是野指针
        *(p++) = i;
    }
    return 0;
}

6.3.指针指向的空间释放

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

int main() {
	int* ps = (int*)malloc(sizeof(int) * 10);
	if (ps == NULL) {
		perror("main::ps");
	}
	*(ps + 0) = 100;
	free(ps);//在空间释放后,ps为定义为NULL,此时ps就为野指针
	return 0;
}
  • 虽然在释放malloc内存后,程序就结束了,但是没有给ps指针赋值为NULL,ps是实打实的是野指针的,若后续代码继续访问ps指针,便成了野指针的访问。(野指针的访问在程序编译链接中不会报错,但是在程序运行中会输出不是预期值

规避野指针方法:

  • 1. 指针初始化
  • 2. 小心指针越界
  • 3. 指针指向空间释放即使置NULL
  • 4. 指针使用之前检查有效性

七、指针运算

  • 指针+- 整数
  • 指针-指针
  • 指针的关系运算

7.1.指针+- 整数

在上面四、五中讲解非常清晰,这里不过多介绍,可以上拉看四、五。


7.2.指针-指针

指针-指针的使用方式必须在连续空间上使用

指针-指针的作用用于返回连续空间的大小

指针-指针的值返回的是结尾指针到开头指针之间有多少个类型变量

举个栗子:

#include <stdio.h>

int main() {
	int arr[10] = {1,2,3,4,5,6,7,8,9,'\0'};
	int* s = arr;
	int* p = NULL;
	int i = 0;
	while(arr[i] != '\0')
	{
		i++;
	}
	p = (arr + i);
	printf("%d\n", p - s); //结果为9
	return 0;
}
  • 在上面循环中,已经把i的数记录了到 '\0' 前的元素个数后把 "arr + i" 赋值给p,那么p就指向了数组下标为 8 的地址
  • "p指针 - s指针“得到的结果为 9 说明arr数组在' \0 '前里面存放了9个整形元素

7.3.指针的关系运算

 允许指向数组元素的指针指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许指向第一个元素之前的那个内存位置的指针进行比较。

 举个栗子

数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较;🫡

int main() {
	char arr[10] = "123456789";
	char* s = arr;
	char* p = &arr + 1;
	printf("%d\n", p - s);
	return 0;
}
  • 我们知道"&arr + 1"是跳过整个数组,那此时 指针p 指向的就是数组最后一个元素的后一位的地址
  • 我们进行"指针 - 指针"得到的结果为:10。没有程序没有报错

数组元素的指针与指向第一个元素之前的那个内存位置的指针比较

int main() {
	char arr[10] = "123456789";
	char* s = arr - 1;
	char* p = (arr + 9);
	printf("%d\n", p - s);
	return 0;
}
  • 实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证 它可行。

八、指针和数组

数组名是首元素地址

举个栗子:

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
	printf("%p\n", arr);
	printf("%p\n", &arr[0]);
	return 0;
}

运行结果:(%p 是打印地址)

可见数组名和数组首元素的地址是一样的。 

既然可以把数组名当成地址存放到一个指针中,我们使用指针来访问一个数组就成为可能。

举个栗子:

int main()
{
    int arr[] = { 1,2,3,4,5,6,7,8,9,0 };
    int* p = arr; //指针存放数组首元素的地址
    int sz = sizeof(arr) / sizeof(arr[0]);
    int i = 0;
    for (i = 0; i < sz; i++)
    {
        printf("&arr[%d] = %p   <====> p+%d = %p\n", i, &arr[i], i, p + i);
    }
    return 0;
}

运行结果为:

所以p+i其实计算的是数组 arr 下标为i的地址。 那我们就可以直接通过指针来访问数组

那我们就可以直接通过指针来访问数组。 如下:

int main()
{
	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
	int* p = arr; //指针存放数组首元素的地址
	int sz = sizeof(arr) / sizeof(arr[0]);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p + i));
	}
	return 0;
}

此时我们就得到一个重要结论:

&arr[0] <==> (p + 0)

=> *&arr[0] <==> *(p + 0) <==> arr[0] 

=> p[0] <==> arr[0]

说明下标引用操作符 是与 指针加x解引用相等的,其实在底层运算中,下标引用操作符是会转换成 指针加x解引用 的方式进行数据的访问的。

九、二级指针

指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里? 这就是 二级指针。

举个栗子:

int main() {
	int a = 0;
	int* pa = &a;
	int** ppa = &pa;
	return 0;
}

说白了就是我们在指针创建的时候说的

解析 int * p = &a :在不才之前的笔记中就有写道,' * ' 是解引用操作符(【C语言】基础操作符大全),但是在此时,' * '变成了一个标识符,标识了变量p 是一个指针变量。前面的 int 标志着 赋值的变量 类型是int整形(这个知识极其重要)。因为指针变量只用来存放地址的变量。(存放在指针变量中的值都被当成地址处理),所以必须把a的地址取出来赋值给指针变量。)的例子

我们知道了a的类型是int所以把指针变量设置为了int类型指针,同理,我们知道我们要赋值给指针变量 ppa 的数据类型是一级指针类型int*)那么我们定义的类型也是int*)我们也要再加上一个    ' * ' 用来表示变量 n 是指针变量int* *ppa)。下图表示各级指针在内存存储图

9.1二级指针的运算

举个栗子:

int main() {
	int a = 0;
	int* pa = &a;
	int** ppa = &pa;
	return 0;
}
  • *ppa 通过对ppa中的地址进行解引用,这样找到的是 int b = 20; *ppa = &b;//等价于 pa = &b; pa , *ppa 其实访问的就是 pa 。如下:
int b = 20;
 *ppa = &b;//等价于 pa = &b;
  • **ppa 先通过 *ppa 找到 pa ,然后对pa进行解引用操作 **ppa = 30; //等价于*pa = 30; //等价于a = 30; *pa ,那找到的是a。如下:
**ppa = 30;
 //等价于*pa = 30;
 //等价于a = 30;

十、字符指针

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

一般使用:


int main()
{
	char ch = 'w';
	char* pc = &ch;
	*pc = 'w';
	return 0;
}

还有一种使用方式如下:

int main()
{
	char* pstr = "hello bit.";//这里是把一个字符串放到pstr指针变量里了吗?
	printf("%s\n", pstr);
	return 0;
}
  • 这里就涉及到了常量字符串的内容,常量字符串是不能被改变的,只会提供首元素地址。不才在之前有写过一篇专门讲字符串的笔记大家可以阅读一下(更利于理解字符指针的内容):【C语言】字符串icon-default.png?t=O83Ahttps://blog.csdn.net/m0_71580879/article/details/127864752

在理解了两种用法后,可以尝试做一下这道题目:

#include <stdio.h>
int main()
{
	char str1[] = "hello bit.";
	char str2[] = "hello bit.";
	char* str3 = "hello bit.";
	char* str4 = "hello bit.";
	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不同。

十一、指针与数组的各种关系

  • 这小节讲的是指针与数组的各种关系,如对数组没有太深入的了解的话没看着小结会很吃力。这里推荐先看不才完的数组笔记后再回来复习这小结 :【C语言】数组icon-default.png?t=O83Ahttps://blog.csdn.net/m0_71580879/article/details/138375047

11.1.指针数组

指针数组是指针还是数组?

答案:是数组

是存放指针的数组。 数组我们已经知道整形数组,字符数组,那自然是有指针数组

我们先了解一下整形数组在内存中的布局和字符数组在内存中的布局: 

我们可以清晰的观察到数组中连续空间的类型,同理,指针数组也是一样

举个栗子: char* arr3[5];

解析 char* arr3[5]:

  • 首先根据优先级arr3会与' [] '结合形成一个数组,数组里面有5个元素 -> arr3[5]
  • 数组存放的元素类型是 char*
  • 所以 char* arr3[5] 就定义了 可以存放5个元素每个元素类型是char* 指针数组

11.2数组指针

 数组指针:指向数组的指针

我们已经熟悉: 整形指针: int * pint; 能够指向整形数据的指针。 浮点型指针: float * pf; 能够指向浮点型数据的指针。

那数组指针应该是:能够指向数组的指针

举个栗子:int (*p2)[5];

  • 根据优先级 变量p2先与括号内的*标识符结合,*标识符表示了p2变量是一个指针变量
  • 出了括号后根据优先级,马上与' [] '数组标识符结合 (*p2)[] ,表示了指针变量p2指向了一个数组
  • (*p2)[5],表示p2指向的数组中有5个元素
  • int (*p2)[5],表示p2指向的数组的5个元素,每个元素都是int类型

这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。

数组指针的使用

既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址

举个栗子:

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
	int(*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p
	//但是我们一般很少这样写代码
	return 0;
}

这样数组指针就可以访问到数组的元素,但是我们一般不这样用,一般把数组指针用在函数的形参中。

举个栗子:

#include <stdio.h>
void print_arr1(int arr[3][5], int row, int col)
{
    int i = 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;
    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,其实相当于第一行的地址,是一维数组的地址
   //可以数组指针来接收
    print_arr2(arr, 3, 5);
    return 0;
}

在上面代码 print_arr2 函数中,我们使用数组指针接收了实参 传来的二维数组的首元素地址即一个一维数组的地址),使用数组指针遍历了二维数组的全部元素。

复习了上面两个小点,我们触类旁通一下下面代码的意思:

int arr[5];
int* parr1[10];
int(*parr2)[10];
int(*parr3[6])[5];

解析:

  • 第一个:以存放5个元素且每个元素是int类型的一维数组
  • 第二个:是一个可以存放10元素且每个元素是int* 类型的 指针数组
  • 第三个:是一个 指向可以存放10元素且每个元素是int 类型的数组 的数组指针
  • 第四个:是一个可以存放6元素且每个元素是 int(*)[5] 数组指针类型的 指针数组

我们单独画出 int(*parr3[6])[5] 的内存分布图

11.3.数组参数、指针参数

一维数组传参,我们形参可以使用:一位数组、一级指针、数组指针或二维数组来接收

void test(int arr[])//ok?
{}
void test(int arr[10])//ok?
{}
void test(int* arr)//ok?
{}
void test2(int* arr[20])//ok?
{}
void test2(int** arr)//ok?
{}
int main()
{
	int arr[10] = { 0 };
	int* arr2[20] = { 0 };
	test(arr);
	test2(arr2);
}

 在上面几个函数形参都是可以定义的

解析:(由上到下依此为12345)

  1. 正确实参整形数组形参使用整形数组来接收时没有问题的。
  2. 同上。
  3. 正确我们知道在数组中,只有两种特殊情况数组名代表着整个数组的地址,那我们观察可以知 arr数组的全部元素都是 int类型,所以使用 int类型指针来接收首元素地址时可行的。
  4. 正确实参是指针数组形参使用实参数组来接收时没有问题,当然对数组了解的足够深入的话就知道此时arr2数组的类型时 " int* [20] ",在形参中使用相同类型(" int* [20] ")来接收自然时没有问题的。 
  5. 正确实参传递了首元素地址,实参的每个元素类型是整形一级指针 " int* ",所以在形参上使用整形二级指针来接收时可行的。

二维数组传参

通过了一维数组传参的例子后,二维数组传参也是异曲同工

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

解析:(由上到下依此为1234567)

分析传参是否配对,我们首先要分析我们实参的元素类型和首元素类型。在这个栗子中,我们可以判断出每个元素的类型时 int 类型。首元素是二维数组第一行的地址,那么首元素类型是" int [5] " 是整形数组类型

  1. 正确。使用相同类型(int [3][5])接收实参毫无问题
  2. 错误。二位数组的问题,只能省略创建多少行,不能省略一行有多少个元素(具体可看不才笔记:【C语言】数组)
  3. 正确。实参首元素类型是" int [5] ",形参类型使用二维数组,每行可以接收5个int元素,类型匹配。
  4. 错误。实参首元素类型是" int [5] ",形参是整形指针(int*)类型,类型不匹配。
  5. 错误。实参首元素类型是" int [5] ",形参是每个元素为int*类型的指针数组类型,类型不匹配。
  6. 正确。实参首元素类型是" int [5] ",形参是数组指针类型且指向的元素类型为 (int [5]),类型匹配。
  7. 错误,实参首元素类型是" int [5] ",形参是二级整形指针(int**)类型,类型错误。

一级指针传参

以 void test1(int *p); 函数为例。

  • 可以接收 int 类型参数的取地址 ,即:int a = 10; test1(&a);
  • 可以接收 int* 类型参数,即:int* p = &a; test1(p);
  • 可以接收整形一维数组 ,即:int arr[5] = { 0 }; test1(arr);

二级指针传参

void test(char** p)
{
    ;
}
int main()
{
	char c = 'b';
	char* pc = &c;
	char** ppc = &pc;
	char* arr[10];
	test(&pc);
	test(ppc);
	test(arr);//Ok?
	return 0;
}

解析:(由上到下依此为123)

  1. 正确,形参可以接收类型是" char* "等类型,&c取地址后的类型是" char* ",类型匹配。
  2. 正确,形参可以接收类型是" char* "等类型,ppc的类型是" char* ",类型匹配。
  3. 正确,形参可以接收类型是" char* "等类型,arr首元素类型是" char* ",类型匹配。

十二、函数指针

我们用过整形指针、字符指针、数组指针,此时我们的函数的地址也可以使用指针获取的

 首先我们先要知道函数的地址是怎么表达的

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

运行结果:

上面结果可知,我们函数名就是函数地址函数名与取地址函数的到的结果是一样的。这里我们可以直接认为相同,因为函数地址不会进行任何的算数运算,因为函数地址进行算数运算没有任何意义

我们得知函数的地址后,我们则需要创建函数指针来保存函数的地址。

我们还是按照数组指针的创建思想来进行,我们创建一个函数指针来接收上面例子的test函数

  • 首先我们需要指针标识符变量结合,即加上括号 (*ps)
  • ps设为指针变量后,我们需要结合函数标识符 " () ",这时我们就得到 (*ps)() ,这样我们雏形的函数指针就设好了
  • test函数有一个" int类型 "形参,所以我们在指针变量中也要加上所对应的形参,即:(*ps)(int),在函数指针的形参设定中,不需要进行形参变量的定义的,只需要标识类型即可
  • 接着我们查看test函数的返回类型是什么,就把所对应返回类型填入函数指针中,即:void (*ps)(int) 。这样我们就把一个函数指针ps定义好啦。
  • 最后我们把函数 test 地址赋值给函数指针ps即可。void (*ps)(int) = test;

经过上面步骤就可以完成函数指针的创建与初始化

函数指针的使用

函数指针的使用和正常函数的使用方式是一样的。 

为了更直观的感受,不才在test函数上增加了打印变量

void test(int a)
{
	printf("%d\n", a);
	printf("hehe\n");
}
int main()
{
	void (*ps)(int) = test;
	int n = 110;
	ps(n);

	return 0;
}

 运行结果:

那使用时一样的,为什么我们还需要搞个函数指针呢?🫡

不才回复:存在即有意义!😎

这里我们就需要复习到函数指针数组

十三、函数指针数组

顾名思义,函数指针数组就是用来存放函数指针的数组

函数指针数组的创建

结合优先级和十二小结创建函数指针来进行大胆推测。我们假设存放5个函数都为void a1(int a, int b)

  • 我们已经知道了函数指针就是 type (* ps)(...)的模式创建,因为括号的优先级高所以ps先与*结合成为指针变量
  • 我们只需要在括号内 加上数组标识符即可根据优先级ps与数组标识符结合了,即:type (* ps[])(...)
  • 那么,我们要存放我们的要求,那就先创建数组 即: ps[5]。
  • 那么我们数组内的每个元素的类型都要求为 void (*)(int, int),那么结合起来                           即 :void (* ps[5])(int, int)

当然如果我们使用类型重定义typedef)的话可以实现和 int类型 定义一样(int a) 左右布局来定义

例如:

void test(int a, int b) {

}

int main() {
	typedef void (* nnt)(int, int);
	int x = 0;
	int u = 0;
	nnt a[2] = { 0 };
	a[0] = test;

	return 0;
}

 不才把 void(*)(int.int) 类型重新命名为 nnt,此时我们就可以使用nnt来进行类型定义了。

函数指针数组的用途:转移表

我们在c语言中制作一个计算器,在选择语句中需要使用switch语句,其中需要使用到大量的case-break语句来进行语句的划分。

选择我们只需要使用函数指针数组转移表)即可以简化多行代码,实现选择语句。

例如:我们使用(先使用switch语句来进行计算机选择语句的逻辑条件)

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-break代码,且case中代码高耦合,只有在函数不同。

现在我们使用转移表,来实现低耦合

int main()
{
    int x, y;
    int input = 1;
    int ret = 0;
    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 (* ps[5])(int, int);为例改造成一个指向5个元素数组且每个元素时函数指针 的指针)

  • 在函数指针数组的基础上,把变量优先级先与指针标识符绑定。即 (*ps)
  • 那么其他照旧 void (* (*ps)[5])(int, int);
  • 这样即表示了ps 时一个指针变量,根据优先级指针变量ps先和数组标识符' [] '结合,那么他指向的就是一个数组,数组里面有五个元素每个元素的类型是 "  void(*)(int, int)  "。

 异曲同工的我们可以继续无限的推理出 函数指针数组的指针的数组、指向函数指针数组的指针的数组的指针...这里不才就不过多介绍了。大伙有兴趣可以自己推理玩一下


十五、回调函数

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

我们使用一下qsort函数来体验一下回调函数是什么。

#include <stdio.h>
//qosrt函数的使用者得实现一个比较函数
int int_cmp(const void* p1, const void* p2)
{
    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), int_cmp);
    for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
    {
        printf("%d ", arr[i]);
    }
    printf("\n");
    return 0;
}

使用qsort函数时,我们需要自己创建一个比较函数,然后把我们写好的比较函数的地址形参的形式传输到qsort中,给qsort确认如何比较。这样我们的比较函数int_cmp函数就形成了回调函数

这里时官方文档连接:点我跳转qsort官方文档。

 在阅读文档时候,我们发现qsort函数使用的形参居然时( void* )类型。什么是(void*)类型呢?

十六、void*类型

void* 类型是无类型指针,用于不知道实参是什么类型的时候可以把形参定义为void*类型,void*类型可以接收所有类型,在使用变量时候我们自己强制类型转换为我们需要类型即可,但是一般是强转换成(char*)并要求使用时提供一个元素大小时是多少字节,在提供一个元素多少字节后,使用char* 循环范围多少个字节空间即可得到一个元素的内容。

#include <stdio.h>
int int_cmp(const void* p1, const void* p2)//确保与bubble函数调用时一致,所以定义为void*
{
    return (*(int*)p1 - *(int*)p2);//程序猿自己创建的函数,知道时整形比较
}
void _swap(void* p1, void* p2, int size)//交换函数
{
    int i = 0;
    for (i = 0; i < size; i++)//传入了一个元素的大小,循环size次即可把元素交换成功
    {
        char tmp = *((char*)p1 + i);
        *((char*)p1 + i) = *((char*)p2 + i);//一个字节一个字节的交换
        *((char*)p2 + i) = tmp;
    }
}
void bubble(void* base, int count, int size, int(*cmp)(void*, void*))//普通冒泡排序
{
    int i = 0;
    int j = 0;
    for (i = 0; i < count - 1; i++)
    {
        for (j = 0; j < count - i - 1; j++)
        {
            if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0)//(char*)base * size可以计算出一个元素的占用字节。
            {
                _swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
            }
        }
    }
}
int main()
{
    int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
    int i = 0;
    bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), int_cmp);
    for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
    {
        printf("%d ", arr[i]);
    }
    printf("\n");
    return 0;
}

以上就是本章所有内容。若有勘误请私信不才。万分感激💖💖 若有帮助不要吝啬点赞哟~~💖💖

ps:表情包来自网络,侵删🌹

若是看了这篇笔记彻底拿捏指针可以在评论区打出:小小指针!拿捏!😎

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/886368.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【2025】基于Hadoop短视频流量数据分析与可视化(源码+文档+调试+答疑)

文章目录 前言一、主要技术&#xff1f;二、项目内容1.整体介绍&#xff08;示范&#xff09;2.运行截图3.部分代码介绍 总结更多项目 前言 随着我国经济的高速发展与人们生活水平的日益提高&#xff0c;人们对生活质量的追求也多种多样。尤其在人们生活节奏不断加快的当下&am…

unix中的exec族函数介绍

一、前言 本文将介绍unix中exec族函数&#xff0c;包括其作用以及使用方法。当一个进程调用fork函数创建一个新进程后&#xff0c;新进程可以直接执行原本正文段的其他内容&#xff0c;但更多时候&#xff0c;我们在一个进程中调用fork创建新的进程后&#xff0c;希望新进程能…

杭州电子科技大学《2019年+2023年861自动控制原理真题》 (完整版)

本文内容&#xff0c;全部选自自动化考研联盟的&#xff1a;《杭州电子科技大学861自控考研资料》的真题篇。后续会持续更新更多学校&#xff0c;更多年份的真题&#xff0c;记得关注哦~ 目录 2019年真题 2023年真题 Part1&#xff1a;2019年2023年完整版真题 2019年真题 2…

ubuntu 开启root

sudo passwd root#输入以下命令来给root账户设置密码 sudo passwd -u root#启用root账户 su - root#要登录root账户 root 开启远程访问&#xff1a; 小心不要改到这里了&#xff1a;sudo nano /etc/ssh/ssh_config 而是&#xff1a;/etc/ssh/sshd_config sudo nano /etc/ssh…

猫猫cpu的缓存

原题过长&#xff0c;放一下题目大意 题目大意 给你 m m m 个 1 1 1 到 n n n 之间的整数&#xff0c;你要找到若干个大小为固定的 k k k 的闭区间&#xff0c;使得所有这些数都在你找到的某个区间内。你需要最小化这些区间的并集的大小&#xff0c;并输出此大小。本题里…

基于单片机的两轮直立平衡车的设计

本设计基于单片机设计的两轮自平衡小车&#xff0c;其中机械部分包括车体、车轮、直流电机、锂电池等部件。控制电路板采用STC12C5A60S2作为主控制器&#xff0c;采用6轴姿态传感器MPU6050测量小车倾角&#xff0c;采用TB6612FNG芯片驱动电机。通过模块化编程完成了平衡车系统软…

calibre-web的翻译translations

calibre-web的翻译translations Windows安装calibre-web&#xff0c;Python-CSDN博客文章浏览阅读539次&#xff0c;点赞10次&#xff0c;收藏11次。pip install calibreweb报错&#xff1a;error: Microsoft Visual C 14.0 or greater is required. Get it with "Microso…

机器学习(5):机器学习项目步骤(二)——收集数据与预处理

1. 数据收集与预处理的任务&#xff1f; 为机器学习模型提供好的“燃料” 2. 数据收集与预处理的分步骤&#xff1f; 收集数据-->数据可视化-->数据清洗-->特征工程-->构建特征集和数据集-->拆分数据集、验证集和测试集 3. 数据可视化工作&#xff1f; a. 作用&…

深入理解 C 语言中的内存操作函数:memcpy、memmove、memset 和 memcmp

目录&#xff1a; 前言一、 memcpy 函数二、 memmove 函数三、 memset 函数四、 memcmp 函数总结 前言 在 C 语言中&#xff0c;内存操作函数是非常重要的工具&#xff0c;它们允许我们对内存进行直接操作&#xff0c;从而实现高效的数据处理。本文将深入探讨四个常用的内存操…

DC00022基于ssm高校社团管理系统web社团管理系统java web+MySQL项目web程序设计

1、项目功能演示 DC00022基于ssm高校社团管理系统web社团管理系统java web项目MySQL 2、项目功能描述 社团管理系统分为普通用户、管理员 2.1 普通用户功能 01 系统登录、系统注册 02 系统首页、新闻公告、规章制度、社团活动、互动交流 03 修改密码 04 个人信息修改 05 我的…

Win10鼠标总是频繁自动失去焦点-非常有效-重启之后立竿见影

针对Win10鼠标频繁自动失去焦点的问题&#xff0c;可以尝试以下解决方案&#xff1a; 一、修改注册表&#xff08;最有效的方法-重启之后立竿见影&#xff09; 打开注册表编辑器&#xff1a; 按下WindowsR组合键&#xff0c;打开运行窗口。在运行窗口中输入“regedit”&#x…

ECP 集成字段非必填配置

导读 INTRODUCTION 非必填设置&#xff1a;ECP主数据同步的时候&#xff0c;经常遇到一个问题&#xff0c;就是ECP报错&#xff0c;但是这个字段两边的ecp顾问与sf顾问都觉得没实际意思&#xff0c;觉得没有传输的必要性&#xff0c;这个时候我们就可以考虑非必输的字段不必输…

【机器学习】集成学习——提升模型准确度的秘密武器

【机器学习】集成学习——提升模型准确度的秘密武器 1. 引言 集成学习&#xff08;Ensemble Learning&#xff09;是一种通过结合多个弱模型来提升整体预测准确性的技术。通过将多个模型的预测结果进行组合&#xff0c;集成学习在复杂任务中展现了极强的泛化能力。本文将探讨…

Redis篇(面试题 - 连环16炮)(持续更新迭代)

目录 &#xff08;第一炮&#xff09;一、Redis&#xff1f;常用数据结构&#xff1f; 1. 项目里面到了Redis&#xff0c;为什么选用Redis&#xff1f; 2. Redis 是什么&#xff1f; 3. Redis和关系型数据库的本质区别有哪些&#xff1f; 4. Redis 的线程模型了解吗&#x…

C++ -引用-详解

博客主页&#xff1a;【夜泉_ly】 本文专栏&#xff1a;【C】 欢迎点赞&#x1f44d;收藏⭐关注❤️ C -引用-详解 1.引用基础1.1是什么1.2特点 2.引用的意义3.引用的应用场景3.1作为参数3.2作为返回值传值返回引用返回 4.权限问题5.与指针的区别6.总结 1.引用基础 1.1是什么 …

计算机网络期末复习真题(附真题答案)

前言&#xff1a; 本文是笔者在大三学习计网时整理的笔记&#xff0c;哈理工的期末试题范围基本就在此范畴内&#xff0c;就算真题有所更改&#xff0c;也仅为很基础的更改数值&#xff0c;大多跑不出这些题&#xff0c;本文包含简答和计算等大题&#xff0c;简答的内容也可能…

话术挂断之后是否处理事件

文章目录 前言联系我们解决方案方案一方案二 前言 流程&#xff1a;自动外呼进入机器人话术。问题&#xff1a;在机器人放音时用户挂断后&#xff0c;话术还会继续匹配流程&#xff0c;如果匹配上的是放音节点&#xff0c;还会进行放音&#xff0c;那么在数据库表conversation…

stm32四足机器人(标准库)

项目技术要求 PWM波形的学习 参考文章stm32 TIM输出比较(PWM驱动LED呼吸灯&&PWM驱动舵机&&PWM驱动直流电机)_ttl pwm 驱动激光头区别-CSDN博客 舵机的学习 参考文章 stm32 TIM输出比较(PWM驱动LED呼吸灯&&PWM驱动舵机&&PWM驱动直流电机)…

Stream流的初步认识,Stream流的思想和获取Stream流

一.Stream流的作用 package com.njau.my_stream;import java.util.ArrayList;/*** 目标&#xff1a;认识Stream流* 案例&#xff1a;将以“张”开头的人名筛选出来到一个新的集合中去&#xff0c;再将其中三个字的名字的筛选出来到新集合中去*/ public class StreamDemo1 {pub…

畅阅读小程序|畅阅读系统|基于java的畅阅读系统小程序设计与实现(源码+数据库+文档)

畅阅读系统小程序 目录 基于java的畅阅读系统小程序设计与实现 一、前言 二、系统功能设计 三、系统实现 四、数据库设计 1、实体ER图 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 博主介绍&#xff1a;✌️大厂码农|毕设布道师…