面灵AI→

华为 OD 面试:机考 400 分,C++ 岗一面二面全流程

轮次
一面+二面
时间
2026-09
来源
牛客网

《面试题目》

  1. 机考情况说明:机考 400 分,原本是 Java 方向,机考用的 C++,被告知只能面 C++ 岗。
  2. 资面:问一下基本情况。
  3. 一面(约 30 分钟):自我介绍。
  4. 了解 C++ 吗?
  5. C++ 的一些基本关键字:const、static。
  6. C++ 指针、内存管理的一大堆问题。
  7. 机考题目讲解。
  8. 手撕:一道比较中等的模拟题,滑窗优化、分组循环。
  9. 二面(近两小时):自我介绍。
  10. 了解 HashMap 吗?原始容量多少、扩容因子多少、红黑树是什么?
  11. 手撕:最长无重复子串(先用 map + 滑窗解决,追问能不能只用一个 for)。
  12. static 有哪些用处,会用到哪些地方?
  13. 了解析构函数吗?为什么要在里面加上 virtual?
  14. 对象和引用。
  15. C++ 和 Java 有什么区别?
  16. C++ 内存分区?JVM 内存分区?
  17. Java 的 == 和 equals 有什么区别?new String(x) 的判断?
  18. 智能指针。
  19. 项目拷打:特别是网络问题底层,问到答不上来为止。
  20. SQL 优化。
  21. 数据库索引,怎么用 explain?
  22. 缓存相关的问题。
  23. Linux 指令(查端口、内存占用、网络)。
  24. git 命令。
  25. 工作中 vibe coding 会不会把数据放到提示词里?会不会给 AI 提供数据权限?
  26. 机考题目讲解:有一道 BFS,要求具体讲讲。
  27. 工作中遇到最难的问题?
  28. 遇到的最难的 bug,怎么定位、怎么解决?
  29. 简历上一个项目:怎么设计、怎么合作、合作期间有什么冲突、怎么解决?
  30. 个人发展展望。
  31. 反问:有什么需要提升的?

《参考解析》

HashMap 的底层结构、容量与扩容

JDK 8 的 HashMap 是「数组 + 链表 + 红黑树」:table 是 Node[],默认初始容量 16(第一次 put 时才分配,构造时只记 threshold),默认负载因子 0.75,size > capacity * 0.75 时扩容为两倍。哈希扰动是 h = key.hashCode() ^ (h >>> 16),把高位异或到低位,减少只用低位取模时的碰撞;下标是 (n - 1) & hash,这也是容量必须是 2 的幂的原因——位运算代替取模,且扩容时元素要么留在原下标、要么移动到「原下标 + 旧容量」,不用重新算哈希。链表长度 ≥ 8 且数组长度 ≥ 64 时树化成红黑树,树节点数退化到 ≤ 6 时转回链表,中间留 7 做缓冲避免反复转换。红黑树是自平衡二叉搜索树,通过颜色约束(根黑、红节点子节点必黑、任一节点到叶子的黑高相同)把树高压在 O(log n),查询从链表的最坏 O(n) 降到 O(log n)。线程不安全,并发写可能丢数据或形成环(JDK 7 的头插法扩容),并发场景用 ConcurrentHashMap(JDK 8 用 CAS + synchronized 锁桶头)。

析构函数为什么要声明为 virtual

一句话:为了让「通过基类指针删除派生类对象」时,派生类的析构函数能被调用到。如果基类析构不是虚函数,delete basePtr; 只会静态绑定到基类的析构,派生类自己申请的资源(堆内存、文件句柄、锁)不会释放,行为是未定义的——实践中表现为内存泄漏。

class Base { public: virtual ~Base() = default; };
class Derived : public Base { int* p = new int[100]; ~Derived() { delete[] p; } };
Base* b = new Derived();
delete b;   // 基类析构是 virtual,先 ~Derived 再 ~Base,p 被释放

细节:析构顺序是「先派生类后基类」,和构造完全相反;一旦基类析构是 virtual,编译器会为类生成虚函数表指针(对象多一个 vptr 的开销,通常 8 字节)。反过来,如果这个类不打算被继承(或者不打算通过基类指针 delete),就不要写 virtual 析构——像 std::string、std::vector 都没有虚析构,就是为了省这个开销。工程上的做法是给多态基类写 virtual ~Base() = default;,或者干脆把基类析构声明为 protected + 非虚,禁止通过基类指针 delete。

C++ 和 Java 的主要区别

