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

news2024/11/18 11:46:45

写在前面

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

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


一、指针的创建

指针的定义方式是: 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:表情包来自网络,侵删🌹

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【STM32开发环境搭建】-1-Keil(MDK) 5.27软件安装和注册教程

目录 1 安装前装备工作 2 安装KEIL(MDK-ARM) 5.27软件 3 注册KEIL(MDK-ARM) 5.27软件&#xff0c;获取License许可证 4 手动安装STM32F0&#xff0c;STM32F1&#xff0c;STM32F4&#xff0c;STM32F7&#xff0c;STM32H7的支持包 4.1 下载STM32的支持包 4.2 安装STM32的支…

JavaScript 中变量命名的最佳实践

全篇大概1500 字&#xff08;含代码&#xff09;&#xff0c;建议阅读时间5分钟。 1. 避免使用 var 关键字&#xff1a;过时的产物 在现代 JavaScript 中&#xff0c;我们通常避免使用 var&#xff0c;而是选择 let 和 const&#xff0c;它们提供更可预测和块范围的行为&#x…

C++初阶:STL详解(七)——list的模拟实现

✨✨小新课堂开课了&#xff0c;欢迎欢迎~✨✨ &#x1f388;&#x1f388;养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; 所属专栏&#xff1a;C&#xff1a;由浅入深篇 小新的主页&#xff1a;编程版小新-CSDN博客 前言&#xff1a; 我们前面已经了解到了lis…

helm3 部署项目应用示例

一、用到的插件 1、存储卷-日志外挂&#xff1a; 存储类自己提前建 2、env变量-存储nacos信息 二、新建项目 # helm create test-gateway 三、修改values.yaml ## 删除内容 # Additional volumes on the output Deployment definition. volumes: [] # - name: foo # se…

助力智能作物植株统计分析,基于YOLOv7全系列【tiny/l/x】参数模型开发构建田间作物场景下智能精准小麦麦穗检测识别计数系统

农业实验研究的一些场景下&#xff0c;尝尝有对指定视野区域内作物植株数量进行便捷化智能自动化统计计数的需求&#xff0c;诸如&#xff1a;棉花植株统计、小麦植株统计、水稻植株统计等等&#xff0c;这些农业实验场景下&#xff0c;单纯依靠人工数数的方式来进行植株计数是…

SpringBoot--yml配置文件的时间/大小的单位转换

原文网址&#xff1a;SpringBoot--yml配置文件的时间/大小的单位转换_IT利刃出鞘的博客-CSDN博客 简介 说明 本文介绍SpringBoot的yml&#xff08;properties&#xff09;配置文件的时间/大小的单位转换。 概述 SpringBoot可以将yml中的配置绑定到一个Java类的字段&#x…

论文笔记——Graph Bottlenecked Social Recommendation

文章地址 代码地址 1.1简介 随着社交网络的出现&#xff0c;社交推荐已经成为个性化服务的重要技术。最近&#xff0c;基于图的社交推荐通过捕捉高阶社交影响显示出了有希望的结果。大多数基于图的社交推荐的经验研究直接将观察到的社交网络纳入公式&#xff0c;并基于社交同…

【注册/登录安全分析报告:孔夫子旧书网】

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 暴力破解密码&#xff0c;造成用户信息泄露短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造成亏损无底洞…

香港科技大学数据建模MSc(DDM)硕士学位项目25/26招生宣讲会-西安专场

香港科技大学数据建模MSc(DDM)硕士学位项目25/26招生宣讲会-西安专场 &#x1f559;时间&#xff1a;2024 年10 月12日&#xff08;周六&#xff09; 16:00 &#x1f3e0;地点&#xff1a; 西安交大南洋大酒店(交通大学青龙寺店) 行政会议室 &#x1f9d1;‍&#x1f393;嘉宾…

0基础学习PyTorch——GPU上训练和推理

大纲 创建设备训练推理总结 在《Windows Subsystem for Linux——支持cuda能力》一文中&#xff0c;我们让开发环境支持cuda能力。现在我们要基于《0基础学习PyTorch——时尚分类&#xff08;Fashion MNIST&#xff09;训练和推理》&#xff0c;将代码修改成支持cuda的训练和推…

