程序中的内存从哪里来?
1、程序与内存
(1)为什么需要内存
- 程序执行时,其指令和数据通常需要被加载到内存中,以便 CPU 能够访问和执行。
- 在某些嵌入式系统或特殊架构中,程序可能直接从外部存储(如闪存)执行,而无需加载到内存中。
(2)内存管理
- 内存本身在物理上是一个硬件器件,由硬件系统提供。
- 有操作系统:操作系统掌握管理所有的硬件内存
- 无操作系统:程序需要直接操作内存。
(3)在一个C语言程序中,能够获取的内存的情况:
- 栈段(Stack Segment)
- 堆段(Heap Segment)
- 代码段(Code Segment)
- 数据段(Data Segment)
2、栈内存
(1)概念:
- 栈内存是计算机内存中用于存储函数调用过程中的局部变量、函数参数、返回地址等信息的内存区域。
- 由编译器和运行时环境共同来提供服务的,程序员无法手工控制
(2)特点
- 自动分配、自动回收
- 栈是自动管理的,程序员不需要手工干预。
- 举例:在C语言中定义一个局部变量就是分配在栈上。
- 反复使用
- 栈内存在程序中其实就是那一块空间,程序反复使用这一块空间。
- 脏内存
- 栈内存由于反复使用,每次使用后程序不会去清理,因此分配到时保留原来的值。
- 因此局部变量不初始化值是随机的。
- 临时性:函数不能返回栈变量的指针,因为这个空间是临时的。
- 栈会溢出
- 因为操作系统事先给定了栈的大小,如果在函数中无穷尽的分配栈内存,总能用完。
- 栈溢出会发生段错误,会访问到不该访问到的地方。
(3)裸机程序中的栈内存管理
- 在裸机环境中,程序员完全负责内存分配,包括栈内存。
- 可以在启动代码中按照需求初始化栈指针寄存器(通常是SP),指向特定的内存位置,从而确定栈的大小和位置。
- 程序员需要确保栈空间足够大以满足程序的需求,同时也要确保不会因为栈溢出等问题导致程序崩溃。
3、堆内存
3.1、堆内存的介绍
(1)概念:堆内存是程序中用于动态分配内存的一种方式,主要用于存储程序运行过程中需要动态创建和释放的数据。
(2)栈内存由栈管理器来管理,堆内存由堆管理器来管理的。
(3)特点
- 生命周期灵活:堆内存的生命周期由程序员控制,可以在任意时间申请和释放。
- 手动管理:需要写代码去申请和释放
- malloc函数申请内存
- free函数释放内存
- 大块内存:适合大块内存场景使用。
- 堆内存是各个进程共用的大块内存。
- 栈内存是各个进程单独使用的小块内存。
- 脏内存:堆内存也是反复使用的,而且使用者用完释放前不会清零,因此也是脏的。
3.2、堆内存的使用举例
(1)示例代码
#include <stdio.h>
#include <stdlib.h>int main(void)
{/* 申请1000个int类型元素的数组 */int * p = (int *)malloc(1000*sizeof(int));if (NULL == p){printf("malloc error.\r\n");return -1;}free(p);p = NULL; /* 防止野指针 */return 0;
}
(2)malloc的函数原型
void *malloc(size_t size);
(3)void和void *
- void *是个指针类型,malloc返回的是一个void *类型的指针,实质上malloc返回的是堆管理器分配给我们本次申请的那段内存空间的首地址
- malloc返回的值其实是一个数字,这个数字表示一个内存地址。
- 为什么要使用void *作为类型?主要原因是malloc帮我们分配内存时只是分配了内存空间,至于这段空间将来用来存储什么类型的元素malloc是不关心的,由我们程序自己来决定。
- 什么是void类型?早期被翻译成空型,这个翻译非常不好,会误导人。void类型不表示没有类型,而表示万能类型。
- void的意思就是说这个数据的类型当前是不确定的,在需要的时候可以再去指定它的具体类型。
- void *类型是一个指针类型,这个指针本身占4个字节,但是指针指向的类型是不确定的,换句话说这个指针在需要的时候可以被强制转化成其他任何一种确定类型的指针,也就是说这个指针可以指向任何类型的元素。
(4)malloc的返回值:成功申请空间后返回这个内存空间的指针,申请失败时返回NULL。所以malloc获取的内存指针使用前一定要先检验是否为NULL。
(5)malloc申请的内存时用完后要free释放。free(p);会告诉堆管理器这段内存我用完了你可以回收了。堆管理器回收了这段内存后这段内存当前进程就不应该再使用了。因为释放后堆管理器就可能把这段内存再次分配给别的进程,所以你就不能再使用了。
(6)再调用free归还内存之前,指向这段内存的指针p一定不能丢(也就是不能给p另外赋值)。因为p一旦丢失这段malloc来的内存就永远的丢失了(内存泄漏),直到当前程序结束时操作系统才会回收这段内存。
3.3、malloc的细节
(1)malloc申请0字节内存:
- int * p = (int *)malloc(0);
- malloc申请0字节内存本身就是一件无厘头事情,一般不会碰到这个需要。
- 如果真的malloc(0)返回的是NULL还是一个有效指针?答案是:实际分配了16Byte的一段内存并且返回了这段内存的地址。这个答案不是确定的,因为C语言并没有明确规定malloc(0)时的表现,由各malloc函数库的实现者来定义。
(2)测试malloc(0)实际分配的内存大小
#include <stdio.h>
#include <stdlib.h>int main(void)
{int * p1 = (int *)malloc(0);int * p2 = (int *)malloc(0);if ((NULL == p1) || (NULL == p2)){printf("malloc error.\r\n");return -1;}//printf("p1 = %p.\r\n",p1);//printf("p2 = %p.\r\n",p2);// malloc申请0字节,实际申请到的内存字节数:32字节int a = (p2-p1)*sizeof(int); printf("a = %d",a);free(p1);free(p2);return 0;
}
(3)gcc中的malloc默认最小是以16Byte为分配单位的。
- 如果malloc小于16Byte的大小时都会返回一个16字节的大小的内存。
- malloc实现时没有实现任意分配大小,它是基于一些预先定义好的内存块大小来进行分配的。
-
最小分配单位不同环境下是不同的,具体是需要写代码确定的。
(4)malloc申请的内存,越界访问
- malloc(20)去访问第25、第250、第2500····会怎么样
- 实战中:120字节处正确,1200字节处正确····终于继续往后访问总有一个数字处开始段错误了。
4、代码段和数据段
4.1、代码段
(1)在编译程序时,编译器会将程序中的不同元素分类并组织成多个“段”。
(2)代码段
- 代码段是程序的指令部分,它是程序可执行文件中指令的集合。
- 这部分内容在程序执行期间通常是只读的,因为它包含了已经编译好的操作指令,比如加法、减法、跳转等操作指令序列。
4.2、数据段
(1)数据段(Data Segment):存储程序中的数据,直观理解就是C语言程序中的全局变量、静态变量和常量。
(2)数据段分为如下三种:
- 已初始化的数据段(.data段):存放初始化为非零的全局变量和静态变量。
- 未初始化的数据段(.bss 段):存放未初始化和初始化为0 的 全局变量和静态变量。
- 只读数据段(.rodata 段):存储常量,如字符串常量和const修饰的变量。
(2)全局变量和静态变量默认值为0的原因
- C语言规定,全局变量在未显式初始化时,存储在bss段。
- 程序启动时,BSS段会被自动初始化为零。
(3)堆、栈和数据段的关系
- 在程序运行时,操作系统会为程序分配的内存:代码段、数据段、堆段和栈段。这些都是内存中不同的区域。
- 堆和栈主要也是存储数据,但堆和栈既不属于代码段,也不属于数据段
- 在有些嵌入式系统等很多场景中,代码是存储在闪存(Flash)中的,而不是在 RAM 中的代码段运行。
(4)const修饰的变量,也就是常量。const实现常量不被修改的方法至少有2种:
- 第一种是编译将const修饰的变量放在数据段的只读数据段去实现不能修改,普遍见于各种单片机的编译器。
- 第二种是由编译器来检查以确保const修饰的变量不被修改,实际上const型的变量还是和普通变量一样存储(gcc中就是这样实现的)。
- 这种情况下,可以使用指针指向const修饰的变量,间接修改常量。所以const修饰不是百分百不能修改。
(5)const修饰的各种情况:
- const + 无stasic + 局部:分配在栈上;局部变量,生命周期为函数调用期间。
- const + static + 局部:分配在 data段/bss 段(gcc)或rodata 段(某些单片机)
- const + 无static + 全局:分配在 data段/bss 段(gcc)或rodata 段(某些单片机)
- const + static + 全局:分配在 data段/bss 段(gcc)或rodata 段(某些单片机)
5、字符串的内存分配
5.1、字符串常量(字符串字面量)
(1)字符串常量是存放在代码段的。例如:
#include <stdio.h>int main()
{printf("Hello, world!"); // 字符串常量存储在只读数据段char *str = "Hello, World!"; // 字符串常量存储在只读数据段*(str+0) = 'a'; // 这行代码会导致段错误(访问不应该访问的内存区域),不能修改字符串常量return 0;
}
(2)这里的 "Hello, world!\n"
是一个字符串常量。它在程序编译时就已经确定,并且在程序的整个生命周期内不会被修改。所以,编译器会将其存储在只读数据段。
(3)char *str = "xx"; 良好的编程习惯是主动加const,const char * str = "xx"; 这样编译时就能发现错误。
5.2、用字符数组定义的字符串
(1)如果是通过字符数组定义的字符串,比如:
#include <stdio.h>char str3[] = "Hello World."; // 存储在data段/bss段 int main()
{char str[] = "Hello World." // 存储在栈段*(str1 + 1) = 'a'; // 合法操作static char str2[] = "Hello World." // 存储在data段/bss段 *(str2 + 1) = 'a'; // 合法操作return 0;
}
(2)字符串使用数组定义时,不是常量,是可以正常修改。
5.3、动态分配的字符串
(1)使用动态内存分配函数(如 malloc
)分配内存来存储字符串,这种字符串存储在堆区。
(2)可以修改。
6、总结
(1)变量
- 局部变量
- 全局变量
- 静态变量
- 常量
(2)内存
- 代码段
- 堆段
- 栈段
- 数据段
- 已初始化的数据段(.data段)
- 未初始化的数据段(.bss 段)
- 只读数据段(.rodata 段)