【C++ 】动态内存管理:new/delete与malloc/free的对比

14 篇文章 1 订阅

在C++中,动态内存管理是一个至关重要的概念。它允许我们在程序运行时根据需要动态地分配和释放内存,为对象创建和销毁提供了灵活性。在C++中,我们通常会用到两对工具:new/delete 和 malloc/free。虽然它们都能够完成类似的任务,但在使用、安全性和灵活性方面存在显著差异。

new/delete

  • 类型安全性: newdelete提供了更好的类型安全性。在使用new创建对象时,会自动调用构造函数进行初始化,而在释放内存时,会自动调用析构函数,确保资源正确释放,避免内存泄漏和资源泄漏的风险。

  • 异常处理: new在分配内存失败时会抛出std::bad_alloc异常,这使得在内存分配失败时更容易处理错误情况,从而增强了程序的健壮性。

  • 自动资源管理: newdelete提供了自动资源管理的功能,使得在对象的生命周期结束时能够正确地释放内存,而不需要手动管理。

使用new和delete

#include <iostream>  
  
class MyClass {  
public:  
    MyClass() {  
        std::cout << "  MyClass()" << std::endl;  
    }  
      
    ~MyClass() {  
        std::cout << "~MyClass()" << std::endl;  
    }  
      
    void show() {  
        std::cout << "Hello!" << std::endl;  
    }  
};  
  
int main() {  
    // 使用new分配内存并创建对象  
    MyClass* obj = new MyClass();  
    obj->show(); // 输出: Hello!  
      
    // 使用delete释放内存并调用析构函数  
    delete obj; // 输出: ~MyClass() 
    
    //数组的用法
    int* pa=new int[6];
    delete[] pa;  
    
    return 0;  
}

malloc/free

  • 内存分配malloc函数根据请求的大小分配一块连续的内存区域,并返回指向该内存的指针。它不会自动初始化内存区域。
  • 内存释放free函数用于释放之前通过malloc分配的内存。它不会调用析构函数或执行任何清理操作。
  • 错误处理:如果malloc无法分配所需的内存,它会返回NULL

使用malloc和free

#include <iostream>  
#include <stdlib.h> // 为了使用malloc和free  
  

int main() {  
    // 使用malloc分配内存(但不调用构造函数)  
    void* memory = malloc(sizeof(MyClass));  
    if (memory == nullptr) {  
        std::cerr << "Memory allocation failed." << std::endl;  
        return 1;  
    }  
      
    // 使用"placement new"在已分配的内存上构造对象  
    MyClass* obj = new(memory) MyClass();  
    obj->show(); // 输出:Hello! 
      
    // 手动调用析构函数来销毁对象(但不释放内存)  
    obj->~MyClass(); // 输出: ~MyClass() 
      
    // 使用free释放之前通过malloc分配的内存  
    free(memory);  
      
    return 0;  
}

mallocfree是C语言中的函数,虽然它们可以在C++中使用,但通常不推荐,主要原因包括:

  • 不安全的类型转换: malloc返回void*指针,需要手动进行类型转换,容易引起错误。

  • 无构造和析构函数调用: mallocfree只是简单地分配和释放内存,并不调用对象的构造和析构函数。这可能导致对象状态未初始化或未正确清理,容易引发潜在的bug。

  • 不便的异常处理: malloc在分配内存失败时返回NULL,需要手动检查返回值,这在处理错误时不如C++的异常机制方便。

operator new 和 operator delete 函数

在 C++ 中,动态内存管理是至关重要的,而 operator newoperator delete 这两个全局函数则承担了其中的重要角色。它们与 newdelete 操作符紧密相关,负责在底层进行动态内存的分配和释放。让我们深入了解这两个函数的功能和工作原理。

operator new

operator new 的主要任务是分配指定大小的内存块。当使用 new 操作符为一个对象动态分配内存时,实际上会调用 operator new 函数来执行内存分配。这个函数会返回一个指向新分配内存的指针。如果内存分配失败,它通常会抛出一个 std::bad_alloc 异常,除非特别指定了不同的行为。

