C语言中的动态内存分配原理与基本素养

目录

malloc简介

calloc、realloc简介

内存泄漏检测模块  

浅析malloc底层原理

内存操作误区与基本素养


malloc简介

C语言中的一切操作都是基于内存的 ,变量和数组都是内存的别名 

内存分配由编译器在编译期间决定 ,定义数组的时候必须指定数组长度 ,数组长度是在编译期就必须确定的 

程序运行的过程中,可能需要使用—些额外的内存空间。mallocfree用于执行动态内存分配和释放

 

malloc所分配的是一块连续的内存 ,malloc字节为单位,并且不带任何的类型信息 , free用于将动态内存归还系统 

void* malloc(size_t size);  

void free(void* pointer);

mallocfree库函数,而不是系统调用 ,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 上移时堆区会增大。

image.png  图片来自网络(很久之前保存的,不知出处了)

 

堆空间的管理方式:空闲链表法、位图法、对象池法...

找到一个满足需求的内存块,分配请求大小的内存,当产生大量小的内存碎片时,整理合并

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都可以通过遵循基本的编程原则和规范来避免。 

因此,在学习的时候要牢记和理解内存操作的基本原则,目的和意义

 

 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值