C语言-内存分配

C语言内存分配

概述(Overview)

当我们编译一个C程序后,会创建一个二进制可执行文件(.exe),当我们执行程序时,这个二进制可执行文件会按照一定的组织方式加载到RAM中.

因为计算机不会直接从辅助存储器(secondary storage)访问程序指令,因为与RAM相比,辅助存储器的访问时间更长.RAM读取速度比辅助存储器快,但是存储容量有限,所以程序员有必要有效地利用这个有限的存储空间.了解关于C的内存布局对于程序员是很有帮助的,因为它可以决定程序执行时内存的使用量.

加载到RAM后,C程序中的内存布局由6个部分(section)组成,从低地址到高地址分别是:代码段(.text), 初始化的数据段(.data), 未初始化的数据段(.bss), 堆空间(heap), 栈空间(stack), 命令行参数以及环境变量区域(command-line arguments and environment variables).

这6个不同的区域存储着代码不同的部分,并拥有自己的读写权限.如果程序试图以不同于预期的方式访问存储在任何段中的值,则会导致段错误(segmentation fault error),这也是导致程序崩溃(program to crash)的主要原因.

C的内存布局简图(Diagram for memory structure of C)

下面提到的图表显示了 RAM 如何将C程序可执行文件加载到多个段中的可视化表示:

Diagram-for-memory-structure-of-C.png

代码段(Text Segment)

  • 编译程序后,会生成一个二进制文件,通过将其加载到RAM来执行我们的程序.该二进制文件就包含指令,这些指令存储在内存的代码段.
  • 代码段具有只读权限(read-only permission),防止程序被意外修改.
  • 代码段在内存中是共享(shareable)的,因此对于常见的应用程序(如文本编辑器, shell 等),内存中需要一份副本。

初始化数据段(Initialize Data Segment)

  • 初始化数据段(也称为数据段)是C程序的计算机虚拟内存空间的一部分,它包含所有的外部(external), 全局(global), 静态(static), 常量(constant) 变量的值,这些变量的值在程序声明变量的时候初始化.
  • 因为变量的值在程序执行过程中会发生变化,所以数据段具有读写权限(read-write permission).我们可以进一步将数据段分为读写区和只读区.const变量位于只读区域下,其余类型的变量位于读写区域.

例如:

#include <stdio.h>

int global_var = 50;//全局变量 global_var 存储于data段的读写区
char *hello = "Hello World";//全局指针变量 hello 存储于data段的读写区,字符串 "Hello World" 存储于data段的只读区
const char *hello_2 = "Hello World";//const指针变量 hello_2 存储于data段的读写区,字符串 "Hello World" 存储于data段的只读区
const int global_var2 = 30;//const变量 global_var2 存储于data段的只读区
static int num_1 = 10;//全局静态变量 num_1 存储于data段的读写区

int main()
{
    static int a = 10;//静态变量 a 存储于data段的读写区
    return 0;
}

未初始化数据段(Uninitialized Data Segment)

  • 未初始化的数据段也被称为bss段(由符号开始的块 block started by symbol),被加载的程序在加载时为bss段分配内存.在C程序执行之前,bss段中的每个数据会被内核初始化为算数0(arithmetic 0)指向空指针的指针(pointers to null pointer).
  • bss段还包括所有被初始化为算数0的静态和全局变量以及指向空指针的指针
  • bss段的数据是可以被改变的,所以这个数据段具有读写权限(read-write permission).
#include <stdio.h>

static int num_1 = 10;//全局静态变量 num_1 存储于data段的读写区
static int num_2 = 0;//全局静态变量 num_2 存储于bss段(被初始化为算数0)
static int num_3;//全局静态变量 num_3 存储于bss段

char *ptr_1 = "Hello";//全局指针变量 ptr_1 存储于data段的读写区
char *ptr_2 = NULL;//全局指针变量 ptr_2 存储于bss段(指向了空指针的指针)
char *ptr_3;//全局指针变量 ptr_3 存储于bss段

int main()
{
    static int a = 10;//静态变量 a 存储于data段的读写区
    static int b;//静态变量 b 存储于bss段
    return 0;
}

堆(Heap)

  • heap是用于存放进程运行时动态分配的内存(dynamically allocated memory),heap通常开始于bss段的末尾,增长方向和收缩方向和stack相反.
  • 通常使用malloc, calloc, free, realloc等命令管理heap的内存分配, 内部使用sbrkbrk系统调用来更改heap的内存分配.
  • heap在动态加载的模块和进程中的所有共享库之间共享.
#include <stdio.h>

int main() 
{
    char *var = (char*)malloc(1);//动态分配于heap中
    return 0;
}

