前面把面向对象设计聊完了——设计模式、SOLID原则。从今天开始进入一个新的板块:内存管理。

这块内容在面试里出现的频率非常高。指针、引用、智能指针、内存泄漏——这些话题的底层都跟内存模型有关。不理解内存分布,很多概念只能死记硬背。

先讲个尴尬的事。

面试的时候,面试官指着一段代码问我:"这个变量存在哪里?"

int globalVar = 100;
static int staticVar = 200;

void someFunction() {
    int localVar = 300;
    static int localStatic = 400;
    int* heapVar = new int(500);
}

我当时脑子一抽,把localStatic说成了栈上。面试官笑了笑说"你再想想?"——那个瞬间,真想找个地缝钻进去。

今天就把内存模型彻底讲清楚,免得跟我一样翻车。

进程内存的五大区域

一个C++程序跑起来之后,内存大致分成这几个区域:

代码区(Text Segment):存放编译后的机器指令。只读的,你不能去修改自己的程序代码。如果你试图写代码区的内容,操作系统会直接给你发个段错误信号。

全局区/数据区(Data/BSS Segment):全局变量和静态变量住这里。初始化过的放Data段,没初始化的(值为0)放BSS段。程序启动时分配,程序结束时释放。

int g_initialized = 42;     // Data段,有初始值
int g_zero;                  // BSS段,默认零初始化
static int s_value = 100;   // Data段,文件作用域的静态变量

void foo() {
    static int s_local = 50; // 也在Data段!虽然写在函数里
}

注意函数里的static变量,虽然作用域在函数内部,但它的存储位置在全局区,不在栈上。这就是我之前答错的原因——static变量的生命周期是整个程序运行期,栈上的变量可做不到。

堆区(Heap):用new/malloc手动分配的内存都在堆上。这是最灵活也最容易出问题的区域。你new出来的东西,得自己delete。忘了就是内存泄漏,delete两次就是未定义行为。

void example() {
    int* p = new int(42);    // 堆上分配
    // ... 使用p
    delete p;                 // 必须手动释放
}

堆的内存管理由操作系统负责,分配和释放都有一定开销。频繁new/delete小块内存,性能会受影响。后面讲内存池的时候会详细说这个。

栈区(Stack):函数调用时自动分配。局部变量、函数参数、返回地址都在栈上。函数返回时自动释放,不需要你操心。

void bar(int param) {         // param在栈上
    int x = 10;               // x在栈上
    double arr[100];          // arr在栈上,100个double
    // 函数返回时,param、x、arr全部自动释放
}

栈的特点是快——分配和释放就是移动一下栈指针,几乎零开销。但栈的空间有限,Linux下默认一般是8MB。你要是搞个大的递归或者在栈上分配太大的数组,就会栈溢出。

在机器人开发里,路径规划的DFS/BFS算法如果递归太深,或者处理大规模地图时在栈上开了大数组,都可能导致栈溢出。遇到这种情况,要么改用堆分配(用vector代替原生数组),要么用ulimit -s调大栈空间。

各区域的生命周期对比

用一张表来理清各区域的生命周期:

区域        分配时机          释放时机          大小限制
代码区      编译时            程序结束          固定
全局区      程序启动前        程序结束          有限
堆          运行时(new)       手动(delete)      受限于物理内存
栈          函数调用时        函数返回时        默认8MB左右

面试经常考的一个点是:全局变量和static变量的区别。存储位置上,它们都在全局区。区别在于作用域——全局变量在整个文件可见(或者通过extern跨文件可见),而函数内的static变量只在函数内可见。但不管哪种,生命周期都是整个程序运行期。

机器人开发中的内存问题

在嵌入式机器人上,内存限制比PC严格得多。一个跑在STM32上的控制程序,可能总共就几百KB的RAM。这时候你必须清楚每个变量放在哪里。

