一、概念介绍

内存泄漏 是指程序在动态申请(分配)了一块内存之后,失去了对这块内存的控制(即没有指针指向它),从而导致无法释放这块内存,造成内存的浪费。

形象地说,这就像你从图书馆借了一本书(申请内存),但后来你把这本书弄丢了,或者忘记借过这本书(丢失了指针)。你无法归还这本书(释放内存),图书馆的书就越变越少(可用内存逐渐耗尽)。如果这种情况持续发生,程序最终会因内存不足而崩溃。

在C++中,内存泄漏主要发生在使用 new / new[]操作符分配内存,但未能使用 delete / delete[] 正确释放的情况下。

二、常见情况

1. 直接丢失指针(最经典的情况)

分配内存后,在释放之前,指针被重新赋值或指向了别的地方,导致原来的内存地址无人知晓,无法释放。

void memoryLeak1() {
    int* ptr = new int(100); // 分配内存
    ptr = new int(200);      // 严重错误!ptr 现在指向了新内存,旧内存泄漏了!
                             // 第一次分配的 int(100) 再也无法被访问或释放。

    delete ptr; // 这只能释放第二个 new int(200)
    // 第一次分配的 int(100) 永久泄漏
}

2. 未在分支或异常情况下释放内存

代码中有多个返回路径(如 if/return, switch, 异常抛出),但只在主路径上写了 delete,在其他分支返回前忘记释放内存。

void memoryLeak2(bool error) {
    int* ptr = new int(100);

    if (error) {
        throw std::runtime_error("Something went wrong!"); // 如果抛出异常,ptr 不会被释放
        // return; // 简单的 return 也会导致同样的问题
    }

    // ... 一些可能也会抛出异常的操作 ...

    delete ptr; // 只有一切正常时才会执行到这里
}

3. 未调用对应的释放形式

使用 new[] 分配数组,却使用 delete 而不是 delete[] 来释放。这种行为是未定义行为,可能导致程序崩溃,但通常也会导致内存泄漏,因为可能只释放了数组的第一个元素,后面的元素都泄漏了。

void memoryLeak3() {
    int* array = new int[10]; // 分配了一个包含10个int的数组

    // ... 使用数组 ...

    delete array;   // 错误!应该是 delete[] array;
                    // 使用 delete 是未定义行为,几乎肯定会导致内存泄漏和程序错误。
}

规则:new对应deletenew[]对应delete[]

4. 循环引用(在使用原始指针和共享指针时都可能发生)

当两个或多个对象通过指针相互引用时,如果使用的是原始的指针,在析构时需要手动打破循环,否则会导致内存泄漏。即使是智能指针,如果使用std::shared_ptr 而不注意,也会造成循环引用。

原始指针的循环引用(依赖手动管理):

class Node {
public:
    Node* next;
    Node* prev;
    // ... 如果两个Node相互指向对方,并且没有正确断开链接,删除它们会很困难,容易泄漏。
};

std::shared_ptr 的循环引用(更隐蔽):

class BadNode {
public:
    std::shared_ptr<BadNode> next;
    std::shared_ptr<BadNode> prev;
    // ... 析构函数 ...
};

void cyclicReferenceLeak() {
    auto node1 = std::make_shared<BadNode>();
    auto node2 = std::make_shared<BadNode>();

    node1->next = node2; // node2 的引用计数变为 2
    node2->prev = node1; // node1 的引用计数变为 2

    // 函数结束,智能指针 node1 和 node2 的引用计数减为 1(而不是0)
    // 因为它们还相互引用着对方,所以对象不会被自动销毁,导致内存泄漏!
}

5. 在析构函数中未能正确释放成员指针

如果一个类的成员变量是指针,并且在构造函数中动态分配了内存,那么必须在析构函数中释放它。否则,当对象销毁时,指针成员所指向的内存就会泄漏。

class MyClass {
private:
    int* data;
public:
    MyClass() {
        data = new int[100]; // 在构造函数中分配内存
    }
    ~MyClass() {
        // 错误!忘记了 ‘delete[] data;’
        // 导致 data 指向的 100 个 int 的内存全部泄漏。
    }
    // 还需要实现拷贝构造函数和拷贝赋值运算符(三法则/五法则)
    // 否则默认的浅拷贝行为会导致双重释放或其他问题。
};

void memoryLeak4() {
    MyClass obj; // 栈上对象,析构函数会被调用
} // 但是析构函数里没写 delete[],所以 obj.data 指向的内存泄漏了

三、避免措施

1.优先使用栈内存

对于小对象和生命周期在局部范围内的对象,直接使用栈变量。栈内存会在作用域结束时自动回收。

void noLeak() {
    int value = 100; // 在栈上分配,函数结束自动回收
    std::vector<int> vec(100); // STL容器在内部管理堆内存,生命周期结束时自动释放
}

2.使用智能指针

std::unique_ptr:用于独占所有权的资源。它不能被拷贝,但可以移动。当unique_ptr 离开作用域时,它所管理的内存会自动被释放。

#include <memory>
void noLeakWithUnique() {
    auto ptr = std::make_unique<int>(100); // 无需手动 delete
    // ... 使用 ptr ...
} // ptr 离开作用域,内存自动释放

std::shared_ptr:用于共享所有权的资源。使用引用计数机制,当最后一个shared_ptr被销毁时,内存才会被释放。注意循环引用问题

void noLeakWithShared() {
    auto ptr1 = std::make_shared<int>(200);
    {
        auto ptr2 = ptr1; // 引用计数+1
        // ... 使用 ptr1 和 ptr2 ...
    } // ptr2 离开作用域,引用计数-1
} // ptr1 离开作用域,引用计数变为0,内存自动释放

3.遵循 RAII 原则:资源获取即初始化

将资源(内存、文件句柄、网络连接等)的生命周期绑定到一个对象的生命周期上。在构造函数中获取资源,在析构函数中释放资源。STL容器(如 std::vector, std::string)和智能指针都是RAII的完美例子。

总结

内存泄漏是指程序动态申请内存后,由于指针被重写、代码提前返回/异常、未正确调用释放操作、对象间循环引用或类的析构函数遗漏等原因,导致无法再释放该内存的现象。长期累积会耗尽可用内存,最终导致程序崩溃。
在现代C++中,避免内存泄漏的最佳实践是遵循RAII原则,优先使用栈对象和标准库容器,并利用std::unique_ptr、std::shared_ptr等智能指针来自动化管理资源生命周期,从而从根本上杜绝手动管理内存带来的风险。

说明本文创作过程中使用deepseek参与代码与文案的编写,不当之处敬请指出。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