计组学习03——C Memory
计组学习 —— C Memory
C Memory Layout
用表格来表示
Stack(~FFFFhex) |
---|
↓ |
↑ |
heap |
static data |
code(~0hex) |
程序的地址空间(address space)通常包含四个区域:
- Stack:包含局部变量,和程序框架(function frame)信息。向下增长
- Heap:从某个地方开始,之后向上增长,可以通过malloc(),realloc()和calloc()请求空间,通常会和指针一起使用,可以动态调整大小。
- Static Data:包含全局变量和静态变量,不会增长(grow)或者减小(shrink),
- Code:代码加载到这里,并且也是程序开始的地方
例如:
#include<stdio.h>
int varGlobal;
int main(){
int varLocal;
int *varDyn=malloc(sizeof(int));
}
-
在函数外声明的变量
Static Data
-
在函数内声明的变量
Stack
- main()是一个function frame,因为需要执行一些功能,放入Stack
- 当main返回时,基本释放了所有的数据,基本上stack会返回到
-
Dynamically allocated
进入Heap
-
总结:
- StaticData: varGlobal
- Stack: main(), valLocal, *varDyn
- Heap: malloc()
The Stack
-
每一个Stack的结构都是一个连续的块,包含了一个单独的程序的局部变量
-
一个Stack的结构(Stack Frame)包括:
-
调用者函数的位置
-
函数参数(Function Arguments)
-
局部变量的空间
-
-
一个栈指针(Stack Pointer)表明最低的/当前的位置在哪里
- 当某个函数结束了调用之后,栈指针会往回移,移动到这个函数被调用之前的位置,但是本来剩下的空间就变成了垃圾,他不会和之前完全相同
Stack Misuse Example
int *getPtr(){
int y;
y=3;
return &y;
}
int main(){
int *stackAddr,content;
stackAddr=getPtr();
content = *stackAddr;
printf("%d",content); /* 3 */
content = *stackAddr;
printf("%d",content); /* ? */
}
因为我们通过函数返回地址,而返回的地址是函数内的局部变量,所以当我们这个函数停止调用之后,Sp指针指到了再getPtr()函数被调用之前,也就是现在这个地址位置已经被回收了,变成了垃圾,那么我在第一次调用printf函数的时候可能因为还没有被使用过,还好好的是3,但是第二次由于这个空间变成了printf被回收之后的垃圾,所以就不一定是什么了。
所以:永远不要让函数返回指向自己的局部变量的指针
这也就是为什么局部变量不给初值,经常会出现不是0的情况,而全局变量就不会有这种问题
Static Data
静态数据的大小不会改变,但是有些时候,数据会改变!!!
两种声明方式:
-
char * str = "hi";
这种方式会把str加载到静态变量里
-
char str[] = "hi";
这种方式会把str加载到栈里!
从技术上来看,静态数据分为两段:
- 只读段
- 读写段
这就是为什么全局变量可以修改,可以不停的被调用。
Code
其实Code也是数据,和data没有任何区别,大家都是01串,只是解释的方式不同而已。
储存代码部分的内存通常是只读的,不可以被修改
但是有一些代码是可以自修改的,though it's generally a very bad idea.
Address
-
地址的大小往往取决于计算机的架构,计算机的32位/64位一般指的是CPU的位宽,也就是数据总线的宽度。
-
如果机器是"byte-addressed"意思就是按字节寻址的,也就是说在寻址范围内,每个地址都对应着存储器中的一个字节,字节作为寻址的基本单位。所以寻址范围完全由地址线的宽度来决定。如果地址总线宽度为n位,那么最多用n位寻址,寻址的范围是2^n
-
这部分的内容可以很深,以后再深挖
Endianness字节顺序
-
Big Endian大尾数法、大端存储
-
Little Endian小尾数法、小端存储
很简单,随着数据的越来越多,如果是高字节数据存储在低地址,就是采取大端存储。如果是低字节数据存储在更低的位置,就是采用小端存储。
无论是大端法还是小端法,数字的表示方式都是一样的,不同的只是他们在内存之中的存储顺序!
数组,指针,寄存器都没有字节顺序的概念!
默认32位存储,小端法,按字节寻址!
动态内存分配 Dynamic Memory Allocation
Heap中的内存更加持久,因为它的内存保留在了函数调用之外
-
malloc(n)
- 申请一个n字节的未被初始化的连续空间,包含垃圾!
- 返回一个指针,指向这个块的开始地址。如果指针是空说明申请失败
- 不同的块不一定相邻
malloc经常被用于数组或者结构体
int *p = (int *) malloc(n*sizeof(int))
;因为malloc 往往返回void *,意味着是全部数据类型,所以最好在前面加一个强制类型转换
-
calloc(size_t num , size_t size)
- 其他部分都和malloc一样,但是calloc会把所有内存初始化为0
- 传入两个参数,一个是元素个数,第二个是每个元素的大小
int *p = (int *) calloc(n,sizeof(int));
-
realloc(void *ptr , size_t size)
- 如果实际需要的内存比最开始申请的多/少,那么就可以使用realloc函数
- 首先接收一个“指向已经存在的数据的指针”
- 如果分配成功
- 如果当前内存段后面有需要的内存空间,那么直接拓展内存空间,返回原指针
- 否则返回新的内存块的位置,并且把原本的内存块释放掉
- 如果分配失败了会返回空指针。此时,原本的指针仍然有效
Release Memory
- free(p)
- free接收一个指针p,要求p指向一个分配的块(block)的初始地址,接下来会释放整个块
- p必须是从 m/c/re alloc()来的,否则会抛出一个system exception
- 不可以在已经被释放的块或者空地址进行free否则会构成安全漏洞!
- 所以最好不要进行p++等指针运算,永远不要丢失掉原本的指针地址!
常见的内存错误!
-
Segmentation Fault
- 运行unix时,尝试访问没有被分配给它的内存,那么unix就会报错
-
Bus Error
- 无效地址对齐
- 访问与设别不对应的物理地址
- 硬件错误
常见的导致内存错误的原因
-
使用未初始化的值
-
使用了你没有的内存
- 将null或者垃圾数据作为指针
- 使用了没有用allocate分配的堆或栈的变量(因为这些变量的地址空间会随着函数调用结束而清空)
- 超出了堆或栈的范围的引用
-
释放无效的内存,释放了堆栈内部的局部变量
-
内存泄漏
- 如果malloc比free更多,那么往往都是有内存泄漏的!
分析内存泄露问题的工具
往往这样的领域是非常大的研究领域,也非常前沿,比如valgrind这样的动态分析工具。
但他们会使得代码运行速度非常非常慢,也无法保证找到所有的内存问题!
所以我们要做自己的“垃圾回收站”
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了