C++ 面试高频 10 题深度解析:从 new/delete 到虚函数表(附 5 个易错点)
1. 内存管理:new/delete 与 malloc/free 的底层差异
核心区别在于 new/delete 是 C++ 运算符,而 malloc/free 是 C 标准库函数。当处理非内部数据类型时,new 会调用构造函数完成对象初始化,delete 会调用析构函数进行资源清理,而 malloc/free 仅进行原始内存分配和释放。
典型易错场景:
class MyClass { public: MyClass() { buffer = new char[100]; } ~MyClass() { delete[] buffer; } private: char* buffer; }; // 错误示例1:混用内存管理方式 MyClass* obj = (MyClass*)malloc(sizeof(MyClass)); // 构造函数未被调用 free(obj); // 析构函数未被调用,内存泄漏 // 错误示例2:数组与非数组混用 MyClass* arr = new MyClass[10]; delete arr; // 应使用 delete[],否则仅第一个元素析构深度对比表:
| 特性 | new/delete | malloc/free |
|---|---|---|
| 语言层面 | C++ 运算符 | C 库函数 |
| 构造函数/析构函数 | 自动调用 | 不调用 |
| 内存计算 | 自动计算大小 | 需手动计算 |
| 失败处理 | 抛出 bad_alloc | 返回 NULL |
| 重载可能性 | 支持运算符重载 | 不可重载 |
| 类型安全 | 是 | 否 |
关键提示:对于自定义类类型,必须使用 new/delete 以保证构造和析构的正确执行。内部数据类型虽可混用,但会降低代码可读性。
2. 虚函数实现机制与多态成本
虚函数通过**虚函数表(vtable)**实现动态绑定。每个含虚函数的类拥有自己的 vtable,其中按声明顺序存储虚函数指针。对象内存布局首部包含指向 vtable 的指针(vptr),运行时通过此指针找到实际调用的函数。
典型内存布局:
class Base { public: virtual void func1() {} virtual void func2() {} int data; }; class Derived : public Base { public: void func1() override {} virtual void func3() {} };对应的内存结构:
Derived 对象内存布局: +-------------------+ | vptr | -> Derived的vtable +-------------------+ | Base::data | +-------------------+ | Derived新增成员 | +-------------------+ Derived的vtable: +-------------------+ | &Derived::func1 | +-------------------+ | &Base::func2 | +-------------------+ | &Derived::func3 | +-------------------+性能影响:
- 每次虚函数调用需要额外解引用操作(通过vptr找到vtable)
- 对象体积增大(每个对象需存储vptr)
- 编译器难以内联优化虚函数
3. const 关键字的全方位应用
const 的正确使用能显著提升代码健壮性,主要应用场景包括:
常量定义:
const int MAX_SIZE = 1024; // 替代#define函数修饰:
// 参数保护 void Print(const std::string& str); // 返回值保护 const char* GetName(); // 成员函数承诺不修改对象状态 int GetValue() const;指针与const的组合:
const char* p1; // 指向常量内容 char* const p2; // 常量指针 const char* const p3; // 双重const易错点分析:
const int* p = new int(10); *p = 20; // 编译错误:不能修改const内容 int* const q = new int(20); q = nullptr; // 编译错误:不能修改指针本身 *q = 30; // 允许修改指向内容4. 深度拷贝与浅拷贝的陷阱
当类包含指针成员时,默认的拷贝构造函数和赋值运算符执行浅拷贝,这会导致双重释放问题:
class String { public: String(const char* str = nullptr) { if (str) { data = new char[strlen(str)+1]; strcpy(data, str); } else data = nullptr; } ~String() { delete[] data; } // 问题根源:没有自定义拷贝构造 private: char* data; }; String s1("hello"); String s2 = s1; // 浅拷贝,两个对象指向同一内存 // 析构时同一内存被释放两次!正确实现:
// 拷贝构造函数 String(const String& other) { if (other.data) { data = new char[strlen(other.data)+1]; strcpy(data, other.data); } else data = nullptr; } // 赋值运算符 String& operator=(const String& rhs) { if (this != &rhs) { delete[] data; // 释放原有资源 if (rhs.data) { data = new char[strlen(rhs.data)+1]; strcpy(data, rhs.data); } else data = nullptr; } return *this; }5. 虚函数表的高级话题
多重继承下的vtable:
class Base1 { virtual void f1(); }; class Base2 { virtual void f2(); }; class Derived : public Base1, public Base2 { void f1() override {} void f2() override {} }; Derived d; Base2* pb2 = &d; // 需要调整指针位置此时对象内存包含两个vptr:
+-------------------+ | vptr (Base1) | +-------------------+ | Base1 members | +-------------------+ | vptr (Base2) | +-------------------+ | Base2 members | +-------------------+ | Derived members | +-------------------+typeid 和 dynamic_cast 的实现:
- 依赖 vtable 中的 RTTI(运行时类型信息)
- 会增加可执行文件体积(约10-20%)
6. 智能指针的工程实践
三种智能指针对比:
| 类型 | 所有权策略 | 线程安全 | 循环引用 | 典型用途 |
|---|---|---|---|---|
| unique_ptr | 独占所有权 | 否 | 无 | 替代原始指针 |
| shared_ptr | 共享所有权 | 是 | 可能 | 多对象共享资源 |
| weak_ptr | 不拥有所有权 | 是 | 解决循环 | 打破循环引用 |
循环引用示例:
struct Node { shared_ptr<Node> next; weak_ptr<Node> prev; // 使用weak_ptr打破循环 }; auto n1 = make_shared<Node>(); auto n2 = make_shared<Node>(); n1->next = n2; n2->prev = n1; // 安全,不会导致引用计数增加7. 移动语义与完美转发
右值引用(&&)实现了资源的高效转移:
class Vector { public: // 移动构造函数 Vector(Vector&& other) noexcept : data(other.data), size(other.size) { other.data = nullptr; // 确保源对象处于可析构状态 } // 移动赋值运算符 Vector& operator=(Vector&& rhs) noexcept { if (this != &rhs) { delete[] data; data = rhs.data; size = rhs.size; rhs.data = nullptr; } return *this; } private: int* data; size_t size; };完美转发保持参数原始类型:
template<typename... Args> void emplace_back(Args&&... args) { // std::forward保持参数的左值/右值特性 new (end++) T(std::forward<Args>(args)...); }8. 类型推导与模板元编程
auto 和 decltype 的区别:
const int x = 42; auto y = x; // y 是 int(忽略顶层const) decltype(x) z = x; // z 是 const int std::vector<int> vec; auto size = vec.size(); // 正确推导size_typeSFINAE 技术示例:
template<typename T> auto print(const T& val) -> decltype(std::cout << val, void()) { std::cout << val; } template<typename T> void print(...) { static_assert(false, "Type not printable"); }9. 异常安全的代码设计
基本保证:确保异常发生时程序处于有效状态强保证:要么操作完全成功,要么状态完全不改变不抛保证:承诺不抛出异常
示例实现:
class File { public: void write(const std::string& content) { std::string newContent = buffer; newContent += content; // 先在副本上操作 FILE* f = fopen(path, "w"); if (!f) throw std::runtime_error("Open failed"); if (fputs(newContent.c_str(), f) == EOF) { fclose(f); throw std::runtime_error("Write failed"); } fclose(f); buffer = std::move(newContent); // 最后更新状态 } private: std::string path; std::string buffer; };10. 现代 C++ 的最佳实践
资源管理:
- 优先使用智能指针而非原始指针
- 使用 RAII 封装资源(文件、锁等)
类型系统:
- 用 enum class 替代传统 enum
- 使用 nullptr 替代 NULL
性能优化:
- 移动语义减少不必要的拷贝
- constexpr 实现编译期计算
多线程:
- 使用 std::atomic 进行无锁编程
- 优先选择 std::async 而非直接创建线程
易错点总结:
- 在构造函数中调用虚函数(不会多态)
- 忽略异常规格说明(noexcept 的影响)
- 错误估计 shared_ptr 的线程安全性(引用计数原子,对象访问非原子)
- 滥用 dynamic_cast(性能开销大)
- 忽略移动操作的 noexcept 声明(影响标准库行为)