MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++中malloc和new内存分配方式差异

2021-01-203.1k 阅读

C++ 中 mallocnew 内存分配方式差异

1. 内存分配的基本概念

在 C++ 编程中,有效管理内存是至关重要的。无论是开发大型软件系统还是小型应用程序,内存的正确分配与释放直接影响程序的性能、稳定性和资源利用效率。mallocnew 是 C++ 中两种常用的内存分配方式,它们在功能上有相似之处,但在实现细节、使用方式和适用场景等方面存在显著差异。

2. malloc 内存分配

2.1 malloc 简介

malloc 是 C 标准库函数,在 <stdlib.h> 头文件中声明。它的主要功能是在堆内存中分配指定字节数的连续内存空间,并返回一个指向分配内存起始地址的指针。如果分配失败,malloc 将返回 NULL

2.2 malloc 的语法

void* malloc(size_t size); 其中,size 参数指定要分配的内存大小,以字节为单位。返回值是一个 void* 类型的指针,需要根据实际使用情况进行类型转换。

2.3 malloc 代码示例

#include <iostream>
#include <stdlib.h>

int main() {
    // 分配一个 int 类型大小的内存空间
    int* ptr = (int*)malloc(sizeof(int));
    if (ptr != NULL) {
        *ptr = 42;
        std::cout << "Allocated memory: " << *ptr << std::endl;
        free(ptr);
    } else {
        std::cout << "Memory allocation failed" << std::cout;
    }
    return 0;
}

在上述代码中,首先使用 malloc 分配了一个 int 类型大小的内存空间,并将返回的 void* 指针转换为 int* 类型。然后对该内存空间进行赋值,并在使用完毕后通过 free 函数释放内存。

2.4 malloc 内存分配机制

malloc 从堆内存中分配内存。堆是一块由操作系统管理的内存区域,程序运行时可以动态地从堆中获取和释放内存。malloc 的实现通常依赖于操作系统提供的系统调用,例如在 Unix - like 系统上可能使用 brksbrk 系统调用,在 Windows 上可能使用 VirtualAlloc 等相关函数。

malloc 被调用时,它会在堆中寻找一块足够大的空闲内存块。如果找到合适的内存块,它会将该内存块标记为已使用,并返回指向该内存块起始地址的指针。如果堆中没有足够大的空闲内存块,malloc 可能会尝试扩展堆(通过系统调用),如果扩展失败则返回 NULL

3. new 内存分配

3.1 new 简介

new 是 C++ 的关键字,用于在堆上分配内存并初始化对象。与 malloc 不同,new 不仅分配内存,还会调用对象的构造函数进行初始化(对于类类型的对象)。

3.2 new 的语法

对于基本数据类型: type* pointer = new type; 对于类类型: class_type* pointer = new class_type(arguments); 其中,arguments 是类构造函数的参数列表。

3.3 new 代码示例

#include <iostream>

class MyClass {
public:
    MyClass() {
        std::cout << "MyClass constructor called" << std::endl;
    }
    ~MyClass() {
        std::cout << "MyClass destructor called" << std::endl;
    }
};

int main() {
    // 分配一个 int 类型的内存空间并初始化
    int* num = new int(10);
    std::cout << "Allocated int: " << *num << std::endl;
    delete num;

    // 分配一个 MyClass 类型的对象
    MyClass* obj = new MyClass();
    delete obj;

    return 0;
}

在上述代码中,首先使用 new 分配了一个 int 类型的内存空间并初始化为 10,然后使用 delete 释放内存。接着,使用 new 分配了一个 MyClass 类型的对象,此时会调用 MyClass 的构造函数进行初始化,最后使用 delete 释放对象,会调用 MyClass 的析构函数。

3.4 new 内存分配机制

