又在函数指针上面犯错了

李肖遥 2024-06-20 22:08
    关注、星标公众号,直达精彩内容



一直觉得C语言较其他语言最伟大的地方就是C语言中的指针,有些人认为指针很简单,而有些人认为指针很难,当然这里的对简单和难并不是等价于对指针的理解程度。

为此在这里对C语言中的指针进行全面的总结,从底层的内存分析,彻底让读者明白指针的本质。

建议大家静下心来再复习一遍。

01 



指针变量

首先读者要明白指针是一个变量,为此作者写了如下代码来验证之:
#include "stdio.h"

int main(int argc, char **argv)
{
    unsigned int a = 10;
    unsigned int *p = NULL;
p = &a;
    printf("&a=%d\n",a);
printf("&a=%d\n",&a);
    *p = 20;
    printf("a=%d\n",a);
    return 0;
}
运行后可以看到a的值被更改了,上面的例子可以清楚的明白指针实质上是一个放置变量地址的特殊变量,其本质仍然是变量。
既然指针是变量,那必然会有变量类型,因此这里必须对变量类型做解释。在C语言中,所有的变量都有变量类型,整型、浮现型、字符型、指针类型、结构体、联合体、枚举等,这些都是变量类型。
变量类型的出现是内存管理的必然结果,相信读者知道,所有的变量都是保存在计算机的内存中,既然是放到计算机的内存中,那必然会占用一定的空间。
问题来了,一个变量会占用多少空间呢,或者说应该分出多少内存空间来放置该变量呢?
为了规定这个,类型由此诞生了,对于32位编译器来说,int类型占用4个字节,即32位,long类型占用8字节,即64位
这里简单说了类型主要是为后面引出指针这个特殊性,在计算机中,将要运行的程序都保存在内存中,所有的程序中的变量其实就是对内存的操作。
计算机的内存结构较为简单,这里不详细谈论内存的物理结构,只谈论内存模型。
将计算机的内存可以想象为一个房子,房子里面居住着人,每一个房间对应着计算机的内存地址,内存中的数据就相当于房子里的人。
既然指针也是一个变量,那个指针也应该被存放在内存中,对于32位编译器来说,其寻址空间为2^32=4GB,为了能够都操作所有内存(实际上普通用户不可能操作所有内存),指针变量存放也要用32位数即4个字节。
这样就有指针的地址&p,指针和变量的关系可以用如下图表示:
从上图可以看到&p是指针的地址,用来存放指针p,而指针p来存放变量a的地址,也就是&a,还有一个*p在C语言中是解引,意思是告诉编译器取出该地址存放的内容。
上面提到过关于指针类型的问题,针对32位编译器而言,既然任何指针都只占用4个字节,那为何还需要引入指针类型呢?
仅仅是为了约束相同类型的变量么?实际上这里不得不提到指针操作,先思考如下两个操作:
上面两个操作的意思是不同的,先说下第一种:p+1操作,如下图所示:
对于不同类型指针而言,其p+1所指向的地址不同,这个递增取决于指针类型所占的内存大小,而对于((unsigned int)p)+1
该意思是将地址p所指向的地址的值直接转换为数字,然后+1,这样无论p是何种类型的指针,其结果都是指针所指的地址后一个地址。
从上述可以看到,指针的存在使得程序员可以相当轻松的操作内存,这也使得当前有些人认为指针相当危险,这一观点表现在C#和Java语言中,然而实际上用好指针可以极大的提高效率。
下面深入一点来通过指针对内存进行操作,现在我们需要对内存6422216中填入一个数据125,我们可以如下操作:
unsigned int *p=(unsigned int*)(6422216);
*p=125;
当然,上面的代码使用了一个指针,实际上C语言中可以直接利用解引操作对内存进行更方便的赋值,下面说下解引操作*

02 



解引用


所谓解引操作,实际上是对一个地址操作,比如现在想将变量a进行赋值,一般操作是a=125,现在我们用解引操作来完成,操作如下:
*(&a)=125;
上面可以看到解引操作符为*,这个操作符对于指针有两个不同的意义,当在申明的时候是申明一个指针,而当在使用p指针时是解引操作,解引操作右边是一个地址,这样解引操作的意思就是该地址内存中的数据。这样我们对内存6422216中填入一个数据125就可以使用如下操作
*(unsigned int*)(6422216)=125;
上面需要将6422216数值强制转换为一个地址,这个是告诉编译器该数值是一个地址。值得注意的是上面的所有内存地址不能随便指定,必须是计算机已经分配的内存,否则计算机会认为指针越界而被操作系统杀死即程序提前终止。

03 



结构体指针

