深入理解指针(详解)

  • 前言
  • 一、指针是什么
    • 1、指针的定义
    • 2、指针的大小
  • 二、指针类型
    • 1、类型
    • 2、不同类型的意义
  • 三、野指针
    • 1、野指针形成原因
    • 2、如何避免野指针
  • 四、指针的运算
    • 1、 指针±整数
    • 2、指针-指针
    • 3、指针的关系运算
  • 五、const修饰指针
    • 1、consr修饰变量
    • 2、const修饰指针变量
  • 六、指针的使用和传址调用
    • 1、strlen的模拟实现
    • 2、指针的传址调用和传址调用
  • 七、数组名的理解
    • 1、数组名本质
    • 2、指针访问数组
  • 八、二级指针
    • 1、二级指针的概念
  • 九、字符指针变量
  • 十、指针数组
  • 十一、数组指针
    • 1、定义
    • 2、使用
  • 十二、数组参数与指针参数
    • 1、一维数组、指针传参
    • 2、二级数组、指针传参
  • 十三、函数指针
    • 1、函数指针的定义
    • 2、函数指针的使用
    • 3、函数指针数组
    • 4、函数指针数组的使用 - 模拟计算器
    • 5、指向函数指针数组的指针
  • 十四、回调函数
    • 1、定义
    • 2、使用-qsort


前言

指针(Pointer)是C语言中最强大、最灵活,但也最容易令人困惑的概念之一。它直接操作内存地址,赋予程序员底层控制能力,使得C语言在系统编程、嵌入式开发、数据结构等领域占据不可替代的地位。然而,指针的不当使用也常常导致程序崩溃、内存泄漏、数据损坏等严重问题。

为什么指针如此重要?如何正确理解指针的本质?指针与数组、函数、动态内存管理之间有何联系?如何避免指针使用中的常见陷阱?

本文将系统性地剖析指针的核心概念,包括:

  1. 指针的本质:内存地址与变量访问
  2. 指针的基本操作(声明、初始化、解引用)
  3. 指针与数组、字符串的关系
  4. 多级指针(指针的指针)与指针运算
  5. 函数指针与回调机制
  6. 动态内存管理与常见错误(悬垂指针、内存泄漏)

无论你是C语言初学者,还是希望深入理解底层机制的开发者,本文都将帮助你掌握指针的精髓,并写出更高效、更安全的代码。

让我们从指针的基础概念开始,逐步深入,揭开它神秘的面纱!


一、指针是什么

1、指针的定义

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

这是官方对指针的定义,其实我们可以理解为:在内存中,内存被细分为一个个大小为一个字节的内存单元,每一个内存单元都有自己对应的地址
在这里插入图片描述
注意:
在这里插入图片描述

我们可以将这些内存单元形象地看成一个个的房间,将内存单元(房间)对应的地址形象地看成房间的门牌号。而我们通过门牌号(地址)就可以唯一的找到其对应的房间(内存单元),即地址指向对应内存单元。所以说,可以将地址形象化的称为“指针”。
指针变量是用于存放地址的变量。(存放在指针中的值都将被当作地址处理)

#include<stdio.h>
int main()
{int a = 10;//在内存中开辟一块空间int* p = &a;//将a的地址取出,放到指针变量p中return 0;
}

总结:

  • 指针变量是用于存放地址的变量。(存放在指针中的值都将被当作地址处理)

2、指针的大小

对于32位的机器,即有32根地址线,因为每根地址线能产生正电(1)或负电(0),所以在32位的机器上能够产生的地址信号就是32个0/1组成的二进制序列。一共 2 32 个地址。同样的算法,在64位的机器上一共能产生 264 个不同的地址。

  • 232 可以用32个bit位进行存储,而8个bit位等价于1个字节,所以在32位的平台下指针的大小为4个字节
  • 264 可以用64个bit位进行存储,所以在64位的平台下指针的大小为8个字节

总结:

在32位平台下指针的大小为4个字节,在64位平台下指针的大小为8个字节。

二、指针类型

1、类型

我们知道,变量的类型有int,float,char等。那么指针有没有类型呢?回答是肯定的。
指针的定义方式是type+ *
**char *** 类型的指针存放的是char类型的变量地址;
**int *** 类型的指针存放的是int类型的变量地址;
**float *** 类型的指针存放的是float类型的变量地址等。

