【C语言深度解剖】:(11)函数指针、函数指针数组、指向函数指针数组的指针、回调函数

news2025/1/21 15:36:26

🤡博客主页:醉竺

🥰本文专栏:《C语言深度解剖》《精通C指针》

😻欢迎关注:感谢大家的点赞评论+关注,祝您学有所成!


✨✨💜💛想要学习更多C语言深度解剖点击专栏链接查看💛💜✨✨ 


目录

1.函数指针

2.函数指针数组

3.指向函数指针数组的指针 

4.回调函数 

4.1 void* 的使用

4.2 使用回调函数,模拟实现qsort

4.3 使用qsort排序结构体 

5.指针和数组笔试题解析 

1.一维数组

2.字符数组

3. 字符串数组

4.字符串指针 

5.二维数组 

6.指针笔试题(难点)

7.对数组和指针的一些思考


1.函数指针

首先看一段代码: 

输出的是两个地址,这两个地址是 test 函数的地址。

对于函数加不加取地址符号” & “效果都一样,调用函数加不加解引用符号” * “也都一样。

那我们的函数的地址要想保存起来,怎么保存?(即函数指针的形式是怎样的?)

下面我们看代码: 

首先,能给存储地址,就要求pfun1或者pfun2是指针,那哪个是指针?

答案是: 

pfun1可以存放。pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void。

阅读两段有趣的代码:

分析上述代码1和代码2分别代表什么含义?

代码1解释:

代码1是 一次函数调用

调用0地址处的一个函数

首先代码中将0强制类型转换成类型为 void (*) ( ) 的函数指针

然后去调用0地址处的函数

第一个” * “号,可无可有,上面已讲过。 

代码2解释:

代码2是 一次函数的声明 

声明的函数名字为 signal

signal函数的参数有2个,第一个参数是int型,第二个参数类型为函数指针型void (*) (int),该函数指针指向的类型是 返回值为void,其中一个参数是int型的函数

 signal函数的返回类型是一个函数指针,该函数指针指向的类型也是 返回值为void,其中一个参数是int型的函数

如果代码按照下面这样子写,大家可能就更容易理解了,不过这种语法是错误的(其实很多复杂指针之所以难学,跟C语言语法风格的设计有很大关系,设计的不够直观):

代码2太复杂,如何简化: 

typedef void(*pfun_t)(int);

pfun_t signal(int, pfun_t);

2.函数指针数组

数组是一个存放相同类型数据的存储空间,那我们已经学习了指针数组,比如:

int* arr[10];
//数组的每个元素是int*

如果一个数组中存放的都是函数的地址,那这个数组就叫函数指针数组,那函数指针数组如何定义呢? 

例子:(计算器)

1.打印菜单及实现函数功能

