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

写在前面

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

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


一、指针的创建

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

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

举个栗子

cpp 复制代码
#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 是跳过多少个字节,也可以知道对指针解引用的时候有多大的权限(能操作几个字节)****,这点是指针的精髓处之一。


五、指针的解引用

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

cpp 复制代码
int main() {
	int i = 100;
	int* a = &i;

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

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

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

cpp 复制代码
#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. 指针未初始化

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

6.2.指针越界访问

cpp 复制代码
#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.指针指向的空间释放

cpp 复制代码
#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.指针-指针

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

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

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

举个栗子:

cpp 复制代码
#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.指针的关系运算

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

举个栗子

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

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

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

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

八、指针和数组

数组名是首元素地址

举个栗子:

cpp 复制代码
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 是打印地址)

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

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

举个栗子:

cpp 复制代码
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的地址。 那我们就可以直接通过指针来访问数组

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

cpp 复制代码
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解引用 的方式进行数据的访问的。

九、二级指针

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

举个栗子:

cpp 复制代码
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二级指针的运算

举个栗子:

cpp 复制代码
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 。如下:
cpp 复制代码
int b = 20;
 *ppa = &b;//等价于 pa = &b;
  • **ppa 先通过 *ppa 找到 pa ,然后对pa进行解引用操作:**ppa = 30; //等价于*pa = 30; //等价于a = 30; *pa ,那找到的是a。如下:
cpp 复制代码
**ppa = 30;
 //等价于*pa = 30;
 //等价于a = 30;

十、字符指针

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

一般使用:

cpp 复制代码
int main()
{
	char ch = 'w';
	char* pc = &ch;
	*pc = 'w';
	return 0;
}

还有一种使用方式如下:

cpp 复制代码
int main()
{
	char* pstr = "hello bit.";//这里是把一个字符串放到pstr指针变量里了吗?
	printf("%s\n", pstr);
	return 0;
}

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

cpp 复制代码
#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不同。

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

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先和*结合。

数组指针的使用

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

举个栗子:

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

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

举个栗子:

cpp 复制代码
#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 函数中,我们使用数组指针接收了实参 传来的二维数组的首元素地址(即一个一维数组的地址),使用数组指针遍历了二维数组的全部元素。

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

cpp 复制代码
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.数组参数、指针参数

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

cpp 复制代码
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* ",所以在形参上使用整形二级指针来接收时可行的。

二维数组传参

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

cpp 复制代码
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);

二级指针传参

cpp 复制代码
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* ",类型匹配。

十二、函数指针

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

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

cpp 复制代码
#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函数上增加了打印变量

cpp 复制代码
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) 左右布局来定义

例如:

cpp 复制代码
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语句来进行计算机选择语句的逻辑条件)

cpp 复制代码
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中代码高耦合,只有在函数不同。

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

cpp 复制代码
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函数来体验一下回调函数是什么。

cpp 复制代码
#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* 循环范围多少个字节空间即可得到一个元素的内容。

cpp 复制代码
#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:表情包来自网络,侵删🌹

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

相关推荐
Source.Liu6 分钟前
【用Rust写CAD】第二章 第四节 函数
开发语言·rust
monkey_meng6 分钟前
【Rust中的迭代器】
开发语言·后端·rust
余衫马9 分钟前
Rust-Trait 特征编程
开发语言·后端·rust
monkey_meng13 分钟前
【Rust中多线程同步机制】
开发语言·redis·后端·rust
Jacob程序员15 分钟前
java导出word文件(手绘)
java·开发语言·word
懒大王就是我20 分钟前
C语言网络编程 -- TCP/iP协议
c语言·网络·tcp/ip
小白学大数据21 分钟前
正则表达式在Kotlin中的应用:提取图片链接
开发语言·python·selenium·正则表达式·kotlin
VBA633723 分钟前
VBA之Word应用第三章第三节:打开文档,并将文档分配给变量
开发语言
半盏茶香24 分钟前
【C语言】分支和循环详解(下)猜数字游戏
c语言·开发语言·c++·算法·游戏
小堇不是码农30 分钟前
在VScode中配置C_C++环境
c语言·c++·vscode