2、不同类型的意义

  1. 指针±整数
    若指针类型为int * 的指针+1,那么它将跳过4个字节的大小指向4个字节以后的内容:
    在这里插入图片描述
  2. 指针解引用
    指针的类型决定了指针解引用的时候能够访问几个字节的内容。
    若指针类型为int *,那么将它进行解引用操作,它将可以访问从指向位置开始向后4个字节的内容:
    在这里插入图片描述

若指针类型为char *,那么将它进行解引用操作,它将可以访问从指向位置开始向后1个字节的内容,以此类推.

总结:

  • 指针的类型决定了指针向前或向后走一步有多大距离。
  • 指针的类型决定了指针在进行解引用操作时,能向后访问的空间大小。

三、野指针

概念: 野指针就是指向位置是不可知的(随机的、不正确的、没有明确限制的)指针。

1、野指针形成原因

  1. 野指针的成因
#include<stdio.h>
int main()
{int* p;*p = 10;return 0;
}

局部指针变量p未初始化,默认为随机值,所以这个时候的p就是野指针。

  1. 指针越界访问
#include<stdio.h>
int main()
{int arr[10] = { 0 };int* p = &arr[0];int i = 0;for (i = 0; i < 11; i++){*p++ = i;}return 0;
}

当指针指向的范围超出arr数组时,p就是野指针。

  1. 指针指向的空间被释放
#include<stdio.h>
int* test()
{int a = 10;return &a;
}
int main()
{int* p = test();return 0;
}

指针变量p得到地址后,地址指向的空间已经释放了,所以这个时候的p就是野指针。(局部变量出了自己的作用域就被释放了)

2、如何避免野指针

  1. 指针初始化
    当指针明确知道要存放某一变量地址时,在创建指针变量时就存放该变量地址。
    当不知道指针将要用于存放哪一变量地址时,在创建指针变量时应置为空指针(NULL)。
#include<stdio.h>
int main()
{int a = 10;int* p1 = &a;//明确知道存放某一地址int* p2 = NULL;//不知道存放哪一地址时置为空指针return 0;
}
  1. 小心指针越界
  2. 指针指向的空间被释放后及时置为NULL
  3. 使用指针之前检查有效性
  4. 在使用指针之前需确保其不是空指针,因为空指针指向的空间是无法访问的。

四、指针的运算

1、 指针±整数

#include <stdio.h>
int main()
{int n = 10;char *pc = (char*)&n;int *pi = &n;printf("%p\n", &n);printf("%p\n", pc);printf("%p\n", pc+1);printf("%p\n", pi);printf("%p\n", pi+1);return 0;
}

运行结果如下:
在这里插入图片描述
我们可以看出, char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。
这就是指针变量的类型差异带来的变化。指针+1,其实跳过1个指针指向的元素。指针可以+1,那也可以-1。

结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)。

2、指针-指针

指针-指针的绝对值是是两个指针之间的元素个数。

//指针-指针 
#include <stdio.h>
int my_strlen(char *s)
{char *p = s;while(*p != '\0' )p++;return p-s;
}int main()
{printf("%d\n", my_strlen("abc"));return 0;
}

运行结果:
在这里插入图片描述

3、指针的关系运算

//指针的关系运算 
#include <stdio.h>
int main()
{int arr[10] = {1,2,3,4,5,6,7,8,9,10};int *p = &arr[0];int sz = sizeof(arr)/sizeof(arr[0]);while(p<arr+sz) //指针的⼤⼩⽐较 {printf("%d ", *p);p++;}return 0;
}

五、const修饰指针

1、consr修饰变量

变量是可以修改的,如果把变量的地址交给⼀个指针变量,通过指针变量的也可以修改这个变量。
但是如果我们希望⼀个变量加上⼀些限制,不能被修改,怎么做呢?这就是const的作⽤。

#include <stdio.h>
int main()
{int m = 0;m = 20;//m是可以修改的 const int n = 0;n = 20;//n是不能被修改的 return 0;
}

上述代码中n是不能被修改的,其实n本质是变量,只不过被const修饰后,在语法上加了限制,只要我们在代码中对n就⾏修改,就不符合语法规则,就报错,致使没法直接修改n。但是如果我们绕过n,使⽤n的地址,去修改n就能做到了,虽然这样做是在打破语法规则。
在这里插入图片描述

