计组学习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)表明最低的/当前的位置在哪里

image

  • 当某个函数结束了调用之后,栈指针会往回移,移动到这个函数被调用之前的位置,但是本来剩下的空间就变成了垃圾,他不会和之前完全相同

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分配的堆或栈的变量(因为这些变量的地址空间会随着函数调用结束而清空)
    • 超出了堆或栈的范围的引用
  • 释放无效的内存,释放了堆栈内部的局部变量

  • 内存泄漏

    image

    • 如果malloc比free更多,那么往往都是有内存泄漏的!

分析内存泄露问题的工具

往往这样的领域是非常大的研究领域,也非常前沿,比如valgrind这样的动态分析工具。

但他们会使得代码运行速度非常非常慢,也无法保证找到所有的内存问题!

所以我们要做自己的“垃圾回收站”

posted @   ZzTzZ  阅读(84)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示