动态内存分配

1 C语言中的动态内存分配

1.1 动态内存分配的意义

  • C语言中的一切操作都是基于内存的。
  • 变量和数组都是内存的别名:
  • 内存分配由编译器在编译期间决定;
    • 定义数组的时候必须指定数组长度;
    • 数组长度是在编译期就必须确定的。

我们在程序运行的过程中,可能需要使用一些额外的内存空间。因此就出现了动态内存分配。

1.2 malloc和free

malloc和free用于执行动态内存分配和释放。

在这里插入图片描述
注意:malloc和free是库函数,而不是系统调用。 其函数原型如下:

  • void* malloc(size_t size);
  • void free(void* pointer);

关于malloc有以下几点需要注意:

  • malloc所分配的是一块连续的内存(不会对内存进行初始化)。
  • malloc以字节为单位,并且不带任何的类型信息。
  • malloc实际分配的内存可能会比请求的多,不能依赖于不同平台下的malloc行为。
  • 当请求的动态内存无法满足时(可能内存已耗尽或者依次申请的内存字节数太大)malloc返回NULL。

关于free有如下几点需要注意:

  • free用于将动态内存归还系统。
  • 当free的参数为NULL时,函数直接返回。

思考:
malloc(0);将返回什么?
分析:malloc(0)是成功的,理论上来说内存是有起始地址和长度这两种属性的。成功的返回值了起始地址,但是长度为0,因此不能使用。但是实际,可能会返回的比申请的多,所以成功分配并且能够使用。如果程序中一直malloc(0)而不free是有可能导致内存泄漏的。

内存泄漏检测模块:
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)
{
    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);
        }
    }
}

main.c:

#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;
}


1.3 calloc和realloc

  • malloc的同胞兄弟:
    • void* calloc(size_t num, size_t size);
    • void* realloc(void* pointer, size_t new_size);
  • calloc的参数代表所返回内存的内存信息:
    • calloc会将返回的内存初始化为0。
  • realloc用于修改一个原先已经分配的内存块大小(不会对扩大的内存进行初始化),也就是重置内存大小:
    • 在使用realloc之后应使用其返回值;
    • 当pointer的第一个参数为NULL,等价于malloc。

2 C++中的动态内存分配

2.1 new和delete

C++中的动态内存分配:

  • C++中通过new关键字进行动态内存申请。
  • C++中的动态内存申请是基于类型进行的。
  • delete关键字用于内存释放。
如:
变量申请:
    Type *pointer = new Type;
    //……
    delete pointer;
数组申请:
    Type *pointer = new Type[N];
    //……
    delete[] pointer;	// 如果使用delete会导致内存泄漏

实例分析:C++中的动态内存分配

#include <stdio.h>

int main()
{
    int* p = new int;

    *p = 5;
    *p = *p + 10;

    printf("p = %p\n", p);
    printf("*p = %d\n", *p);

    delete p;

    p = new int[10];

    for(int i=0; i<10; i++)
    {
        p[i] = i + 1;

        printf("p[%d] = %d\n", i, p[i]);
    }

    delete[] p;

    return 0;
}

new关键字的初始化:

int *pi = new int(1); 
float *pf = new float(2.0f); 
char *pc = new char(‘c’);

编程实验:初始化动态内存

#include <stdio.h>

int main()
{
    int* pi = new int(1);
    // int* pa = new int[1];

    float* pf = new float(2.0f);
    char* pc = new char('c');

    printf("*pi = %d\n", *pi);
    printf("*pf = %f\n", *pf);
    printf("*pc = %c\n", *pc);

    delete pi;
    delete pf;
    delete pc;

    return 0;
}

2.2 new、malloc和delete、free的区别

new关键字与malloc函数的区别:

newmalloc
new关键字是C++的一部分,在所有的C++编译器中都被支持malloc是由C库提供的函数,在某些系统开发中是不能调用的
new以具体的类型为单位进行内存分配malloc以字节为单位进行内存分配
new在申请单个类型变量时可以进行初始化malloc仅根据需要审定定量的内存空间不具备内存初始化的特性
new能够触发构造函数的调用malloc仅分配需要的内存空间
对象的创建只能使用newmalloc不适合面向对象开发

delete关键与free函数的区别:

deletefree
delete在所有C++编译器中都被支持free在某些系统开发中是不能调用的
delete能够触发析构函数的调用free仅归还之前分配的空间
对象的销毁只能使用deletefree不适合面向对象开发
#include <iostream>
#include <string>
#include <cstdlib>

using namespace std;

class Test
{
    int* mp;
public:
    Test()
    {
        cout << "Test::Test()" << endl;
        
        mp = new int(100);
        
        cout << *mp << endl;
    }
    ~Test()
    {
        delete mp;
        
        cout << "~Test::Test()" << endl;
    }
};

int main()
{
    Test* pn = new Test;	// 调用构造函数
    Test* pm = (Test*)malloc(sizeof(Test));	// 仅分配内存空间,对象未初始化
    
    delete pn;	// 调用析构函数
    free(pm);	// 仅释放所分配的内存空间
    
    return 0;
}


3 动态内存申请的结果

3.1 编译器默认new实现

问题:动态内存申请一定成功吗?

常见的动态内存分配代码:
在这里插入图片描述
我们必须知道如下事实:

  • malloc函数申请失败时返回NULL值。
  • new关键字申请失败时(根据编译器的实现不同而不同):
    • 返回NULL值(古老的编译器)。
    • 抛出std::bad_allic异常。

问题:new语句中的异常是怎么抛出来的?

我们先来看一下new关键字在C++规范中的标准行为:

在堆空间申请足够大的内存:

  • 成功:
    • 在获取的空间中调用构造函数创建对象。
    • 返回对象的地址。
  • 失败:
    • 抛出std::bad_alloc异常。

new在分配内存时:

  • 如果空间不足,会调用全局的new_handler()函数。
  • new_handler()函数中抛出std::bad_alloc异常。

可以自定义new_handler()函数:

  • 处理默认的new内存分配失败的情况。

new_handler()的定义和使用:
在这里插入图片描述

我们需要知道并不是所有的编译器都有new_handler()函数,比如vc、g++中没有new_handler函数,bcc中是有的。以VC中为例,来分析一下异常是如何抛出的,new操作符的源码如下:

/***
*new.cxx - defines C++ new routine
*
*       Copyright (c) Microsoft Corporation.  All rights reserved.
*
*Purpose:
*       Defines C++ new routine.
*
*******************************************************************************/


#ifdef _SYSCRT
#include <cruntime.h>
#include <crtdbg.h>
#include <malloc.h>
#include <new.h>
#include <stdlib.h>
#include <winheap.h>
#include <rtcsup.h>
#include <internal.h>

void * operator new( size_t cb )
{
    void *res;

    for (;;) {

        //  allocate memory block
        res = _heap_alloc(cb);

        //  if successful allocation, return pointer to memory

        if (res)
            break;

        //  call installed new handler
        if (!_callnewh(cb))
            break;

        //  new handler was successful -- try to allocate again
    }

    RTCCALLBACK(_RTC_Allocate_hook, (res, cb, 0));

    return res;
}
#else  /* _SYSCRT */

#include <cstdlib>
#include <new>

_C_LIB_DECL
int __cdecl _callnewh(size_t size) _THROW1(_STD bad_alloc);
_END_C_LIB_DECL

void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
        {       // try to allocate size bytes
        void *p;
        while ((p = malloc(size)) == 0)
                if (_callnewh(size) == 0)
                {       // report no memory
                static const std::bad_alloc nomem;
                _RAISE(nomem);
                }

        return (p);
        }

