09内存管理

一、C进程内存布局

(1)、了解一个文件所需的Linux命令

(2)、了解程序在电脑上运行的过程

(3)、C进程内存布局(内存条中的虚拟内存)

任何一个程序,正常运行都需要内存资源,用来存放诸如变量、常量、函数代码等等。这些不同的内容,所存储的内存区域是不同的,且不同的区域有不同的特性。因此我们需要研究其内存布局,逐个了解不同内存区域的特性。

每个C语言进程都拥有一片结构相同的虚拟内存,所谓的虚拟内存,就是从实际物理内存映射出来的地址规范范围,最重要的特征是所有的虚拟内存布局都是相同的,极大地方便内核管理不同的进程。例如三个完全不相干的进程p1、p2、p3,它们很显然会占据不同区段的物理内存,但经过系统的变换和映射,它们的虚拟内存的布局是完全一样的。

  • PM:Physical Memory,物理内存。
  • VM:Virtual Memory,虚拟内存。

将其中一个C语言进程的虚拟内存放大来看,会发现其内部包下区域:

  • 栈(stack)
  • 堆(heap)
  • 数据段
  • 代码段

虚拟内存中,内核区段对于应用程序而言是禁闭的,它们用于存放操作系统的关键性代码,另外由于 Linux 系统的历史性原因,在虚拟内存的最底端 0x0 ~ 0x08048000 之间也有一段禁闭的区段,该区段也是不可访问的。

虚拟内存中各个区段的详细内容:

二、栈内存

(1)、什么东西存储在栈内存中?

1、环境变量

--- 比如我们之前配置的cygwin环境,每当编译程序的时候,会将其所需的环境(动静态库文件)一起编译进C进程内存中

2、命令行参数

3、局部变量(即被花括号括起来的变量,都为局部变量)

  • 示例代码:
// 普通函数1
void func1(int a, int b)  // 函数参数(函数形参:也就是说实际的参数不在此处)
{
    // int a = 100;       // 相当于在函数整体里面定义了该变量,写上面去是为了承接传过来的参数(实参)
    // int b = 200;
}

// 主函数
int main(int argc, char const *argv[])
{ 
    // 1、环境变量
    // 2、命令行参数
    // a、命令行参数个数(argc)
    printf("命令行参数个数(argc) == %d\n", argc);   // 栈区
    
    // b、命令行参数变量的名字(argv)
    printf("命令行参数名字分别为 == \n");
    for (int i = 0; i < argc; i++)
    {
        printf("argv[%d] == %s\n", i, argv[i]);   // 栈区
    }
    
    // 3、局部变量(被花括号{}涵盖住的变量)
    // a、普通的局部变量
    int num1 = 100;     // 栈区
    
    // b、函数的形参
    func1(100, 200);    // 调用函数,函数实参(100和200)
    
    return 0;
}

(2)、栈内存有什么特点?

  • 栈空间有限,尤其在嵌入式环境下。因此不可以用来存储尺寸太大的变量。
  • 每当一个函数被调用,栈就会向下增长一段,用以存储该函数的局部变量。
  • 每当一个函数退出,栈就会向上缩减一段,将该函数的局部变量所占内存归还给系统。
  • 注意:

栈内存的分配和释放,都是由系统规定的,我们无法干预。

  • 示例代码:
// 普通函数2
void func2(void)  
{
    char buf[1024*1024*7] = {0};
}

// 普通函数3
void func3(void)  
{
    char buf[1024*1024*7] = {0};
}

// 主函数
int main(int argc, char const *argv[])
{                    
    // (2)、- 栈空间有限,尤其在嵌入式环境下。因此不可以用来存储尺寸太大的变量。
    // char buf[1024*1024*8] = {0};
    /*
        空间大小单位说明:
            1TB== 1024GB==1024*1024MB==1024*1024*1024KB==1024*1024*1024*1024B == 1024*1024*1024*1024*8b
     
        栈空间大小说明:
            一般为(2M-10M),申请的栈区内存如果超过其大小,就会报段错误:Segmentation fault (core dumped)
     
        查询栈空间的大小:
            ulimit -s(ubuntu16.04系统栈空间默认为:8192KB == 8M)

        修改占空间的大小:
            ulimit -s 9000(9000就是你要修改的栈空间的大小);
     */

    // (3)、- 每当一个函数被调用,栈就会向下增长一段,用以存储该函数的局部变量。
    // - 每当一个函数退出,栈就会向上缩减一段,将该函数的局部变量所占内存归还给系统。
    func2();
    func3();

    return 0;
}

