进程的内存布局

  • 程序:就是我们写好的代码并编译完成的那个二进制文件,它被存放与磁盘中,它是死的。
  • 进程:把磁盘中的二进制文件"拷贝"到内存中取执行它,让运行起来,它是活的。

所有的程序被执行起来之后,系统会为他分配各种资源内存,用来存放该进程中用到的各种变量、常量、代码等等。这些不同的内容将会被存放到内存中不同的位置(区域),不同的内存区域他的特性有差别

每一个进程所拥有的内存都是一个虚拟的内存,所谓的虚拟内存是用物理内存中映射(投影)而来的,对于每一个进程而言所有的虚拟内存布局都是一样的。让每个进程都以为自己独自拥有了完整的内存空间

物理内存(Physical Memory)-----------------------------虚拟内存(Virtual Memory)

Memory1

虚拟内存的布局(区域)

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

Memory2

Memory3

栈空间

Memory4

特点:

  • 空间非常有限,尤其在嵌入式的环境下,因此应该尽可能少去使用栈空间内存,特别是要存放比较大的数据。
  • 每当一个函数被调用的时候,栈空间会向下增长一段,用来存放该函数的局部变量
  • 当一个函数退出的时候,栈空间会向上回缩一段,该空间的所有权将归还系统
  • 栈空间的分配与释放,用户是无法干预的,全部由系统来完成。
1
2
3
4
5
ulimit ‐a  //查看系统栈空间内存

stack size (kbytes, ‐s) 8192 //通过上一条命令打印出来的,表示当前64位系统 默认只有8M栈内存

ulimit ‐s 10240 // 临时修改为 10M ,重启后会回到默认值

静态变量

在 C 语言中有两种静态变量:

  • 全局变量: 定义在函数体之外的变量
  • 静态的局部变量: 定义在函数体内部而且被 static 修饰的变量
1
2
3
4
5
6
7
8
9
int c ;      // 在函数体之外,属于全局变量 ‐‐> 静态变量

int func(int argc , char * argv[] ) // argc argv 属于main函数的局部变量
{
int a ; // 局部变量

static int b ; // 静态局部变量
// 静态的局部变量 初始化语句只会被被执行一次
}

static 修饰的作用:

  • 修饰局部变量: 使得局部变量的存放位置从栈空间改为数据段
  • 修饰全局变量: 可以缩小全局变量的可见范围,由原本的拷文件可见变为本文可见,可以使得重名概率降低
  • 修饰函数: 使得函数为本文将可见

为什么会有静态变量:

  • 当我们需要把一个变量引用到不同的函数内部甚至不在同一个.c 文件中,可以使用全局变量来实现。
  • 当我们需要一个局部变量用来记录某个值,并希望这个值不会被重新初始化的情况下可以使用静态的局部变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int k = 1000 ;

void func(void)
{
int a = 250 ;
static int b = 100 ;
printf("a:%d , b:%d \n " , ++a , ++b );
}

int main(void)
{
func();
func();
func();
return 0;
}


// 运行结果:
a:251 , b:101 //b被static修饰,每次运行func(),不会被重新初始化为100
a:251 , b:102
a:251 , b:103

数据段

  1. 内容

    • .bss 未初始化的静态数据,会被自动初始化为0
    • .data 已初始化的静态数据
    • .rodata 存放常量数据 ,比如"Hello" , 是不允许修改的(只读)

    Memory5

  2. 特点

    • 没有初始化则自动初始化为0
    • 初始化语句只会被执行一次(在程序被加载的过程中已经初始化结束)
    • 静态数据的内存从程序运行之初就存在,直到程序退出才会被释放(与进程共生死)

代码段

  1. 内容:

    • 用户的代码 (比如我们自己写函数 func…main)
    • 系统初始化代码,由编译器为我们添加的

    Memory6

堆内存

基础知识

  1. 概念:堆内存,又称为动态内存、自由内存、简称堆。唯一一个由开发者随意分配与释放的内存空间。具体的申请大小,使用的时常都是由我们自己来决定。

  2. 堆内存空间的基本特性

    • 相对与栈空间来说,堆空间大很多(堆的大小受限于物理内存),系统不会对对空间进行限制。
    • 相对与栈空间来说,堆内存是从下往上增长的。
    • 堆空间的内存称为匿名内存,不像栈空间那样有个名字,只能通过指针来访问
    • 堆空间内存的申请与释放都是由用户自己完成,用户申请之后需要手动去释放,直到程序退出。