new 操作符的实现依赖于两个步骤:首先调用 operator new 函数来分配内存,然后调用对象的构造函数进行初始化。operator new 是一个全局函数,其默认实现会调用 malloc 来分配内存。但在 C++ 中,operator new 可以被重载,以便根据不同的需求实现自定义的内存分配策略。

new 表达式被执行时,它首先调用 operator new 函数来获取足够的内存空间。如果内存分配成功,new 会调用对象的构造函数来初始化该内存空间。如果内存分配失败,new 会抛出一个 std::bad_alloc 异常(而不是像 malloc 那样返回 NULL)。

4. mallocnew 的差异

4.1 分配对象时的初始化差异

  • mallocmalloc 仅分配内存,不会调用对象的构造函数。这意味着对于类类型的对象,使用 malloc 分配内存后,对象处于未初始化状态,其成员变量的值是不确定的。
  • newnew 不仅分配内存,还会调用对象的构造函数进行初始化。对于基本数据类型,new 也可以进行初始化赋值。例如 int* num = new int(10);,这里 new 分配内存并将其初始化为 10

4.2 内存分配失败的处理差异

  • malloc:当 malloc 无法分配所需内存时,它返回 NULL。调用者需要检查返回值是否为 NULL 来判断内存分配是否成功。例如:
int* ptr = (int*)malloc(sizeof(int));
if (ptr == NULL) {
    // 处理内存分配失败
}
  • new:当 new 无法分配内存时,它会抛出一个 std::bad_alloc 异常。在 C++ 中,通常使用 try - catch 块来捕获和处理这个异常。例如:
try {
    int* num = new int(10);
} catch (std::bad_alloc& e) {
    // 处理内存分配失败
}

4.3 返回值类型差异

  • mallocmalloc 返回一个 void* 类型的指针。由于 void* 类型指针不指向任何具体类型,因此在使用 malloc 分配的内存时,需要将其显式转换为所需的类型指针。例如 int* ptr = (int*)malloc(sizeof(int));
  • newnew 返回一个指向所分配对象类型的指针。例如 int* num = new int(10);new 直接返回 int* 类型指针,无需额外的类型转换。

4.4 内存释放方式差异

  • malloc:使用 malloc 分配的内存需要使用 free 函数来释放。free 函数也是 C 标准库函数,在 <stdlib.h> 头文件中声明。例如 free(ptr);,其中 ptrmalloc 返回的指针。
  • new:使用 new 分配的内存需要使用 delete 操作符来释放(对于单个对象)或 delete[] 操作符来释放数组。delete 操作符不仅释放内存,还会调用对象的析构函数(对于类类型的对象)。例如 delete num; 用于释放单个对象,delete[] arr; 用于释放数组,其中 arr 是通过 new[] 分配的数组指针。

4.5 对数组分配的支持差异

  • mallocmalloc 可以分配数组内存,但它不会像 new 那样对数组元素进行初始化。例如 int* arr = (int*)malloc(5 * sizeof(int)); 分配了一个能容纳 5 个 int 类型元素的数组内存,但数组元素的值是未定义的。释放时使用 free(arr);
  • newnew 有专门的语法来分配数组内存,并且对于类类型数组,会调用每个元素的构造函数进行初始化。例如 int* arr = new int[5]; 分配了一个包含 5 个 int 类型元素的数组,MyClass* objArr = new MyClass[3]; 分配了一个包含 3 个 MyClass 对象的数组,并调用每个 MyClass 对象的构造函数。释放数组内存时使用 delete[] arr;delete[] objArr;

4.6 可重载性差异

  • mallocmalloc 是 C 标准库函数,其实现由标准库提供,一般不能被重载。虽然在一些特殊情况下可以通过链接自定义的标准库实现来修改其行为,但这并不是常规意义上的重载。
  • newnew 操作符依赖于 operator new 函数,operator new 可以被重载。通过重载 operator new,可以实现自定义的内存分配策略,例如内存池、对齐分配等。同样,operator delete 也可以被重载,用于自定义内存释放行为。

