C++ 面试高频 10 题深度解析:从 new/delete 到虚函数表(附 5 个易错点)
2026/7/13 11:11:50 网站建设 项目流程

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/deletemalloc/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 | +-------------------+

性能影响

  1. 每次虚函数调用需要额外解引用操作(通过vptr找到vtable)
  2. 对象体积增大(每个对象需存储vptr)
  3. 编译器难以内联优化虚函数

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_type

SFINAE 技术示例

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++ 的最佳实践

  1. 资源管理

    • 优先使用智能指针而非原始指针
    • 使用 RAII 封装资源(文件、锁等)
  2. 类型系统

    • 用 enum class 替代传统 enum
    • 使用 nullptr 替代 NULL
  3. 性能优化

    • 移动语义减少不必要的拷贝
    • constexpr 实现编译期计算
  4. 多线程

    • 使用 std::atomic 进行无锁编程
    • 优先选择 std::async 而非直接创建线程

易错点总结

  1. 在构造函数中调用虚函数(不会多态)
  2. 忽略异常规格说明(noexcept 的影响)
  3. 错误估计 shared_ptr 的线程安全性(引用计数原子,对象访问非原子)
  4. 滥用 dynamic_cast(性能开销大)
  5. 忽略移动操作的 noexcept 声明(影响标准库行为)

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询