申请堆空间内存

方法:

  • malloc
  • calloc
  • realloc
  1. malloc (只是申请内存而已,并不会清空)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    malloc (向系统申请内存)
    头文件:
    #include <stdlib.h>
    函数原型:
    void *malloc(size_t size);
    参数分析:
    size ‐‐> 需要申请的内存 (字节为单位)
    返回值:
    成功 返回一个指向成功申请到内存的指针(入口地址)
    失败 返回 NULL

    示例 :

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    int *p = (int *)malloc(10 * sizeof(int)); // 申请一个可以存放10个整型的 堆空间

    for (int i = 0; i < 10; i++)
    {
    // 重新赋值
    *(p + i) = i;
    }

    for (int i = 0; i < 10; i++)
    {
    printf("*(p+%d):%d\n", i, *(p + i));
    }

    // &p 打印的是栈空间地址 p中所存放的地址为堆空间地址
    printf("&p:%p‐‐‐>%p\n", &p, p);

    free(p); // 不再使用该堆空间时需要释放
  2. calloc (会把内存进行清空)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    calloc (向系统申请内存)
    头文件:
    #include <stdlib.h>
    函数原型:
    void *calloc(size_t nmemb, size_t size);
    参数分析:
    nmemb ‐‐ > N 块内存(连续的)
    size ‐‐ > 每一块内存的大小尺寸
    返回值:
    成功 返回一个指向成功申请到内存的指针(入口地址)
    失败 返回 NULL

    示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 申请10块连续的内存 ,每一块为 整型大小
    int * p = calloc( 10 , sizeof(int));

    for (size_t i = 0; i < 10; i++)
    {
    *(p+i) = i+ 998 ;
    }

    for (int i = 0; i < 10; i++)
    {
    printf("*(p+%d):%d\n" , i , *(p+i));
    }

    /不再使用该堆空间时需要释放
    free(p);
  3. realloc

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    realloc (重新申请空间)
    头文件:
    #include <stdlib.h>
    函数原型:
    void *realloc(void *ptr, size_t size);
    参数分析:
    ptr ‐‐> 需要 扩容/缩小 的内存的入口地址8 size ‐‐> 目前需要的大小
    返回值:
    成功 返回修改后的地址
    失败 NULL

    示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    // 申请10块连续的内存 ,每一块为 整型大小
    int * p = calloc( 10 , sizeof(int));

    for (size_t i = 0; i < 10; i++)
    {
    *(p+i) = i+ 998 ;
    }
    for (int i = 0; i < 10; i++)
    {
    printf("*(p+%d):%d\n" , i , *(p+i));
    }

    printf("重新分配前p:%p\n" , p );

    // 如果重新申请内存需要另找宝地 , 那么 原本的地址会被释放掉
    int * p1 = realloc( p , 128);

    p = NULL ; // 让p不要成为野指针, 需要让它指向NULL

    printf("重新分配后p1:%p\n" , p1 );

    //realloc 会帮我们把原有的数进行搬运
    for (int i = 0; i < 10; i++)
    {
    printf("*(p1+%d):%d\n" , i , *(p1+i));
    }

    //不再使用该堆空间时需要释放
    free(p1);

清空与释放

  1. 清空

    方法:

    • bzero
    • memset
  2. 释放

    方法:

    • free
    1
    2
    3
    4
    5
    6
    7
    8
    9
    free(释放堆内存)
    头文件:
    #include <stdlib.h>
    函数原型:
    void free(void *ptr);
    参数分析:
    ptr ‐‐> 需要释放的内存的入口地址
    返回值:

    释放内存的含义:

    • 释放内存仅仅意为着,当前内存的所有权交回给系统
    • 释放内存并不会清空内存的内容
    • 也不会改变指针的指向,需要手动把指针指向NULL ,不然就成野指针了

总结

  • malloc 申请内存时,内存中的值时随机值,可以使用 bzero 清空
  • calloc 申请内存时,内存中的值会被初始化为 0
  • free 只能释放堆空间的内存,不能释放其它区域的内存

补充:

1
2
3
4
5
6
7
8
9
10
11
char * p = malloc(32);
scanf("%[^\n]s" , p ) ; // [^\n] 表示只有 回车符 才是结束标记