5. 适用场景分析

5.1 malloc 的适用场景

  • 与 C 代码交互:当编写与 C 代码兼容的 C++ 代码时,例如在混合 C 和 C++ 的项目中,malloc 可能是必要的选择,因为 C 语言中没有 new 关键字,使用 malloc 可以确保与 C 代码在内存管理上的一致性。
  • 简单内存分配需求:对于简单的数据类型(如 intchar 等)且不需要初始化的情况,malloc 可以提供一种简洁的内存分配方式。例如在一些性能敏感的算法实现中,只需要快速分配一块内存空间用于临时存储数据,不关心初始化值,malloc 可以满足需求。

5.2 new 的适用场景

  • 类对象的创建:在 C++ 中,当需要创建类类型的对象时,new 是首选。它不仅分配内存,还能确保对象被正确初始化,调用构造函数完成对象的初始化逻辑,这对于对象的正确使用至关重要。
  • 异常处理友好:由于 new 分配内存失败时抛出异常,这使得在 C++ 的异常处理机制下,代码能够更优雅地处理内存分配失败的情况。特别是在复杂的函数调用链中,异常机制可以将错误信息向上传递,而不需要在每个可能的调用点检查返回值。
  • 自定义内存管理:当需要实现自定义的内存分配策略时,new 的可重载性使得通过重载 operator newoperator delete 来实现诸如内存池、对齐分配等高级内存管理技术变得容易。

6. 内存分配陷阱与注意事项

6.1 内存泄漏问题

无论是 malloc 还是 new,如果分配的内存没有被正确释放,都会导致内存泄漏。在使用 malloc 时,忘记调用 free 函数;在使用 new 时,忘记调用 delete(对于单个对象)或 delete[](对于数组),都会使分配的内存无法被再次使用,从而造成内存泄漏。例如:

// 使用 malloc 导致内存泄漏
void memoryLeakWithMalloc() {
    int* ptr = (int*)malloc(sizeof(int));
    // 没有调用 free(ptr)
}

// 使用 new 导致内存泄漏
void memoryLeakWithNew() {
    int* num = new int(10);
    // 没有调用 delete num;
}

6.2 数组分配与释放的匹配问题

在使用 new[] 分配数组内存时,必须使用 delete[] 来释放;使用 malloc 分配数组内存时,必须使用 free 来释放。如果不匹配,可能会导致未定义行为。例如:

// 错误的数组释放方式
void wrongArrayDeletion() {
    int* arr = new int[5];
    // 错误:应该使用 delete[] arr;
    delete arr;
}

6.3 异常安全问题

在使用 new 时,由于它可能抛出异常,在编写代码时需要注意异常安全。例如,如果在分配多个对象时,部分对象分配成功但后续对象分配失败,需要确保已分配的对象能够被正确释放,以避免内存泄漏。一种常见的方法是使用智能指针(如 std::unique_ptrstd::shared_ptr),它们可以自动管理内存释放,提高代码的异常安全性。

7. 总结差异对实际编程的影响

在实际的 C++ 编程中,理解 mallocnew 的差异对于编写高效、健壮的代码至关重要。对于简单的内存分配需求且与 C 代码兼容性要求高的场景,malloc 是合适的选择;而对于 C++ 特有的类对象创建、异常处理和自定义内存管理需求,new 则更具优势。

正确选择内存分配方式不仅能避免内存泄漏、未定义行为等常见问题,还能提高程序的性能和可维护性。同时,在编写代码时要遵循内存分配和释放的规则,注意异常安全,合理使用智能指针等工具来简化内存管理,确保程序在各种情况下都能稳定运行。通过深入理解和正确运用 mallocnew,开发人员可以更好地掌控内存资源,编写出高质量的 C++ 程序。

希望通过以上详细的介绍和分析,读者能够全面掌握 mallocnew 在 C++ 中的内存分配方式差异,并在实际编程中做出明智的选择。