浅析C语言中的内存模型

Author: takahashi

目录

前言

动态区

        栈区(stack)

        堆区(heap)

        堆区与栈区之间的区别

静态区

        全局区(static)

        代码块(text segment/code segment)

        常量区(const)

参考文章:


前言

        近来学习深觉自己在语言的底层方面理解欠缺, 正好之前有看过一点内存模型相关的文章(虽然没看懂), 正好就花点时间研究一下这方面相关的知识。以此文整合一下自己学到的知识, 文章中有错误或不足之处还请师傅们斧正。

        重点讲堆栈与堆区两个动态区的概念, 简单分析其中的区别和各自的优势。

动态区

        栈区(stack)

                1. 栈区也被称为堆栈, 栈区的分配和释放都由系统自动分配, 栈区存放函数的参数值以及函数的局部变量,以及函数调用开辟的栈帧;按照程序的调用顺序依次入栈。函数结束返回时自动释放空间,栈区使用LIFO结构。 栈区的内存地址是连续且固定长度的。

                2. 在Windows中默认栈区大小上限为1M或者2M, 如果在分配内存时剩余栈的空间不足以分配通常会抛出段错误(segmentation fault)或者是缓冲区溢出(Buffer overflow)的异常报错告警, 在Linux系统中栈区的默认上限为8M。

                3. 栈是一种限定性线性表,是数据结构的一种。将线性表的插入和删除操作限制为仅在表的一端进行,通常将表中允许进行插入、删除操作的一端称为栈顶,因此栈顶的当前位置是动态变化的,它由一个称为栈顶指针的位置指示器来指示。同时表的另一端被称为栈底当栈中没有元素时称为空栈。栈的插入操作被形象地称为进栈入栈(push),删除操作称为出栈退栈(pop)

                4. 栈指针是一个指向栈区域内部的指针,它的值是一个地址,这个地址位于栈区的下界和栈区的上界之间。栈指针把这个栈区域分为两个部分,一个是已经使用的区域,一个是没有使用的区域。

                5. 在函数调用结束后, 局部变量先出栈, 然后是参数, 最后是栈顶指针指向的地址。

                6. 栈帧保存了每一个函数的返回位置、实参、局部变量、返回值地址

PS: 图片来源博客

        堆区(heap)

                1. 堆区的内存由自己手动分配手动释放的, 如果在使用完后没有及时释放在程序运行完后将由操作系统自动回收, 堆区的内存地址通常是不连续的, 每个堆区都有一个固定8bytes长度的头部标识信息, 且由于内存对齐制度,后面的块长度如果不足8字节则补空对齐。

                (PS:看的文章有点驳杂,暂时没找到个讲的比较全又比较清晰的文章,有关内存对齐的补充可以看看下面讲malloc的文章)

                2. 堆区是一种经过排序之后的树形结构, 也就是二叉树, 堆中某个节点的值总是不大于或不小于其父节点的值 。

                3. 在C语言中堆内存通常使用 <stdlib.h>中的malloccalloc函数来进行分配, 也可以在分配之后使用realloc重新分配堆区大小, 而在Java中则使用new关键字来进行分配。两者不同之处是Java会在堆内存使用完后自动回收, 而C则需要在使用完后使用freedelete(C++)函数手动进行回收,并且需要将指针置空,尽量避免野指针的出现

                4. 对堆来说,频繁分配和释放(malloc / free)不同大小的堆空间势必会造成内存空间的不连续,从而造成大量碎片,导致程序效率降低;而对栈来讲,则不会存在这个问题。 在日常编写代码时需要尽量减少使用频次,不要频繁地申请和释放内存,数据量不大或非动态内存也尽量使用栈内存。 在运行中如果内存管理没有做好的话是有可能会出现内存泄露的情况的, 会严重危害服务器运行内存和影响程序运行效率。

                功能:释放一块堆内存,不能重复释放,也不能释放非法地址,但是可以释放 NULL
                注意:释放的仅仅是使用权,里面的数据不会全部清理,是会清理前4个字节为0

                           程序一旦结束属于他的所有资源都会被操作系统回收

                5. 在内存对齐之后每个内存块之间会留4个字节的长度记录malloc的维护信息,这些维护信息决定了malloc下次分配内存的位置,以及借助这个维护信息计算出每个内存块的大小,当这些信息被破坏时,就会造成堆损坏。

                6. 在使用malloc创建了一个堆后,返回的指针地址指向堆的头部地址,如果此时对指针进行地址的修改,则会造成堆损坏,且此时访问数据有可能会造成数组越界。

