C语言函数内存分配机制及函数栈帧详解

2024-03-13 21:18

本文主要是介绍C语言函数内存分配机制及函数栈帧详解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

  • 1. 函数内存分配细节
  • 2. 函数栈帧的由来
  • 3. 函数栈帧的共享
  • 4. 函数的内存分配规律小结

1. 函数内存分配细节

我们先看带有一个自定义函数时的内存分配情况。

int add(int a, int b) {int c = 25;printf("%10s: %p\n", "add_c", &c);printf("%10s: %p\n", "add_b", &b);printf("%10s: %p\n", "add_a", &a);return a + b + c;
}int main() {printf("------- Function Addresses -------\n\n");int (*fp1)(int, int) = add;printf("%10s: %p\n", "add_func", fp1);int (*fp)(void) = main;printf("%10s: %p\n", "main_func", fp);printf("\n");printf("------- Stack Frame Addresses -------\n");printf("\n");fp1(2, 3);printf("\n");fp1(5, 8);printf("\n");printf("------- Main Function Local Variable Addresses -------\n\n");int a = 55;int b = 88;printf("%10s: %p\n", "main_b", &b);printf("%10s: %p\n", "main_a", &a);printf("%10s: %p\n", "main_fp", &fp);printf("%10s: %p\n", "main_fp1", &fp1);return 0;
}

显示:

------- Function Addresses -------add_func: 0x100003bd0main_func: 0x100003c50------- Stack Frame Addresses -------add_c: 0x7ffeefbff3e4add_b: 0x7ffeefbff3e8add_a: 0x7ffeefbff3ecadd_c: 0x7ffeefbff3e4add_b: 0x7ffeefbff3e8add_a: 0x7ffeefbff3ec------- Main Function Local Variable Addresses -------main_b: 0x7ffeefbff440main_a: 0x7ffeefbff444main_fp: 0x7ffeefbff448main_fp1: 0x7ffeefbff450

这段代码中,共有mainadd两个函数,main函数有4个本地变量,add函数有2个形参及1个本地变量。

先不考虑程序运行的时间顺序,只看内存的分配地址。从上到下,内存地址从低位到高位排列。正如上面的标题所分离,这里的内存分成3大区域,分别是addmain两个函数地址所在区域、add函数的形参及其本地变量的内存区域,以及main函数中4个本地变量的内存区域。

addmain两个函数的地址都在最低位。函数地址的内存区域,按函数的声明顺序分配内存地址。

接着是add函数的形参a, b及本地变量c的地址。这三个变量中,本地变量c的地址最低,然后是第2个形参b, 最后才是第1个形参a

从进栈顺序来看,第1个形参a先进栈,然后是第2个形参b进栈,最后是add函数本地变量c进栈。其规律是,最先声明的变量先进栈。

最后是main函数4个本地变量的内存区域。同样,也是最先声明的变量先进栈。

2. 函数栈帧的由来

我们看到,main函数调用了2次add函数,第2次调用,其2个形参与其1个本地变量的地址,与第1次调用时的地址都是完全一样的。根据此特点,我们稍微调整一下文本示意图:

                     1st            2nd
--------------------------------------------var  | val ||  var  | val-----------------------------
0x7ffeefbff3e4: add_c |  25 || add_c |  25
0x7ffeefbff3e8: add_b |  3  || add_b |  8
0x7ffeefbff3ec: add_a |  2  || add_a |  5

因为add函数中的变量c每次调用被初始化为“25”,因此它两次调用的值都不会变,但均在“0x7ffeefbff3e4”这个地址上存储其值。而add函数中的形参ab两次调用所传入的值都不一样,但在不同调用期间其所分配的地址也都是固定的,分别为“0x7ffeefbff3ec”及“0x7ffeefbff3e8”。

也就是说,每调用一次add函数,都会以“0x7ffeefbff3ec”的地址为该函数的栈区,依序将各变量压进栈区。退出函数后,这一部分的内存区域的数据不会被改变,再一次调用函数时,再次修改该内存区域的内容。

这个特点,对C语言程序员来讲很重要。因为形参ab会随着实参的变化而变化,因此对于形参,我们能犯错误的机会较少。而对于函数内部的自动变量来讲,我们非常容易犯错。

如果从函数内部返回一个数值,因为是传值的原因,问题不大。

int test() {int a = 3;return a;
}int main() {int x = test();
}

当从test函数返回a时,其值“3”被赋值于main函数中的x变量,main函数即与test函数断开了连接,以后无论哪个进程、哪段代码重新调用test函数,main函数中的x变量都不会受到影响。

