C语言之深入解析如何理解指针和结构体指针、指针函数、函数指针

news2024/11/22 10:03:17

一、指针变量

  • 首先要明白指针就是一个变量,可以使用如下代码来验证:
#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 = 10
&a = 6422216
a = 20
  • 可以看到 a 的值被更改了,因此可以清楚地明白指针实质上是一个放置变量地址的特殊变量,其本质仍然是变量。既然指针是变量,那必然会有变量类型。
  • 在 C 语言中,所有的变量都有变量类型,整型、浮现型、字符型、指针类型、结构体、联合体、枚举等,这些都是变量类型。变量类型的出现是内存管理的必然结果,我们都知道,所有的变量都是保存在计算机的内存中,既然是放到计算机的内存中,那必然会占用一定的空间,那么一个变量会占用多少空间呢?或者说应该分出多少内存空间来放置该变量呢?
  • 为了规定这个问题,类型由此诞生,对于 32 位编译器来说,int 类型占用 4 个字节,即 32 位,long 类型占用 8 字节,即 64 位。在计算机中,将要运行的程序都保存在内存中,所有的程序中的变量其实就是对内存的操作。计算机的内存结构较为简单,这里暂时不详细谈论内存的物理结构,只谈论内存模型。将计算机的内存可以想象为一个房子,房子里面居住着人,每一个房间对应着计算机的内存地址,内存中的数据就相当于房子里的人。

在这里插入图片描述

  • 既然指针也是一个变量,那个指针也应该被存放在内存中,对于 32 位编译器来说,其寻址空间为 232 = 4GB,为了能够都操作所有内存(实际上普通用户不可能操作所有内存),指针变量存放也要用 32 位数即 4 个字节,这样就有指针的地址 &p,指针和变量的关系可以用如下图表示:

在这里插入图片描述

  • 可以看到 &p 是指针的地址,用来存放指针 p,而指针 p 来存放变量 a 的地址,也就是 &a,还有一个 *p 在 C 语言中是“解引”,意思是告诉编译器取出该地址存放的内容。可以自行思考一下:&(*p) 和 *(&p) 是什么意思,该怎么去理解?
  • 关于指针类型的问题,针对 32 位编译器而言,既然任何指针都只占用 4 个字节,那为何还需要引入指针类型呢?仅仅是为了约束相同类型的变量么?实际上不得不提到指针操作,先思考如下两个操作:p+1 和 ((unsignedint)p)+1,该怎么理解?
  • 这两个操作的意思是不同的,先说下第一种 p+1 操作,如下图所示:

在这里插入图片描述

  • 对于不同类型指针而言,其 p+1 所指向的地址不同,这个递增取决于指针类型所占的内存大小,而对于 ((unsigned int)p)+1,则意思是将地址 p 所指向的地址的值直接转换为数字,然后 +1,这样无论 p 是何种类型的指针,其结果都是指针所指的地址后一个地址。
  • 从上述的分析可以看到,指针的存在使得程序员可以相当轻松的操作内存,这也使得当前有些人认为指针相当危险,这一观点表现在 C# 和 Java 语言中,然而实际上用好指针可以极大的提高效率。
  • 再深入一点来通过指针对内存进行操作,现在需要对内存 6422216 中填入一个数据 125,可以进行如下操作:
unsigned int *p = (unsigned int*)(6422216);
*p = 125;
  • 当然,上面的代码使用了一个指针,实际上 C 语言中可以直接利用解引操作对内存进行更方便的赋值。

二、解引用

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

三、结构体指针

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

在这里插入图片描述

四、强制类型转换

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

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;
}
  • 运行结果如下:
student->name = 10
student->gender = 20
  • 可以看到 a[100] 被强制转换为 stu 结构体类型,当然不使用强制类型转换也是可以的,只是编译器会报警报。如下所示,数组 a[100] 的前 12 个字节被强制转换为一个 struct stu 类型,对数组进行了说明,其它数据类型也是一样的,本质上都是一段内存空间:

在这里插入图片描述

五、void 指针

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

int main(int argc, char **argv) {
    int a = 10;
    void *p;
    p = &a;
    printf("p = %d\n",*p);
    return 0;
}
  • 编译上面的代码可以发现,编译器报错,无法正常编译:
error:invalid use of void expression
  • 这说明编译器确实是在解引时无法确定 *p 的大小,因此必须告诉编译器 p 的类型或者 *p 的大小,那么如何告诉呢?其实很简单,用强制类型转换即可,如下:
*(int*)p
  • 因此上面的代码就可以优化为:
#include <stdio.h>

int main(int argc, char **argv) {
    int a = 10;
    void *p;
    p = &a;
    printf("p = %d\n", *(int*)p);
    return 0;
}
  • 运行结果如下:
p = 10
  • 可以看到结果确实是正确的,也和预期的想法一致,由于 void 指针没有空间大小属性,因此 void 指针也没有 ++ 操作。
  • 总结:void 指针仅仅是一个没有指定类型的指针,即该指针只有地址数据属性,不具备解引时的空间大小属性。

六、函数指针

① 函数指针的使用说明

  • 函数指针在 Linux 内核中用的非常多,而且在设计操作系统的时候也会用到,既然函数指针也是指针,那函数指针也占用 4 个字节(32 位编译器)。
  • 以一个简单的例子说明:
#include <stdio.h>

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;
}
  • 运行结果如下:
add (10, 20) = 30
  • 可以看到,函数指针的申明为:
返回类型(*函数名)(参数列表)
  • 函数指针的解引操作与普通的指针有点不一样,对于普通的指针而言,解引只需要根据类型来取出数据即可,但函数指针是要调用一个函数,其解引不可能是将数据取出,实际上函数指针的解引本质上是执行函数的过程,只是这个执行函数是使用的 call 指令并不是之前的函数,而是函数指针的值,即函数的地址。其实执行函数的过程本质上也是利用 call 指令来调用函数的地址,因此函数指针本质上就是保存函数执行过程的首地址。
  • 函数指针的调用如下:
函数指针调用(*(实参列表)
  • 为了确认函数指针本质上是传递给 call 指令一个函数的地址,如下所示两段代码:
#include <stdio.h>

void add (void) {
	printf("hello add\n");
}

int main (int arg, char **argv) {
	void (*p (void);
	p = add;
	(*р) О;
	return 0;
}
#include <stdio.h>

void add (void) {
	printf("hello add\n");
}

int main (int arg, char **argv) {
	add();
	return 0;
}
  • 两段代码编译后的汇编指令,如下:
0×4015d5 	push    ebp
0×4015d6	mov     ebp, esp
0×4015d8	and     esp, 0xfffffff0
0×4015db	sub     esp, 0x10
0×4015de	call    0x401690 <__main>
0×4015e3	mov     exa, DWORD PTR [esp+0xc]
0×4015eb	call    exa
0×4015ef	mov     exa 0x0
0×4015f1 	leave
0×4015f6    left
0×4015f7	ret
0×4015d5 	push    ebp
0×4015d6	mov     ebp, esp
0×4015d8	and     esp, 0xfffffff0
0×4015db	call    0x401680 <__main>
0×4015e0	call    0x4016c0 <add>
0×4015e5	mov     exa 0x0
0×4015ea	leave
0×4015eb    left
  • 可以看到,使用函数指针来调用函数时,其汇编指令多了如下:
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 的首地址,这样就完成了函数的调用。细心的您是否发现一个有趣的现象,上述过程中函数指针的值和参数一样是被放在栈帧中,这样看起来就是一个参数传递的过程,因此可以看到,函数指针最终还是以参数传递的形式传递给被调用的函数,而这个传递的值正好是函数的首地址。
  • 函数指针并不是和一般的指针一样可以操作内存,因此函数指针可以看作是函数的引用申明。

② 函数指针的应用

  • 在 Linux 驱动面向对象编程思想中用的最多,利用函数指针来实现封装。如下所示:
#include <stdio.h>

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 中最常用的一种方式,这种方式的好处在于无需按照结构体的顺序一对一。

③ 回调函数

  • 有时候会遇到这样一种情况,当 A 将一个功能交给 B 完成时,A 和 B 同步工作,这个时候该功能函数并未完成,这个时候 A 可以定义一个 API 来交给 B,而 A 只要关心该 API 就可以了,而无需关心具体实现,具体实现交给 B 完成即可,这种情况下就会用到回调函数(Callback Function),现在假设 A 需要一个 FFT 算法,这时 A 将 FFT 算法交给 B 来完成,现在来让实现这个过程:
#include <stdio.h>

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 是一个被调用函数,回调函数中申明的函数指针必须和被调用函数的类型完全相同。

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

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

相关文章

Docker如何连接至本地私服Harbor中 推送镜像、查看镜像、下载镜像

1.基础环境 win/mac/linux装有Docker私服 Harbor Docker Docker中文站 Docker官方站 下载并安装Docker Harbor Harbor GitHub Harbor 官方地址 下载并安装 Harbor (测试的话 推荐用Docker镜像来启动Harbor 减少环境问题) 2.配置流程 登录Harbor 启动后 访问 Harbor 的…

状态空间模型与卡尔曼滤波

1.状态空间模型 状态空间模型 (State Space Model)&#xff0c;包括两个方程模型&#xff1a; 一是状态方程模型&#xff0c;反映动态系统在输入变量作用下在某时刻所转移到的状态&#xff1b; 二是输出或测量方程模型&#xff0c;它将系统在某时刻的输出和系统的状态及输入变…

1.Echarts的简单使用

目录 1 下载echarts.min.js 2 使用示例 3 其他参数 Echarts是做前端数据可视化的&#xff0c;数据可视化是将数据做成图表&#xff0c;常见的可视化库有D3.js&#xff0c;ECharts.js,Highchart.js,AntV等 ECharts官网地址 Apache ECharts 进入后点击所有示例 示例中有各…

【MySQL】《狂飙》电视剧火了,如果程序一直狂飙,扛不住了,怎么办呢?

程序如果狂飙了怎么办呢&#xff1f;《狂飙》电视剧你看了吗&#xff1f;程序“狂飙”&#xff0c;你担心吗&#xff1f;性能优化步骤适合创建索引的情况不适合创建索引的情况Explain 性能分析id说明select_type 说明type字段说明Extra说明一下《狂飙》电视剧你看了吗&#xff…

PMI开通便捷的人民币付款

人民币付款通道正式开通啦当PMI持证人士通过PMI全球英文网站(www.pmi.org )进行证书更新缴费时&#xff0c;在原有美金支付的基础上&#xff0c;开通了新的人民币付款通道&#xff0c;包含支付宝和多种信用卡支付方式&#xff0c;持证人士可以按需选择<美金USD>或<人民…

基于梯度下降神经网络训练整体流程

文章目录基于梯度下降神经网络训练整体流程梯度下降优化算法一、简介二、梯度下降方法2.1 批量梯度下降法BGD2.2 随机梯度下降法SGD三、传统梯度下降法面临的挑战四、优化器4.1 Momentum4.2 Adagrad4.3 Adam4.4 对比与选择过拟合、拟合和欠拟合一、防止过拟合方法1.1 早停基于梯…

FFMPEG Vcl Player 7.0 For Delphi Crack

FFMPEG Vcl Player For Delphi 7.0【www_flashavconverter_com】是一个基于 directshow 和 ffmpeg 的 vcl 播放器&#xff0c;用于解码和播放视频/音频。 新增&#xff1a;升级到最新的FFMPEG Runtime(5.1.x)并支持Delphi 11.2 支持 Dash 回放 支持播放AES加密网络流 Nvidia 卡…

一文搞懂MD5、SHA-1、SHA-2、SHA-3,哪个算法比较安全

MD5、SHA-1、SHA-2、SHA-3都是比较常见的单向散列函数&#xff0c;这几种单向散列函数都有自己的特性。下面&#xff0c;给大家介绍一下它们的区别&#xff0c;以及MD5、SHA-1、SHA-2、SHA-3的安全性如何&#xff0c;哪种算法比较安全&#xff1f;一、简介单向散列函数是指对不…

JavaWeb:过滤器与监听器

一、过滤器Filter 1.1 过滤器Filter概述 Filter表示过滤器&#xff0c;是JavaWeb三大组件&#xff08;Servlet、Filter、Listener&#xff09;之一。Servlet我们之前都已经介绍过了&#xff0c;Filter和Listener我们今天都会进行介绍。 过滤器可以把对资源的请求 拦截 下来&…

Python的判断语句

进行逻辑判断&#xff0c;是生活中常见的行为。 同样&#xff0c;在程序中&#xff0c;进行逻辑判断也是最为基础的功能。 布尔类型和比较运算符 布尔类型的字面量&#xff1a; true 表示真&#xff08;是、肯定&#xff09;false 表示假&#xff08;否、否定&#xff09; …

【tkinter】用不到50行Python代码,写一个扫雷小游戏

文章目录定制按钮生成雷区主流程基础知识&#xff1a;StringVartkinter布局 定制按钮 学会了布局和绑定事件&#xff0c;就可以开发一些简单的应用&#xff0c;比如扫雷小游戏。从外观来看&#xff0c;扫雷就是一个按钮矩阵&#xff0c;左键点击按钮&#xff0c;如果按钮里埋…

myBaits Target Capture Kits;myBaits 靶向捕获试剂盒,快速捕获富集目标序列

myBaits Target Capture Kits可以快速捕获富集目标序列&#xff0c;提高NGS研究效率&#xff0c;兼容Illumina&#xff0c;PacBio 和 Nanopore等多种测序平台。Arbor Biosciences使用oligo合成专利技术&#xff0c;为您提供高质量、高性价比的捕获试剂盒。适用于各种基因组类型…

pytest简介

介绍pytest是一个非常成熟的全功能的Python测试框架&#xff0c;主要有以下几个特点&#xff1a;简单灵活&#xff0c;容易上手支持参数化能够支持简单的单元测试和复杂的功能测试&#xff0c;还可以用来做selenium/appnium等自动化测试、接口自动化测试&#xff08;pytestrequ…

我写了一个脚本,实现了图片分类问题模型训练的全自动化训练

众所周知,图片分类问题属于计算机视觉中比较容易解决的问题之一 但 这几天被数据集的问题搞得焦头烂额, 照理说分类问题的数据集应该比较好制作 但 如果之前没有现成的数据集 也会变得比较麻烦 直到我偶然发现了一个HuggingFace的图片搜索API 无限次调用 而且不需要身份验证 真…

如何实现报表集成?(五)——集成案例分享

在前面几篇&#xff0c;我们分别给大家介绍了报表工具的系统集成架构、用户同步/单点登录、资源集成和权限集成&#xff0c;从整体上对报表工具实现系统集成的各个方面都作了针对性的阐述。那别的用户实际上是如何做报表集成的呢&#xff1f; 这一篇&#xff0c;我们来看一下某…

408—二叉树与树

二叉树的一些概念&#xff1a;//二叉树有严格的左右子树之分&#xff0c;度数为2的树则没有对此进行要求。常见的两种二叉树&#xff1a;//如上E为满二叉树&#xff0c;每一层的结点个数都达到了当层能达到的最大结点数//满二叉树自上而下&#xff0c;从左到右依次进行编号&…

易于理解的完全立方体计算的多路数组聚集方法

自己琢磨半天终于搞懂了&#xff0c;可能是自己悟性不够吧-_-|| 多路数组聚集其实就是对维度(dimension)进行选择&#xff0c;保留一些常用的可以很方便地生成别的子立方体的立方体(cube)。对一个维做聚集(aggregation)其实就是按照这个维度的方向做加法&#xff0c;把这个维度…

Linux下的动静态库及链接

目录 常识 动态链接 静态链接 两者的比较 动态库与静态库 常识 我们平时写的代码和标准库是两回事&#xff0c;像C标准库提供给我们一些函数方便使用&#xff0c;降低程序员工作成本。比如写个printf("hello world"); 我们只是调用了库里的函数&#xff0c;并没…

字节跳动“技穷”,火山引擎“啃老”

文丨智能相对论作者丨沈浪「云」的赛道正在细化&#xff0c;是显而易见的趋势&#xff0c;诸如汽车云、营销云、视频云、零售云等等&#xff0c;大致可以理解为通过云计算等技术推动特定行业场景实现数字化转型的解决方案。之所以出现这种情况&#xff0c;存在多个层面的原因&a…

Kubernetes部署Postgresql

环境&#xff1a;Postgresql的Docker镜像 ->参考Docker安装部署PostgresqlCentos7.x kubernetes1.23.7 docker1.13.1 postgres12.7pg的docker镜像已经上传到云平台。Kubernetes参考&#xff1a;https://kubernetes.io/zh-cn/docs/concepts/configuration/configmap基本命…