结构体指针和普通变量指针一样,结构体指针只占4个字节(32位编译器),只不过结构体指针可以很容易的访问结构体类型中的任何成员,这就是指针的成员运算符->。
上图中p是一个结构体指针,p指向的是一个结构体的首地址,而p->a可以用来访问结构体中的成员a,当然p->a*(p)是相同的。


04 



强制类型转换

为何要在这里提强制类型转换呢,上面的测试代码可以看到编译器会报很多警告,意思是告诉程序员数据类型不匹配,虽然并不影响程序的正确运行,但是很多警告总会让人感到难受。
因此为了告诉编译器代码这里没有问题,程序员可以使用强制类型转换来将一段内存转换为需要的数据类型,例如下面有一个数组a,现在将其强制转换为一个结构体类型stu:
#include 

typedef struct STUDENT
{

    int      name;
    int    gender;
}stu;

int a[100]={10,20,30,40,50};

int main(int argc, char **argv)
{
    stu *student;
    student=(stu*)a;
    printf("student->name=%d\n",student->name);
    printf("student->gender=%d\n",student->gender);
    return 0;
}
上面的程序运行结果如下:
可以看到a[100]被强制转换为stu结构体类型,当然不使用强制类型转换也是可以的,只是编译器会报警报。
上图为程序的示意图,图中数组a[100]的前12个字节被强制转换为了一个struct stu类型,上面仅对数组进行了说明,其它数据类型也是一样的,本质上都是一段内存空间。

05 



void指针

为何在这里单独提到空指针类型呢?主要是因为该指针类型很特殊。
void类型很容易让人想到是空的意思,但对于指针而言,其并不是指空,而是指不确定。
在很多时候指针在申明的时候可能并不知道是什么类型或者该指针指向的数据类型有多种再或者程序员仅仅是想通过一个指针来操作一段内存空间。这个时候可以将指针申明为void类型。
但是问题来了,由于void类型原因,对于确定的数据类型解引时,编译器会根据类型所占的空间来解引相应的数据,例如int p,那么p就会被编译器解引为p指针的地址的4个字节的空间大小。
但对于空指针类型来说,编译器如何知道其要解引的内存大小呢?先看一段代码:
#include 

int main(int argc, char **argv)
{
    int a=10;
    void *p;
    p=&a;
    printf("p=%d\n",*p);
    return 0;
}
编译上面的程序会发现,编译器报错,无法正常编译。
这说明编译器确实是在解引时无法确定*p的大小,因此这里必须告诉编译器p的类型或者*p的大小,如何告诉呢?很简单,用强制类型转换即可,如下:
*(int*)p
这样上面的程序就可以写为如下:
#include 

int main(int argc, char **argv)
{
    int a=10;
    void *p;
    p=&a;
    printf("p=%d\n",*(int*)p);
    return 0;
}
编译运行后:
可以看到结果确实是正确的,也和预期的想法一致。由于void指针没有空间大小属性,因此void指针也没有++操作。

06 



函数指针


6.1 函数指针使用

函数指针在Linux内核中用的非常多,而且在设计操作系统的时候也会用到,因此这里将详细讲解函数指针。既然函数指针也是指针,那函数指针也占用4个字节(32位编译器)。
下面以一个简单的例子说明:
#include 

int  add(int a,int b)
{
    return a+b;
}

int main(int argc, char **argv)
{
    int (*p)(int,int);
    p=add;
    printf("add(10,20)=%d\n",(*p)(10,20));
    return 0;
}
程序运行结果如下:
可以看到,函数指针的申明为:
函数指针的解引操作与普通的指针有点不一样。
对于普通的指针而言,解引只需要根据类型来取出数据即可,但函数指针是要调用一个函数,其解引不可能是将数据取出,实际上函数指针的解引本质上是执行函数的过程,只是这个执行函数是使用的call指令并不是之前的函数,而是函数指针的值,即函数的地址。
其实执行函数的过程本质上也是利用call指令来调用函数的地址,因此函数指针本质上就是保存函数执行过程的首地址。函数指针的调用如下:
为了确认函数指针本质上是传递给call指令一个函数的地址,下面用一个简单例子说明:
上面是编译后的汇编指令,可以看到,使用函数指针来调用函数时,其汇编指令多了如下:
0x4015e3    mov    DWORD PTR [esp+0xc],0x4015c0
0x4015eb    mov    eax,DWORD PTR [esp+0xc]
0x4015ef    call   eax
分析:第一行mov指令将立即数0x4015c0赋值给寄存器esp+0xc的地址内存中,然后将寄存器esp+0xc地址的值赋值给寄存器eax(累加器),然后调用call指令,此时pc指针将会指向add函数,而0x4015c0正好是函数add的首地址,这样就完成了函数的调用。
细心的读者是否发现一个有趣的现象,上述过程中函数指针的值和参数一样是被放在栈帧中,这样看起来就是一个参数传递的过程。
因此可以看到,函数指针最终还是以参数传递的形式传递给被调用的函数,而这个传递的值正好是函数的首地址。
从上面可以看到函数指针并不是和一般的指针一样可以操作内存,因此作者觉得函数指针可以看作是函数的引用申明。