我们可以看到这⾥⼀个确实修改了,但是我们还是要思考⼀下,为什么n要被const修饰呢?就是为了不能被修改,如果p拿到n的地址就能修改n,这样就打破了const的限制,这是不合理的,所以应该让p拿到n的地址也不能修改n,那接下来怎么做呢?

2、const修饰指针变量

⼀般来讲const修饰指针变量,可以放在的左边,也可以放在的右边,意义是不⼀样的。

#include <stdio.h>
//代码1 - 测试⽆const修饰的情况 
void test1()
{int n = 10;int m = 20;int *p = &n;*p = 20;//ok?p = &m; //ok?
}//代码2 - 测试const放在*的左边情况 
void test2()
{int n = 10;int m = 20;const int* p = &n;*p = 20;//ok?p = &m; //ok?
}
//代码3 - 测试const放在*的右边情况 
void test3()
{int n = 10;int m = 20;int * const p = &n;*p = 20; //ok?p = &m; //ok?
}
//代码4 - 测试*的左右两边都有const 
void test4()
{int n = 10;int m = 20;int const * const p = &n;*p = 20; //ok?p = &m; //ok?
}
int main()
{//测试⽆const修饰的情况 test1();//测试const放在*的左边情况 test2();//测试const放在*的右边情况 test3();//测试*的左右两边都有const test4();return 0;
}

结论:const修饰指针变量的时候

  • const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本⾝的内容可变。
  • const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。

六、指针的使用和传址调用

1、strlen的模拟实现

库函数strlen的功能是求字符串⻓度,统计的是字符串中 \0 之前的字符的个数。
函数原型如下:

size_t strlen ( const char * str );

参数str接收⼀个字符串的起始地址,然后开始统计字符串中 \0 之前的字符个数,最终返回⻓度。
如果要模拟实现只要从起始地址开始向后逐个字符的遍历,只要不是 \0 字符,计数器就+1,这样直到 \0 就停⽌。

int my_strlen(const char * str)
{int count = 0;assert(str);while(*str){count++;str++;}return count;
}
int main()
{int len = my_strlen("abcdef");printf("%d\n", len);return 0;
}

2、指针的传址调用和传址调用

在这里插入图片描述
在这里插入图片描述
传址调⽤,可以让函数和主调函数之间建⽴真正的联系,在函数内部可以修改主调函数中的变量;所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采⽤传值调⽤。如果函数内部要修改主调函数中的变量的值,就需要传址调⽤。

七、数组名的理解

1、数组名本质

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

这⾥我们使⽤ &arr[0] 的⽅式拿到了数组第⼀个元素的地址,但是其实数组名本来就是地址,⽽且是数组⾸元素的地址
在这里插入图片描述
但是数组名是首元素地址,如何理解下面代码?
在这里插入图片描述
其实数组名就是数组⾸元素(第⼀个元素)的地址是对的,但是有两个例外:

  • sizeof(数组名),sizeof中单独放数组名,这⾥的数组名表⽰整个数组,计算的是整个数组的⼤⼩,单位是字节
  • &数组名,这⾥的数组名表⽰整个数组,取出的是整个数组的地址(整个数组的地址和数组⾸元素的地址是有区别的)
    除此之外,任何地⽅使⽤数组名,数组名都表⽰⾸元素的地址。
    在这里插入图片描述

2、指针访问数组

在这里插入图片描述
数组名arr是数组⾸元素的地址,可以赋值给p,其实数组名arr和p在这⾥是等价的。那我们可以使⽤arr[i]可以访问数组的元素,那p[i]是否也可以访问数组呢?
在这里插入图片描述
同理arr[i]应该等价于*(arr+i),数组元素的访问在编译器处理的时候,也是转换成⾸元素的地址+偏移量求出元素的地址,然后解引⽤来访问的。

注意:在数组传参的时候,传递的是数组名,也就是说本质上数组传参传递的是数组⾸元素的地址。

八、二级指针

1、二级指针的概念

在这里插入图片描述
对于⼆级指针的运算有:

  • *ppa 通过对ppa中的地址进⾏解引⽤,这样找到的是 pa , *ppa 其实访问的就是 pa
  • **ppa 先通过 *ppa 找到 pa ,然后对 pa 进⾏解引⽤操作: *pa ,那找到的是 a