但是,如果从test函数返回的是该本地变量的指针,则就需要特别小心了。

int *test(int a, int b) {int c = a + b;return &c;
}int main() {int *x = test(2, 3);printf("%d\n", *x);  // 5test(20, 30);printf("%d\n", *x);  // 50
}

test函数返回的是两数相加结果的变量的指针。第一次调用时,main函数的x指针变量的值为5,这没问题。但第2次调用test函数后,还是同样的地址,但该地址所存储的值已经被第2次调用所改变,此时再来打印x的值,已经悄悄地被改变了。

其实,在遇到这类问题的时候,编译器会给出警示:

Address of stack memory associated with local variable 'c' returned

即,在栈区中返回了本地变量的地址。原因如同上面所分析的一样,栈区中某个固定的地址,其内容是就像万花筒一样,千变万化而不可预料。

对于上例,我们说内存地址从“0x7ffeefbff3ec”到“0x7ffeefbff3e4”的内存空间为栈帧stack frame),即系统将在这里进行进栈出栈操作。存放在这一区域的本地变量,因为由系统根据需要来分配内存地址及改变其值,因此也称为自动变量。

推而广之,如果从某个函数中返回char *类型的字符指针,是安全的,因为字符指针是一种静态变量,其生命周期与全局变量一样,存活于整个应用程序期间,其内存空间不在栈帧中,而在特定的内存区域,不会被随意修改。但如果传回char str[n]类型的字符数组,同属于本地变量,就需要我们特别小心了。

3. 函数栈帧的共享

现在,我们再加入另外一个有3个形参的sub函数。

int sub(int a, int b, int c) {int d = a - b - c;printf("%10s: %p\n", "sub_d", &d);printf("%10s: %p\n", "sub_c", &c);printf("%10s: %p\n", "sub_b", &b);printf("%10s: %p\n", "sub_a", &a);return d;
}int add(int a, int b) {int c = 25;printf("%10s: %p\n", "add_c", &c);printf("%10s: %p\n", "add_b", &b);printf("%10s: %p\n", "add_a", &a);return a + b + c;
}int main() {printf("------- Function Addresses -------\n\n");int (*fp2)(int, int, int) = sub;printf("%10s: %p\n", "sub_func", fp2);int (*fp1)(int, int) = add;printf("%10s: %p\n", "add_func", fp1);int (*fp)(void) = main;printf("%10s: %p\n", "main_func", fp);printf("\n");printf("------- Stack Frame Addresses -------\n");printf("\n");fp1(2, 3);printf("\n");fp2(15, 7, 1);printf("\n");printf("------- Main Function Local Variable Addresses -------\n\n");int a = 55;int b = 88;printf("%10s: %p\n", "main_b", &b);printf("%10s: %p\n", "main_a", &a);printf("%10s: %p\n", "main_fp", &fp);printf("%10s: %p\n", "main_fp1", &fp1);printf("%10s: %p\n", "main_fp2", &fp2);return 0;
}

显示:

------- Function Addresses -------sub_func: 0x100003ac0add_func: 0x100003b60main_func: 0x100003be0------- Stack Frame Addresses -------add_c: 0x7ffeefbff3d4add_b: 0x7ffeefbff3d8add_a: 0x7ffeefbff3dcsub_d: 0x7ffeefbff3d0sub_c: 0x7ffeefbff3d4sub_b: 0x7ffeefbff3d8sub_a: 0x7ffeefbff3dc------- Main Function Local Variable Addresses -------main_b: 0x7ffeefbff438main_a: 0x7ffeefbff43cmain_fp: 0x7ffeefbff440main_fp1: 0x7ffeefbff448main_fp2: 0x7ffeefbff450

函数地址按是按声明的顺序,从低位到高位分配空间。main函数内的各个局域变量,还是按声明的顺序进栈。

注意“Stack Frame Addresses”部分,add函数及sub函数的形参、局域变量都共享相同的内存空间! 且因为sub函数的形参、局域变量的数量较多,因此最后一个变量d依照从高位内存到低位内存进栈的顺序最后一个进栈。

4. 函数的内存分配规律小结

综上,函数的内存分配规律如下:

  1. 函数在内存低位分配空间,且依声明的顺序从低位到高位分配。
  2. 函数地址与函数所使用的数据相分离,函数所使用的数据在较高位的内存区域中分配。
  3. 函数所使用的数据所占用的内存空间称为函数栈帧,按形参、函数局域变量的声明顺序先后压入函数栈帧中。
  4. 函数栈帧的范围从高位内存向低位内存的方向延展。
  5. 众多函数所使用的数据,都共享同一函数栈帧,因此如同上海十里洋场,啥货都有。
  6. 函数栈帧中的数据,最主要是函数的局域变量的值,随时变化,难以预料,因此最好不要从函数栈帧中返回局域变量的指针。
  7. 主函数中的各局域变量,也是根据声明的顺序,沿高位内存到低位内存的方向压栈。

