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

C++中malloc和new内存分配失败处理

2023-07-183.3k 阅读

C++ 中 mallocnew 内存分配失败处理

malloc 内存分配失败处理

在 C++ 中,malloc 是 C 标准库提供的用于动态内存分配的函数。它的原型如下:

void* malloc(size_t size);

malloc 函数尝试分配指定字节数 size 的内存块。如果分配成功,它返回一个指向已分配内存起始地址的指针;如果分配失败,它返回 NULL

malloc 失败原因

  1. 系统内存不足:当系统可用内存不足以满足 malloc 请求的大小时,分配会失败。例如,在一个物理内存有限的系统上,连续进行大量的大内存块分配,可能导致内存耗尽。
  2. 内存碎片:随着程序不断地分配和释放内存,内存空间会变得碎片化。即使总的可用内存足够,但由于碎片的存在,可能无法找到一块连续的足够大的内存块来满足 malloc 的请求。

malloc 失败处理方式

  1. 检查返回值:在调用 malloc 后,必须立即检查其返回值是否为 NULL。如果是 NULL,则表示内存分配失败,需要采取相应的措施。
#include <stdio.h>
#include <stdlib.h>

int main() {
    // 尝试分配1000000000字节的内存
    char* buffer = (char*)malloc(1000000000);
    if (buffer == NULL) {
        perror("malloc failed");
        return 1;
    }
    // 使用分配的内存
    // ...
    free(buffer);
    return 0;
}

在上述代码中,调用 malloc 分配 1000000000 字节的内存。如果分配失败,buffer 将为 NULL,通过 perror 函数打印错误信息,然后程序以错误码 1 退出。

  1. 重试机制:在某些情况下,可以尝试多次调用 malloc,因为可能在第一次失败后,系统释放了一些内存,使得后续的分配能够成功。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    char* buffer = NULL;
    int maxTries = 5;
    int tries = 0;
    while (tries < maxTries && (buffer = (char*)malloc(1000000000)) == NULL) {
        tries++;
        sleep(1); // 等待1秒后重试
    }
    if (buffer == NULL) {
        perror("malloc failed after multiple tries");
        return 1;
    }
    // 使用分配的内存
    // ...
    free(buffer);
    return 0;
}

此代码中,程序最多尝试 5 次分配内存,每次失败后等待 1 秒再重试。如果最终还是失败,则打印错误信息并退出。

  1. 调整分配策略:如果内存分配失败,可以考虑调整分配策略。例如,减小分配的内存块大小,或者采用更高效的内存管理方式。
#include <stdio.h>
#include <stdlib.h>

int main() {
    size_t initialSize = 1000000000;
    size_t reducedSize = 500000000;
    char* buffer = (char*)malloc(initialSize);
    if (buffer == NULL) {
        buffer = (char*)malloc(reducedSize);
        if (buffer == NULL) {
            perror("malloc failed even with reduced size");
            return 1;
        }
    }
    // 使用分配的内存
    // ...
    free(buffer);
    return 0;
}

这里先尝试分配较大的内存块,如果失败,再尝试分配较小的内存块。

new 内存分配失败处理

在 C++ 中,new 是用于动态内存分配的操作符。它有两种形式:常规 new定位 new

常规 new

常规 new 用于在堆上分配内存并初始化对象。例如:

int* ptr = new int; // 分配一个 int 类型的内存空间并初始化
*ptr = 42;
delete ptr;

当使用 new 分配内存时,如果分配失败,它会抛出 std::bad_alloc 异常。

new 失败原因

  1. 系统内存不足:与 malloc 类似,当系统没有足够的可用内存来满足 new 请求时,内存分配会失败。
  2. 全局内存分配限制:在某些操作系统或运行时环境中,可能存在对全局内存分配的限制。例如,进程可能被限制在一定的虚拟地址空间内,当接近这个限制时,new 操作可能失败。

new 失败处理方式

  1. 异常处理:由于 new 会抛出 std::bad_alloc 异常,因此可以使用 try - catch 块来捕获并处理这个异常。
#include <iostream>
#include <new>

int main() {
    try {
        int* bigArray = new int[1000000000];
        // 使用分配的内存
        // ...
        delete[] bigArray;
    } catch (const std::bad_alloc& e) {
        std::cerr << "Memory allocation failed: " << e.what() << std::endl;
        return 1;
    }
    return 0;
}

在上述代码中,try 块中尝试分配一个包含 1000000000 个 int 类型元素的数组。如果分配失败,catch 块捕获 std::bad_alloc 异常,并打印错误信息,然后程序以错误码 1 退出。

  1. 使用 nothrow 版本的 new:C++ 提供了 nothrow 版本的 new,它在内存分配失败时不会抛出异常,而是返回 NULL
#include <iostream>
#include <new>

