当前位置: 首页 > news >正文

深入理解C语言内存空间、函数指针(三)(重点是函数指针)

文章目录

    • 内存分配思想
      • 只读空间:
      • 数据段
      • 堆空间
      • 栈空间
    • 函数基本
    • 值传递
    • 连续空间传递
      • 结构体变量
      • 数组名 标签
      • 连续空间的只读性,
    • 函数指针


内存分配思想

内存属性
1、大小
2、在哪里(也就是内存地址空间很多,我们需要知道在哪里,当产品需要对内存分配的时候。一般都是默认随机的。)

section 分段概念
内存分配的学习主要目的是:C语言编译的几个段的理解,什么变量放在什么位置,有助于一些其他项目的理解。

函数名就可以理解为一块代码的代名词,类似于是数组的标签。只不过数组的空间是一块一块的,连续的,
地址查看使用的是%p。

一般变量(全局和局部)和函数的地址分配空间是不一样的,
局部变量一般在高地址
全局变量、代码一般在低地址(仅作为参考,)

内核空间(高地址,这一段的地址空间,是不能访问的。)

栈空间 局部变量

运行时堆空间,malloc

静态段:只读空间
全局变量地址空间(初始化空间、未初始化空间) DATA、BSS段
只读数据段 (例如hello word) TEXT段
代码段 地址 TEXT段

只读空间:

代码段属于:只读空间,
因为代码写什么样子就是什么样子,在运行过程中肯定不会进行自动修改,

字符串是在可读空间?

text data bss dec hex
一般text、data、bss都是在静态数据,
![[Pasted image 20250521102534.png]]

编译过程,就是重新分配内存空间过程。

数据段

size build

text 代码段
data 数据段
bss 未初始化的数据段
这三个影响代码量的大小。

程序运行过程中,栈空间是不断的申请和释放。

局部变量在栈空间,当这个函数执行的时候就是临时存储一些这个函数的变量,当执行完这个函数以后,直接就会释放掉这个变量占用的内存空间,也就是出栈,

进入一个函数,内存就是开辟一个空间,存储这个函数的所有的局部变量,函数执行完成以后,栈空间就释放了,指针就不会指向这个开辟的空间。

并且栈空间不会占用静态段,运行的时候才会出现。

dss和data都是全局变量。
只不过如果我定义了以下

int b;

则会认为这个变量在dss段,因为没有写等号,所以在bss段,但是编译器会默认给0。也就是属于未初始化数据段。

DATA 全局初始化以后的变量存放的空间,
局部变量都是在栈空间,

static int a;

这种情况下,都是放在全局空间,可能是bss空间,因为没有初始化。
如果是

static int a=0;

就会在DATA空间。

nm查看静态空间的段名,首地址

访问的属性还是局部的,但是数据放在全局段中(静态段)。
原本这个地址就是存放的是a,不会被释放掉,这个地址一直会存放a这个变量。 占据整个代码量的大小。

堆空间

运行时的空间:运行时函数内部使用的变量。函数一旦返回就释放,生存周期是函数内。

运行时可以自由分配和释放的空间,是堆空间。自己管理。
生存周期是由程序员决定。
主要是:分配和释放,
分配:malloc,返回一个分配好的地址。只需要接收返回的地址。
常用用法

char * p
p = (char *)malloc();对于这个* p的读法,由设计决定。
只是提供一个没有任何内容的空间,给我们用
输入参数:分配内存的大小,单位是Byte。
malloc( 5 * sizeof(int) )
if(p == NULL)
{做一个判断,是否分配成功。
}

释放:

free(p)

栈空间:

静态空间:只读,整个程序结束时,是释放内存,生存周期最长。

栈空间

