上篇聊了new/delete,有人后台说想看内存泄漏。

来了。这个话题在机器人开发里太重要了,因为机器人程序是长时间运行的——一跑就是几天、几周、甚至几个月。内存泄漏这种问题,在短时间的测试里根本发现不了,等上了产线才爆出来。

讲个真实的故事。

去年帮朋友排查一个bug。他们做AGV的,机器人跑得好好的,大概每跑6个小时就崩一次。重启之后又正常了,再过6小时又崩。

我接过去看,第一反应是内存泄漏。用valgrind跑了一下,果然——每处理一帧激光雷达数据,就泄漏4KB内存。

原因是什么呢?一个同事在解析雷达数据的时候,每帧都new了一个缓冲区,但在某个异常分支里忘了delete。

void processLidarScan(const uint8_t* raw_data, int size) {
    uint8_t* buffer = new uint8_t[size];
    
    if (size < MIN_SIZE) {
        return;  // 这里直接return了,buffer没delete!
    }
    
    // 正常处理...
    delete[] buffer;
}

size < MIN_SIZE的时候直接return了,buffer没释放。每帧泄漏一次,6小时下来内存就爆了。

修复很简单,加一行delete就行。但这种bug,在代码review的时候很难发现,因为逻辑上是"正确"的——异常数据确实应该提前返回。而且测试的时候,异常数据出现的频率不高,偶尔泄漏一点根本看不出来。

内存泄漏到底是什么?

简单说:你申请了内存,但用完之后没还回去。

操作系统给你的内存是有限的。每次new,你就少一块可用内存。如果一直new不delete,可用内存越来越少,直到系统拒绝分配,程序就崩了。

在机器人开发里,内存泄漏的后果比一般程序更严重。因为机器人程序是7x24小时运行的,内存泄漏是渐进式的——刚开始没事,跑着跑着内存越来越少,最后OOM被系统kill。

更可怕的是,内存泄漏不一定每次都崩。有时候你测试了几个小时都没事,觉得没问题就上线了。结果产线上跑了两天才崩,这时候排查起来就特别痛苦——因为你根本不知道是哪行代码泄漏的。

四种最常见的内存泄漏场景

场景一:提前return,忘了delete。 就是开头那个例子。函数里有多个return路径,某个路径上忘了释放。

void processData() {
    int* data = new int[1000];
    
    if (!validate(data)) {
        delete[] data;  // 这次没忘
        return;
    }
    
    if (!transform(data)) {
        // 忘了delete!
        return;
    }
    
    delete[] data;
}

场景二:异常抛出,析构没执行。 如果new之后、delete之前抛了异常,delete就不会被执行。

void loadData() {
    int* data = new int[1000];
    
    doSomething();  // 这里可能抛异常
    
    delete[] data;  // 异常发生时,这行不会执行
}

场景三:容器里的指针没释放。 你往vector里存了裸指针,clear的时候只清了指针,没释放指针指向的对象。

std::vector<Sensor*> sensors;
sensors.push_back(new Lidar());
sensors.push_back(new Camera());

sensors.clear();  // 只清了指针,Lidar和Camera对象没析构!

场景四:基类析构函数不是虚函数。 这是C++特有的坑。如果你通过基类指针delete一个派生类对象,但基类析构不是虚的,派生类的析构函数不会被调用。

class Sensor {
public:
    ~Sensor() {}  // 不是虚函数!
};

class Lidar : public Sensor {
public:
    ~Lidar() { /* 释放雷达资源 */ }
};

Sensor* s = new Lidar();
delete s;  // 只调用了Sensor的析构,Lidar的析构没调用!

怎么检测内存泄漏?

工具一:Valgrind。 Linux下最常用的内存检测工具。

valgrind --leak-check=full ./your_program

它会告诉你哪行代码new了内存但没delete,泄漏了多少字节。缺点是程序会跑得特别慢,大概慢10-30倍。

工具二:AddressSanitizer (ASAN)。 GCC和Clang内置的内存检测工具,编译时加个flag就行。

g++ -fsanitize=address -g your_code.cpp -o your_program

比Valgrind快很多,大概只慢2倍。而且能检测的bug类型更多——除了内存泄漏,还能检测越界访问、use-after-free等。

工具三:Visual Studio内置工具。 Windows下用Visual Studio的调试器,内置了内存诊断功能。可以在运行时查看内存使用情况的变化趋势。

工具四:代码review。 最原始但最有效的方法。每次看到new,就去找对应的delete。如果找不到,就是泄漏。

怎么避免内存泄漏?

方法一:用智能指针。 这是现代C++的标准做法。std::unique_ptr和std::shared_ptr会在离开作用域时自动释放内存。

#include <memory>

void loadData() {
    auto data = std::make_unique<int[]>(1000);
    // 离开作用域时自动释放,不用手动delete
}

方法二:用容器代替裸数组。 std::vector会自动管理内存,不用你操心。

// 不好
int* data = new int[1000];
// ...
delete[] data;

// 好
std::vector<int> data(1000);
// 离开作用域自动释放

方法三:RAII惯用法。 把资源管理绑定到对象的生命周期上。后面会专门讲。

在机器人开发里,我强烈建议用智能指针和容器。裸的new/delete只在极少数性能敏感的场景下使用。

还有一个实战技巧很多教程没提到:在机器人长运行测试中,你可以用/proc/meminfo或者top命令定时记录进程的内存占用,画一条内存使用趋势图。如果内存曲线是稳定的锯齿形(涨上去又降下来),说明正常;如果是一条缓慢上升的直线,十有八九有泄漏。我们团队之前就是用这个方法,在一个跑了72小时的压力测试里发现了泄漏点,最后定位到一个回调函数里每次都在new一个字符串对象但始终没有释放,日积月累就出了大问题。这种长时间监控的方法虽然笨,但在工程实践中特别管用。我见过不少新人只会用工具扫描,不会手动监控分析,面试时说出来也能体现你的工程素养和排查问题的能力。

给正在准备面试的你一点建议

内存泄漏在面试里经常考,特别是结合项目经验来问。

我建议你做三件事:

第一,知道内存泄漏的常见场景——提前return、异常抛出、容器里的裸指针、基类非虚析构。

第二,会用至少一种检测工具——Valgrind或ASAN。面试的时候说"我用过Valgrind检测内存泄漏",比只说"我知道内存泄漏"有说服力得多。

第三,知道怎么避免——智能指针、容器、RAII。

这里分享一个排查内存泄漏的实战经验:AddressSanitizer(ASAN)比Valgrind更快,适合在开发阶段使用。编译时加上-fsanitize=address -g选项就行,运行时ASAN会自动检测内存泄漏并精确报告泄漏的代码行号。在CI流程里集成ASAN,每次提交代码都自动跑一遍检测,泄漏问题当场就能抓住。而且ASAN只慢2倍左右,Valgrind慢10到20倍,所以ASAN更适合日常高频使用。

面试的时候,如果你能结合项目经验讲一个你排查过的内存泄漏bug,面试官会对你印象深刻。


如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。 你的支持是我持续更新的最大动力。

「机器人软件开发面试·从入门到精通」连载系列 上一篇:第13篇 动态内存分配new/delete——堆内存管理的入门 下一篇预告:第15篇 引用的本质——和指针到底什么区别?

有任何问题欢迎评论区留言,我会尽量回复。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