int main() {
    int* bigArray = new (std::nothrow) int[1000000000];
    if (bigArray == NULL) {
        std::cerr << "Memory allocation failed" << std::endl;
        return 1;
    }
    // 使用分配的内存
    // ...
    delete[] bigArray;
    return 0;
}

这里使用 new (std::nothrow) 分配内存,然后检查返回值是否为 NULL。如果是 NULL,则表示内存分配失败,打印错误信息并退出。

mallocnew 内存分配失败处理的比较

  1. 错误处理方式
    • malloc 通过返回 NULL 表示内存分配失败,需要手动检查返回值。这种方式比较传统,与 C 语言的错误处理风格一致。
    • new 则通过抛出 std::bad_alloc 异常来表示失败,需要使用 try - catch 块进行处理。异常处理机制使得错误处理代码可以与正常代码分离,提高了代码的可读性和可维护性。
  2. 内存初始化
    • malloc 只负责分配内存,不会对分配的内存进行初始化。如果需要初始化,需要额外调用 memset 等函数。
    • new 在分配内存后会调用对象的构造函数进行初始化,这对于 C++ 中的类对象非常重要,因为构造函数可能会执行一些必要的初始化操作,如初始化成员变量、分配内部资源等。
  3. 内存释放
    • malloc 分配的内存需要使用 free 函数释放。
    • new 分配的内存需要使用 delete 操作符释放(对于数组使用 delete[])。并且 delete 在释放内存前会调用对象的析构函数,清理对象占用的资源。
  4. 异常安全性
    • 使用 malloc 时,如果在分配内存后,初始化或其他操作过程中发生异常,需要手动释放之前分配的内存,否则会导致内存泄漏。
    • new 结合异常处理机制,在异常发生时,会自动调用析构函数释放对象占用的资源,保证了异常安全性。

示例对比:mallocnew 处理复杂对象

假设我们有一个简单的类 MyClass

class MyClass {
private:
    int data;
public:
    MyClass() : data(0) {
        std::cout << "MyClass constructor" << std::endl;
    }
    ~MyClass() {
        std::cout << "MyClass destructor" << std::endl;
    }
};
  1. 使用 malloc 分配 MyClass 对象
#include <iostream>
#include <stdlib.h>
#include <new>

int main() {
    MyClass* obj = (MyClass*)malloc(sizeof(MyClass));
    if (obj == NULL) {
        std::cerr << "Memory allocation failed" << std::endl;
        return 1;
    }
    // 手动调用构造函数
    new (obj) MyClass();
    // 使用对象
    // ...
    // 手动调用析构函数
    obj->~MyClass();
    free(obj);
    return 0;
}

在这个例子中,使用 malloc 分配内存后,需要手动调用构造函数初始化对象,使用完后手动调用析构函数并使用 free 释放内存。如果在手动调用构造函数后发生异常,需要手动调用析构函数并释放内存,否则会导致内存泄漏。

  1. 使用 new 分配 MyClass 对象
#include <iostream>
#include <new>

int main() {
    try {
        MyClass* obj = new MyClass();
        // 使用对象
        // ...
        delete obj;
    } catch (const std::bad_alloc& e) {
        std::cerr << "Memory allocation failed: " << e.what() << std::endl;
        return 1;
    }
    return 0;
}

使用 new 分配对象时,构造函数会自动调用,并且在 delete 时析构函数会自动调用。如果分配过程中发生异常,异常处理机制会保证对象资源的正确释放。

实际应用中的考虑

  1. 性能:在一些对性能要求极高的场景下,malloc 可能会更具优势,因为它相对简单,没有构造函数和析构函数的开销。但对于复杂对象,new 的自动初始化和清理功能更方便,虽然会带来一定的性能开销。
  2. 代码风格和可维护性:如果项目遵循 C++ 的面向对象编程风格,new 结合异常处理通常会使代码更清晰、易维护。而在一些与 C 代码兼容性要求较高的项目中,malloc 可能更合适。
  3. 内存管理策略:对于大型项目,需要设计合理的内存管理策略。例如,可以使用智能指针(如 std::unique_ptrstd::shared_ptr)来管理 new 分配的内存,避免手动释放导致的内存泄漏。对于 malloc,也可以封装一些内存管理函数,提供更安全和方便的内存分配和释放接口。

内存分配失败时的系统资源管理

当内存分配失败时,除了处理程序逻辑上的错误,还需要考虑系统资源的管理。

  1. 文件描述符:如果程序在内存分配失败前打开了文件描述符,应该确保在处理失败时正确关闭这些文件描述符,以避免资源泄漏。
#include <iostream>
#include <fstream>
#include <new>

int main() {
    std::ifstream file("example.txt");
    if (!file) {
        std::cerr << "Failed to open file" << std::endl;
        return 1;
    }
    try {
        int* bigArray = new int[1000000000];
        // 使用分配的内存和文件
        // ...
        delete[] bigArray;
    } catch (const std::bad_alloc& e) {
        std::cerr << "Memory allocation failed: " << e.what() << std::endl;
        file.close(); // 关闭文件描述符
        return 1;
    }
    file.close();
    return 0;
}
  1. 网络连接:如果程序建立了网络连接,在内存分配失败时,应妥善关闭网络连接,以避免占用网络资源。
