字节跳动面试流程笔记
嵌入式软开 一面
日期: 2025 年 8 月 1 日
职位: 嵌入式软开
一、整体流程与时长
- 总时长:约 45 分钟
- 面试形式:视频会议
阶段 | 时长(分钟) | 内容概览 |
---|---|---|
一、简历 & 项目深挖 | 20 | 自我介绍 → 项目背景 → 细节追问 |
二、八股问答(概念题) | 10 | C++多态,Linux内存,RTOS线程,堆栈区别等 |
三、手写代码(算法题) | 20 | 寻找下一个更大的数 (ACM)自己写所有 |
二、阶段一:简历 & 项目深挖(20 分钟)
-
自我介绍
- 按照自己的实际情况答
-
工作经历的内容
- 重点介绍一段工作的具体内容
- 明确你的角色、职责范围、以及成功
-
技术细节追问
- 相关实现细节,但是不是垂直方向,很多面试官不太懂,没有细节追问
三、阶段二:八股问答(概念题,10 分钟)
1.虚函数和纯虚函数的区别:
-
虚函数适用于“有默认行为,但允许派生类覆盖”的场景;
-
纯虚函数适用于“定义接口规范,强制派生类必须提供具体实现”的场景。
纯虚函数必须在派生类实现
#include <iostream>
class Animal {
public:
virtual void speak() { // 虚函数
std::cout << "Animal sound" << std::endl;
}
virtual void move() = 0; // 纯虚函数
};
// 派生类必须实现 move()
class Dog : public Animal {
public:
void speak() override { // 重写虚函数
std::cout << "Woof!" << std::endl;
}
void move() override { // 实现纯虚函数
std::cout << "Dog runs" << std::endl;
}
};
int main() {
// Animal a; // 错误:Animal 是抽象类,包含纯虚函数 move()
Dog d;
Animal* p = &d;
p->speak(); // 调用 Dog::speak() —— 多态
p->move(); // 调用 Dog::move()
return 0;
}
Tip:抽象类(abstract class)是一种包含纯虚函数(pure virtual function)的类,用来定义接口规范,不能被实例化,只能被继承并由派生类实现其所有纯虚函数后才能实例化。
2.堆和栈的定义:
我是举例说明的,用的链表,当然用下面的更简单:
void foo() {
int a = 10; // 在栈上分配
int* p = new int[1000]; // 在堆上分配
// ...
delete[] p; // 释放堆内存
} // 函数返回时,a 自动出栈
-
栈——快速、自动、生命周期短,但空间有限;
-
堆——灵活、空间大,但需要手动或 GC 管理,分配回收开销大。
3.函数定义的struct是在堆还是栈:
(应该是这个问题,早上面试刚睡醒有点懵,不知道是不是这个,按这个回答的)
分配方式 | 示例语法 | 存储位置 | 管理 |
---|---|---|---|
自动(栈) | Point p; |
栈 | 编译器自动 |
动态(堆) | new Point / malloc |
堆 | 程序员/运行时 |
静态(数据段) | static Point s; |
数据段(BSS/RO) | 程序开始/结束 |
反思:当时问题一连串,有点多,也不知道回答对了没。
4.RTOS线程:
这个我很久远了,记不清楚了,没有回答上来细节,以下是答案:
---
### RTOS 中的线程(Task)
1. **基本概念**
* 在大多数 RTOS(如 FreeRTOS、ThreadX、μC/OS-II)里,线程(也常称为 Task)是系统调度的最小单位。
* 每个线程拥有自己独立的**栈空间**、**程序计数器(PC)**、**寄存器上下文**和若干**线程局部变量**。
2. **调度机制**
* **优先级调度**:RTOS 通常支持多级优先级,数值越高优先级越高。调度器总是选取就绪队列中最高优先级的线程运行。
* **时间分片**(可选):同一优先级下可启用时间片 Round-Robin,使各线程轮流运行。
* **抢占 vs 非抢占**:大多数 RTOS 都支持抢占式调度,即当更高优先级的线程变为就绪态时,立即中断当前线程并切换。
3. **确定性与开销**
* **上下文切换时间短**:RTOS 内核极简,系统调用、调度开销通常在几十到几百微秒级。
* **可预测性**:RTOS 保证最坏延迟边界(Worst-Case Latency),关键任务能在可控时间内获得 CPU。
4. **线程间通信和同步**
* **消息队列(Message Queue)**、**信号量(Semaphore/Mutex)**、**事件标志(Event Flags)**、**邮箱(Mailbox)** 等。
* 这些机制都设计得十分轻量,且大多都是**禁止中断时可用**、在中断上下文与线程上下文之间安全交互。
5. **栈管理**
* 每个线程需分配固定大小的栈,栈大小由开发者在创建时指定。
* RTOS 往往提供**栈水印检查**功能,方便在线检测栈溢出。
5.mmap 在linux
不太会,没做准备,下面是答案:
后期再看。
在 Linux 中,`mmap` 是一个非常重要的系统调用(和库函数封装),它可以将一个文件或者匿名内存区域映射到进程的虚拟地址空间,从而用内存指针的方式直接读写文件或共享内存,而不必再通过 `read`/`write` 系统调用。它的原型定义在 `<sys/mman.h>` 中:
```c
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
int munmap(void *addr, size_t length);
```
---
### 一、参数详解
| 参数 | 含义 |
| -------- | --------------------------------------------------------------------------------------------------- |
| `addr` | 建议的映射起始地址,通常设为 `NULL` 让内核自动选择。 |
| `length` | 要映射的字节长度,**必须**按页(通常 4 KiB)对齐。 |
| `prot` | 访问权限,按位或:<br>`PROT_READ` 读 / `PROT_WRITE` 写 / `PROT_EXEC` 可执行 / `PROT_NONE` 无权限 |
| `flags` | 映射类型和选项,包括:<br>`MAP_SHARED`(写入同步回文件)<br>`MAP_PRIVATE`(写时拷贝)<br>`MAP_ANONYMOUS`(匿名映射,不关联文件,`fd` 忽略)等 |
| `fd` | 要映射的文件描述符;若 `flags` 包含 `MAP_ANONYMOUS`,则须设为 `-1`。 |
| `offset` | 文件偏移量,映射从文件的哪个字节开始;必须是页大小的整数倍。 |
调用成功时返回映射区的起始地址;失败时返回 `(void*)-1`,并将 `errno` 设为错误码。
---
### 二、常用场景
1. **文件 I/O 加速**
直接将文件内容映射到内存,随机读写就像操作数组,避免多次系统调用和内核缓冲区拷贝。
2. **进程间共享内存(IPC)**
两个进程都 `mmap` 同一文件(或使用 `MAP_ANONYMOUS|MAP_SHARED` 加上 `fork`),即可在用户空间共享数据。
3. **申请大块匿名内存**
`mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)` 相当于 `malloc`,但更易控制对齐、权限,也可配合 `mprotect` 动态调整权限。
4. **内存映射设备**
通过 `/dev/mem` 或特定设备驱动的 `mmap` 接口,将外设寄存器空间映射到用户态。
---
### 三、示例代码
```c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <file>\n", argv[0]);
exit(1);
}
const char *path = argv[1];
int fd = open(path, O_RDWR);
if (fd < 0) { perror("open"); exit(1); }
struct stat st;
if (fstat(fd, &st) < 0) { perror("fstat"); exit(1); }
size_t len = st.st_size;
// 1. 文件共享读写映射
char *data = mmap(NULL, len,
PROT_READ | PROT_WRITE,
MAP_SHARED,
fd, 0);
if (data == MAP_FAILED) { perror("mmap"); exit(1); }
// 2. 修改文件内容(内存修改自动写回)
for (size_t i = 0; i < len; i++) {
if (data[i] == 'a') data[i] = 'A';
}
// 3. 取消映射并关闭
if (munmap(data, len) < 0) { perror("munmap"); exit(1); }
close(fd);
return 0;
}
```
---
### 四、常见注意事项
* **页对齐**:`length` 和 `offset` 必须是系统页面大小(`getpagesize()`)的整数倍,否则会被向下取整。
* **写时拷贝**:`MAP_PRIVATE` 下对映射区的写操作不会回写文件,而是创建私有的物理页。
* **同步持久化**:`MAP_SHARED` 写后可用 `msync` 强制刷回磁盘;不调用 `msync` 时写入仍可能延迟。
* **权限变更**:可用 `mprotect` 在运行时变更映射区的 `PROT_*`。
* **资源释放**:使用完必须调用 `munmap`,否则映射区会一直占用虚拟地址和内核资源。
* **错误检查**:映射失败时需检查 `errno`,如 `ENOMEM`(内存不足)、`EACCES`(权限不足)等。
---
### 五、内核实现概要
* **VMA(虚拟内存区域)**:进程的 `mm_struct` 维护一个红黑树(或链表)管理所有 `mmap` 区间,每个区间是一个 `struct vm_area_struct`。
* **按需分页**:对映射区首次访问时触发缺页中断,内核查找对应 VMA、调用 `file->mmap` 或匿名页分配,建立物理页映射。
* **卸载映射**:`munmap` 时拆除页表项,回收 VMA 结构,并视情况写回或丢弃脏页。
---
**总结**:
* `mmap` 在用户态提供了高效的“内存—文件”或“进程间”共享映射机制,
* 正确使用可以大幅提升大文件 I/O 或 IPC 的性能,
* 也需要注意对齐、同步和资源管理。
四、阶段三:手写代码(算法题,20 分钟)
题目描述 给定一个正整数 n,找出由它的各位数字重新排列后,比 n 大的最小整数。如果不存在这样的排列,就返回 –1。
- 输入:一个正整数 n(如 1243)
- 输出:下一个更大的排列(如 1324),或 –1
示例
输入 | 输出 | 说明 |
---|---|---|
1243 | 1324 | 下一个比 1243 大的最小数 |
1234 | 1243 | 自然升序后的下一个 |
4321 | –1 | 已经是降序,无法更大 |
115 | 151 | 注意有重复数字 |
解题思路:经典“下一个排列”算法
要在 O(n) 时间内就地(原地)找到“下一个”字典序排列,核心分为四步:
-
从右向左找到第一个升序拐点 找最大的索引
i
,使得a[i] < a[i+1]
。- 如果不存在这样的
i
,说明整个序列是非增(降序)的,已经是最大排列 → 返回 –1。
- 如果不存在这样的
-
在后缀中找到比
a[i]
大的最小元素 从数组尾部向左找到第一个j
,满足a[j] > a[i]
。- 这样交换后能保证新数比原先大,但尽可能小。
-
交换
a[i]
与a[j]
保证前缀增大最少量。 -
将
i+1
至末尾的后缀反转 原本后缀是降序的,反转后变升序,得到最小的尾部排列。
详细步骤举例
以 1243
为例:
原数列:1 2 4 3
步骤1:从右找拐点 a[i]<a[i+1]
i 从 2(值 4)开始:
a[2]=4 ≥ a[3]=3 → i-- 变 1(值 2)
a[1]=2 < a[2]=4 → 拐点 i=1
步骤2:在后缀 [2..3] (4,3) 找第一个 > a[1]=2 的元素
从尾部开始 j=3:a[3]=3 > 2 → j=3
步骤3:交换 a[1] 与 a[3]
1 3 4 2
步骤4:反转后缀 i+1..末尾 → 位置 2..3:
后缀原 [4,2] → 反转为 [2,4]
最终:1 3 2 4 → 1324
C++ 实现
#include <iostream>
#include <string>
#include <algorithm>
#include <climits>
using namespace std;
int nextGreaterNumber(int n) {
string s = to_string(n);
int i = s.size() - 2; // 从倒数第二个字符开始
// 1. 找拐点
while (i >= 0 && s[i] >= s[i+1]) --i;
if (i < 0) return -1; // 已经是最大排列
// 2. 找后缀中第一个比 s[i] 大的
int j = s.size() - 1;
while (s[j] <= s[i]) --j;
// 3. 交换
swap(s[i], s[j]);
// 4. 反转后缀
reverse(s.begin() + i + 1, s.end());
// 转回整数并防溢出
long long m = stoll(s);
return (m > INT_MAX ? -1 : (int)m);
}
int main() {
int n;
// 从标准输入读取一个正整数
while (cin >> n) {
int ans = nextGreaterNumber(n);
if (ans == -1) {
cout << "-1";
} else {
cout << ans;
}
cout << "\n";
}
return 0;
}
复杂度分析
- 时间复杂度:O(n) — 查找拐点 O(n)、找替换元素 O(n)、反转后缀 O(n),总体线性。
边界与注意事项
- 无更大排列:当整个序列为降序时,返回 –1。
- 重复数字:算法同样适用,交换和反转逻辑不变。
- 整数溢出:转换回整数后,要判断是否超出 32 位限制,否则返回 –1。
- 返回值类型:题目可按需求返回整数、字符串或原地修改数组。
五、结束与问答环节
我问了为什么这是秋招但是发实习生的岗位面试邀请,面试官也不知道XD。。。
六、准备建议
最好就是放松,也不用着急,因上努力,果上随缘,放平心态,面试越多越熟练。
本记录由张子健整理,用于面试复盘与自我提升,欢迎交流改进意见。