/*
 * Copyright (c) 1992-2002 by P.J. Plauger.  ALL RIGHTS RESERVED.
 * Consult your license regarding permissions and restrictions.
 V3.13:0009 */
#endif  /* _SYSCRT */

可以看到两种版本都会调用_callnewh函数,这个函数中会判断new_handler是否可用,可用的话就不会抛出异常,否则会直接抛出异常。

问题:如何跨编译器统一new的行为,提高代码的可移植性?

解决方案如下:

全局范围(不推荐):

  • 重新定义new/delete的实现,不抛出任何异常。
  • 自定义new_handler()函数,不抛出任何异常。

类层次范围:

  • 重载new/delete,不抛出任何异常。

单次使用内存分配:

  • 使用notrhow参数,指名new不抛出任何异常。

结论:

  • 古老的编译器new失败时才会返回NULL,现代编译器则会抛出bad_alloc异常,只是实现细节不同。
  • 不是所有的编译器都遵循C++的标准规范。
  • 编译器可能重定义new的实现,并在实现中抛出bad_alloc异常。
  • 编译器的默认实现中,可能没有设置全局的new_handler()函数。
  • 对于一致性要求较高的代码,需要考虑new的具体细节。

3.2 重载new操作符直接返回NULL分析

g++中如果new返回NULL会继续调用构造函数,可能会导致段错误的发生;bcc和vc中如果new返回NULL不会继续调用构造函数。new后面如果加上了throw(),会告诉编译器如果内存申请失败直接返回NULL,并且不能进行构造函数的调用。

注意:重载返回NULL和编译器默认的new实现返回NULL是两种没有任何关联的行为。

编程实验:动态内存申请

#include <iostream>
#include <new>
#include <cstdlib>
#include <exception>

using namespace std;

class Test
{
    int m_value;
public:
    Test()
    {
        cout << "Test()" << endl;
        
        m_value = 0;
    }
    
    ~Test()
    {
        cout << "~Test()" << endl;  
    }
    
    void* operator new (unsigned int size) throw()
    {
        cout << "operator new: " << size << endl;
        
        // return malloc(size);
        
        return NULL;
    }
    
    void operator delete (void* p)
    {
        cout << "operator delete: " << p << endl;
        
        free(p);
    }
    
    void* operator new[] (unsigned int size) throw()
    {
        cout << "operator new[]: " << size << endl;
        
        // return malloc(size);
        
        return NULL;
    }
    
    void operator delete[] (void* p)
    {
        cout << "operator delete[]: " << p << endl;
        
        free(p);
    }
};

void my_new_handler()
{
    cout << "void my_new_handler()" << endl;
}

void ex_func_1()
{
    new_handler func = set_new_handler(my_new_handler);
    
    try
    {
        cout << "func = " << func << endl;
        
        if( func )
        {
            func();
        }
    }
    catch(const bad_alloc&)
    {
        cout << "catch(const bad_alloc&)" << endl;
    }
}

void ex_func_2()
{
    Test* pt = new Test();
    
    cout << "pt = " << pt << endl;
    
    delete pt;
    
    pt = new Test[5];
    
    cout << "pt = " << pt << endl;
    
    delete[] pt; 
}

void ex_func_3()
{
    int* p = new(nothrow) int[10];
    
    // ... ...
    
    delete[] p; 
    
    int bb[2] = {0};
    
    struct ST
    {
        int x;
        int y;
    };
    
    ST* pt = new(bb) ST();
    
    pt->x = 1;
    pt->y = 2;
    
    cout << bb[0] << endl;
    cout << bb[1] << endl;
    
    pt->~ST();	// 如果显示的指定了创建对象的内存,就需要显示的调用析构函数
}

int main(int argc, char *argv[])
{
    // ex_func_1();
    // ex_func_2();
    // ex_func_3();
    
    return 0;
}



参考资料:

  1. C语言进阶剖析教程
  2. C++深度解析教程
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值