【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); //结果为9return 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");elseprintf("str1 and str2 are not same\n");if (str3 == str4)printf("str3 and str4 are same\n");elseprintf("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);}elseprintf("输入有误\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.rhkb.cn/news/438250.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于Springboot+Vue的饮食营养管理信息系统(含源码数据库)

1.开发环境 开发系统:Windows10/11 架构模式:MVC/前后端分离 JDK版本: Java JDK1.8 开发工具:IDEA 数据库版本: mysql5.7或8.0 数据库可视化工具: navicat 服务器: SpringBoot自带 apache tomcat 主要技术: Java,Springboot,mybatis,mysql,vue 2.视频演示地址 3.功能 系统中…

Hystrix学习

系列文章目录 JavaSE基础知识、数据类型学习万年历项目代码逻辑训练习题代码逻辑训练习题方法、数组学习图书管理系统项目面向对象编程&#xff1a;封装、继承、多态学习封装继承多态习题常用类、包装类、异常处理机制学习集合学习IO流、多线程学习仓库管理系统JavaSE项目员工…

MAC M1 安装brew 配置环境变量,安装dart

一. 下载 brew 1. 终端输入 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 2. 如遇到下载失败情况&#xff0c;需要VPN/代理 curl: (7) Failed to connect to raw.githubusercontent.com port 443 after 8 m…

MongoDB简介

1、说到MongoDB就必须说下什么是NoSQL? NoSQL(NoSQL Not Only SQL)&#xff0c;意即反SQL运动&#xff0c;指的是非关系型的数据库&#xff0c;是一项全新的数据库革命性运动&#xff0c;早期就有人提出&#xff0c;发展至2009年趋势越发高涨。NoSQL的拥护者们提倡运用非关系…

Java | Leetcode Java题解之第454题四数相加II

题目&#xff1a; 题解&#xff1a; class Solution {public int fourSumCount(int[] A, int[] B, int[] C, int[] D) {Map<Integer, Integer> countAB new HashMap<Integer, Integer>();for (int u : A) {for (int v : B) {countAB.put(u v, countAB.getOrDefa…

被字节恶心到了

字节 日常逛 xhs 看到一篇吐槽贴&#xff0c;表示被公司恶心到了&#xff1a; 这位网友表示&#xff0c;最近是公司举办了 Q2 和 H1 的优秀员工表彰&#xff0c;自己的 1&#xff08;直属领导&#xff09;评上了&#xff0c;但仔细一看&#xff0c;1 获奖的所有产出都是自己的&…

实时数字人DH_live使用案例

参看: https://github.com/kleinlee/DH_live ubuntu 测试 apt install ffmpeg 下载安装: git clone https://github.com/kleinlee/DH_live.git cd DH_liveconda create -n dh_live python=3.12 conda activate dh_live pip install -r requirements.txt pip install torch …

突发:Sam万字长文,OpenAI o1超越人类,o1模型训练原理、微调、能力来源-AI已死,大模型当立

OpenAl o1大模型&#xff1a;原理、突破、前景及影响 北京时间2024年9月13日凌晨&#xff0c;OpenAI正式发布了新的人工智能模型o1&#xff08;o是orion猎户座&#xff0c;1代表从头再来&#xff0c;也意味着后续将出现更多序列&#xff09;&#xff0c;就是此前OpenAI一直在高…

银河麒麟V10 SP1如何进入救援模式?

银河麒麟V10 SP1如何进入救援模式&#xff1f; 1、准备工作2、进入BIOS/UEFI进入救援模式注意事项 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 在使用银河麒麟高级服务器操作系统V10 SP1时&#xff0c;如果遇到系统无法正常启动或需要进…

240 搜索二维矩阵 II

解题思路&#xff1a; \qquad 解这道题最重要的是如何利用从左到右、从上到下为升序的性质&#xff0c;快速找到目标元素。 \qquad 如果从左上角开始查找&#xff0c;如果当前matrix[i][[j] < target&#xff0c;可以向右、向下扩展元素都是升序&#xff0c;但选择哪个方向…