#include <iostream>
#include <winsock2.h>
#include <new>

#pragma comment(lib, "ws2_32.lib")

int main() {
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
        std::cerr << "WSAStartup failed" << std::endl;
        return 1;
    }
    SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock == INVALID_SOCKET) {
        std::cerr << "Socket creation failed" << std::endl;
        WSACleanup();
        return 1;
    }
    try {
        int* bigArray = new int[1000000000];
        // 使用分配的内存和网络连接
        // ...
        delete[] bigArray;
    } catch (const std::bad_alloc& e) {
        std::cerr << "Memory allocation failed: " << e.what() << std::endl;
        closesocket(sock);
        WSACleanup();
        return 1;
    }
    closesocket(sock);
    WSACleanup();
    return 0;
}
  1. 线程资源:如果程序使用了多线程,在内存分配失败时,需要确保线程资源(如线程句柄、互斥锁等)得到正确的清理。
#include <iostream>
#include <thread>
#include <mutex>
#include <new>

std::mutex mtx;

void threadFunction() {
    std::lock_guard<std::mutex> lock(mtx);
    // 线程逻辑
}

int main() {
    std::thread th(threadFunction);
    try {
        int* bigArray = new int[1000000000];
        // 使用分配的内存
        // ...
        delete[] bigArray;
    } catch (const std::bad_alloc& e) {
        std::cerr << "Memory allocation failed: " << e.what() << std::endl;
        th.join(); // 等待线程结束
        return 1;
    }
    th.join();
    return 0;
}

内存分配失败处理的最佳实践

  1. 尽早检查:无论是使用 malloc 还是 new,都应该在分配内存后尽早检查是否成功。避免在内存分配后执行大量操作,然后才发现内存分配失败,导致前面的操作白费,并且可能难以排查错误。
  2. 日志记录:在处理内存分配失败时,记录详细的日志信息是很有帮助的。这可以帮助开发人员在调试时了解失败的具体情况,例如失败时的系统内存状态、程序的运行阶段等。
#include <iostream>
#include <fstream>
#include <new>
#include <cstdlib>

void logError(const char* message) {
    std::ofstream logFile("error.log", std::ios::app);
    if (logFile) {
        logFile << message << std::endl;
        logFile.close();
    }
}

int main() {
    try {
        int* bigArray = new int[1000000000];
        // 使用分配的内存
        // ...
        delete[] bigArray;
    } catch (const std::bad_alloc& e) {
        std::cerr << "Memory allocation failed: " << e.what() << std::endl;
        logError("Memory allocation failed in main function");
        return 1;
    }
    return 0;
}
  1. 资源清理:确保在内存分配失败时,程序占用的其他资源(如文件描述符、网络连接、线程资源等)能够得到正确的清理,以避免资源泄漏。
  2. 优雅降级:在某些情况下,当内存分配失败时,可以尝试进行优雅降级。例如,减少功能的复杂度,以适应有限的内存资源。比如图形处理程序在内存不足时,可以降低图像的分辨率进行处理。
  3. 性能优化:在考虑内存分配失败处理的同时,也要关注性能。避免在处理失败时引入过多的额外开销,影响程序的正常运行。例如,在重试内存分配时,合理设置重试次数和重试间隔,避免过度占用系统资源。

内存分配失败处理的常见错误

  1. 未检查返回值或未捕获异常:这是最常见的错误之一。忘记检查 malloc 的返回值或未使用 try - catch 块捕获 new 抛出的异常,会导致程序在内存分配失败时继续执行,可能引发未定义行为。
  2. 资源泄漏:在内存分配失败处理过程中,如果没有正确清理其他已分配的资源(如文件描述符、网络连接等),会导致资源泄漏。例如,在使用 malloc 分配内存后,手动调用构造函数初始化对象,若构造函数中打开了文件描述符,而在内存分配失败时未关闭文件描述符,就会造成资源泄漏。
  3. 错误处理逻辑复杂度过高:过于复杂的错误处理逻辑可能导致代码难以理解和维护。例如,在 try - catch 块中嵌套过多的条件判断和操作,使得错误处理代码的可读性变差。
  4. 重试策略不合理:在重试内存分配时,如果重试次数过多或重试间隔不合理,可能会导致程序长时间等待,占用过多系统资源。例如,无限重试内存分配,或者重试间隔过短,会使系统资源被大量消耗。

通过了解 mallocnew 内存分配失败的原因、处理方式,以及遵循最佳实践,避免常见错误,可以编写更健壮、可靠的 C++ 程序,有效应对内存分配失败的情况,提高程序的稳定性和性能。在实际开发中,应根据项目的具体需求和特点,选择合适的内存分配方式和失败处理策略。