内存分配
分配方式
静态存储区域分配
由编译器自动分配和释放, 在程序编译时已分配好
在程序整个运行期间都存在, 直到整个程序运行结束时才被释放, 如全局变量与static变量
栈分配
内存中栈区主要用于分配局部变量空间, 由编译器自动分配和释放, 处于相对较高地址, 栈地址向低地址
方向增长
在执行函数时函数内局部变量存储单元都可以在栈上创建, 函数执行结束时这些存储单元将被自动释放
栈内存分配运算内置于处理器指令集中, 其运行效率一般很高, 但是分配内存容量有限
堆分配
内存中堆区则主要用于分配程序员申请的内存空间, 堆地址向高地址
方向增长
运行时使用内存分配函数来申请任意内存, 使用完后再使用内存释放函数来释放内存
动态内存整个生存期由程序员决定, 若在堆上分配了内存空间, 必须及时释放, 否则将会出现内存泄漏等错误
- malloc
void *malloc(size_t size);
malloc 用于从堆中分配内存
参数 | 说明 |
---|---|
size_t | 申请空间字节数 成功会返回该内存空间地址(不会自动进行初始化) 失败返回 NULL |
void * | 该函数返回类型不确定, 通过接收指针变量从而进行类型转换 |
分配内存时需注意, 即使在程序关闭时系统会自动回收该手动申请内存, 也要进行手动释放, 以保证内存能够在不需要时返回至堆空间, 使内存能够合理分配使用
- free
void free(void *ptr);
free用于从堆中释放内存, 释放需将指针指向NULL, 否则会出现野指针
#include<stdio.h>
#include<malloc.h>
int main(void){
char *p1 = (char *)malloc(sizeof(char));
char *p2 = (char *)malloc(sizeof(char));
printf("p1 = %p\n", p1);
printf("p2 = %p\n", p2);
free(p1);
p1 = NULL;
free(p2);
p2 = NULL;
}
动态分配内存位于堆区中, 堆地址向高地址方向
增长
#include <stdio.h>
#include <malloc.h>
int main(void) {
const int SIZE = 4;
int *p = (int *)malloc(SIZE * sizeof(int));
for(int i = 0; i < SIZE; i++) {
printf("&p[%d]=%p\n", i, &p[i]);
}
free(p);
p = NULL;
}
对比
碎片
频繁分配和释放不同大小堆空间会造成内存空间不连续, 产生大量碎片, 导致程序效率降低
栈不存在这个问题
效率
- 栈
栈是操作系统提所提供数据结构, 计算机在底层对栈提供支持
如分配专门寄存器存放栈地址, 压栈出栈都有专门执行指令, 所以栈效率较高
一般而言, 只要栈剩余空间大于所申请空间, 系统就将为程序提供内存, 否则将报异常提示栈溢出
- 堆
堆是由 C_C++ 函数库提供, 为分配一块堆内存, 操作系统有一个记录空闲内存地址的链表
当系统收到程序申请时会遍历该链表, 寻找第一个空间大于所申请空间的堆节点, 然后将该节点从空闲节点链表中删除, 并将该节点空间分配给程序
对于大多数系统, 会在这块内存空间首地址处记录本次分配大小, 这样 delete 语句才能正确释放本内存空间
另外, 由于找到堆节点大小不一定正好等于申请大小, 系统会自动将多余部分重新放入空闲链表中, 堆分配效率比栈要低得多
申请大小限制
栈是一块连续内存区域
地址增长方向向下(内存地址减小方向)进行, 栈顶地址和栈最大容量一般由系统预先规定好, 若申请空间超过栈剩余空间时, 会提示溢出错误
相对于堆, 能够从栈中获得空间相对较小
操作系统用链表来存储空闲内存地址(内存区域不连续), 链表遍历方向由低地址向高地址进行
因此, 堆内存申请大小受限于计算机系统中有效虚拟内存
存储内容
栈一般用于存放函数参数与局部变量等
函数调用时, 主函数中调用处下一条指令(即函数调用语句下一条可执行语句)地址第一个进栈是, 然后是函数各参数
大多数 C 编译器中, 参数是由右往左入栈, 最后是函数中局部变量(注意 static 变量是不入栈)
当本次函数调用结束后, 遵循先进后出规则, 局部变量先出栈, 然后是参数, 最后栈顶指针指向最开始保存地址, 也就是主函数中下一条指令, 程序由该点继续运行
void Func(int i) {
printf("%d, %d, %d, %d\n", i, i++, i++, i++);
}
int main(void) {
int i = 1;
Func(i);
return 0;
}
由于栈”先进后出”规则, 输出结果是”4, 3, 2, 1”
堆具体存储内容由程序员根据需要决定
malloc
#include<stdio.h>
#include<malloc.h>
int main(void) {
int *p = (int *)malloc(sizeof(int));
free(p);
p = NULL;
return 0;
}
malloc其采用内存池方式, 以减少内存碎片和系统调用开销
malloc采用隐式链表结构将堆区分成连续、大小不一内存块, 包含已分配块和未分配块, 以块作为内存管理基本单位
malloc采用显示链表结构来管理所有空闲块, 使用一个双向链表将空闲块连接起来, 每一个空闲块记录了一个连续、未分配地址
申请
- 空闲链表
空闲存储空间以空闲链表方式组织(地址递增), 每个块包含一个长度、一个指向下一块指针以及一个指向自身存储空间指针
申请请求时, malloc会扫描空闲链表, 直到找到一个足够大块为止(首次适应), 因此每次调用malloc时耗时均不同
查找算法
- 首次适配
第一次找到足够大内存块就分配, 会产生很多内存碎片
- 下一次适配
等第二次找到足够大内存块就分配,会产生比较少内存碎片
- 最佳适配
对堆进行彻底搜索, 从头开始遍历所有块, 使用块容量大于目标容量且差值最小的块被选择
查找内存块存在
- 若与请求大小相符
将其从链表中移走并返回
- 若该块太大
将内存块分为两部分, 尾部部分返回用户, 剩下部分留在空闲链表中(更改头部信息), 因此malloc是分配一块连续内存
查找内存块不存在
- 若申请空间小于128k
调用brk()
, 通过移动堆区末尾地址指针_enddata$
申请空间
- 若申请空间大于128k
mmap()
系统调用函数来在虚拟地址空间中(堆和栈中间, 称为文件映射区域)找一块空间来开辟
mmap功能
-
映射磁盘文件到内存中
-
匿名映射, 不映射磁盘文件, 而向映射区申请一块内存
释放
释放时, 首先搜索空闲链表, 找到可插入被释放块的合适位置
若与被释放块相邻任一边是一个空闲块, 则将这两个块合为一个更大块, 以减少内存碎片
管理
brk
、sbrk
、mmap
都属于系统调用, 若每次申请内存都调用这三个, 会产生系统调用影响性能
其次, 这样申请内存容易产生碎片, 因为堆是从低地址到高地址, 若高地址内存没有被释放, 低地址内存就不能被回收
所以malloc采用内存池管理方式(ptmalloc)
Ptmalloc 采用边界标记法将内存划分成很多块, 从而对内存分配与回收进行管理
为了内存分配函数malloc高效性, ptmalloc会预先向操作系统申请一块内存供用户使用, 当申请和释放内存时, ptmalloc会将这些内存管理起来, 并通过一些策略来判断是否将其回收给操作系统
这样最大好处是使用户申请和释放内存时候更加高效, 避免产生过多内存碎片
操作
内存函数
sizeof
sizeof
是单目运算符
参数是数组、指针、类型、对象、函数等, 返回一个对象或者类型所占内存字节数
参数类型 | 返回值 | 举例 |
---|---|---|
数组类型 | 数组总字节 | int b[5] sizeof(b)=20 |
具体字符串或者数值 | 根据具体类型转化 | sizeof(8)= 4; 自动转化为int类型 |
指针 | 64位系统: 8 32位系统: 4 |
memcpy
/**
* @desc: 从存储区 src 复制 n 个字节到存储区 des
* @param {void *} des, 指向用于存储复制内容的目标数组, 类型强制转换为 void* 指针
* @param {void *} src, 指向要复制数据源, 类型强制转换为 void* 指针
* @param {size_t} n, 被复制字节数
* @return {void *}, 指向目标存储区 str1 指针
*/
void *memcpy(void *des, const void *src, size_t n)
memset
/**
* @desc: 复制字符 c(一个无符号字符)到参数 str 所指向字符串前 n 个字符
* @param {void *} str, 指向要填充内存块
* @param {int} c, 要被设置值
* @param {size_t} n, 被复制字节数
* @return {void *}, 返回一个指向存储区 str 指针
*/
void *memset(void *str, int c, size_t n)
共享内存
#include <sys/ipc.h>
#include <sys/shm.h>
创建
/**
* @brief 创建或打开一块共享内存区
* @param key 进程间通信键值, ftok()返回值
* @param size 共享存储段长度(字节)
* @param shmflg 函数行为及共享内存权限
* IPC_CREAT 若不存在则创建
* IPC_EXCL 若已存在则返回失败
* @return
* success 共享内存标识符
* fail -1
*/
int shmget(key_t key, size_t size,int shmflg);
映射
/**
* @brief 将共享内存段映射到调用进程的数据段中, 让进程某个指针指向此共享内存
* @param shmid 共享内存标识符, shmget()返回值
* @param shmaddr 共享内存映射地址, 若为NULL则由系统自动指定
* @param shmflg 共享内存段的访问权限和映射条件
* 0 共享内存具有刻度可写权限
* SHM_RDONLY 只读
* SHM_RND shmaddr非空时才有效
* @return
* success 共享内存段映射地址
* fail -1
*/
void *shmat(int shmid, const void *shmaddr, int shmflg);
解除映射
/**
* @brief 将共享内存和当前进程分离, 仅是断开联想而不删除共享内存
* @param shmaddr 共享内存映射地址
* @return
* success 0
* fail -1
*/
int shmdt(const void* shmaddr);
// SharedMemoryServer.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main(void)
{
key_t key = ftok("./", 0xFF);
if (key == -1)
{
perror("ftok error");
}
int shmid = shmget(key, 1024, IPC_CREAT | 0666);
if (shmid < 0)
{
perror("shmget error");
}
char *shmadd = shmat(shmid, NULL, 0);
if (shmadd < 0)
{
perror("shmat error");
}
printf("copy data to shared memory\n");
bzero(shmadd, 1024);
strcpy(shmadd, "Hello World\n");
return 0;
}
// SharedMemoryClient.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main(void)
{
key_t key = ftok("./", 0xFF);
if (key == -1)
{
perror("ftok error");
}
int shmid = shmget(key, 1024, IPC_CREAT | 0666);
if (shmid < 0)
{
perror("shmget error");
}
char *shmadd = shmat(shmid, NULL, 0);
if (shmadd < 0)
{
perror("shmat error");
}
printf("data = [%s]\n", shmadd);
int ret = shmdt(shmadd);
if (ret < 0)
{
perror("shmdt error");
}
else
{
printf("deleted shared meomry\n");
}
shmctl(shmid, IPC_RMID, NULL);
return 0;
}