目录
malloc简介
C语言中的一切操作都是基于内存的 ,变量和数组都是内存的别名
内存分配由编译器在编译期间决定 ,定义数组的时候必须指定数组长度 ,数组长度是在编译期就必须确定的
程序运行的过程中,可能需要使用—些额外的内存空间。malloc和free用于执行动态内存分配和释放
malloc所分配的是一块连续的内存 ,malloc以字节为单位,并且不带任何的类型信息 , free用于将动态内存归还系统
void* malloc(size_t size);
void free(void* pointer);
malloc和free是库函数,而不是系统调用 ,malloc实际分配的内存可能会比请求的多 ,不能依赖于不同平台下的malloc行为
当请求的动态内存无法满足时malloc返回NULL,当free的参数为NULL时,函数直接返回
// 测试当前malloc最大申请字节数量 test.c
#include <stdio.h>
#include <malloc.h>
unsigned int max = 0;
int main()
{
unsigned int blockSize[] = {1024 * 1024, 1024, 1}; // 1M, 1K, 1B
for(int i = 0; i < 3; i++)
{
while( 1 )
{
void* pBlock = malloc(max + blockSize[i]);
if( pBlock )
{
max = max + blockSize[i]; //先测试能申请多少M,再测试多少K,再测试多少字节
printf("malloc size : %u Bytes\n", max);
free(pBlock);
}
else
{
break;
}
}
}
printf("maximum malloc size : %u Bytes\n", max); // 得到的是当前的结果,可能会变
}
malloc(0); 将返回什么?
#include <stdio.h>
#include <malloc.h>
int main()
{
int* p=(int*)malloc(0);
printf("%p\n",p);
free(p);
return 0;
}
不停地malloc(0)不释放会内存泄漏,malloc得到的内存空间往往要比实际申请的大,现代操作系统一般都是4字节对齐(即malloc(1)得到的也许是4)
calloc、realloc简介
void* calloc(size_t num, size_t size); 以类型大小为单位申请内存并初始化为0
void* realloc(void* mem_address, size_t new_size); 用于修改一个原先已经分配的内存块大小 (重置内存大小),当mem_address的第—个参数为NULL时,等价于malloc
#include <stdio.h>
#include <malloc.h>
#define SIZE 5
int main()
{
int i = 0;
int* pI = (int*)malloc(SIZE * sizeof(int));
short* pS = (short*)calloc(SIZE, sizeof(short));
for(i=0; i<SIZE; i++)
{
printf("pI[%d] = %d, pS[%d] = %d\n", i, pI[i], i, pS[i]);
}
printf("Before: pI = %p\n", pI);
pI = (int*)realloc(pI, 2 * SIZE * sizeof(int));
printf("After: pI = %p\n", pI);
for(i=0; i<10; i++)
{
printf("pI[%d] = %d\n", i, pI[i]);
}
free(pI);
free(pS);
return 0;
}
内存泄漏检测模块
#include <stdio.h>
#include "mleak.h"
void f()
{
MALLOC(100);
}
int main()
{
int* p = (int*)MALLOC(3 * sizeof(int));
f();
p[0] = 1;
p[1] = 2;
p[2] = 3;
FREE(p);
PRINT_LEAK_INFO();
return 0;
}
mleak.h
#ifndef _MLEAK_H_
#define _MLEAK_H_
#include <malloc.h>
#define MALLOC(n) mallocEx(n, __FILE__, __LINE__)
#define FREE(p) freeEx(p)
void* mallocEx(size_t n, const char* file, const line);
void freeEx(void* p);
void PRINT_LEAK_INFO();
#endif
mleak.c
#include "mleak.h"
#define SIZE 256
typedef struct
{
void* pointer;
int size;
const char* file;
int line;
} MItem;
static MItem g_record[SIZE]; /* 记录动态内存申请的操作 */
void* mallocEx(size_t n, const char* file, const line) //const int line
{
void* ret = malloc(n); /* 动态内存申请 */
if( ret != NULL )
{
int i = 0;
/* 申请成功遍历全局数组,记录此次操作 */
for(i=0; i<SIZE; i++)
{
/* 查找位置 */
if( g_record[i].pointer == NULL )
{
g_record[i].pointer = ret;
g_record[i].size = n;
g_record[i].file = file;
g_record[i].line = line;
break;
}
}
}
return ret;
}
void freeEx(void* p)
{
if( p != NULL )
{
int i = 0;
/* 遍历全局数组,释放内存空间,并清除操作记录 */
for(i=0; i<SIZE; i++)
{
if( g_record[i].pointer == p )
{
g_record[i].pointer = NULL;
g_record[i].size = 0;
g_record[i].file = NULL;
g_record[i].line = 0;
free(p);
break;
}
}
}
}
void PRINT_LEAK_INFO()
{
int i = 0;
printf("Potential Memory Leak Info:\n");
/* 遍历全局数组,打印未释放的空间记录 */
for(i=0; i<SIZE; i++)
{
if( g_record[i].pointer != NULL )
{
printf("Address: %p, size:%d, Location: %s:%d\n", g_record[i].pointer, g_record[i].size, g_record[i].file, g_record[i].line);
}
}
}
下面内容是个人理解,可能有些有误
浅析malloc底层原理
代码段、数据段、堆区、内存映射段、栈区
- .text为代码段,.data,.bss都是数据段。三者的内容为当可执行文件加载到内存时映射的内容
- 堆区为程序运行期间动态申请使用,栈区为代码执行提供局部变量的空间
- 内存映射段,用于文件映射,加载动态链接库(为代码段提供部分功能),malloc申请大于128k的内存使用mmap
- 下图是Linux下的4G虚拟地址空间,可以发现当 program break 上移时堆区会增大。
图片来自网络(很久之前保存的,不知出处了)
堆空间的管理方式:空闲链表法、位图法、对象池法...
找到一个满足需求的内存块,分配请求大小的内存,当产生大量小的内存碎片时,整理合并
malloc是库函数,虚拟内存的申请依赖于两个系统调用,调用他们可以实现program break的移动实现堆内存的申请
简单实现一个malloc函数,参考 https://wenku.baidu.com/view/90e4d6f8770bf78a652954c6.html 修改了部分逻辑错误
代码没有碎片整理,没有大于128k特殊处理,仅供理解原理
#include <stdio.h>
#include <unistd.h>
int has_initialized = 0;
void* last_valid_address;
void* managed_memory_start;
void malloc_init()
{
last_valid_address = sbrk(0); // 从操作系统获取最后一个有效地址
printf("malloc_init: %p\n", last_valid_address);
managed_memory_start = last_valid_address; // 还没有任何内存去管理
has_initialized = 1; // 标记已经初始化
}
// 内存控制块,申请时,返回真实地址,释放时回退一个内存控制块大小
struct mem_control_block
{
int is_available; // 是否为空闲块
int size; // 整个内存块的大小:内存控制块+内存块
};
void Free(void* firstbyte)
{
printf("\nFree: %p\n\n", firstbyte);
struct mem_control_block* mcb;
mcb = (struct mem_control_block*)((char*)firstbyte - sizeof(struct mem_control_block));
mcb->is_available = 1; // 标记为空闲可用
}
void* Malloc(long numbytes)
{
printf("Malloc bengin\n");
void* ret = 0; // 返回搜寻到的内存块
// 初始化last_valid_address, managed_memory_start,如果已经初始化可以在区间搜索了
if(!has_initialized)
malloc_init();
// 实际申请的内存块大小应该由 一个内存控制块 和 用户请求内存大小 组成
numbytes = numbytes + sizeof (struct mem_control_block);
printf("numbytes: %ld + %d = %ld\n", numbytes-8, 8, numbytes);
// 从管理的内存起始开始搜索[managed_memory_start, last_valid_address]
void* current_location = managed_memory_start; // 标记当前遍历的内存块地址
while(current_location != last_valid_address)
{
struct mem_control_block* current_location_mcb = (struct mem_control_block*)current_location;
printf("current_location_mcb: %p\n", current_location_mcb);
if(current_location_mcb->size >= numbytes && current_location_mcb->is_available != 0)
{
printf("ok current_location_mcb->size %d\n", current_location_mcb->size);
current_location_mcb->is_available = 0; // 标记该内存块已被使用
ret = current_location; // 得到返回结果
break;
}
current_location = current_location + current_location_mcb->size;
}
if(!ret)
{
sbrk(numbytes); // 向上移动 program break
ret = last_valid_address;
last_valid_address = last_valid_address + numbytes;
struct mem_control_block* current_location_mcb = ret;
printf("current_location_mcb: %p\n", current_location_mcb);
current_location_mcb->is_available = 0;
current_location_mcb->size = numbytes;
}
ret = ret + sizeof (struct mem_control_block);
printf("Malloc end\n\n");
return ret;
}
int main()
{
int* p1, *p2, *p3, *p4, *p5;
p1=(int*)Malloc(4); // 第一次malloc
p2=(int*)Malloc(10);
p3=(int*)Malloc(100); // 第一次malloc
Free(p3);
p4=(int*)Malloc(10);
p5=(int*)Malloc(80);
printf("%p\n", p1);
printf("%p\n", p2);
printf("%p\n", p3);
printf("%p\n", p4);
printf("%p\n", p5);
Free(p1);
Free(p2);
Free(p4);
Free(p5);
return 0;
}
内存操作误区与基本素养
指针变量中的值是非法的内存地址,进而形成野指针 ,野指针不是NULL指针,是指向不可用内存地址的指针
C语言中无法判断一个指针所保存的地址是否合法
野指针的由来 :局部指针变量没有被初始化 ,指针所指向的变量在指针之前被销毁 ,使用已经释放过的指针 ,进行了错误的指针运算 ,进行了错误的强制类型转换
常见内存错误 :结构体成员指针未初始化 ,结构体成员指针未分配足够的内存 ,内存分配成功但并未初始化 ,内存操作越界
#include <stdio.h>
#include <malloc.h>
int main()
{
int* p1 = (int*)malloc(40);
int* p2 = (int*)1234567; //p2是一个野指针
int i = 0;
for(i=0; i<40; i++)
{
*(p1 + i) = 40 - i; //由于指针运算产生了野指针,改写了非法的内存地址
}
free(p1); //释放了p1指向的内存空间,但为将p1置为NULL
printf("%p\n",p1);
for(i=0; i<40; i++)
{
p1[i] = p2[i]; //使用了已经释放的内存空间
}
return 0;
}
基本素养
绝不返回局部变量和局部数组的地址,任何变量在定义后必须0初始化
字符数组必须确认0结束符后才能成为字符串
任何使用与内存操作相关的函数必须指定长度信息
通常在哪个函数申请的空间,应在那个函数释放
动态内存申请之后,应该立即检查指针值是否为NULL , 防止使用NULL指针。
free指针之后立即赋值为NULL
malloc操作和free操作必须匹配, 防止内存泄露和多次释放
// 无处不在的野指针
#include <stdio.h>
#include <string.h>
#include <malloc.h>
struct Student
{
char* name;
int number;
};
char* func()
{
char p[] = "D.T.Software"; // 字符数组,只读存储区一份,栈内一份,返回的指针指向栈内的字符数组
return p;
}
void del(char* p)
{
printf("%s\n", p);
free(p);
}
int main()
{
struct Student s; // 由于没有初始化,产生野指针(name)
char* p = func(); // 产生野指针
strcpy(s.name, p); // 使用野指针
s.number = 99;
p = (char*)malloc(5);
strcpy(p, "D.T.Software"); // 产生内存越界 , 操作了野指针
del(p);
return 0;
}
内存错误是实际产品开发中最常见的问题,然而绝大多数的bug都可以通过遵循基本的编程原则和规范来避免。
因此,在学习的时候要牢记和理解内存操作的基本原则,目的和意义