while(getchar() != '\n');
printf("%s\n" , p );

*(p+3) = 'W'; // 堆空间中的内容是可改的
printf("%s\n" , p );

//不再使用该堆空间时需要释放
free(p);

c

拓展练习

  1. TEST1

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // 把p修改为二级指针, 用来接收 指针str的地址
    void GetMemory1(char **p) // p ==== NULL //原本为char *p
    {
    *p = (char *)malloc(100); // 向系统申请100个字节的堆空间, 让p 指向该区域
    }

    void Test1(void)
    {
    char *str = NULL;
    GetMemory1(&str); // str ==== NULL //原本为GetMemory1(str);
    // 经过GetMemory1 的操作后 str的指向依然没有变换还是指向NULL

    strcpy(str, "hello world");
    // 提示: 拷贝字符串, 把"hello world" 拷贝到str 所指向的内存空间中
    //原本拷贝函数会出现段错误

    printf("%s\n", str);
    free(str) // 释放申请的堆空间
    }

    int main(int argc, char const *argv[])
    {
    Test1();
    return 0;
    }

    Memory7

    在原始代码中:
    GetMemory1(str);strTest1 函数中定义的一个指向字符的指针变量。通过将 str 作为参数传递给 GetMemory1 函数,实际上是将 str 的值(即指向字符的指针)复制给了 GetMemory1 函数中的参数 p。他只改了 p 的内存分配,没改到 str

    更改后的代码中:
    GetMemory1(&str); : 将指针 str 的地址作为参数传递给 GetMemory1 函数,是指针 str 的地址而不是 str 的值。
    GetMemory1 函数中的 p 修改为二级指针,用来接收指针 str 的地址。**p 就是 &str*p = (char *)malloc(100); 中,*p 实际上就是 str

  2. test 2

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    char *GetMemory2(void)
    {
    // 数组 p 所存放的位置为 栈空间, 当函数 GetMemory2 退出返回时,该区域会被系统回收
    // 不应该返回该内存中的地址
    // 可以使用 static 来修饰该数组, 使其的内存区域改为数据段
    static char p[] = "hello world"; // 原本为char p[] = "hello world";
    return p;
    }

    void Test2(void)
    {
    char *str = NULL;
    str = GetMemory2();
    printf("TEST‐2:%s\n", str);
    }

    int main(int argc, char const *argv[])
    {
    Test2();
    return 0;
    }

    Memory8

  3. test 3

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    char *GetMemory3(void)
    {
    // 直接返回 常量区的内存地址 , 注意该区域只读
    return "hello world";
    }

    void Test3(void)
    {
    char *str = NULL;
    str = GetMemory3();
    printf("TEST‐3:%s\n", str);
    }

    int main(int argc, char const *argv[])
    {
    Test3();
    return 0;
    }
  4. test 4(第一个 test1的改版)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    void GetMemory4(char **p, int num)
    {
    *p = (char *)malloc(num);
    }

    void Test4(void)
    {
    char *str = NULL;
    GetMemory4(&str, 100);
    strcpy(str, "hello");
    printf("TES‐04:%s\n", str);
    free(str); // 释放申请的堆空间
    }

    int main(int argc, char const *argv[])
    {
    Test4();
    return 0;
    }
  5. test 5

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    void Test5(void)
    {
    char *str = (char *)malloc(100);
    strcpy(str, "hello");
    free(str); // 把堆空间进行释放, 但是str依然指向堆空间的位置
    if (str != NULL)
    {
    strcpy(str, "world"); // 虽然可以拷贝, 但是属于非法访问
    printf("TEST‐5:%s\n",str);
    }
    }

    int main(int argc, char const *argv[])
    {
    Test5();
    return 0;
    }
  6. test 6

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    void Test6()
    {
    char *str = (char *)malloc(100);
    strcpy(str, "hello");
    str += 6; //str=str+6
    if (str != NULL)
    {
    strcpy(str, "world");
    printf("TEST‐6:%s\n", str); // 输出 wrold
    printf("TEST‐6:%s\n", str‐ = 6); // 输出 Hello
    }
    free(str); // 释放的时候必须使用 最初申请得到的那个首地址(入口地址)
    }

    int main(int argc, char const *argv[])
    {
    Test6();
    return 0;
    }

    Memory9