栈(Stack)

  • stack拥有先进后出(First-In/Last-Out,FILO)的特性,向下增长到低地址(取决于计算架构).stack的增长方向与heap相反.
  • stack存储局部变量, 传递给函数的参数, 函数的返回地址.

栈指针寄存器(stack pointer register)也称SP寄存器,用来维护和管理函数调用过程中栈帧变化,SP总是指向正在运行的函数的栈帧的栈顶.
函数被调用时将值传递到栈内,栈帧存储函数的临时变量和一些自动变量,例如返回地址和调用者环境(内存寄存器)的详细信息.
每次函数递归调用自身时,都会创建一个新的栈帧,它允许一个栈帧中的一组变量不会干扰函数不同实例的其他变量,这就是递归函数的工作原理.
具体可参考笔者的另一篇文章:C语言函数的栈帧与调用原理.

#include <stdio.h>

void foo()
{
    int a, b; //当函数被调用时,局部变量 a, b 存储于stack
}

int main()
{
    int local = 5; //局部变量 local 存储于stack
    char name[26]; //局部变量 name 存储于stack
    foo();
    return 0;
}

命令行参数以及环境变量区域(Command-Line Arguments And Environment Variables)

  • 当程序使用从控制台传递的参数(如 argc 和 argv 以及其他的环境变量)执行时,这些变量存储在这个内存段中.

在下面这个例子展示了命令行参数是如何在程序中传递和使用的.

#include <stdio.h>

int main(int argc, char *argv[])
{
    int i;

    // first value in argv stores file name
    printf("File name = %s\n", argv[0]);
    printf("Number of arguments passed = %d\n", argc - 1);
    for (i = 1; i < argc; i++)
    {
        printf("Value of Argument_%d = %s\n", i, argv[i]);
    }

    return 0;
}
╰─❯ gcc memory_layout.c
╰─❯ ./a.out 10 11 12
File name = ./a.out
Number of arguments passed = 3
Value of Argument_1 = 10
Value of Argument_2 = 11
Value of Argument_3 = 12

这段内存中存储了argc(argument counter)和argv(argument value)的值,其中argc存储传递参数的数量,argv存储实际参数的值以及文件名.

总结(Conclusion)

  • 当执行C语言程序时,二进制代码被加载到 RAM 中,并被分为五个不同的区域: text, data, bss, heap, stack, command-line arguments.
  • 代码指令存储在text段中,这是可共享的内存.如果从控制台执行代码时传递参数,则参数的值将存储在内存中的command-line arguments段中.
  • data段存储了程序中预先初始化的全局, 静态, 外部变量.bss段存储了所有未初始化的全局和静态变量.
  • 堆栈存储函数的所有局部变量和参数.它们还存储指令的函数返回地址,该地址将在函数调用后执行.
  • 堆栈和堆彼此相反地增长.
  • 堆存储程序中所有动态分配的内存,并由 malloc, calloc, free 等命令管理.
#include <stdlib.h>

int a = 0; // a在bss段(因为被初始化为算数0)
char *p1;  // p1在bss段

int main()
{
    int b;                   // b在stack(局部变量)
    char s[] = "abc";        // s在stack, "abc\0"在data-ro
    char *p2;                // p2在stack
    char *p3 = "123456";     // p3在stack上, "123456\0"在data-ro
    static int c = 0;        // c在bss(因为被初始化为算数0)
    p1 = (char *)malloc(10); // heap
    p2 = (char *)malloc(20); // heap
    return 0;
}

参考文献(References)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,657评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,662评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,143评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,732评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,837评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,036评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,126评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,868评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,315评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,641评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,773评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,859评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,584评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,676评论 2 351

推荐阅读更多精彩内容

  • 0.目录 内存构成 内存分配 1.内存构成 C的内存基本上分为4部分:静态存储区、堆区、栈区以及常量区。 栈由编译...
    lllnan阅读 107评论 0 0
  • 摘自网络,记录一下,稍做修改,已备温故而知新。 1 程序,代码可以被看做是一个人,人就需要有房子住,操作系统好比是...
    红色海_阅读 246评论 0 0
  • (JG-2014-08-20)(前半部分经过网上多篇文章对比整理)(后半部分根据ExceptionalCpp、C+...
    JasonGao阅读 5,599评论 2 23
  • c++内存分配方式 1.内存分配简单介绍: 一个由C/C++编译的程序占用的内存分为以下几个部分:1、栈区(sta...
    王王王王王景阅读 1,116评论 0 0
  • C语言中内存分配 在任何程序设计环境及语言中,内存管理都十分重要。在目前的计算机系统或嵌入式系统中,内存资源仍然是...
    一生信仰阅读 1,153评论 0 2