C语言 内存
内存
- 什么是内存
- 软件在运行是,临时用来存储数据的
- 操作系统会将内存按照字节划分为 N 多个小格子
- 什么是内存地址
- 其实就是格子的编号
- 32 位操作系统:以 32 位的二进制表示
- 64 位操作系统:以 64 位的二进制表示
- 内存地址的作用
- 快速操作内存中存储的数据
- C 语言中如何获取内存地址
&变量名
- 获取的内存地址值,其实是首地址,就是一个数据的第一个字节的内存地址
- 数组内存地址
- 是第一个元素的第一个字节空间,也就是首地址
- 索引:偏移量
- 如何内存中存储数据
- 确定首地址
- 确定数据的数据类型
内存管理
任何一个程序,正常运行都需要内存资源,用来存放诸如变量、常量、函数代码等等。这些不同的内容,所存储的内存区域是不同的,且不同的区域有不同的特性。因此我们需要研究清楚内存布局,逐个了解不同内存区域的特性。
虚拟内存
每个C语言进程都拥有一片结构相同的虚拟内存,所谓的虚拟内存,就是从实际物理内存映射出来的地址规范范围,最重要的特征是所有的虚拟内存布局都是相同的,极大地方便内核管理不同的进程。例如三个完全不相干的进程p1、p2、p3,它们很显然会占据不同区段的物理内存,但经过系统的变换和映射,它们的虚拟内存的布局是完全一样的。
PM:Physical Memory,物理内存。
VM:Virtual Memory,虚拟内存。
可以开启两个终端分别运行 demo1
和 demo2
打印虚拟内存地址相同,存储的值不一样;
理解:
- 将内存中一块空间拿出来给程序运行(内存是独立)
- 将拿出中的内存空间,映射成一个内存结构(这个就是虚拟内存)
- 虚拟内存,地址值是从 0 开始的。
内存布局
将其中一个C语言进程的虚拟内存放大来看,会发现其内部包下区域:
- 栈(stack)
- 堆(heap)
- 数据段
- 代码段
虚拟内存中,内核区段对于应用程序而言是禁闭的,它们用于存放操作系统的关键性代码,另外由于 Linux 系统的历史性原因,在虚拟内存的最底端 0x0 ~ 0x0804 8000
之间也有一段禁闭的区段,该区段也是不可访问的。
栈内存
什么东西存储在栈内存中?
- 环境变量
- 命令行参数
- 局部变量(包括形参)
栈内存有什么特点?
- 空间有限,尤其在嵌入式环境下。因此不可以用来存储尺寸太大的变量。
- 每当一个函数被调用,栈就会向下增长一段,用以存储该函数的局部变量。(每一个函数都自己独立的空间)
- 每当一个函数退出,栈就会向上缩减一段,将该函数的局部变量所占内存归还给系统。(系统控制)
注意:
栈内存的分配和释放,都是由系统规定的,我们无法干预。
堆内存
堆内存(heap)又被称为动态内存、自由内存,简称堆。堆是唯一可被开发者自定义的区段,开发者可以根据需要申请内存的大小、决定使用的时间长短等。但又由于这是一块系统“飞地”,所有的细节均由开发者自己把握,系统不对此做任何干预,给予开发者绝对的“自由”,但也正因如此,对开发者的内存管理提出了很高的要求。对堆内存的合理使用,几乎是软件开发中的一个永恒的话题。
堆内存基本特征:
- 相比栈内存,堆的总大小仅受限于物理内存,在物理内存允许的范围内,系统对堆内存的申请不做限制。
- 相比栈内存,堆内存从下往上增长。
- 堆内存是匿名的,只能由指针来访问。
- 自定义分配的堆内存,除非开发者主动释放,否则永不释放,直到程序退出。
总结:
项目 | 栈内存 | 堆内存 |
---|---|---|
控制者 | 系统 | 开发者 |
访问方式 | 变量和指针 | 指针 |
大小限制 | 有限 | 受物理内存限制 |
存储数据 | 局部变量、形参、环境变量、命令行参数 | 数据结构、大型数据集合 |
释放方式 | 系统自动释放 | 手动释放或者程序退出 |
适用场景 | 函数调用、临时数据存储 | 大型数据结构、动态数据存储 |
数据段与代码段
数据段
- .bss 段:存放未初始化的静态数据和初始化的全局变量,它们将被系统自动初始化为0
#include <stdio.h> // .bss 特点:由系统自动初始为 0 // .bss 2. 未初始化的全局变量 int globalArr[3]; int main(int argc, const char* argv[]) { // .bss 1. 未初始化的静态变量 static int arr[3]; for ( int i = 0; i < 3; i++ ) { printf("%d \n", arr[i]); } for ( int i = 0; i < 3; i++ ) { printf("%d \n", globalArr[i]); } return 0; }
- .data段:存放已初始化的静态数据和已初始化的全局变量
#include <stdio.h> // .bss 2. 已初始化的全局变量 int globalArr[3] = { 1 , 2 ,3 }; int main(int argc, const char* argv[]) { // .data 段:1. 以初始的静态变量 static int arr[3] = { 1 ,2 , 3 }; for ( int i = 0; i < 3; i++ ) { printf("%d \n", arr[i]); } for ( int i = 0; i < 3; i++ ) { printf("%d \n", globalArr[i]); } return 0; }
- .rodata(Read-Only Data) 段:存放常量数据,如字符串常量、const 修饰的全局变量,这些数据在程序执行期间不被修改,因此被存放在只读段中
#include <stdio.h> // const 修饰的全局变量 const int a = 10; const int b = 10; int main(int argc, const char* argv[]) { // .rodata 段:只读数据,可复用数据 // 1. 字符串常量 char* str = "123"; char* str2 = "123"; printf("%p \n", str); printf("%p \n", str2); // a 和 b 的地址是一样的 printf("%p \n", a); printf("%p \n", b); return 0; }
代码段
- .text 段: 是整个程序中最关键的部分,它包含了程序的机器码,cpu可以直接执行的指令(如你编写的所有函数 main函数 的机器码),以及程序自动生成的代码(如启动代码等等)
- .init 段: 并不是所有操作编译器都有该段明确物理段,在程序启动时
main
执行之前会进行一些初始化工作(全局\静态变量初始化)可以视为一个初始化段他只是一个概念
静态数据
C语言中,静态数据有两种:
全局变量:定义在函数外部的变量,全局变量也是一种静态数据。
静态局部变量:定义在函数内部,且被 static
修饰的变量。
示例:
#include <stdio.h>
#include <unistd.h>
void fun() {
// static 修饰的数据,存储的数据段中,初始的在 .data 段中 未初始化的 .bss 段
// 特点:只会初始化一次,只有程序结束才会销毁
static int b = 9;
printf("%d \n", b);
b++;
}
int main(int argc, const char* argv[]) {
while ( 1 ) {
fun();
sleep(1);
}
return 0;
}
- 为什么需要静态数据?
全局变量在默认的情况下,对所有文件可见,为某些需要在各个不同文件和函数间访问的数据提供操作上的方便。 - 当我们希望一个函数退出后依然能保留局部变量的值,以便于下次调用时还能用时,静态局部变量可帮助实现这样的功能。
- 注意:
- 若定义时未初始化,则系统会将所有的静态数据自动初始化为0
- 静态数据初始化语句,只会执行一遍。
静态数据从程序开始运行时便已存在,直到程序退出时才释放。
- 注意:
- static修饰局部变量:使之由栈内存临时数据,变成了静态数据。
- static修饰全局变量:使之由各文件可见的静态数据,变成了本文件可见的静态数据。
- static修饰函数:使之由各文件可见的函数,变成了本文件可见的静态函数。
动态内存申请
申请堆内存:malloc() / calloc()
malloc (配置内存空间)
头文件 #include <stdlib.h>
定义函数 void *malloc(size_t size)
;
函数说明 malloc()用来配置内存空间, 其大小由指定的 size 决定. ,
返回值 若配置成功则返回一指针, 失败则返回 NULL. ,返回是一个指针指针指向一片连续的内存空间。
范例
int *ptr = (int *)malloc(10 * sizeof(int));
示例:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, const char* argv[]) {
// malloc 在堆中申请一篇连续的空间,大小有 参数决定:单位是字节数
int* p = malloc(sizeof(int) * 5);
// 如果申请不到内存空间返回 NULL, 所以需要判断一下
if ( p == NULL ) {
perror("申请堆内存失败 \n");
return -1;
}
// malloc 申请内存空间是不会初始化的
for ( int i = 0; i < 5; i++ ) {
printf("%d \n", p[i]);
}
return 0;
}
calloc (配置内存空间)
头文件 #include <stdlib.h>
定义函数 void *calloc(size_t nmemb, size_t size);
函数说明 calloc()用来配置nmemb个相邻的内存单位, 每一单位的大小为size, 并返回指向第一个元素的指针.
这和使用下列的方式效果相同:malloc(nmembsize);不过, 在利用 calloc()配置内存时会将内存内容初始化
为 0.
返回值 若配置成功则返回一指针, 失败则返回 NULL.
范例 / 动态配置 10 个 struct test 空间 */
#include <stdlib.h>
main()
{
int *ptr = (int *)calloc(sizeof(int), 10);
}
清零堆内存:bzero()
bzero (将一段内存内容全清为零)
头文件 #include <strings.h>
定义函数 void bzero(void *s, int n)
;
函数说明 bzero()会将参数 s 所指的内存区域前 n 个字节, 全部设为零值.
int *ptr = (int *)malloc(10 * sizeof(int));
bzero(ptr, 10 * sizeof(int))
释放堆内存:free()
int *ptr = (int *)malloc(10 * sizeof(int));
bzero(ptr, 10 * sizeof(int))
// some code 对*ptr进行运算完毕后
free(ptr);
ptr = NULL;
realloc(重新分配内存空间)
realloc 是 C 语言标准库中的一个函数,用于重新分配内存。当你需要改变之前通过 malloc、calloc 或 realloc 分配的内存块的大小时,可以使用 realloc 函数。这个函数尝试在原有的内存块后面分配额外的内存,如果成功,它将原有内存块中的数据复制到新的内存块中,并返回新内存块的指针;如果失败,它将返回 NULL,并且原有内存块保持不变。
定义函数 void *realloc(void *ptr, size_t new_size);
参数:
- 要扩容的指针
- 扩容的内存大小
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, const char* argv[]) {
int len = 10;
int* p = malloc(len * sizeof(int));
if ( p == NULL ) {
perror("申请空间失败 \n");
return -1;
}
for ( int i = 0; i < len; i++ ) {
p[i] = len - i;
}
for ( int i = 0; i < len; i++ ) {
printf("%d \n", p[i]);
}
// 扩容 realloc
// 参数1: 要扩容的指针 , 参数2: 扩容后的大小
// 注意点:
// - 1. 新增加的空间,保存的数据值是不确定的
// - 2. 使用 realloc 扩容成功后返回的指针可能可以原来的指针一样,
// 是否一样取决于 malloc 原来指向的空间是否能够进行追加, 若不能追加才会开辟新的内存空间,返回新的地址值
// - 3. 使用 free 释放扩容后的指针,就行了原来的旧的指针不用释放,在 realloc 底层会帮忙释放
int len2 = 20;
int* p2 = realloc(p, len * sizeof(int));
if ( p2 == NULL ) {
perror("扩容失败 \n");
return -1;
}
printf("%p \n", p);
printf("%p \n", p2);
for ( int i = 0; i < len2; i++ ) {
printf(" p2 = %d \n", p2[i]);
}
// 释放空间
// free(p);
// free(p); // 错误的
free(p2);
// 释放内存,没有回收指针,会造成野指针。
// for ( int i = 0; i < len; i++ ) {
// printf("%d \n", p[i]);
// }
// 回收指针
p = NULL;
return 0;
}
使用 realloc 函数注意点:
- 新增加的空间,保存的数据值是不确定的
- 使用 realloc 扩容成功后返回的指针可能可以原来的指针一样,是否一样取决于 malloc 原来指向的空间是否能够进行追加, 若不能追加才会开辟新的内存空间,返回新的地址值
- 使用 free 释放扩容后的指针,就行了原来的旧的指针不用释放,在 realloc 底层会帮忙释放
free(释放申请好的内存空间)
作用:释放申请的内存空间
注意点:释放内存完毕后记得把指针赋值为NULL ,防止出现野指针。
int* p = malloc(10 * sizeof(int));
free(p);
for(int i = 0 ; i < 10 ; i++){
printf("%d \n" , *(p+1)) ; // 这里大概率获取的是错误的数据,不要使用
}