在标准的实现中,operator new 通常会调用底层的 malloc 函数来实际分配内存,但开发者也可以重载 operator new 以提供自定义的内存分配行为。

operator delete

operator new 相对应,operator delete 的任务是释放之前通过 operator new 分配的内存。当使用 delete 操作符释放一个对象时,实际上会调用 operator delete 函数来执行内存释放。这个函数通常会调用底层的 free 函数来回收内存。

operator new 类似,开发者也可以重载 operator delete 以提供自定义的内存释放行为。

//调用构造和析构
	A* p = (A*)::operator new(sizeof(A)); //分配
    new(p) A(); //构造
    p->~A();   //析构
    ::operator delete(p); //释放

    //调用构造和析构
	A* p1 = new A;
	delete p1;

	//不会调用构造和析构
	A*p2 = (A*)operator new(sizeof(A));
	operator delete(p2);

operator new 和 operator delete 源码:
在这里插入图片描述
详细代码,以及示例可以到[cplusplus]查看(https://legacy.cplusplus.com/reference/new/operator%20new/?kw=operator%20new)

/*
operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败,
尝试执行空 间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。
*/
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
		//如果申请内存失败了,这里会抛出bad_alloc 类型异常
		static const std::bad_alloc nomem;
		_RAISE(nomem);
	}
	return (p);
}
/*
operator delete: 该函数最终是通过free来释放空间的
*/
void operator delete(void *pUserData)
{
	_CrtMemBlockHeader * pHead;
	RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
	if (pUserData == NULL)
		return;
	_mlock(_HEAP_LOCK); /* block other threads */
	__TRY
	/* get a pointer to memory block header */
	pHead = pHdr(pUserData);
	/* verify block type */
	_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
	_free_dbg( pUserData, pHead->nBlockUse );
	__FINALLY
	_munlock(_HEAP_LOCK); /* release other threads */
	__END_TRY_FINALLY
	return;
}
/*
free的实现
*/
#define free(p) _free_dbg(p, _NORMAL_BLOCK)

operator new 的三种形式

  1. throwing

    void* operator new(std::size_t size) throw (std::bad_alloc);
    
    • 当分配失败时会抛出 std::bad_alloc 异常。
  2. nothrow

    void* operator new(std::size_t size, const std::nothrow_t& nothrow_value) throw();
    
    • 当分配失败时返回 nullptr,不会抛出异常。
  3. placement

    void* operator new(std::size_t size, void* ptr) throw();
    
    • 在指定的 ptr 地址上分配内存,不会抛出异常。
    • 这种形式通常用于在已分配的内存上构造对象,例如在内存池中使用。

placement new 的应用

  • 通过在已分配的内存上构造对象,实现在内存池中的对象管理。
  • 调用形式为 new(p) A();,其中 p 可以是动态分配的内存也可以是栈中的缓冲区。
#include <new>

// 示例:在已分配的内存上构造对象
void* mem = operator new(sizeof(A)); // 从内存池中获取内存
A* obj = new(mem) A(); // 在 mem 地址上构造 A 对象

placement new 只是返回传入的指针 ptr,不会进行内存分配,而是通过调用对象的构造函数在指定的地址上创建对象。

重载 operator newoperator delete

在某些应用场景中,开发者可能需要实现自定义的内存分配和释放策略,比如使用内存池、堆栈分配器或共享内存等。这时,可以通过重载 operator newoperator delete 来实现。

重载 operator new

在 C++ 中,operator new 可以被重载,允许开发者自定义内存分配方式。在某些应用场景中,开发者可能需要实现自定义的内存分配和释放策略,比如使用内存池、堆栈分配器或共享内存等。这时,可以通过重载 operator new 和 operator delete 来实现。

class A {
public:
    A() {
        std::cout << "A()" << std::endl;
    }

    ~A() {
        std::cout << "~A()" << std::endl;
    }

