简介:本文系统梳理 C++ 面试中的高频考点,涵盖指针与引用、new/delete 与 malloc/free 的本质差异、内存泄漏的排查思路,以及三类智能指针的适用场景。同时深入解析继承与多态的实现原理,帮助读者构建完整的知识体系,从容应对技术面试与工程实践。
C++ 作为一门兼顾高性能与抽象能力的系统级语言,在面试中往往考察候选人对底层机制的理解深度。本文围绕指针与引用、动态内存管理、智能指针以及面向对象三大特性展开,力求把常见问题讲透,而不是停留在背诵层面。
指针和引用常被放在一起比较,但二者在语义上并不等价。
nullptr),引用在定义时必须绑定到一个有效对象,且不存在“空引用”这一合法状态。int**),引用没有多级形式。选择原则很清晰:当目标对象一定存在且不需要改绑时用引用,接口更安全;当需要表达“可能没有”、需要遍历或需要重新指向时用指针。现代 C++ 中,裸指针更多承担“观察者”角色,所有权应交由智能指针表达。
这是面试中出现频率极高的对比题,核心差异可以从以下几个维度展开。
1. 所属层次不同
malloc/free 是 C 标准库函数,只负责从堆上申请和归还原始内存;new/delete 是 C++ 运算符,除了内存分配,还负责对象的构造与析构。
2. 生命周期动作不同
// new:先分配内存,再调用构造函数std::string* s = new std::string("hello");// delete:先调用析构函数,再释放内存delete s;// malloc:只分配原始内存,不调用构造函数void* raw = malloc(sizeof(std::string));// 必须手动定位构造,否则对象未初始化new (raw) std::string("hello");// 必须手动调用析构,再 freestatic_cast<std::string*>(raw)->~std::string();free(raw);
上述对比清楚说明:new 分配的是“对象”,malloc 分配的是“字节”。
3. 返回类型与参数
malloc 需要显式指定字节数,返回 void*,在 C++ 中必须强制转换;new 根据类型自动计算大小,返回正确类型的指针,无需转换。
4. 失败行为
malloc 失败返回 NULL,需要调用方检查;new 失败默认抛出 std::bad_alloc 异常,也可以通过 new(std::nothrow) 形式改为返回空指针。
5. 可扩展性
new 和 delete 可以被重载,用于定制内存池或追踪分配;malloc/free 是库函数,不能重载。此外,new 有数组形式 new[],必须与 delete[] 配对,混用会导致未定义行为。
内存泄漏的本质是“申请了但未释放”。在代码层面,可以先用低成本手段排查。
1. 代码审查
最容易出问题的是类的构造中申请资源、析构中却忘记释放。检查每一个 new 是否有对应 delete,每一个 new[] 是否配对 delete[],是第一步。
2. 日志计数法
在分配与释放处打印日志,统计申请次数与释放次数是否一致:
void* operator new(size_t size) {void* p = malloc(size);log("alloc %p, size=%zu", p, size);return p;}
运行后对比两侧日志,可以快速判断是否存在净泄漏。
3. 静态分析工具
现代编译器与静态分析器能在编译期发现部分资源未释放、重复释放等问题,建议纳入持续集成流程。
4. 运行时检测工具
业界常见的动态检测方案(如基于插桩的内存检查器)可以在程序退出时输出泄漏点的调用栈、行号与内存大小,适合集成到调试构建中。另一类工具通过内存占用曲线与快照对比,观察哪些对象持续不释放,适合长时间运行的服务。跨平台场景下,也存在覆盖泄漏、越界、重复释放等问题的成熟检测工具。使用方式通常为在命令行中附加检测参数运行目标程序,例如以完整泄漏检查模式启动。
裸指针容易遗漏释放,现代 C++ 用智能指针表达所有权。
1. unique_ptr:独占所有权
同一时刻只有一个 unique_ptr 持有对象,不能拷贝,只能移动。生命周期结束时自动析构释放,零额外开销,是默认首选。
auto p = std::make_unique<Widget>();auto q = std::move(p); // 所有权转移,p 变为空
2. shared_ptr:共享所有权
内部维护引用计数,多个指针共享同一对象,计数归零时释放。适合对象被多方共同持有的场景。注意计数操作存在原子开销,且要警惕循环引用。
3. weak_ptr:弱引用
不增加引用计数,专门用于打破 shared_ptr 之间的循环引用。它不能直接解引用,需要先调用 lock() 提升为 shared_ptr,并检查是否为空。
std::shared_ptr<Node> a = std::make_shared<Node>();std::shared_ptr<Node> b = std::make_shared<Node>();a->peer = b;b->peer = a; // 循环引用,计数永不归零// 改为 weak_ptr 即可打破环
选择原则:优先 unique_ptr,需要共享时用 shared_ptr,需要观察但不持有生命周期时用 weak_ptr。
继承允许在复用已有类能力的基础上扩展新功能,体现“is-a”关系,减少重复代码。
多态则让同一接口在不同对象上表现出不同行为。其核心机制是:用父类指针或引用指向子类实例,调用虚函数时,实际执行的是子类重写后的版本。这依赖虚函数表在运行期完成动态绑定:
struct Base {virtual void run() { /* 默认实现 */ }virtual ~Base() = default;};struct Derived : Base {void run() override { /* 子类实现 */ }};void invoke(Base& b) { b.run(); } // 运行期决定调用哪个版本
需要注意两点:一是基类析构函数应声明为虚函数,否则通过基类指针删除子类对象会导致资源泄漏;二是多态带来灵活性,但也引入一次间接调用开销,在性能敏感路径上需权衡。
面试考察的从来不是孤立的知识点,而是候选人能否把语言机制、内存模型与工程实践串成一条线。理解指针与引用的语义边界、分清 new 与 malloc 的职责、掌握智能指针的所有权表达、并能用工具与日志定位泄漏,构成了 C++ 工程师的基本功。把这些内容内化为可解释、可推理的知识,才是应对面试与真实项目的关键。