九、字符指针变量

我们知道,在指针的类型中有一种指针类型叫字符指针char * 。
字符指针的一般使用方法为:

#include<stdio.h>
int main()
{char ch = 'w';char* p = &ch;return 0;
}

代码中,将字符变量ch的地址存放在了字符指针p中。

其实,字符指针还有另一种使用方式:

#include<stdio.h>
int main()
{char* p = "hello csdn.";printf("%c\n", *p);//打印字符'h'printf("%s\n", p);//打印字符串"hello csdn."return 0;
}
#include<stdio.h>
int main()
{char* p = "hello csdn.";printf("%c\n", *p);//打印字符'h'printf("%s\n", p);//打印字符串"hello csdn."return 0;
}

代码中,字符指针p中存放的并非字符串"hello csdn.",字符指针p中存放的是字符串"hello csdn.“的首元素地址,即字符’h’的地址。
所以,当对字符指针p进行解引用操作并以字符的形式打印时只能打印字符’h’。我们知道,打印一个字符串只需要提供字符串的首元素地址即可,既然字符指针p中存放的是字符串的首元素地址,那么我们只要提供p(字符串首地址)并以字符串的形式打印,便可以打印字符串"hello csdn.”。
注意:代码中的字符串"hello csdn."是一个常量字符串。

这里有一道题目,可以帮助我们更好的理解字符指针和常量字符串:

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

在这里插入图片描述
题目中str1和str2是两个字符数组,比较str1和str2时,相当于比较数组str1和数组str2的首元素地址,而str1与str2是两个不同的字符数组,创建数组str1和数组str2是会开辟两块不同的空间,它们的首元素地址当然不同。
在这里插入图片描述
而str3和str4是两个字符指针,它们指向的都是常量字符串"hello csdn."的首元素地址,所以str3和str4指向的是同一个地方。
在这里插入图片描述

注意:常量字符串与普通字符串最大的区别是,常量字符串是不可被修改的字符串,既然不能被修改,那么在内存中没有必要存放两个一模一样的字符串,所以在内存中相同的常量字符串只有一个。

十、指针数组

指针数组也是数组,是用于存放指针的数组。

int* arr3[5];
char* arr4[10];//数组arr4包含10个元素,每个元素是一个一级字符型指针。
char** arr5[5];//数组arr5包含5个元素,每个元素是一个二级字符型指针。

十一、数组指针

1、定义

我们已经知道了,整型指针是指向整型的指针,字符指针是指向字符的指针,那么数组指针应该就是指向数组的指针了。整型指针和字符指针,在使用时只需取出某整型/字符型的数据的地址,并将地址存入整型/字符型指针即可。

#include<stdio.h>
int main()
{int a = 10;int* pa = &a;//取出a的地址存入整型指针中char ch = 'w';char* pc = &ch;//取出ch的地址存入字符型指针中return 0;
}

数组指针也是一样,我们只需取出数组的地址,并将其存入数组指针即可。

#include<stdio.h>
int main()
{int arr[10] = { 0 };int(*p)[10] = &arr;//&arr - 数组的地址return 0;
}

解释:p先和结合,说明p是⼀个指针变量,然后指针指向的是⼀个⼤⼩为10个整型的数组。所以p是⼀个指针,指向⼀个数组,叫数组指针。
这⾥要注意:[]的优先级要⾼于
号的,所以必须加上()来保证p先和*结合。
在这里插入图片描述
在这里插入图片描述

2、使用

数组指针有一个简单的使用案例,那就是打印二维数组:

#include<stdio.h>
void print(int(*p)[5], int row, int col)
{int i = 0;for (i = 0; i < row; i++)//行数{int j = 0;for (j = 0; j < col; j++)//列数{printf("%d ", *(*(p + i) + j));}printf("\n");//打印完一行后,换行}
}
int main()
{int arr[3][5] = { { 1, 2, 3, 4, 5 }, { 2, 3, 4, 5, 6 }, { 3, 4, 5, 6, 7 } };print(arr, 3, 5);//传入二维数组名,即二维数组首元素地址,即二维数组第一行的地址return 0;
}