可以从几条线答:运行方式——C++ 编译成机器码直接跑(有 AOT 优化、无运行时托管),Java 编译成字节码由 JVM 解释/JIT 执行,可以跨平台「一次编写到处运行」;内存管理——C++ 手动 new/delete,靠 RAII、智能指针(unique_ptr/shared_ptr/weak_ptr)和「谁申请谁释放」的约定,Java 全靠 GC(分代收集、多种收集器),不用管释放但会有 STW 停顿;对象模型——C++ 支持多重继承、模板(编译期泛型、零开销抽象)、运算符重载、栈上对象和值语义(拷贝构造/移动语义),Java 只有单继承 + 接口、泛型是类型擦除(运行期看不到泛型参数)、对象都在堆上、只有引用语义;内存布局——C++ 对象可以直接按字节布局(sizeof、内存对齐、reinterpret_cast),能和 C 互操作,Java 有对象头、padding、指针压缩,布局由 JVM 决定;异常——C++ 有 noexcept、异常规格放松、还有不抛异常的 std::error_code 风格,Java 区分受检异常/非受检异常;生态——C++ 是系统编程/游戏/嵌入式/高频交易的主场,Java 是企业级后端 + 大数据的主场。面试里如果能补一句「C++ 用 RAII 把资源生命周期绑定到对象生命周期,Java 用 try-with-resources + GC 达到类似效果」,会显得理解更深。

C++ 内存分区 vs JVM 内存分区

C++ 程序的内存分区:栈(函数栈帧、局部变量、返回地址,自动管理,大小受限,溢出即 SIGSEGV)、堆(new/malloc 分配,手动或智能指针管理)、全局/静态存储区(.data 已初始化、.bss 未初始化,程序启动到结束都在)、常量区(字符串字面量、const 全局量,通常只读,写入会崩)、代码区(只读可执行的机器指令)。注意「自由存储区」和堆在标准里是两回事:new 分配在自由存储区,malloc 在堆上,只不过主流实现里两者共用同一块内存。

JVM 的分区按「线程私有 / 线程共享」划:线程私有的程序计数器(记录当前字节码行号,唯一不会 OOM 的区域)、虚拟机栈(每方法一个栈帧:局部变量表、操作数栈、动态链接、方法出口,-Xss 控制,深度溢出抛 StackOverflowError)、本地方法栈;线程共享的堆(对象实例与数组,GC 主战场,-Xms/-Xmx)、方法区(类元信息、运行时常量池,JDK 8 起用本地内存实现的元空间,-XX:MaxMetaspaceSize);此外还有不属于运行时数据区的直接内存(DirectByteBuffer,受 -XX:MaxDirectMemorySize 和物理内存限制)。一个容易加分的对照:C++ 的「栈上对象」在 Java 里不存在(逃逸分析后的标量替换只是编译器优化),C++ 的「常量区/全局区」在 JVM 里对应方法区中的常量池与静态变量。

static 关键字在 C++ 和 Java 里的用法

C++ 中 static 有三层含义:修饰全局变量/函数时表示内部链接,只在当前编译单元可见(替代匿名 namespace 的更老写法);修饰类的成员变量/成员函数时表示属于类而不属于对象,静态成员变量要在类外定义分配存储(C++17 起可用 inline static 就地初始化),静态成员函数没有 this 指针,不能访问非静态成员;修饰函数内局部变量时表示静态存储期,只初始化一次、函数退出后仍存在(C++11 起局部静态变量的初始化是线程安全的,常见于 Meyers 单例)。

Java 中 static 表示「属于类」:静态变量(类加载的准备阶段分配内存并给零值,初始化阶段赋真值)、静态方法(没有 this,不能直接访问实例成员)、静态代码块(类初始化时执行一次)、静态内部类(不持有外部类引用,常用于实现单例)、静态导入。两者最本质的一致点是「与对象实例解绑、生命周期跟随类/程序」,差异在于 Java 的静态成员归 Class 对象所有、可以被反射访问,而 C++ 的静态成员就是一块全局存储,且多了一个「内部链接」的语义。

最长无重复子串:滑窗与「只用一个 for」的写法

标准滑窗:右指针扩张,遇到重复字符就把左指针跳到「上次出现位置 + 1」,同时更新答案。

public int lengthOfLongestSubstring(String s) {
    int[] last = new int[128];          // 字符上次出现的下标
    Arrays.fill(last, -1);
    int ans = 0, left = 0;
    for (int right = 0; right < s.length(); right++) {
        char c = s.charAt(right);
        if (last[c] >= left) left = last[c] + 1;   // 只在窗口内才收缩
        last[c] = right;
        ans = Math.max(ans, right - left + 1);
    }
    return ans;
}

面试官追问的「只用一个 for」,指的就是上面这种用数组记录下标、左指针直接跳的写法:内层 while 被省掉了,整体仍是 O(n),只是要注意 last[c] >= left 这个判断——必须保证只在「上次出现的位置还在当前窗口内」时才移动 left,否则 left 会倒退。用 HashMap<Character,Integer> 版本同理,只是常数更大;字符集固定(ASCII 128)时用数组最快。如果题目要求返回子串本身,记录 start = left、len = ans 再 substring 即可。