三、数据段与代码段

(1)、数据段细分成如下几个区域:

1、.bss (Block Started bySymbol)段: 存放未初始化的静态数据,它们将被系统自动初始化为0

2、.data段:存放已初始化的静态数据

3、.rodata段:存放常量数据

(2)、代码段细分成如下几个区域:

1、.text段:存放用户代码

2、.init段:存放系统初始化代码(执行main函数之前,栈和堆的初始化信息会被先执行)

示例代码:

#include <stdio.h>

// 全局变量(即没有被花括号{}包含的)
int num1;               // 数据段中.bss区域
int num3 = 100;         // 数据段中.data区域

// 主函数
int main(int argc, char const *argv[])
{
    // (1)、代码段:
    /*
        .text: 用户代码
        .inti: 系统初始化堆栈代码

        说明:代码段中的.init存放了栈和堆的初始化信息,这些信息会在
        执行main函数之前,完成初始化,将其内存从硬盘中加载到内存条中,形成
        内存中的堆栈区域
    */

    // (2)、数据段:
    // 1、.bss (Block Started bySymbol)段:  存放未初始化的静态数据,它们将被系统自动初始化为0
    // a、全局变量:但没有初始化的变量(num1)
    // b、局部变量:使用static来修饰的(变量也没有初始化)
    static int num2;               

    // 2、.data段:存放已初始化的静态数据
    // a、全局变量:已经初始化的变量(num3)
    // b、局部变量:使用static来修饰的(变量有初始化)
    static int num4 = 200;

    // 3、.rodata段:存放常量数据
    // 1、整型常量:100(整型int), 200L(长整型long int), 300LL(长长整型long long int), 400ULL(无符号长长整型unsigned long long int)
    // 2、浮点型常量:3.14(双精度浮点型), 6.18L(长双精度浮点型)
    // 3、字符常量:'a', '8'
    // 4、字符串常量:"nihao"
    // 5、科学计数法常量:e
    
    //  栈区       数据段中rodata段(常量区)
    char buf[128] = "woyaoqukankan";
    *(buf+0) = 'a'; // 可以的,因为修改的是buf数组里面的内存(已经将常量区的内存复制于此),而不是常量区的内存

    //  栈区       数据段中rodata段(常量区)
    char *p = "woyaoqukankan";
    *(p+0) = 'a';   // 不可以, 因为p指针只存放地址(这个地址在常量区,所以不能够修改数据),不存放内存

    return 0;
}

四、静态数据

C语言中,静态数据有两种:

  • 全局变量:定义在函数外部的变量(没有被花括号{}的涵盖)。
  • 静态局部变量:定义在函数内部(被花括号{}涵盖),且被static修饰的变量。

为什么需要静态数据?

  1. 全局变量在默认的情况下,对所有文件可见,为某些需要在各个不同文件和函数间访问的数据提供操作上的方便。

寻找工程里面的的全局变量和函数(加个extern关键字)图解:

  1. 当我们希望一个函数退出后依然能保留局部变量的值,以便于下次调用时还能用时,静态局部变量可帮助实现这样的功能。
#include <stdio.h>
int b_func1(void)
{
    static int count1 = 0;  // 数据段中的data段, 只会初始化一次
    static int count2;      // 数据段中的bss段 
    count1++;

    return count1;
}

int main(int argc, char const *argv[])
{
    // (2)、当我们希望一个函数退出后依然能保留局部变量的值,以便于下次调用时还能用时,静态局部变量可帮助实现这样的功能。
    b_func1();
    b_func1();
    int ret =  b_func1();
    printf("ret = %d\n", ret);
    return 0;
}
  1. 注意事项
  • 注意1:
    • 若定义时未初始化,则系统会将所有的静态数据自动初始化为0
    • 静态数据初始化语句,只会执行一遍。
    • 静态数据从程序开始运行时便已存在,直到程序退出时才释放。(注意:所以不能够用太多的静态数据)
  • 注意2:
    • static修饰局部变量:使之由栈内存临时数据,变成了静态数据。
    • static修饰全局变量:使之由各文件可见的静态数据,变成了本文件可见的静态数据。
    • static修饰函数:使之由各文件可见的函数,变成了只有本文件可见的静态函数。