在这里我们打印一个三行五列的二维数组。传参时我们传入二维数组的数组名,明确打印的起始位置;传入行数和列数,明确打印的数据范围。
通过上面对&数组名和数组名的认识,我们知道了这里传入的数组名代表的是二维数组的首元素地址,而二维数组的首元素第一行的元素,即传入的是一维数组的地址,所以我们必须用数组指针进行接收。
打印时,通过表达式 * (*(p+i)+j ) 锁定打印目标:
在这里插入图片描述

十二、数组参数与指针参数

1、一维数组、指针传参

在这里插入图片描述

总结:⼀维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式

2、二级数组、指针传参

void test1(int arr[2][3], int row, int column)
{for (int i = 0; i < row; i++){for (int j = 0; j < column; j++){printf("%d ", arr[i][j]);}printf("\n");}
}void test2(int(*p)[3], int row, int column)
{for (int i = 0; i < row; i++){for (int j = 0; j < column; j++){printf("%d ", *(*(p+i) + j));}printf("\n");}
}#include<stdio.h>
int main()
{int arr[2][3] = { {1,2,3},{7,8,9} };test1(arr,2,3);test2(arr, 2, 3);return 0;
}

在这里插入图片描述
所以,根据数组名是数组⾸元素的地址这个规则,⼆维数组的数组名表⽰的就是第⼀⾏的地址,是⼀维数组的地址。第⼀⾏的⼀维数组的类型就是 int [3] ,所以第⼀⾏的地址的类型就是数组指针类型 int(*)[3] 。那就意味着⼆维数组传参本质上也是传递了地址,传递的是第⼀⾏这个⼀维数组的地址,那么形参也是可以写成指针形式的。

总结:⼆维数组传参,形参的部分可以写成数组,也可以写成指针形式。

十三、函数指针

1、函数指针的定义

2、函数指针的使用

我们知道,整型指针是指向整型的指针,数组指针是指向数组的指针,其实,函数指针就是指向函数的指针。
和学习数组指针一样,学习函数指针我们也需要知道三点

( )的优先级要高于 * 。
一个变量除去了变量名,便是它的变量类型。
一个指针变量除去了变量名和 * ,便是指针指向的内容的类型。

#include<stdio.h>
int Add(int x, int y)
{return x + y;
}
int main()
{int(*p)(int, int) = &Add;//取出函数的地址放在函数指针p中return 0;
}

那么,函数指针p的类型我们是如何创建的呢?

  • 首先,p是一个指针,所以必须先与 * 结合,而( )的优先级高于 * ,所以我们要把 * 和p用括号括起来,让它们先结合。
  • 指针p指向的内容,即函数Add的类型是int (int,int),所以函数指针p就变成了int(*p)(int,int)。
  • 去掉变量名p后,便是该函数指针的变量类型int( * )(int,int)。
    在这里插入图片描述
    知道了如何创建函数指针,那么函数指针应该如何使用呢?
  1. 函数指针的赋值
    对于数组来说,数组名和&数组名它们代表的意义不同,数组名代表的是数组首元素地址,而&数组名代表的是整个数组的地址。但是对于函数来说,函数名和&函数名它们代表的意义却是相同的,它们都代表函数的地址(毕竟你也没有听说过函数有首元素这个说法吧)。所以,当我们对函数指针赋值时可以赋值为&函数名,也可以赋值为函数名。
	int(*p)(int, int) = &Add;int(*p)(int, int) = Add;
  1. 通过函数指针调用函数
    **方法一:**我们知道,函数指针存放的是函数的地址,那么我们将函数指针进行解引用操作,便能找到该函数了,于是就可以通过函数指针调用该函数。
#include<stdio.h>
int Add(int x, int y)
{return x + y;
}
int main()
{int a = 10;int b = 20;int(*p)(int, int) = &Add;int ret = (*p)(a, b);//解引用找到该函数printf("%d\n", ret);return 0;
}

可以理解为, * 和&是两个相反的操作符,像正号(+)和负号(-)一样,一个 * 操作符可以抵消一个&操作符。
在这里插入图片描述
**方法二:**我们在函数指针赋值中说到,函数名和&函数名都代表函数的地址,我们可以赋值时直接赋值函数名,那么通过函数指针调用函数的时候我们就可以不用解引用操作符就能找到函数了。

#include<stdio.h>
int Add(int x, int y)
{return x + y;
}
int main()
{int a = 10;int b = 20;int(*p)(int, int) = Add;int ret = p(a, b);//不用解引用printf("%d\n", ret);return 0;
}