6.2 函数指针应用

在linux驱动面向对象编程思想中用的最多,利用函数指针来实现封装,下面以一个简单的例子说明:
#include 

typedef struct TFT_DISPLAY
{

    int   pix_width;
    int   pix_height;
    int   color_width;
    void (*init)(void);
    void (*fill_screen)(int color);
    void (*tft_test)(void);

}tft_display;

static void init(void)
{
    printf("the display is initialed\n");
}

static void fill_screen(int color)
{
    printf("the display screen set 0x%x\n",color);

}

tft_display mydisplay=
{
    .pix_width=320,
    .pix_height=240,
    .color_width=24,
    .init=init,
    .fill_screen=fill_screen,
};

int main(int argc, char **argv)
{

    mydisplay.init();
    mydisplay.fill_screen(0xfff);
    return 0;
}
上面的例子将一个tft_display封装成一个对象,上面的结构体成员中最后一个没有初始化,这在Linux中用的非常多。
最常见的是file_operations结构体,该结构体一般来说只需要初始化常见的函数,不需要全部初始化。
上面代码中采用的结构体初始化方式也是在Linux中最常用的一种方式,这种方式的好处在于无需按照结构体的顺序一对一。

6.3 回调函数

有时候会遇到这样一种情况,当上层人员将一个功能交给下层程序员完成时,上层程序员和下层程序员同步工作,这个时候该功能函数并未完成,这个时候上层程序员可以定义一个API来交给下层程序员。
而上层程序员只要关心该API就可以了而无需关心具体实现,具体实现交给下层程序员完成即可(这里的上层和下层程序员不指等级关系,而是项目的分工关系)。
这种情况下就会用到回调函数(Callback Function),现在假设程序员A需要一个FFT算法,这个时候程序员A将FFT算法交给程序员B来完成,现在来让实现这个过程:
#include 

int  InputData[100]={0};
int OutputData[100]={0};

void FFT_Function(int *inputData,int *outputData,int num)
{
    while(num--)
    {

    }
}

void TaskA_CallBack(void (*fft)(int*,int*,int))
{

    (*fft)(InputData,OutputData,100);
}

int main(int argc, char **argv)
{

    TaskA_CallBack(FFT_Function);
    return 0;
}
上面的代码中TaskA_CallBack是回调函数,该函数的形参为一个函数指针,而FFT_Function是一个被调用函数。

可以看到回调函数中申明的函数指针必须和被调用函数的类型完全相同。


版权声明:本文来源网络,免费传达知识,版权归原作者所有。如涉及作品版权问题,请联系我进行删除。

‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧  END  ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧

关注我的微信公众号,回复“星球”加入知识星球,有问必答。



点击“阅读原文”查看知识星球详情,欢迎点分享、收藏、点赞、在看。