屏蔽工程里面其它文件来本文件找自己的变量和函数(static)图解:

五、堆内存

堆内存(heap)又被称为动态内存、自由内存,简称堆。堆是唯一可被开发者自定义的区段,开发者可以根据需要申请内存的大小、决定使用的时间长短等。但又由于这是一块系统“飞地”,所有的细节均由开发者自己把握,系统不对此做任何干预,给予开发者绝对的“自由”,但也正因如此,对开发者的内存管理提出了很高的要求。对堆内存的合理使用,几乎是软件开发中的一个永恒的话题。

  • 堆内存基本特征:
    • 相比栈内存,堆的总大小仅受限于物理内存,在物理内存允许的范围内,系统对堆内存的申请不做限制。(你的内存条越大,系统越放开,那么你能申请的堆内存也就越大)
    • 相比栈内存,堆内存从下往上增长。
    • 堆内存是匿名的,只能由指针来访问。
    • 自定义分配的堆内存,除非开发者主动释放,否则永不释放,直到程序退出。

  • 相关API:
    • 申请堆内存:malloc() / calloc()
    • 清零堆内存:bzero()
    • 释放堆内存:free()

  • 注意:
    • malloc()申请的堆内存,默认情况下是随机值,一般需要用 bzero() 来清零。
    • calloc()申请的堆内存,默认情况下是已经清零了的,不需要再清零。
    • free()只能释放堆内存,并且只能释放整块堆内存,不能释放别的区段的内存或者释放一部分堆内存。
  • 释放堆内存的含义:
    • 释放堆内存意味着将堆内存的使用权归还给系统。
    • 释放堆内存并不会改变指针的指向。
    • 释放堆内存并不会对堆内存做任何修改,更不会将内存清零。
  • 示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>

int main(int argc, char const *argv[])
{
    // (1)、使用malloc函数申请内存空间
    // 1、定义并初始化
    char *p_buf = malloc(sizeof(char)*5);       // 申请定义个堆空间
    /*
        一般使用sizeof(数据类型)*数量来确定申请的堆空间大小,因为会更加精准,不浪费空间
        也可以使用malloc(100)来申请空间,但是可能会存放浪费空间的现象
    */
    bzero(p_buf, sizeof(char)*5);               // 通过p_buf指针去到堆空间,将其内存全部清零(malloc申请的空间里面没有初始化,可能有乱码)

    // 2、使用
    // a、指针样式的赋值操作
    for (int i = 0; i < 5; i++)
    {
       *(p_buf+i) = i;
        printf("*(p_buf+%d) == %d\n", i, *(p_buf+i));
    }

    // b、数组样式的赋值操作
    for (int i = 0; i < 5; i++)
    {
        p_buf[i] = i*10;
        printf("p_buf[%d] == %d\n", i, p_buf[i]);
    }
    
    // 3、释放堆空间
    free(p_buf);   
    /*
        说明:
            - 释放堆内存意味着将堆内存的使用权归还给系统。
            - 释放堆内存并不会改变指针的指向。
            - 释放堆内存并不会对堆内存做任何修改,更不会将内存清零。
    */     
    for (int i = 0; i < 5; i++)
    {
        p_buf[i] = i*5;
        printf("p_buf[%d] == %d\n", i, p_buf[i]);
    }
    

    // (2)、calloc函数
    char *p2_buf = calloc(5, sizeof(char));  // 等同于malloc(sizeof(char)*5);以及已经初始化了
    for (int i = 0; i < 5; i++)
    {
       *(p2_buf+i) = i;
        printf("*(p2_buf+%d) == %d\n", i, *(p2_buf+i));
    }



    return 0;
}

至此,希望看完这篇文章的你有所收获,我是Bardb,译音八分贝,道友,下期见!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Bardb

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值