C语言—深入理解指针(详)

发布于:2025-07-20 ⋅ 阅读:(14) ⋅ 点赞:(0)


前言

指针(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");
	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;
}

在这里插入图片描述
题目中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函数的地址传递给test2
	return 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函数会在其内部通过该函数指针调用该函数,那么我们的这个自定义比较函数就被称为回调函数。


网站公告

今日签到

点亮在社区的每一天
去签到