.Net 6.0 监听Windows网络状态切换

上次发了一个文章获取windows网络状态&#xff0c;判断是否可以访问互联网。传送门&#xff1a;获取本机网络状态 这次我们监听网络状态切换&#xff0c;具体代码如下&#xff1a; public class WindowsNetworkHelper {private static Action<bool>? _NetworkStatusCh…

初步认识产品经理

产品经理 思考问题的维度 1️⃣为什么要抓住核心用户&#xff1f; 所有和产品有关系的群体就是用户&#xff0c;存在共性和差异了解用户的付费点&#xff0c;更好的优化产品是否使用&#xff1a;&#xff08;目标用户-已使用产品&#xff1a;种子用户-尝鲜&#xff1b;核心用…

【在Linux世界中追寻伟大的One Piece】命名管道

目录 1 -> 命名管道 1.1 -> 创建一个命名管道 1.2 -> 匿名管道与命名管道的区别 1.3 -> 命名管道的打开规则 1.4 -> 例子 1 -> 命名管道 管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。如果我们想在不相关的进程之间交换数据&…

C++多重继承

C多重继承 一个类可以从多个类继承&#xff0c;只需在类的基类列表中&#xff08;即冒号后&#xff09;指定更多的基类&#xff0c;用逗号分隔即可。例如&#xff0c;如果程序有一个名为Output的特定类要在屏幕上打印&#xff0c;我们希望派生类Rectangle&#xff08;长方形&a…

Netgear-WN604 downloadFile.php 信息泄露复现(CVE-2024-6646)

0x01 产品描述&#xff1a; NETGEAR WN604是一款功能强大的双频AC1200无线路由器,非常适合中大型家庭和企业使用。它支持最新的802.11ac无线标准,能提供高达1200Mbps的无线传输速度。路由器具备千兆有线网口和3个100Mbps有线网口,可满足有线和无线设备的接入需求。此外,它还内置…

JavaWeb——Vue组件库Element(5/6):案例:组件实现(概述、Form表单、Table表格、Pagination 分页、效果展示、完整代码)

目录 概述 Form表单 Table表格 Pagination 分页 效果展示 完整代码 概述 在刚才制作出来的页面当中&#xff0c;上面项目的名称已制作好&#xff0c;左侧的菜单栏也已配置好。 接下来主要处理的是右侧主展示区域当中的组件编写。 在右侧的主展示区域&#xff0c;主要有…

java版鸿鹄电子招投标系统功能架构设计 核心功能设计 鸿鹄电子招投标采购系统源码

java版鸿鹄电子招投标系统功能架构设计 核心功能设计 鸿鹄电子招投标采购系统源码

数据结构--绪论

1.数据结构的基本概念 1.1数据结构基本概念以及术语 &#xff08;1&#xff09;数据结构是相互之间存在一种或多种特定关系的数据元素的集合。 &#xff08;2&#xff09;数据对象是具有相同性质的数据元素的集合&#xff0c;是数据的一个子集。 &#xff08;3&#xff09;数…

sql server每天定时执行sql语句

sql server每天定时执行sql语句 1、打开SQL Server Management Studio 2、鼠标右击【SQL Server 代理】&#xff0c;选择【启动(S)】&#xff0c;如已启动&#xff0c;可以省略此步骤&#xff1b; 3、右键&#xff0c;新建-》作业&#xff0c;在作业上-》新建作业&#xff…

《RabbitMQ篇》基本概念介绍

MQ功能 解耦 MQ允许不同系统或组件之间松散耦合。发送者和接收者不需要直接连接&#xff0c;从而提高了系统的灵活性和可维护性。异步处理 使用MQ可以实现异步消息传递&#xff0c;发送者可以将消息放入队列后立即返回&#xff0c;不必等待接收者处理。这提高了系统的响应速度…