函数内存分配机制并不复杂。函数栈帧的特点,更是与汇编语言如此之近。这是C语言作为一门高级语言,在方便编码的同时,又能灵活地操纵底层细节的一个例子。了解并掌握函数内存分配机制,可让我们在高效地使用指针时清楚地知道自己在干什么,从而避免出现一些不易察觉的bug。

这篇关于C语言函数内存分配机制及函数栈帧详解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/806224

相关文章

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

NameNode内存生产配置

Hadoop2.x 系列,配置 NameNode 内存 NameNode 内存默认 2000m ,如果服务器内存 4G , NameNode 内存可以配置 3g 。在 hadoop-env.sh 文件中配置如下。 HADOOP_NAMENODE_OPTS=-Xmx3072m Hadoop3.x 系列,配置 Nam

hdu1171(母函数或多重背包)

题意:把物品分成两份,使得价值最接近 可以用背包,或者是母函数来解,母函数(1 + x^v+x^2v+.....+x^num*v)(1 + x^v+x^2v+.....+x^num*v)(1 + x^v+x^2v+.....+x^num*v) 其中指数为价值,每一项的数目为(该物品数+1)个 代码如下: #include<iostream>#include<algorithm>

OpenHarmony鸿蒙开发( Beta5.0)无感配网详解

1、简介 无感配网是指在设备联网过程中无需输入热点相关账号信息,即可快速实现设备配网,是一种兼顾高效性、可靠性和安全性的配网方式。 2、配网原理 2.1 通信原理 手机和智能设备之间的信息传递,利用特有的NAN协议实现。利用手机和智能设备之间的WiFi 感知订阅、发布能力,实现了数字管家应用和设备之间的发现。在完成设备间的认证和响应后,即可发送相关配网数据。同时还支持与常规Sof

科研绘图系列:R语言扩展物种堆积图(Extended Stacked Barplot)

介绍 R语言的扩展物种堆积图是一种数据可视化工具,它不仅展示了物种的堆积结果,还整合了不同样本分组之间的差异性分析结果。这种图形表示方法能够直观地比较不同物种在各个分组中的显著性差异,为研究者提供了一种有效的数据解读方式。 加载R包 knitr::opts_chunk$set(warning = F, message = F)library(tidyverse)library(phyl

透彻!驯服大型语言模型(LLMs)的五种方法,及具体方法选择思路

引言 随着时间的发展,大型语言模型不再停留在演示阶段而是逐步面向生产系统的应用,随着人们期望的不断增加,目标也发生了巨大的变化。在短短的几个月的时间里,人们对大模型的认识已经从对其zero-shot能力感到惊讶,转变为考虑改进模型质量、提高模型可用性。 「大语言模型(LLMs)其实就是利用高容量的模型架构(例如Transformer)对海量的、多种多样的数据分布进行建模得到,它包含了大量的先验

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

K8S(Kubernetes)开源的容器编排平台安装步骤详解

K8S(Kubernetes)是一个开源的容器编排平台,用于自动化部署、扩展和管理容器化应用程序。以下是K8S容器编排平台的安装步骤、使用方式及特点的概述: 安装步骤: 安装Docker:K8S需要基于Docker来运行容器化应用程序。首先要在所有节点上安装Docker引擎。 安装Kubernetes Master:在集群中选择一台主机作为Master节点,安装K8S的控制平面组件,如AP

C++操作符重载实例(独立函数)

C++操作符重载实例,我们把坐标值CVector的加法进行重载,计算c3=c1+c2时,也就是计算x3=x1+x2,y3=y1+y2,今天我们以独立函数的方式重载操作符+(加号),以下是C++代码: c1802.cpp源代码: D:\YcjWork\CppTour>vim c1802.cpp #include <iostream>using namespace std;/*** 以独立函数

嵌入式Openharmony系统构建与启动详解

大家好,今天主要给大家分享一下,如何构建Openharmony子系统以及系统的启动过程分解。 第一:OpenHarmony系统构建      首先熟悉一下,构建系统是一种自动化处理工具的集合,通过将源代码文件进行一系列处理,最终生成和用户可以使用的目标文件。这里的目标文件包括静态链接库文件、动态链接库文件、可执行文件、脚本文件、配置文件等。      我们在编写hellowor