栈空间(Stack)是程序运行时的重要内存区域,主要用于存储与函数调用、中断处理等相关的临时数据。

  1. 函数调用上下文
  • 返回地址(LR)​​:子函数执行完毕后需返回父函数的位置,由链接寄存器(LR,X30)保存,调用子函数前会压入栈中。
  • 帧指针(FP)​​:指向当前函数栈帧的底部(高地址),用于界定函数栈边界,通常由X29寄存器保存,入栈后形成函数调用链。
  • 调用者寄存器​:部分需跨函数保留的寄存器(如ARMv7的R4-R11,ARM64的X19-X28)会在子函数中压栈保护,防止被覆盖。
  1. 局部变量
  • 函数内部定义的非静态局部变量​(如int a;)存储在栈帧中,生命周期仅限于函数执行期间,函数返回后自动释放。
  • 示例:函数内数组、结构体等临时变量。
  1. 函数参数
  • 超出寄存器容量的参数​:ARM调用约定中,前几个参数通过寄存器传递(如ARM64的X0-X7),超出部分会压入调用者的栈空间。
  • 可变参数函数​:如printf()的多余参数需通过栈传递。
  1. 中断/异常上下文
  • 发生中断或异常时,CPU自动将关键寄存器(PC、LR、CPSR等)​​ 压入当前模式栈(如IRQ模式栈),用于恢复现场。
  • 中断服务程序(ISR)中的局部变量也占用栈空间。
  1. 临时数据与中间结果
  • 编译器生成的临时计算结果​(如复杂表达式中间值)。
  • 寄存器溢出​:当寄存器不足时,部分中间变量暂存到栈中

ARM栈空间的核心作用是支撑函数调用链与临时数据存储,具体包括:函数返回地址(LR)、帧指针(FP)、局部变量、多余参数、中断上下文及编译器临时数据。其设计遵循架构规范(如AAPCS),通过栈指针(SP)和帧指针(FP)协同管理栈帧边界。开发中需警惕栈溢出风险,尤其在资源受限的嵌入式系统中。

栈顶地址(Stack Top Address)是计算机科学中栈(Stack)这一数据结构的关键概念,指栈中最后一个被插入元素的内存地址。栈是一种后进先出(LIFO)的线性表,所有操作(插入/删除)仅在栈顶进行。

  • 操作唯一性​:所有入栈(PUSH)和出栈(POP)操作均通过修改栈顶地址完成:
    • 入栈​:栈顶地址向低地址移动 → 新元素存入新地址。
    • 出栈​:栈顶地址向高地址移动 → 释放当前元素。
  • 核心功能​:
    • 存储函数调用的返回地址、参数、局部变量;
    • 实现递归和中断处理时的上下文保护。

函数基本

函数名也是连续空间的代名词,
函数具备三要素
1、函数名—标签(通过地址访问)
2、输入参数
3、返回值
定义函数,必须具备三要素,不然编译器是不知道这个是函数的

void fun (int, int, char)  顺序不同传递效果也不同。
{实现具体代码
}

如何用指针保存函数?

char *p
char (*p)[10]  读数组int (*p) (int, int, char) 保存的是函数地址
右边优先级高,   

函数名称

定义函数,调用函数。

int (*myshow)(const char *,...)
一定需要将*myshow增加括号,这样我们就知道myshow是一个指针,
printf("hello word\n")
myshow = printf();函数名本身也是一个存储地址的,标签? myshow = ( int (*) (const char *, ....) )0x804832; /* 假设这个地址就是printf函数的地址,我们直接拿到这个地址用。前提是我们已经知道这个地方就是一个函数,因此我们将这个地址变成一个函数声明。 */
首先是变成一个*
(*)0x804832
又需要知道对应的函数返回值类型
(char *)0x804832
又因为是读函数所以还需要增加输入,并且*的修饰关系也需要注意
myshow = (int (*) (const char *,.....)) 0x804832
等于(*)修饰的是后面的一堆内容。
就是将这个地址(里面存的是函数),赋值给myshow,然后后续调用这个myshow就是相当于是访问这个函数。接着就是调用这个函数,
myshow("===================\n")不是太理解?函数

函数名就是一个地址这个一定要谨记。

输入参数:

实参是要传递的真是数据。

形参是接受的数据,要传递进去的。

实参传递给形参,

传递的形式:
copy
不用管数据是什么,直接就copy给接受的数据,


#include <stdio.h>void myswap(int buf) //int可以理解为,我在编译的时候预留了4个字节,作为接收器。
{}int main()
{int = 20;myswap( );return 0;
}

值传递


#include <stdio.h>void myswap(int a, int b) //int可以理解为,我在编译的时候预留了4个字节,作为接收器。首先要留出来类型,不然没办法接受。
{int c;c = a;a = b;b = c;}int main()
{int a = 20;int b = 30;myswap(a,b);return 0;
}

swap的变量是在栈空间,所以用完就释放掉了。出栈。
另外一个角度就是,只是值传递,
形参的目的是预先分配,不然过来的值,没有地方存放呐,

我传递的地址,就不一样了。