在这里插入图片描述

3、函数指针数组

	int(*pArr[10])(int, int);//数组pArr有10个元素,每个元素的类型是int(*)(int,int)

函数指针数组的创建只需在函数指针创建的基础上加上[ ]即可。
比如,你要创建一个函数指针数组,这个数组中存放的函数指针的类型均为int(*)(int,int),如果你要创建一个函数指针为该类型,那么该函数指针的写法为int(*p)(int,int),现在你要创建一个存放该指针类型的数组,只需在变量名的后面加上[ ]即可,int(*pArr[10])(int,int)。

4、函数指针数组的使用 - 模拟计算器

函数指针数组一个很好的运用场景,就是计算机的模拟实现:

#include<stdio.h>
//菜单
void menu()
{printf("|----------------------------|\n");printf("|----------- 0.Exit ---------|\n");printf("|----------- 1.Add  ---------|\n");printf("|----------- 2.Sub  ---------|\n");printf("|----------- 3.Mul  ---------|\n");printf("|----------- 4.Div  ---------|\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;
}#include<stdio.h>
int main()
{int input = 0;//输入选项int a = 0;//第一个操作数int b = 0;//第二个操作数int ret = 0;//计算结果int(*Parr[5])(int,int) = {0,Add,Sub,Mul,Div};//加0是因为让下标刚好对应选项int sz = sizeof(Parr) / sizeof(Parr[0]);do {menu();printf("请输入:\n");scanf_s("%d", &input);if (input == 0){printf("程序退出!\n");break;}else if (input > 0 && input < sz){printf("请输入两个需要计算的数:");scanf_s("%d %d", &a, &b);ret = Parr[input](a, b);printf("ret = %d\n", ret);}else{printf("输入错误!请重新输入!");}} while (input);return 0;
}

代码中,函数指针数组存放的是一系列参数和返回类型相同的函数名,即函数指针。将0放在该函数指针数组的第一位是为了让用户输入的数字input与对应的函数指针下标相对应。
该代码若不使用函数指针数组,而选择使用一系列的switch分支语句当然也能达到想要的效果,但会使代码出现许多重复内容,而且当以后需要增加该计算机功能时又需要增加一个case语句,而使用函数指针数组,当你想要增加计算机功能时只需在数组中加入一个函数名即可。

5、指向函数指针数组的指针

既然存在函数指针数组,那么必然存在指向函数指针数组的指针。

	int(*p)(int, int);//函数指针int(*pArr[5])(int, int);//函数指针数组int(*(*pa)[5])(int, int) = &pArr;//指向函数指针数组的指针

在这里插入图片描述
所以pa就是一个指向函数指针数组的指针,该函数指针数组中每个元素类型是int(*)(int, int)。

十四、回调函数

1、定义

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。

#include<stdio.h>
void test1()
{printf("hello\n");
}
void test2(void(*p)())
{p(); //指针p被用来调用其所指向的函数
}
int main()
{test2(test1);//将test1函数的地址传递给test2return 0;
}

在该代码中test1函数不是由该函数的实现方直接调用,而是将其地址传递给test2函数,在test2函数中通过函数指针间接调用了test1函数,那么函数test1就被称为回调函数。

2、使用-qsort

在这里插入图片描述

  1. qsort函数的第一个参数是待排序的内容的起始位置;
  2. 第二个参数是从起始位置开始,待排序的元素个数;
  3. 第三个参数是待排序的每个元素的大小,单位是字节;
  4. 第四个参数是一个函数指针。

qsort函数的返回类型为void。
qsort函数的第四个参数是一个函数指针,该函数指针指向的函数的两个参数的参数类型均为const void*,返回类型为int。当参数e1小于参数e2时返回小于0的数;当参数e1大于参数e2时返回大于0的数;当参数e1等于参数e2时返回0。

列如,我们要排一个整型数组:

#include<stdio.h>
int compare(const void* e1, const void* e2)
{return *((int*)e1) - *((int*)e2);
}//自定义的比较函数
int main()
{int arr[] = { 2, 5, 1, 8, 6, 10, 9, 3, 5, 4 };int sz = sizeof(arr) / sizeof(arr[0]);//元素个数qsort(arr, sz, 4, compare);//用qsort函数将arr数组排序return 0;
}

最终arr数组将被排为升序。

注意:qsort函数默认将待排序的内容排为升序,如果我们要排为降序可将自定义的比较函数的两个形参的位置互换一下即可。

在qsort函数中我们传入了一个函数指针,最终qsort函数会在其内部通过该函数指针调用该函数,那么我们的这个自定义比较函数就被称为回调函数。

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

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

相关文章

小谈相机的学习过程

前言博主本人并非专职相机开发&#xff0c;还涉及系统的其他几个模块&#xff0c;虽然都属于owner&#xff0c;但是都还在学习探索的一个过程&#xff0c;自认为掌握还不够细致&#xff0c;此篇文章仅梳理&#xff0c;总结&#xff0c;印证自己近五年相机模块的一个学习过程&am…

CentOS7 内网服务器yum修改

1、首先确定的内网服务器是有yum源代理服务器的2、修改 /etc/yum.conf 配置文件&#xff0c;增加代理ip和端口号proxyhttp://ip.ip.ip.ip:port3、备份源是文件sudo cp /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.bak4、修改配置文件 vi CentOS-Base…

基于单片机自行车自动防盗报警系统设计

摘 要 本文阐述了自行车防盗报警系统原理&#xff0c;介绍如何用stc89c52单片机实现防盗报警&#xff0c;分析各个部分的工作原理&#xff0c;并给出了原理图和源程序。此设计电路由震动传感器、单片机、无线超再生发射/接收电路、LED显示器和蜂鸣器组成。由于超再生接收是一种…

【深度学习】神经网络反向传播算法-part4

七、反向传播算法反向传播Back Propagation 简称 BP 。 训练神经网络的核心算法之一&#xff0c;通过计算损失函数&#xff0c;相对于每个权重参数的梯度&#xff0c;来优化神经网络的权重1. 前向传播前向传播是把数据经过各层神经元的运算并逐层向前传输&#xff0c;知道输出层…

CTF之服务器端模板注入(SSTI)与赛题

概念定义服务器端模板注入(Server-Side Template Injection)服务端接受攻击者的输入&#xff0c;将其作为Web应用内容的一部分&#xff0c;在进行代码编译渲染的过程中&#xff0c;进行了语句的拼接&#xff0c;执行了所插入的恶意内容&#xff0c;从而导致信息泄露、代码执行、…

除了某信,就是这款软件来替代了!

引言 哈喽&#xff0c;我是小索奇。有时候会有一个普遍的需求&#xff0c;想在几个设备之间传个文件或者发个消息&#xff0c;除了微信&#xff0c;想一想你还能用什么软件&#xff1f; 今天就是为了解决这个问题&#xff0c;给大家介绍一款软件 Localsend 来解决。 内容模块…

Vue2.x封装预览PDF组件

一、为什么用PDFObject插件&#xff1f; PDFObject 是一个轻量级的 JavaScript 库&#xff0c;主要用于在网页中嵌入和预览 PDF 文件。它通过简单的 API 调用&#xff0c;可以在浏览器中实现 PDF 文件的显示&#xff0c;而无需依赖任何插件。以下将详细介绍 PDFObject 的特点、…

undefined reference to ‘end‘

相关问题&#xff1a; 一、undefined reference to _exit undefined reference to ‘end‘ warning: _close is not implemented and will always fail 一、环境&#xff1a; ubuntu24.04实体机、 arm-none-eabi-gcc gcc version 13.2.1 20231009 (15:13.2.rel1-2) 二…

nginx定制http头信息

修改http响应头信息&#xff0c;相关Nginx模块&#xff1a;ngx_http_headers_moduleexpires语法&#xff1a;expires [modified] time;expires [modified] time;默认值&#xff1a;expires off;作用域&#xff1a;http, server, location, if in location用途&#xff1a;控制缓…

主机安全---开源wazuh安装

Wazuh 简介 Wazuh 是一款免费开源的终端安全监控平台&#xff0c;支持威胁检测、完整性监控、事件响应和合规性管理&#xff0c;适用于企业级安全运维场景。其核心组件包括&#xff1a; Wazuh Indexer&#xff1a;基于 OpenSearch 的日志存储与检索组件。Wazuh Server&#x…

GaussDB 数据库架构师修炼(四) 备份容量估算

1 影响备份容量关键要素业务总数据量备份数据保留周期备份周期备份数据的压缩比平均每天的新增数据量平均每天新增日志数据量2 备份容量的估算方法公式备份容量C &#xff1d; 自动全量备份容量C1 &#xff0b; 自动差量备份容量C2 &#xff0b; 自动日志归档 容量C3 &#xff…

《R for Data Science (2e)》免费中文翻译 (第0章) --- Introduction

写在前面 本系列推文为《R for Data Science (2e)》的中文翻译版本。所有内容都通过开源免费的方式上传至Github&#xff0c;欢迎大家参与贡献&#xff0c;详细信息见&#xff1a; Books-zh-cn 项目介绍&#xff1a; Books-zh-cn&#xff1a;开源免费的中文书籍社区 r4ds-zh-cn…

如何 ASP.NET Core 中使用 WebSocket

如何在 ASP.NET Core 中使用 WebSocket在现代 Web 应用程序中&#xff0c;WebSocket 连接非常流行且使用率极高。它可以帮助企业满足数字环境需求&#xff0c;并处理来自最终用户的实时数据。它还能提升生产力、输出率和用户体验。如果您还没有使用 WebSocket&#xff0c;那么您…

Python之--元组

定义是 Python 中内置的不可变序列。在 Python 中使用&#xff08;&#xff09;定义元组&#xff0c;元素与元素之间使用英文的逗号分隔。元组中只有一个元素的时候&#xff0c;逗号也不能省略。元组的创建方式&#xff08;1&#xff09;使用&#xff08;&#xff09;直接创建元…

工业相机GigE数据接口的优势及应用

工业相机不同的数据接口适用的应用场景也不同&#xff0c;选择合适的数据额接口&#xff0c;可大大提高效率。今天我们来看看常见的GigE接口的优势及应用。基于GigE Vision标准的千兆以太网&#xff08;GigE&#xff09;相机通过提供快速、灵活且成本效益高的成像解决方案&…

【53】MFC入门到精通——MFC串口助手(二)---通信版(发送数据 、发送文件、数据转换、清空发送区、打开/关闭文件),附源码

文章目录1 完整 功能展示2 添加控件变量及声明2.1 添加控件及变量2.2 SerialPortDlg.h: 头文件3 函数实现3.1 数据发送3.1.2 写数据、字符串转3.2 发送文件3.2.1 打开文件3.2.2 发送文件3.3 清空发送区4 完整MFC项目项下载1 完整 功能展示 串口通信助手 页面展示&#xff0c;功…

算法学习笔记:27.堆排序(生日限定版)——从原理到实战,涵盖 LeetCode 与考研 408 例题

堆排序&#xff08;Heap Sort&#xff09;是一种基于二叉堆数据结构的高效排序算法&#xff0c;由计算机科学家 J. W. J. Williams 于 1964 年提出。它结合了选择排序的思想和二叉堆的特性&#xff0c;具有时间复杂度稳定&#xff08;O (nlogn)&#xff09;、原地排序&#xff…

I/O 多路复用select,poll

目录 I/O多路复用的介绍 多进程/多线程模型的弊端 网络多路复用如何解决问题&#xff1f; 网络多路复用的常见实现方式 常见的开源网络库 select详细介绍 select函数介绍 套接字可读事件,可写事件,异常事件 fd_set类型介绍 select的两次拷贝&#xff0c;两次遍历 se…

最终分配算法【论文材料】

文章目录一、最终分配算法1.1 平衡的情况1.2 不平衡的情况1.3 TDM 约束一、最终分配算法 上一步合法化后&#xff0c;group 的 TDM 情况大致分为两类&#xff0c;一类是平衡的&#xff0c;最大的一些 group 的 TDM 比较接近。另外一种情况就是不平衡的&#xff0c;最大的 group…

《大数据技术原理与应用》实验报告七 熟悉 Spark 初级编程实践

目 录 一、实验目的 二、实验环境 三、实验内容与完成情况 3.1 Spark读取文件系统的数据。 3.2 编写独立应用程序实现数据去重。 3.3 编写独立应用程序实现求平局值问题。 四、问题和解决方法 五、心得体会 一、实验目的 1. 掌握使用 Spark 访问本地文件和 HDFS 文件的…