// 嵌入式场景:内存非常紧张
const int MAX_WAYPOINTS = 50;    // 常量,放代码区(只读)
float waypointBuffer[50][3];     // 全局数组,占600字节,放全局区

class PathPlanner {
    static int planCount;         // 静态成员,全局区
    float localPath_[20][3];     // 成员变量,跟随对象——对象在堆上它就在堆上
};

ROS2的程序在PC上跑,内存宽裕很多,但也不是随便浪费的。比如点云数据,一帧可能有几十万个点。如果你每帧都new一块内存来存,GC都来不及(C++没有GC,所以你得手动管理)。这就是为什么ROS2的消息传递大量使用智能指针——把堆内存的生命周期交给shared_ptr管理。

还有一个容易踩的坑:在回调函数里返回局部变量的指针。

int* getData() {
    int data[100];
    // 填充数据...
    return data;  // 大错特错!data在栈上,函数返回就释放了
}

返回的是一个已经释放的栈地址,读到的是垃圾数据,写的话会破坏其他数据。正确做法是要么用vector返回,要么用new在堆上分配(记得释放)。

面试中的经典考点

几个高频问题:

"栈和堆的区别是什么?"栈是编译器自动管理的,速度快但空间有限;堆是程序员手动管理的,灵活但有泄漏风险。栈是连续的内存空间,堆是不连续的(通过空闲链表管理)。

"static变量存在哪里?"全局区(Data/BSS段),不在栈上。即使写在函数内部,它也只是作用域在函数内,存储位置不变。

"new和malloc有什么区别?"new是C++的操作符,会调用构造函数;malloc是C的库函数,只分配原始内存,不调用构造。对应的,delete会调用析构函数,free不会。在C++里应该优先用new/delete。

"内存泄漏怎么排查?"工具层面:Valgrind、AddressSanitizer(ASAN)。代码层面:智能指针代替裸指针,RAII管理资源。工程层面:code review时重点关注new/delete配对。

给正在准备面试的你

内存模型是C++面试的必考内容。你不需要把每个区域的地址范围背下来(那没意义),但你必须清楚:一个变量声明之后,它存在哪里,什么时候被分配,什么时候被释放。

这个认知会贯穿后面很多话题——智能指针、RAII、移动语义、内存池,全都建立在内存模型的基础上。

分享一个面试加分技巧:面试时如果被问到内存模型,可以主动提到栈帧的概念。每次函数调用都会在栈上创建一个栈帧,包含局部变量、参数、以及返回地址。理解栈帧有助于你明白为什么递归调用太深会导致栈溢出。在机器人开发里,导航算法中的递归搜索就曾经导致过栈溢出的问题,最后改成迭代方式加显式栈才解决。用GDB调试时,bt命令可以查看调用栈,看到每一层函数的栈帧和局部变量,非常实用的调试手段,建议大家掌握。

再聊一个面试中容易拉开差距的话题:怎么用内存布局知识优化程序性能。在机器人开发里,结构体的成员变量顺序会影响内存占用——因为对齐的原因,把大类型放前面、小类型放后面可以减少padding。比如一个结构体里有double、bool、int、char,按double, int, bool, char排列比double, bool, int, char更紧凑。你可以用sizeof和offsetof来验证实际的内存布局。另外在Linux下用/proc/self/maps可以查看进程的内存映射,清楚地看到代码段、数据段、堆、栈的分布范围。面试时如果你能提到用offsetof验证结构体布局、用maps查看进程内存分布,面试官会觉得你不仅懂理论,还会用工具验证。

下篇聊内存对齐——一个容易被忽略但面试会考的性能细节。为什么struct的大小经常比你想象的大?这个知识点跟今天讲的内存模型直接相关。


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

「机器人软件开发面试·从入门到精通」连载系列 上一篇:第49篇 SOLID原则——面试官问"你怎么设计类的"评判标准 下一篇预告:第51篇 内存对齐——一个容易被忽略但面试会考的性能细节

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

Logo

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

更多推荐