#include <stdio.h>void myswap(int *a, int *b) //int可以理解为,我在编译的时候预留了4个字节,作为接收器。首先要留出来类型,不然没办法接受。
{int c;c = *a;*a = *b;*b = c;这个内容操作的地址存储的值,其实这个地方不是太理解?通过地址去改变里面的内容,利用*p去实现。说白了可以通过*p去改变变量的值。
}int main()
{int a = 20;int b = 30;myswap(&a,&b);return 0;
}

值传递 可以看出来是一种保护
地址传递 就是一种修改了。

连续空间传递

数组和结构体

结构体变量

struct abc{int a;int b;int c;
}struct abc buf;传递实参:
fun(buf)传递形参  形式的接收,一定要保证形式是一样的,因此需要搞一个和buf一样的结构空间,因此就需要写出来一个。这个a1就是和buf的形式一样,不然没办法接收buf,然后将buf里面的内容copy到a1。
void fun(struct abc a1)另外一种就是地址传递
void fun(struct abc *a2)
再次啰嗦一下,struct abc想想成int就可以了。在资源不是充足的情况下,一般结构体使用指针的形式传递,这样可以节约内存。

数组名 标签

int abc[10];
这个abc是一个连续空间的首地址,这是编译器能看出来的。直接就摒弃了值传递,直接就是地址传递fun(abc)  //abc就是一个这个数组的首地址,void fun(int *p) //利用数组标签
{}void fun(int p[10]) 
这样写实际上编译器还是只关系p的首地址,说白了这样写只是为了给人看,让别人一眼就能看出来,哦 这个是传递的数组是包含10个元素。
{}

一般在连续空间传递的时候,使用的是地址传递。

连续空间的只读性,

数组、结构体、字符串

函数指针

函数指针的声明 ​核心目的正是存储函数的首地址,但更重要的是保留函数的类型信息​(包括返回值类型和参数列表)

举个例子,如果我们

int a =10;
int *p = &a;
b = *p;

解释:可以知道b=10,这因为首先我们知道了指针变量p里面存储的一个int类型变量地址,因此我们在解引用的时候编译器是知道怎么使用这个地址的,这样就使得b=10.

int a = 10;    // 声明一个整型变量a,并赋值为10。编译器为a分配一块内存(比如地址0x1000),在这块内存中存储了数值10。
int *p = &a;   // 声明一个指向整型的指针变量p。// &a 获取变量a的地址(即0x1000)。// 将地址0x1000 赋值给指针p。所以p存储的值就是0x1000,且p知道它指向的内存存储的是一个int。
int b = *p;    // *p 是解引用操作:读取指针p所指向的内存地址(0x1000)处的值。// 因为p的类型是int*,编译器知道它指向一个int。// 因此,编译器会从地址0x1000开始,读取sizeof(int)个字节(通常是4字节),并将这4个字节解释为一个整数。// 这个整数就是10,然后将10赋值给整型变量b。

如果这个指针不声明什么类型,那么“因此,编译器会从地址0x1000开始,读取sizeof(int)个字节(通常是4字节),并将这4个字节解释为一个整数。”是不能成立的,就导致这个10可能取不出来。

  • 指针存储地址:​p = &a; 这句执行后,变量 p 里面存储的值就是变量 a 在内存中位置的地址(如 0x1000)。它没有存 a 的值 10,存的是 a在哪里

  • 指针带有类型信息:​​ 声明 int *p; 中的 int * 部分极其关键。这告诉编译器:

    • p 是一个指针。
    • p 指向的是整数类型​ (int) 的数据。
  • 解引用依赖类型信息:​b = *p; 这条语句起作用的关键正是编译器理解了 p 的类型是 int *

    • 找地址:​​ 编译器先知道要去 p 存储的地址(0x1000)取值。
    • 知道读多少字节:​p 的类型 (int *) 告诉编译器,0x1000 处存储的是一个 int。编译器知道 int 在特定系统上占多少字节(比如4个字节)。所以它会从这个地址开始连续读4个字节
    • 知道如何解释:​p 的类型 (int *) 还告诉编译器,读取出来的4个字节要按照整数的二进制表示格式来解释​(比如小端序、大端序等)。
    • 正确赋值:​​ 把这样解释出来的整数值(即10)赋给 b
  • 地址(0x1000)就像是一个门牌号。

  • 指针类型(int *)就像是贴在门上的标签,写着“这里面住着一个整数(整整齐齐的,占4个房间/字节)”。

  • 解引用(*p)就像是你拿着门牌号去找这个门,并且看了门上的标签,所以你知道:1) 找到正确的门(地址);2) 进门后往里面走多远(读4个字节);3) 把里面看到的东西理解成一个数字(解释为整数)。