void menu()
{
	printf("*********1.Add   2.Sub*********\n");
	printf("*********3.Mul   2.Div*********\n");
	printf("*********0.Exit*********\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;
}

方法1:分支循环实现计算器

int main()
{
	int ret = 0;
	int input = 0;

	do
	{
		menu();
		int x = 0, y = 0; // 参与计算的两个数字
		int ret = 0; // 保存计算结果
		printf("请选择计算方式->");
		scanf("%d", &input);

		switch (input)
		{
		case 0:
			printf("退出游戏!\n");
			break;
		case 1:
			printf("请分别输入两个数字x和y:\n");
			scanf("%d%d", &x, &y);
			ret = Add(x, y);
			printf("运算结果为:%d\n", ret);
			break;
		case 2:
			printf("请分别输入两个数字x和y:\n");
			scanf("%d%d", &x, &y);
			ret = Sub(x, y);
			printf("运算结果为:%d\n", ret);
			break;
		case 3:
			printf("请分别输入两个数字x和y:\n");
			scanf("%d%d", &x, &y);
			ret = Mul(x, y);
			printf("运算结果为:%d\n", ret);
			break;
		case 4:
			printf("请分别输入两个数字x和y:\n");
			scanf("%d%d", &x, &y);
			ret = Div(x, y);
			printf("运算结果为:%d\n", ret);
			break;
		default:
			printf("输入不符合条件请重新输入!\n");
			break;
		}

	} while (input);

	return 0;
}

方法2:函数指针数组的应用->转移表,改善方式一的冗余 

int main()
{
	int ret = 0;
	int input = 0;
	int (*pfArr[5])(int, int) = { 0,Add,Sub,Mul,Div }; // 转移表

	do
	{
		menu();
		int x = 0, y = 0; // 参与计算的两个数字
		int ret = 0; // 保存计算结果
		printf("请选择计算方式->");
		scanf("%d", &input);

		if (input >= 1 && input <= 4)
		{
			printf("请分别输入两个数字x和y:\n");
			scanf("%d%d", &x, &y);
			ret = pfArr[input](x, y);
			printf("运算结果为:%d\n", ret);
		}
		else if (input == 0)
		{
			printf("退出游戏!\n");
			break;
		}
		else
		{
			printf("输入不符合条件请重新输入!\n");
		}

	} while (input);

	return 0;
}

方式3:回调函数->实现计算器(第4节会学习回调函数)

void Cal(int (*pfun)(int, int))
{
	int x = 0, y = 0;
	printf("请分别输入两个数字x和y:\n");
	scanf("%d%d", &x, &y);
	int ret = pfun(x, y);
	printf("运算结果为:%d\n", ret);
}

int main()
{
	int ret = 0;
	int input = 0;

	do
	{
		menu();
		int x = 0, y = 0; // 参与计算的两个数字
		int ret = 0; // 保存计算结果
		printf("请选择计算方式->");
		scanf("%d", &input);

		switch (input)
		{
		case 0:
			printf("退出游戏!\n");
			break;
		case 1:
			Cal(Add);
			break;
		case 2:
			Cal(Sub);
			break;
		case 3:
			Cal(Mul);
			break;
		case 4:
			Cal(Div);
			break;
		default:
			printf("输入不符合条件请重新输入!\n");
			break;
		}

	} while (input);

	return 0;
}

3.指向函数指针数组的指针 

指向函数指针数组的指针:首先是一个指针,该指针指向一个 数组,数组的元素都是 函数指针 ; 如何定义? 

void test(const char* str)
{
	printf("%s\n", str);
}
int main()
{
	//函数指针pfun
	void (*pfun)(const char*) = test;
	//函数指针的数组pfunArr
	void (*pfunArr[5])(const char* str);
	pfunArr[0] = test;
	//指向函数指针数组pfunArr的指针ppfunArr
	void (*(*ppfunArr)[5])(const char*) = &pfunArr;
	return 0;
}

4.回调函数 

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进 行响应。 

首先演示一下qsort函数的使用:

4.1 void* 的使用

在C语言中,void* 是一种特殊的指针类型,它表示一个指向未知类型的指针。这种指针可以指向任何类型的数据,但它不知道所指向数据的具体类型。void* 主要用于以下几种情况: 

  1. 函数指针参数或返回值:当函数需要处理多种类型的数据,但具体类型不确定时,可以使用 void* 作为参数类型或返回类型。例如,标准库函数 memcpy 就使用了 void* 作为参数。

  2. 动态内存分配malloc 函数返回 void* 类型的指针,因为它可以分配任何类型的内存。在使用时,通常需要将 void* 强制转换为实际需要的类型。

  3. 类型无关的代码:有时,我们希望编写可以处理任何类型的代码,例如通用数据结构或容器,这时可以使用 void* 来实现类型无关性。

 示例1:作为函数参数

#include <stdio.h>

void print_value(void* ptr) {
    // 强制类型转换
    int value = *(int*)ptr;
    printf("%d\n", value);
}

int main() {
    int x = 10;
    print_value((void*)&x);
    return 0;
}

在这个例子中,print_value 函数接受一个 void* 类型的参数,并在函数内部将其强制转换为 int* 类型,然后打印出该指针指向的整数值。 

示例2:动态内存分配

#include <stdio.h>
#include <stdlib.h>

int main() {
    // 动态分配一个整型大小的内存
    void* ptr = malloc(sizeof(int));

    // 强制类型转换后使用
    *(int*)ptr = 20;

    printf("%d\n", *(int*)ptr);

    // 释放内存
    free(ptr);

    return 0;
}

在这个例子中,我们使用 malloc 分配内存,它返回一个 void* 类型的指针。我们将其强制转换为 int* 类型,以便能够存储一个整数值。

注意事项:

  • 使用 void* 时,必须确保类型转换是正确的,否则可能会导致未定义行为。
  • void* 指针不能直接进行算术操作,例如自增或自减,因为编译器不知道指针指向的数据类型大小。
  • 在使用 void* 指针前,应确保它确实指向了正确的数据类型,否则在解除引用时可能会出现问题。 

4.2 使用回调函数,模拟实现qsort

(这里内部结构采用冒泡的方式) 

#include <stdio.h>
int int_cmp(const void* p1, const void* p2)
{
    return (*(int*)p1 - *(int*)p2);
}
void _swap(void* p1, void* p2, int size)
{
    int i = 0;
    for (i = 0; i < size; i++)
    {
        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)
            {
                _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 };
    //char *arr[] = {"aaaa","dddd","cccc","bbbb"};
    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;
}

4.3 使用qsort排序结构体 

5.指针和数组笔试题解析 

注意一下代码测试环境为visual stdio 2022 x86环境下(地址/指针 大小为4字节) 

1.一维数组

2.字符数组

下面一些代码存在一些问题:

  1. strlen(*arr):这里 *arr 是数组的第一个元素,是一个字符,不是字符串的地址,所以这是错误的。

  2. strlen(arr[1]):这里 arr[1] 是数组的第二个元素,是一个字符,不是字符串的地址,所以这是错误的。

  3. strlen(&arr):这里 &arr 是整个数组的地址,但是 strlen 需要一个以 \0 结尾的字符串的地址,所以这是错误的。

  4. strlen(&arr + 1):这里 &arr + 1 是数组后面的内存地址,这并不是一个有效的字符串地址,所以这是错误的。

  5. strlen(&arr[0] + 1):这里 &arr[0] + 1 是数组的第二个元素的地址,但是数组没有以 \0 结尾,所以这也是错误的。

  • 使用 strlen 函数时,需要确保传递的参数是一个以 \0 结尾的字符串的地址。
  • strlen(*arr)strlen(arr[1])strlen(&arr)strlen(&arr + 1) 和 strlen(&arr[0] + 1) 都是错误的,因为它们传递的参数不是字符串的地址。
  • 由于数组 arr 没有以 \0 结尾,所以 strlen(arr) 和 strlen(arr + 0) 也可能导致未定义行为。

为了避免这些问题,确保在使用 strlen 时传递的参数是一个以 \0 结尾的字符串的地址,并且在使用 sizeof 时理解你正在计算的是数组的大小还是指针的大小。

3. 字符串数组

下面一些代码存在一些问题: 

  1. strlen(*arr):这是错误的,*arr 是数组第一个元素,即字符 'a',而不是一个字符串的地址。因此,strlen(*arr) 会导致未定义行为,因为 strlen 预期的是一个字符串的地址。

  2. strlen(arr[1]):这也是错误的,arr[1] 是数组第二个元素,即字符 'b',同样不是一个字符串的地址。因此,strlen(arr[1]) 也会导致未定义行为。

  3. strlen(&arr):这是错误的,&arr 是整个数组的地址,strlen 会尝试计算从该地址开始直到遇到 null 字符的字符数。但是,因为 &arr 指向的是整个数组,而不是字符串的起始位置,所以这可能会导致计算出一个错误的结果,或者在某些情况下导致未定义行为。

  4. strlen(&arr + 1):这是错误的,&arr + 1 是数组后面的内存地址,它不指向任何有效的字符串。因此,strlen(&arr + 1) 会导致未定义行为。

4.字符串指针 

下面一些代码存在一些问题:

  1. strlen(*p):这是错误的,*p 是字符串的第一个字符,不是一个字符串的地址,所以这是未定义行为。

  2. strlen(p[0]):这也是错误的,p[0] 是字符串的第一个字符,不是一个字符串的地址,所以这是未定义行为。

  3. strlen(&p):这是错误的,&p 是指针 p 的地址,不是一个字符串的地址,所以这是未定义行为。

  4. strlen(&p + 1):这也是错误的,&p + 1 是指针 p 后面的

5.二维数组 

sizeof(a[3]):这是错误的,因为 a 只有 3 行,a[3] 超出了数组的范围,这将导致未定义行为。正确的做法是确保索引在数组的范围内。

但是在Visua Stdio运行中,并没有报错,结果反而是16为什么呢?

  • 在 C 语言中,sizeof 运算符返回的是操作数的大小,以字节为单位。当你尝试计算 sizeof(a[3]) 时,实际上你在尝试获取数组的第四行(记住数组索引是从 0 开始的)的大小。然而,由于你的数组 a 只有 3 行,a[3] 实际上是一个越界的访问。
  • 在 Visual Studio 中,当你尝试访问越界的数组行时,你可能会得到一个看似合理的值(比如 16 字节),这是因为 sizeof 运算符不会实际访问内存,它只是返回类型的大小。在这种情况下,a[3] 被当作一个指向 int[4](即一个有 4 个整数的数组)的指针,因此 sizeof(a[3]) 返回的是 int[4] 的大小,即 4 个整数乘以每个整数的大小(通常在 32 位系统中是 4 字节,在 64 位系统中是 8 字节,取决于 int 的大小)。
  • 这就是为什么你得到了 16 字节的结果,因为它相当于 4 * sizeof(int)。然而,这并不意味着 a[3] 是一个有效的数组行,它只是 sizeof 运算符根据 a[3] 的类型推断出的结果。实际上,访问 a[3] 是未定义行为,可能会导致程序崩溃或其他意外结果。

下面对二维数组的进行一些拓展:

总结: 

6.指针笔试题(难点)

笔试题1:

笔试题2:

 笔试题3

笔试题4: 

笔试题5 :

笔试题6:

笔试题7:

笔试题8:


7.对数组和指针的一些思考

 想继续深入学习指针,可以订阅下方”精通C指针“专栏 哦~

《精通C指针》icon-default.png?t=N7T8http://t.csdnimg.cn/gbpQp

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

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

相关文章

Django Celery 的配置及使用---最详细教程

Django Celery 的配置及使用 Redis提供队列消息功能 一、安装redis 系统版本&#xff1a;Ubuntu 20.041、获取最新软件包 sudo apt update sudo apt install redis-server2、安装完成后&#xff0c;Redis服务器会自动启动。查看redis是否启动成功 sudo systemctl status …

JVM运行时内存:虚拟机栈

文章目录 1. 概述2. 栈针3. 栈针内部结构3.1 局部变量表3.2 操作数栈3.3 动态链接3.4 方法返回地址3.5 一些附加信息 运行时内存整体结构如下图所示: 1. 概述 如何理解栈管运行&#xff0c;堆管存储&#xff1f; 角度一&#xff1a;GC;OOM角度二&#xff1a;栈、堆执行效率角…

信息管理系统升级改造项目:需求分析工具与实践

关键词&#xff1a;出入境信息管理系统、升级改造项目、需求分析实践、逆向工程、PowerDesigner、Axure Pro、信息系统优化策略 文章重点&#xff1a;本文以出入境信息管理系统的升级改造项目为背景&#xff0c;详细阐述了信息系统需求分析的实践过程&#xff0c;特别是如何通过…

Python实现缓存机制库之cachetools使用详解

概要 在数据密集型应用中,缓存是优化性能和响应速度的关键技术之一。Python的cachetools库提供了一套灵活且强大的工具,用于在Python项目中实现缓存机制。本文将全面介绍cachetools的安装、特性、基本与高级功能,并结合实际应用场景,展示其在项目中的应用。 安装 安装cac…

文字游侠AI丨简直是写作神器,头条爆文一键生成稳定赚米!附渠道和详细教程(只需四步)!

在数字时代的浪潮中&#xff0c;人们不断寻求网络空间中的商机&#xff0c;期望在互联网的浩瀚海洋里捕捉到稳定的财富。随着人工智能技术的突飞猛进&#xff0c;越来越多的AI工具被融入到各行各业&#xff0c;开辟了新天地&#xff0c;带来了创新的盈利模式。 其中&#xff0c…

PCB供电夹子DIY

在刷小红书的时候&#xff0c;看到了清华卓晴教授【https://zhuoqing.blog.csdn.net/】DIY的供电夹子&#xff0c;感觉对于自己DIY PCB的时候供电会比较方便&#xff0c;物料也比较简单&#xff0c;打算复刻一下。 使用物料 1、小夹子&#xff0c;文具店都有卖&#xff0c;选…

【Transformer-BEV编码(9)】Sparse4D v2 v3源代码分析。稀疏感知方向新的baseline,相机参数泛化能力差的问题。

前言&#xff1a; 基于BEV的稠密融合算法或许并不是最优的多摄融合感知框架。同时特征级的多摄融合也并不等价于BEV。这两年&#xff0c;PETR系列(PETR, PETR-v2, StreamPETR) 也取得了卓越的性能&#xff0c;并且其输出空间是稀疏的。在PETR系列方法中&#xff0c;对于每个in…

这10款安卓APP,简直好用到爆!

AI视频生成&#xff1a;小说文案智能分镜智能识别角色和场景批量Ai绘图自动配音添加音乐一键合成视频http://AI视频生成&#xff1a;小说文案智能分镜智能识别角色和场景批量Ai绘图自动配音添加音乐一键合成视频 1.追书——追书神器 追书神器是小说追新大神&#xff0c;全网实…

Online RL + IL :Policy Improvement via Imitation of Multiple Oracles

NIPS 2020 paper code 如何利用多个次优专家策略来引导智能体在线学习&#xff0c;后续有多个文章研究该设定下的RL。 Intro 论文探讨了在强化学习&#xff08;RL&#xff09;中&#xff0c;如何通过模仿多个次优策略&#xff08;称为oracle&#xff09;来提升策略性能的问题…

Qt 跨平台客户端开发框架

Qt 是一个流行的跨平台应用程序开发框架&#xff0c;用于开发图形用户界面&#xff08;GUI&#xff09;应用程序。尽管 Qt 提供了丰富的工具和功能&#xff0c;但在开发 Qt 客户端应用程序时&#xff0c;仍然可能面临一些技术难点。北京木奇移动技术有限公司&#xff0c;专业的…

没人讲清楚!我来讲!---- Ubuntu 20.04中下载配置Snort3,参数讲解及实现协议警报

文章目录 Snort一、Snort介绍1.1 概述1.2 主要功能1.3 关键特性 二、安装Snort三、配置Snort规则集四、配置Snort4.1 配置网卡4.2 启动参数4.3 自定义规则参数4.4 警报测试 最近刚好有网络安全的学习需求&#xff0c;看了好多文章&#xff0c;感觉都没有讲的很清楚。于是总结了…

JavaScript-基本数据类型和变量

基本数据类型 JavaScript支持数字、字符串和布尔值3种基本数据类型 字符串型 字符串型是JavaScript用来表示文本的数据类型&#xff0c;字符串通常由单引号或双引号括起来&#xff0c;如果字符串存在特殊字符&#xff0c;可以用转义字符代替 数字型 数字型也是JavaScript中的基…

【自然语言处理】二元文法模型

实验名称 二元文法模型 实验目的1.掌握N-gram文法的公式&#xff1b; 2.理解语言模型的实现过程&#xff1b; 3.掌握简单的平滑方法&#xff1b; 4.用代码编程实现2元语言模型&#xff0c;即一阶马尔可夫链。 实验内容&#xff1a;使用免费的中文分词语料库&#xff0c;如人民…

软考:数据流图案例

阅读下列说明和图&#xff0c;回答问题1至问题4。 一、说明 某医院欲开发病人监控系统。该系统通过各种设备监控病人的生命体征&#xff0c;并在生命体征异常时向医生和护理人员报警。该系统的主要功能如下&#xff1a; &#xff08;1&#xff09;本地监控&#xff1a;定期获…

Ubuntu20.4部署Cuda12.4

准备Ubuntu20.4 VM 安装Cuda12.4 1.进入如下界面安装安装Cuda12.4版本&#xff1a; CUDA Toolkit 12.4 Update 1 Downloads | NVIDIA Developerhttps://developer.nvidia.com/cuda-downloads?target_osLinux&target_archx86_64&DistributionUbuntu&target_vers…

Swift知识点(三)

11. init、deinit、可选链、协议、元类型 构造和析构 构造方法 构造方法是一种特殊的方法 一个对象创建完毕后&#xff0c;都需要调用构造方法进行初始化&#xff08;比如属性的初始化&#xff09; 验证&#xff1a;init方法是在对象创建完毕的时候调用 回到存储属性 在对…

【资源汇总】GIS/RS相关软件包+数据分享(直接获取附链接)

01软件类 ArcGIS 10.2 链接&#xff1a;https://pan.baidu.com/s/1euHa3eTiaTjiOu-zxsi9eA?pwdnjov ArcGIS Pro 2.8.6 链接&#xff1a;https://pan.baidu.com/s/1Y3AQshCGL7tA1zdUc7s9PQ?pwdlkic ENVI 5.3 链接&#xff1a;https://pan.baidu.com/s/14k4IVlYIheNOr2to…

visual studio 2017重命名解决方案或项目名称

1.解决方案->右键->重命名->新的名字 2.项目->右键->重命名->新的名字 3.修改程序集和命名空间名称 项目->右键->属性->修改程序集名称和命名空间名称 4.搜索换名 Ctrl-F->输入旧名称->搜索->将所有旧名称改为新名称&#xff08;注意是整…

【吊打面试官系列】Java高并发篇 - 创建线程的有哪些方式?

大家好&#xff0c;我是锋哥。今天分享关于 【创建线程的有哪些方式&#xff1f;】面试题&#xff0c;希望对大家有帮助&#xff1b; 创建线程的有哪些方式&#xff1f; 1、继承 Thread 类创建线程类 2、通过 Runnable 接口创建线程类 3、通过 Callable 和 Future 创建线程 …

Nginx性能优化系列 | Nginx的location规则配置详解

Nginx性能优化系列 | Nginx的location规则配置详解 1. Nginx设置过滤条件 1. Nginx设置过滤条件 如果请求一个不存在网站接口路径&#xff0c;为避免被听云检测到过多错误次数触发告警&#xff0c;可以在Nginx层面设置对错误的请求路径直接返回200正确码 # vim /usr/local/ngi…