[sql-03] 求阅读至少两章的人数

准备数据 CREATE TABLE book_read (bookid varchar(150) NOT NULL COMMENT 书籍ID,username varchar(150) DEFAULT NULL COMMENT 用户名,seq varchar(150) comment 章节ID ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT 用户阅读表insert into book_read values(《太子日子》…

MindSearch 部署到Github Codespace 和 Hugging Face Space

和原有的CPU版本相比区别是把internstudio换成了github codespace。 教程是https://github.com/InternLM/Tutorial/blob/camp3/docs/L2/MindSearch/readme_github.md 复现步骤&#xff1a; 根据教材安装环境和创建硅基流动 API 然后启动前后端 然后按照教材部署到 Huggi…

安宝特案例 | 某知名日系汽车制造厂,借助AR实现智慧化转型

案例介绍 在全球制造业加速数字化的背景下&#xff0c;工厂的生产管理与设备维护效率愈发重要。 某知名日系汽车制造厂当前面临着设备的实时监控、故障维护&#xff0c;以及跨地域的管理协作等挑战&#xff0c;由于场地分散和突发状况的不可预知性&#xff0c;传统方式已无法…

计算机的错误计算(一百零六)

摘要 探讨含有变元负的整数次方的多项式的计算精度问题。 计算机的错误计算&#xff08;一百零五&#xff09;给出了一个传统多项式的错误计算案例&#xff1b;本节探讨含有变元负的整数次方的多项式的计算精度问题。 例1. 已知 计算 若在Python下计算&#xff0c;则有&…

猎板PCB大讲堂:PCB谐振效应及其对设计的影响

在PCB设计中&#xff0c;谐振效应是一个不可忽视的问题&#xff0c;它可能导致信号完整性问题、电源分配系统&#xff08;PDS&#xff09;工作异常&#xff0c;甚至成为EMI辐射源。以下是关于PCB谐振效应的一些详细信息&#xff1a; 1. 谐振产生的原因&#xff1a; - PCB中…

d2l | 目标检测数据集:RuntimeError: No such operator image::read_file

目录 1 存在的问题2 可能的解决方案3 最终的解决方案3.1 方案一&#xff08;我已弃用&#xff09;3.2 方案二&#xff08;基于方案一&#xff09;3.3 方案三&#xff08;基于方案一&#xff09; 1 存在的问题 李沐老师提供的读取香蕉数据集的函数如下&#xff1a; def…

Ubuntu系统设置bond双网卡

这里我的服务器是Ubuntu 22.04.3 LTS,是高阶版本,设置网卡需要通过netplan 根据你的Ubuntu版本(如使用Netplan或/etc/network/interfaces),选择相应的配置方法。 我这边以root用户登录进服务器,就不需要普通用户每次在命令前添加sudo 1.通常/etc/netplan下配置文件名形…

IDEA开发SpringBoot项目基础入门教程。包括Spring Boot简介、IDEA创建相关工程及工程结构介绍、书写配置文件、Bean对象管理等内容

文章目录 0. 关于本文1. 概述1.1 Spring简介1.2 Spring Boot简介1.3 传统的开发方式1.3.1 简述1.3.2 缺点 1.4 Spring Boot的优点 2. 创建一个简单的Spring Boot应用程序2.1 在IDEA创建项目2.2 pom配置文件内容2.3 启动类2.4 创建Controller 3. 从Maven工程创建Spring Boot工程…

数据结构~二叉搜索树

文章目录 一、二叉搜索树的概念二、二叉搜索树的结构二叉搜索树的性能分析二叉搜索树的插入二叉搜索树的查找二叉搜索树的删除 三、二叉搜索树key和key/value使用场景四、二叉搜索树的练习将二叉搜索树就地转化为已排序的双向循环链表从前序与中序遍历序列构造二叉树二叉树的前…

jmeter-请求参数加密-MD5加密

方法1 &#xff1a;使用jmeter自带的函数助手digest Tool(工具)---Function Helper Dialog(函数助手对话框) 第一个参数是要md5加密的值&#xff0c;第二个参数是保存加密后值的变量 &#xff08; 此处变量是从txt文件导入的&#xff0c;所以使用的是${wd} &#xff09; …