没有类型信息(没有门上的标签),编译器拿着地址也不知道该怎么读、读多少字节、解释成什么类型的数据。

以上啰嗦了一堆,是为了铺垫出下面这解释**函数指针**。

既然变量可以通过指针访问或者获取,那么函数同样也可以通过指针获取或者访问。但是我们要先明确函数指针是什么样子,

 void (*)(void)

前面一个void表示一个无返回值
后面一个void表示不需要输入参数

同理我也可以声明其他类型的函数指针

 void (*)(int):表示没有返回值,并且输入参数是int类型int (*)(void):表示返回值是int类型 并且没有输入参数某结构体类型 (*)(float):表示函数的返回值是某结构体类型,输入参数是float类型。如:struct MyStruct {int data;
};
// 声明函数指针
struct MyStruct (*funcPtr)(float);  

一定要加括号。

  • 指针函数​:返回指针的函数(如 struct MyStruct* func(float))。
  • 函数指针​:指向函数的指针(如 struct MyStruct (*funcPtr)(float)

函数指针的声明语法 void (*funcPtr)(void) 明确了两点:

  • 存储函数地址​:funcPtr = foofoo 函数的入口地址存入指针变量 funcPtr
  • 保留类型信息​:通过声明中的 (void)(无参数)和 void(无返回值),编译器知道如何正确调用该函数。

同理只要看到 void (*funcPtr)(void) 这种就是函数指针声明,并且funcPtr就是存储函数的入口地址的变量,而两边的void 就是告诉编译器通过声明中的 (void)(无参数)和 void(无返回值),编译器知道如何正确调用该函数。 理解的思路和指针变量是完全一样的,因此我声明了什么类型的函数指针,那么我就只能存储什么类型的函数。不然就是犯指针变量的错误例如

float a =10;
int *p = &a;   错误,完全错误!!!!
b = *p;

这就相当于是驴唇不对马嘴,同理在函数指针这个地方一样,如果声明的和实际的函数类型不一样,就会出现驴唇不对马嘴。然后就意味着访问出错,一不小心就会出现踩内存或者直接编译就不会通过,这样更好。

结合例子进行错误示范:

若直接使用普通指针变量(如 void* p)存储函数地址:

void* p = (void*)foo;  // 强制类型转换存储地址

此时 p 虽然存储了 foo 的地址,​但丢失了类型信息。后续无法直接通过 p 调用函数:

p();  // 错误!编译器无法识别 p 是可调用函数
(*p)(); // 错误!同上

普通指针变量的问题
1、无法直接调用函数​:编译器无法将 void* 识别为函数指针类型,因此 p()(*p)() 会触发编译错误
2、类型不安全​:若强制调用(如 ((void (*)(void))p)()),需手动匹配类型。一旦签名不匹配(如参数错误),会导致未定义行为(崩溃、数据损坏)

函数指针是 ​​“类型化的地址”​,普通指针是 ​​“无类型地址”​

正确方式(函数指针):

void foo(void) { printf("Hello"); }
void (*funcPtr)(void) = foo;  // 声明函数指针并初始化
funcPtr();  // 正确:输出 "Hello"

函数指针的核心价值在于:

  1. 动态调用​:运行时切换不同函数(如策略模式)。
  2. 回调机制​:将函数作为参数传递(如 qsort 的比较函数)。
  3. 类型安全​:编译器可检查参数和返回值类型是否匹配。
  • 必须声明函数指针变量​:目的是存储函数地址 ​并保留完整的调用签名​(返回类型 + 参数类型)。
  • 不可用普通指针替代​:普通指针(如 void* p)存储函数地址后,无法直接调用函数,且强制转换易引发运行时错误。
  • 核心价值​:实现动态调用、回调等高级功能,同时确保类型安全。

知道了函数指针的作用,那么我们就在上面的基础上再结合typedef关键字使用。

首先typedef是一个能起别名的关键字

typedef  int  a;

这是我们明确的int类型变量我们通过关键字起了一个别名a

typedef void (*pFunction)(void);

typedef 的作用:从变量定义到类型别名​

  • 未使用 typedef​:
    void (*pFunction)(void); 表示声明了一个函数指针变量pFunction,指向无参数、无返回值的函数。
  • 使用 typedef​:
    typedef void (*pFunction)(void);pFunction 定义为类型别名,代表“指向无参数、无返回值函数的指针”这一类型本身。
    此时 pFunction 不是变量,而是类型名称​(类似于 intchar*)。

类比于变量的理解,其实一样的,变量你就能看懂,int a;:表示a是一个int类型变量,但是typedef int a; a就变成了int类型。

通过“去掉 typedef 分析原类型”的方法理解别名定义:

  • 原声明:void (*pFunction)(void) → 变量 pFunction 是函数指针。
  • 加上 typedeftypedef void (*pFunction)(void)pFunction 成为该函数指针的类型名。
    关键​:typedef 将变量名(如 pFunction)转换为类型名,原变量定义中的标识符成为新类型的名称。

同理放在函数这里效果也是一样的,只是函数在声明的时候比较复杂一点,看起来唬人,其实本质是一样的。所以从这个地方就能看出来,在C语言中这些基本语法要从本质上去理解清楚,不然你当代码复杂的时候,你会发现你看起来很吃力,但是其实都是一样的道理,都是从内存出发,访问不同的内存,只不过我们在访问内存的时候根据不同的类型,去告诉编译器访问不同的字节,其本质的效果都是一样的。所以不要害怕,一定要从本质层面理解,机理层面理解,这样才能一通百通。

void (*func1)(void) = foo;  // 未用别名,语法更复杂
void (*func2)(void) = bar;
typedef void (*pFunction)(void);  // 定义函数指针类型别名
pFunction myFunc = foo;           // 使用别名声明变量pFunction func1 = foo;  // func1 指向函数 foo
pFunction func2 = bar;  // func2 指向函数 bar

此时在看上面这两段代码是不是豁然开朗,变得通俗易懂。

文章源码获取方式:
如果您对本文的源码感兴趣,欢迎在评论区留下您的邮箱地址。我会在空闲时间整理相关代码,并通过邮件发送给您。由于个人时间有限,发送可能会有一定延迟,请您耐心等待。同时,建议您在评论时注明具体的需求或问题,以便我更好地为您提供针对性的帮助。

【版权声明】
本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议。这意味着您可以自由地共享(复制、分发)和改编(修改、转换)本文内容,但必须遵守以下条件:
署名:您必须注明原作者(即本文博主)的姓名,并提供指向原文的链接。
相同方式共享:如果您基于本文创作了新的内容,必须使用相同的 CC 4.0 BY-SA 协议进行发布。

感谢您的理解与支持!如果您有任何疑问或需要进一步协助,请随时在评论区留言。

http://www.dtcms.com/a/274311.html

相关文章:

  • Redis 主从复制及哨兵模式模拟部署
  • 3.检查函数 if (!CheckStart()) return 的妙用 C#例子
  • PBR渲染
  • 【网络安全】理解安全事件的“三分法”流程:应对警报的第一道防线
  • leaflet【十二】自定义图层——海量数据加载
  • 安全监测预警平台的应用场景
  • 机器学习数据集加载全攻略:从本地到网络
  • Git Submodule 介绍和使用指南
  • FS820R08A6P2LB——英飞凌高性能IGBT模块,驱动高效能源未来!
  • Vscode 下载远程服务器失败解决方法
  • Jenkins 版本升级与插件问题深度复盘:从 2.443 到 2.504.3 及功能恢复全解析
  • 和鲸社区深度学习基础训练营2025年关卡2(3)pytorch
  • 限流算法
  • GT IP核仿真测试
  • 关于大模型引用特定网页或文章的思考
  • 稳石氢能受邀参加亚洲氢能与燃料电池技术应用论坛,荣获2025中国制氢装备技术创新企业。
  • P1484 种树,特殊情形下的 WQS 二分转化。
  • 【leetcode】1486. 数组异或操作
  • 国际学术期刊IJCAST发布最新一期论文
  • 声明式 vs 编程式:Spring事务管理全对比
  • windows exe爬虫:exe抓包
  • Redis的高级特性与应用实战指南
  • Kubernetes高级调度1
  • 用鼠标点击终端窗口的时候出现:0;61;50M0;61;50M0;62;50M0
  • Typecho图片自动Webp转换插件开发指南
  • Pycharm测试连接neoj4
  • LeetCode 148 排序链表解析:高效归并排序实现
  • 【AI大模型】BERT微调文本分类任务实战
  • Python PDFplumber详解:从入门到精通的PDF处理指南
  • 扫描文件 PDF / 图片 纠斜 | 图片去黑边 / 裁剪 / 压缩