李肖遥 公众号“技术让梦想更伟大”,作者:李肖遥,专注嵌入式,只推荐适合你的博文,干货,技术心得,与君共勉。
评论 (0)
  •     这篇分享对PCB电化学迁移(ECM, Electrochemical Migration)的学习。     ECM发生在导体之间,是一种需要PCB通电才能持续的电化学反应,和电镀的原理相同。反应条件是:     1. 有电场。存在电位差/电压降,一般来说电位差/电压降越大,电化学反应的速度越快。     2. 有可溶性电解质和溶剂,这个和化学腐蚀机理相同。     3. 有迁移通道。即电荷
    电子知识打边炉 2024-07-14 17:40 23浏览
  • ADB(Android Debug Bridge)是Google提供的命令行工具,用于帮助开发者与安卓设备进行通信。它在安卓应用开发和设备管理中非常重要,因为它可以帮助开发者安装、调试和卸载应用,访问设备文件系统,以及获取设备日志等。通过ADB,开发者能够在多种测试场景中模拟用户操作,优化应用性能和用户体验。 要使用ADB,首先需要确保电脑已安装ADB驱动,并且手机已开启USB调试模式。在Windows系统中,通常不需要手动下载ADB驱动,因为Windows 8/10/11已经自带了相应的驱动
    丙丁先生 2024-07-15 07:27 43浏览
  • 概述 SiPM测试系统的信号处理板使用了ADI的单片4通道的高速差分ADC信号,所以FPGA需要通过LVDS接口来收取差分高速ADC送出的差分串行数据。 本文讨论FPGA如何例化LVDS模块,以及几种用来收取外部ADC采样后送来的高速差分串行数据。10代器件LVDS实例化界面 在Intel的10代器件中有Arria、Cyclone、MAX以及Stratix几种,我们使用的是Cyclone系列10代GX产品。图1:LVDS模块例化界面 如图1所示为LVDS例化界面,具体使用方法可以参考1“LVDS
    coyoo 2024-07-14 12:25 86浏览
  • “颠覆与涅槃,颠覆自我与重新涅槃,让企业在变革中更完美的蜕变。”这是小鹏汽车董事长兼CEO何小鹏,在2024年开年时候说的话。如今2024年已经过去了一半,小鹏汽车的处境,似乎并没有变得更好,反而是状况不断。据不完全统计,近一年来,从小鹏汽车离职的高管团队,并不在少数。从此前宣布加入英伟达的智驾负责人吴新宙,到后面的AI负责人刘兰个川(Patrick)、软件负责人Parixit Aghera,再到此次离职的矫青春,小鹏汽车近一年都处于内部动荡之中。而在这种动荡之外,小鹏汽车也正在经历一场“冰与火
    刘旷 2024-07-15 09:56 44浏览
  •     PCB表面绝缘电阻(SIR, Surface Insulative Resistance)有IPC和Bellcore GR78-CORE两个主要标准。    IPC-TM-650 方法2.6.3.7 是针对SIR的。这个标准的英文版本可以免费从IPC官网下载获得。IPC(国际电子工业联接协会)是PCB标准化主要组织。    这个测试方法针对PCB裸板(即安装元器件之前的PCB),而且是使用IPC推荐的测试板/图案(Vehicle
    电子知识打边炉 2024-07-14 22:07 28浏览
  •     这篇分享对化学腐蚀/离子迁移的学习。    化学腐蚀的本质是氧化还原反应,即原电池。反应条件是:     1. 有可溶性电解质。电解质可能来自PCB的电镀或清洗工序,也可能来自残留的助焊剂(flux)。常见的有氯离子(有卤flux、汗液、盐雾)、酸(flux的酸);     2. 有溶剂。常见的是水(湿气),也可以是有机溶剂。树脂材料内部会吸附湿气,PCB加工过程有湿气,质量差的孔内壁会吸附湿气,V-cut分
    电子知识打边炉 2024-07-14 17:07 26浏览
  • 在+24V输入增加了软起电路和防反接电路,先看电路原理图,如下,防反接电路的原理一目了然,G极达到Vth值,U28和U5就会导通,电流从D极导通到S极,详细规格书见下面。VDS,RDS(on)和ID很重要,如下,之前选的这款为何还是有问题呢,可以从温度和电压电流的应力入手,考察U28的软起参数是否合理。首先,看一看之前测的温度参数,用K型线的热电偶点的,如下,充电的时候,U28温度达到了123℃,如下,放电的时候,U28温度达到了120℃,如下,从上面的数据看,这样的U28温度,我们是接收不了的
    liweicheng 2024-07-13 19:06 25浏览
  •     PCB上不同电气网络的连线之间,要有绝缘间隙(Clearance)和爬电距离(Creepage Distance)。画过PCB的朋友都知道,原理图上的电气连线(Wire)对应PCB Layout上的电气连线,PCB Layout上的电气连线对应铜导线。铜导线是在PCB覆铜层上,利用掩模(mask)和化学蚀刻(Etching)的方法,把PCB Layout上的铜导线部分留下,并去除非导线部分而做出来的。不同电气网络的连线中间要足够绝缘,足够干净,否则轻则漏电,重则短路。
    电子知识打边炉 2024-07-14 16:06 23浏览
  •   读报见文《中国的AI价格战和“不知道怎么用AI”的日本人 - FT中文网》  如题,好奇,中日两国对AI表现怎么是这样呢?  我人在中国,看新闻与现实,“中国的AI价格战”不足为奇。  没去过日本,只有看新闻,好奇的是日本人“不知道怎么用AI”?  第一想到的是日本不是很早就搞机器人了吗?  百度看看,日本什么时候开始使用机器人?  日本在20世纪80年代开始在各个领域推广使用机器人。日本将1980年称之为“机器人普及元年”。到了1985年以后,日本进入了被称为“智能机器人的时代”。  智能
    自做自受 2024-07-13 22:40 175浏览
  • UAVDT数据集是一个专为无人机图像检测而设计的数据集,其特点包括丰富的标注和多样化的场景,对无人机图像处理领域的研究具有重要的价值。 UAVDT(Unmanned Aerial Vehicle for Detection and Tracking)数据集是为了在无人机图像中进行目标检测和跟踪研究而创建的。这个数据集主要由从无人机捕获的高清视频序列组成,涵盖了各种环境和场景,如城市、乡村、森林和海边等。 在UAVDT数据集中,图像主要包括小型车辆、行人和自行车等类别的目标,这些目标在图像中被
    丙丁先生 2024-07-15 07:33 52浏览
我要评论
0
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