    // 重载 operator new
    void* operator new(size_t size) {
        std::cout << " A() operator new" << std::endl;
        return malloc(size);
    }

    // 重载 operator new 以支持 nothrow
    void* operator new(size_t size, const std::nothrow_t& nothrow_value) {
        std::cout << "A() operator new nothrow" << std::endl;
        return malloc(size);
    }
};

int main() {
    A* p1 = new A;  // 调用 A::operator new
    delete p1;

    A* p2 = new(std::nothrow) A;  // 调用 A::operator new nothrow
    delete p2;

    return 0;
}

运行结果:

A() operator new
A() 
~A() 
A() operator new nothrow
A() 
~A() 

如果类 A 中没有定义对 operator new 的重载,那么 new Anew(std::nothrow) A 都将会使用全局的 operator new(size_t size)

自定义参数的 operator new 重载

operator new 重载可以添加自定义参数,这些参数可以在分配内存时传递。虽然这些参数在标准的 operator new 中没有实际用途,但可以用于调试和检测。

void* operator new(size_t size, int x, int y, int z) {
    std::cout << "X=" << x << "  Y=" << y << " Z=" << z << std::endl;
    return malloc(size);
}

这种重载看起来可能没有太大的实际作用,因为标准的 operator new 只需完成内存分配任务。然而,通过对这类重载的巧妙应用,可以在动态内存分配的调试和检测中发挥作用。

placement new

placement new 本身是 operator new 的一个重载,它允许在已分配的内存上构造对象,常用于内存池等场景。其调用形式为 new(ptr) A();,其中 ptr 是已分配内存的指针。

void* operator new(size_t size, void* ptr) {
    return ptr; // 只是简单返回指针,不进行实际内存分配
}

一般情况下,不建议修改 placement new 的实现,因为它通常与 new(ptr) A(); 配合使用,其职责只需简单返回指针。

malloc/free和new/delete的区别
特征new/deletemalloc/free
分配内存的位置自由存储区堆内存自由存储区堆内存
分配成功的返回值类型完整类型指针void*void*
分配失败的返回值默认抛出异常返回NULLNULL
分配内存的大小由编译器根据类型计算得出必须显式指定字节数
处理数组的支持有(使用new[])无(但可使用realloc)
已分配内存的扩充可以(使用realloc)无法直观地处理
是否相互调用可以,看具体的实现不可调用
客户能否指定处理函数或重新制定分配器可以无法通过用户代码进行
处理函数重载允许与否允许不允许
构造函数与析构函数调用调用不调用

malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是:

  1. malloc和free是函数,new和delete是操作符
  2. malloc申请的空间不会初始化,new可以初始化
  3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,如果是多个对象,[]中指定对象个数即可
  4. malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
  5. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
  6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理

在C++中,一般推荐使用newdelete进行动态内存管理,因为它们提供了更好的类型安全性,并能自动调用构造函数和析构函数,从而减少了内存泄漏和资源泄漏的风险。

什么是内存泄漏,内存泄漏的危害

  • 内存泄漏是指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

  • 内存泄漏的危害在于长期运行的程序出现内存泄漏会影响很大,例如操作系统、后台服务等。出现内存泄漏会导致系统资源越来越稀缺,进而影响系统的响应速度,最终可能导致系统性能下降甚至卡死的情况发生。内存泄漏问题是软件开发中常见但危害严重的问题之一,因此在开发过程中应该严格注意内存管理,避免内存泄漏的发生。

然而,在需要与C代码交互或需要更底层的内存管理控制时,mallocfree可能会被用到。但在这种情况下,需要格外小心以确保正确地管理内存和对象的生命周期。

需要注意的是,尽管mallocfree可以在C++中使用,但并不推荐在C++中经常使用它们进行动态内存管理。因为这可能会导致一些问题,比如忘记初始化或清理对象的资源,从而导致内存泄漏或其他问题。相反,应该优先考虑使用newdelete来利用C++提供的面向对象特性和自动内存管理功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

SuhyOvO

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值