#include <stdio.h>
#include <stdlib.h>
int main(){
    int *p = (int *)malloc(100);  // 分配一个100字节大小的malloc堆
    p++; // 将指针往高位移动一个块, 指向了原本p[1]所在的地址, 
    printf("%d\n", &p[24]);  // 此时访问的p[24]不在malloc分配的堆块范围内, 属于非法访问, 如果访问到了被使用中的内存则会造成脏数据,
							 // 但是只要不访问到一些特殊的内存地址也并不会有异常抛出
    free(p);  // 如果在此时指针偏移的情况下释放堆会造成堆损坏, 在释放内存时会检查堆块内存的完整性, 如果没有通过则会造成堆损坏, 但是不会有异常抛出
			  // 此处不多深究堆损坏的原因和修复方案, 感兴趣可以自行学习。
    return 0;
}

        堆区与栈区之间的区别

                1. 栈区的速度是要比堆区快的, 且因为栈区内存用完即立刻释放, 在面对某些不需要多次复用的代码时要比堆区更为可靠。

        因为访问模式使从中分配内存和取消分配内存变得微不足道(指针/整数只是递增或递减),而堆的分配或释放则涉及到更为复杂的簿记工作。而且堆栈中的每个字节都倾向于被非常频繁地重用,这意味着它倾向于被映射到处理器的高速缓存中,从而使其非常快。堆的另一个性能损失是,堆(通常是全局资源)通常必须是多线程安全的,即,每个分配和释放都必须(通常)与程序中的“所有”其他堆访问同步。

                2. 与栈区不同的是, 栈区的内存是从高位开始向低地址扩展的数据结构, 而堆区的内存是从低地址向高地址扩展的数据结构

                3. 栈区的空间是固定的, 随线程分配; 堆区的空间是动态的, 由自己手动分配和释放。

静态区

        全局区(static)

                1. data段, 这些数据会在程序结束后由操作系统自动释放, data段由三部分组成

                读写(Read & Write)数据段: 初始化过的全局变量+静态变量

                只读(Read Only)数据段: 未初始化过的全局变量+静态变量 (BSS段)

                只读(Read Only)数据段: 常量

                2. BSS段(bss segment)通常是指用来存放程序中未初始化初始化为0的全局变量的一块区域 (C语言规定未显式初始化的全局变量值默认为0)

        代码块(text segment/code segment)

                1. text段, 代码段就是程序中的可执行部分,直观来讲代码段就是由一个个函数的堆叠来组成的, 并且是只读的(某些架构也许会允许修改, 没去深入研究过)

        

        常量区(const)

                1. 用于存放定义的文字常量与宏以及不可修改的常量的静态区域, 由系统分配和释放内存

#include <stdio.h>

int main(){
	// 此处指针s虽然存放在栈区, 但是所指向的字符串takahashi存放在常量区
	char* s = "takahashi";
	const char Str1 = "a";  		// 此处Str1存放在栈区中
	char Str2 = "a";        		// 此处Str2同样也存放在栈区当中
	char strings1[9] = "takahashi"  // 此处strings1存放在堆区当中
	const strings2[9] = "takahashi";// 此处strings2存放在哪个区当中?
	return 0;
}

参考文章:

C语言 内存分配 代码区、常量区、静态区(全局区)、堆区、栈区_XD742971636的博客-CSDN博客_c 常量区

C/C++程序内存的各种变量存储区域和各个区域详解_jirryzhang的博客-CSDN博客_变量在内存中的存储示意图

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
C语言,||和|都是用于条件判断的逻辑运算符,但它们有着不同的作用和使用方式。 - ||是逻辑或运算符,用于连接两个条件,只要其一个条件为真,整个条件表达式就为真。如果第一个条件已经为真,那么不再计算第二个条件,因为无论第二个条件的结果如何,整个表达式都为真。这种情况下,我们可以说||是“短路或”运算符。 - |是按位或运算符,用于对两个操作数的每个对应位进行或运算。它将两个二进制数的每个位都进行或操作,生成一个新的二进制数。 因此,||和|的区别在于它们的功能和用途不同。||用于逻辑条件判断,而|用于位运算。在条件语句,应该使用||来进行逻辑判断,而不是使用|进行位运算。 这是因为如果你使用|进行逻辑判断,它将对两个条件的每个位进行或运算,而不是对整个条件进行判断。这可能会导致错误的结果。所以,为了保证正确的逻辑判断,应该始终使用||而不是|。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [浅析C语言堆和栈的区别](https://download.csdn.net/download/weixin_38672731/12796316)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [必须知道的C语言知识细节:|和||、&和&&区别](https://blog.csdn.net/qq_43351159/article/details/107516529)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [C语言——| 和 ||——& 和 && 的区别用法](https://blog.csdn.net/liu17234050/article/details/104178651)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值