简历易忘技能
1
- 熟悉 C/C++,C++ 11/14/17常用特性(lambda 表达式、智能指针等);掌握面向对象设计及常用设计模式;
lambda 表达式
1 | [capture](parameters) mutable/constexpr -> return_type { 函数体 } |
① 默认 = 按值捕获,会将变量的当前值复制一份,存储在闭包对象内部;② & 按引用捕获,直接操作外部变量。
捕获对象成员变量时需捕获 this。
捕获列表指外部变量,参数列表指传入内部的参数。
mutable 在按值捕获时可以修改外部变量的内部副本,否则会因为修改 const 导致编译报错。
Lambda 表达式 = 编译器生成的匿名类 + 该类的一个实例(闭包对象):首先,编译器会为这个 Lambda 生成一个具有唯一名称的类,然后 Lambda 捕获列表中的变量会成为这个匿名类的成员变量,Lambda 的函数体会被编译成这个匿名类的一个成员函数 operator()。这个成员函数默认是 const 的,除非使用了 mutable 关键字。
当定义 Lambda 表达式时,比如 auto lambda = [...]{...};),编译器实际上是在创建这个匿名类的一个实例。这个实例就叫做闭包对象,当调用 Lambda 函数的时候,实际上就是在调用这个闭包对象的 operator() 成员函数。
智能指针
原理是定义一个模板类,在类的析构函数中写上对应的资源释放操作,使智能指针对象超出作用域以后自动调用析构函数释放内存。 unique_ptr 和 shared_ptr 还重载了解引用 * 和 -> 运算符,让它们使用起来和普通指针比较接近
shared_ptr :引用计数 + 控制块。本身是一个类,它的成员变量中,有2个指针,一个指向管理对象的指针,另一个是指向控制块的指针。
控制块中主要有强引用计数器和弱引用计数器,原子变量增减(线程安全),由第一个管理该对象的 shared_ptr 创建,后续的 shared_ptr 复制指向控制块的指针,以共享控制块和计数器(指向对象的指针也复制了)。
unique_ptr 显式删除了拷贝构造函数和拷贝赋值运算符,把它们设置为 delete,从而实现对对象的独占,所以它无法被复制,但是可以移动(通过移动构造或者移动赋值来转移所有权)。
weak_ptr 维护的是弱引用关系,因为它不拥有这个对象,所以不能直接通过解引用*或者箭头运算符访问。它除了解决多个 shared_ptr 循环引用的问题,还主要用来观察 shared_ptr 管理的对象是否还有效,需要访问时可以通过 lock() 尝试获取一个 shared_ptr。
面向对象设计特性
C++ 面向对象的三大特性分别是封装、继承和多态。
首先第一个封装,指的是将数据和函数组合在一起,通过 private、public、protected 这三个关键字对访问权限进行限制,使得对外只暴露必要接口,而隐藏内部具体的实现细节,提高安全性。
第二个继承,指的是子类继承父类中允许访问的成员,使子类可以复用和扩展父类的功能,而不需要重复编写相同代码,提高代码复用性。
第三个多态,指的是同一个接口但是有不同的实现。它分为静态多态和动态多态。
静态多态是在编译期就确定具体的函数调用。具体来说,可以通过函数重载来实现,也就是同一个函数名拥有不同的参数类型或者参数数量;另外模板泛型编程和运算符重载也属于静态多态。
动态多态是在运行时确定具体调用。父类使用 virtual 关键字定义虚函数,子类用 override 重写父类方法,底层使用虚函数表和虚函数指针机制来实现。
面向对象设计原则
① 单一职责原则:一个类只负责一项职责。
② 开闭原则:对扩展开放、对修改关闭。新增功能靠加代码(子类继承拓展新功能),不靠改老代码。
③ 里氏替换原则:子类能无缝替换父类而不破坏程序正确性。
④ 接口隔离原则:一个类不应该被迫实现用不到的方法,应该使用多个小而专的接口,而不是一个大而全的接口。
⑤ 依赖倒置原则:高层模块不应依赖于低层模块,高层模块和低层模块都依赖抽象,不依赖具体实现。面向接口编程。
高层指的是整体的业务决策,低层指的是实施决策的各个具体实现的接口。
设计模式
单例模式
C++ 中实现一个单例模式,也就是要确保全局只有唯一实例。
首先第一点,必须把这个类的构造函数设置为 private,防止外部直接创建多个实例。第二点,要暴露一个 public 的 static 接口,让外部可以获取这个实例。第三点,要把这个类的拷贝构造函数和拷贝赋值运算符禁用,设置为 delete,防止通过拷贝方式创建多个实例。
1 | class Singleton { // 简易懒汉式单例模式 |
饿汉式:在程序加载阶段直接创建实例,线程安全但浪费资源。
懒汉式:把创建实例的操作放到公共获取接口中,只有第一次调用接口时才进行创建,所以是延迟创建。在多线程情况下,就必须保证初始化时的线程安全。
工厂模式
创建型设计模式,核心思想在于将对象的创建过程与使用过程进行分离。
定义一个专门的“工厂类”,客户端不直接使用 new 关键字来创建所需的对象,而是通过调用这个工厂类的方法,并传入相应的参数,由工厂类来负责实例化具体的对象。
在需要增加新的产品类型,或者修改某个产品的创建方式时,只需要修改工厂内部的逻辑,而完全不需要改动外部的代码,这使得系统更加灵活、易于维护和扩展。
主要有 UI 框架的控件工厂,跨平台、拓展修改自定义控件样式;游戏的角色工厂,根据不同类型创建不同属性、技能的角色。
主要是:创建逻辑是否复杂/易变,类型繁多且经常拓展。
观察者模式
当一个对象的状态发生改变时,所有订阅它的对象(观察者)都会得到自动通知并更新。
主要在 GUI 开发中使用,Qt 的信号槽就是它的实现:connect 相当于订阅;emit 相当于通知;槽函数相当于观察者收到通知后的处理。
在 MVC 里,Model 是被观察者,View 是观察者。Model 数据变了发信号,View 收到后刷新。
在 MVVM 里,ViewModel 是被观察者,View 是观察者,只是订阅被框架自动化了,通过 Q_PROPERTY 发的 NOTIFY statusChanged 信号。
2
- 熟悉 TCP/IP 四层网络模型,掌握 HTTP、TCP/UDP 等常见协议;了解 select/epoll 等常见 I/O 多路复用机制;
网络模型
从上至下分别是应用层、传输层、网络层和网络接口层。
TCP/IP 模型的应用层对应 OSI 的应用层、表示层和会话层。将这三层合并后,让应用程序自行管理数据格式会更加高效。应用层负责应用进程间的交互,来完成特定的网络应用,它的协议定义的是应用进程间通信和交互的规则。其中协议有很多,比如 DNS、HTTP、HTTPS 和 SMTP 等等。
TCP/IP 的传输层对应 OSI 的传输层,负责向两台主机中进程间的通信提供通用的数据传输服务,主要使用 TCP 和 UDP 两种协议。其中 TCP 提供的是面向连接、可靠的数据传输服务,而 UDP 提供的是无连接、不可靠的数据传输服务。
TCP/IP 的网络层对应 OSI 的网络层,使用的是无连接的 IP 协议,利用 IP 地址为分组交换网上的不同主机提供通信服务。而 OSI 同时考虑了面向连接和无连接的通信服务。在传输层和网络层对于连接服务的支持上,TCP/IP 模型比 OSI 模型具备更加明确的划分和更加精简的设计。
最下层的网络接口层对应 OSI 模型的数据链路层和物理层,但它没有属于 TCP/IP 体系的具体协议,主要负责将网络层交付的 IP 数据报组装成帧,并利用物理介质,比如光缆或者无线电等等,将比特流发送到物理链路的另一端。比起 OSI 的分层设计,它具有更好的兼容性,对硬件的限制也更少。
HTTP
TCP/UDP
I/O 多路复用
传统的“一个连接对应一个线程”的模型,在连接数量多的时候,会有大量的线程和上下文切换,易造成硬件瓶颈,IO 多路复用技术可以使用单个线程或少量线程来同时监视和处理大量的网络连接
| 维度 | select | poll | epoll |
|---|---|---|---|
| 数据结构 | fd_set 位图 |
pollfd 数组 |
内核红黑树 + 就绪链表 |
| fd 数量上限 | 默认 1024(FD_SETSIZE) |
无硬上限 | 无硬上限(受系统资源限制) |
| 每次调用 | 将整个集合从用户态拷贝到内核 | 同样每次拷贝整个数组 | epoll_ctl 注册一次,epoll_wait 不重复拷贝监听集合 |
| 查找就绪 | 线性遍历 O(n) 整个位图 | 线性遍历 O(n) 整个数组 | 回调返回就绪 fd,就绪链表 |
| 平台 | 跨平台 | 类 Unix | Linux 专有 |
select 和 poll 每次都要遍历和拷贝全部 fd 性能较差。
epoll 的工作机制:epoll_create 创建实例,初始化了红黑树和就绪链表,epoll_ctl 将 fd 加入红黑树并注册回调;fd 就绪时回调把它加入就绪链表;epoll_wait 直接取就绪链表中的事件,拷贝 fd 返回给用户。
选型
- 连接数大、活跃连接比例低(典型高并发服务器):epoll 优势明显。
- 连接数少或者几乎都活跃:三者差距不大,select/poll 反而更简单、可移植。
- 触发模式:LT(水平触发,默认,没处理完会重复通知);ET(边缘触发,状态变化才通知,需配合非阻塞 IO 循环读写到
EAGAIN)。
LT(水平触发,默认):只要 fd 对应缓冲区仍处于就绪状态(读缓冲区有数据、写缓冲区有空间),每次 epoll_wait 都会返回该事件,持续提醒处理,直到数据被完全读走。编程简单,不怕漏读,与 select/poll 语义一致。
ET(边缘触发,EPOLLET):只在 fd 状态发生变化(新数据到达、缓冲区由满变为非满)时通知一次。如果没有一次性处理完,剩余数据不会再次通知,除非又有新数据到来。
ET 的使用规则
- fd 必须设为非阻塞:循环读取到最后一次时没有数据可读,阻塞模式会把整个事件循环线程卡住。
- 读:循环
read,直到返回 -1 且errno == EAGAIN(不要用”读到的字节数小于缓冲区”来判断)。 - 写:循环
write直到写完或EAGAIN;通常只在遇到EAGAIN时才注册EPOLLOUT,写完后取消。 - 多线程下配合
EPOLLONESHOT,避免同一个 fd 被多个线程同时处理。
对比
- ET 减少
epoll_wait的唤醒次数,高并发下更高效,但编码复杂、容易漏事件。 - LT 编码简单,多数场景性能差距不大。例如 Nginx 使用 ET,Redis 使用 LT。
- LT 的坑:监听了
EPOLLOUT但没数据可写时必须取消,否则会一直触发,CPU 空转。
Reactor 是”就绪通知”:epoll 通知 fd 可读了,由用户自己去 read,再处理业务;Proactor 是”完成通知”:用户先发起异步读,内核把数据读好放进缓冲区后通知用户。
基于 epoll 的高并发框架一般采用主从 Reactor 加线程池(muduo):主线程专门 accept,把连接分给多个 IO 线程,每个线程一个事件循环,连接固定归属一个线程,避免锁竞争,耗时业务再交给线程池,
3
- 熟悉 Windows/Linux 多线程编程,管道、共享内存进程间通信,线程同步与互斥技术;
进程与线程
进程是操作系统进行资源分配的基本单位,而线程是程序执行和 CPU 调度的基本单位。一个进程可以包含多个线程,而且必须至少有一个主线程。多线程主要是用来完成一个进程中的多个子任务。
在资源分配上,多个进程间的资源是相互独立的,每个进程都需要分配独立的地址空间和资源,在进程切换时开销会比较大。而多个线程共享同一进程中的大部分资源,只是各自有独立的栈、寄存器等线程上下文,所以线程间切换的开销会比较小。
也因此在安全性上,进程之间是相互隔离的,一个进程崩溃一般不会直接影响其他进程的稳定性;而线程因为共享同一个进程中的地址空间,所以一个线程崩溃,会导致整个程序和其他线程都一起崩溃。
IPC 进程间通信
| 方式 | 适用范围 | 特点 |
|---|---|---|
匿名管道 pipe |
有亲缘关系的进程(父子、兄弟) | 单向字节流,最简单 |
| 命名管道 FIFO | 任意进程 | 管道有个文件路径名 |
| 共享内存 | 任意进程 | 最快(数据不经过内核拷贝),需要自己做同步 |
| 消息队列 | 任意进程 | 有消息边界,带类型 |
| 信号 | 通知用 | 只能传很少的信息 |
| socket | 任意进程,或跨机器 | 最通用 |
首先最基础的是管道,它分为用于父子等有亲缘关系进程的匿名管道,和所有进程的命名管道。它们本质上是内核管理的字节流缓冲文件,半双工通信。最常见的就是 Linux Shell 命令中的竖杠。
第二种是消息队列,引入了格式化结构化的消息链表,支持了消息按类型的传输,并且使用的是全双工的通信方式。
第三种是共享内存,可以让多个进程映射到同一块物理内存上。但是它需要引入其他同步方式,比如 P/V 操作、信号量或者互斥量,来控制共享内存的读写,实现同步和互斥。
然后还有信号,信号主要用于系统事件通知。最后一种是套接字 Socket,主要用来实现客户端与服务端之间的网络通信。
匿名管道:pipe(fd) 创建管道,fd[0] 是读端,fd[1] 是写端。唯一的传递方式是通过 fork 继承文件描述符
命名管道:mkfifo() 创建
共享内存:
| 步骤 | 函数 | 作用 |
|---|---|---|
| 1 | shm_open(name, flags, mode) |
创建/打开一个共享内存对象,返回 fd。name 必须以 / 开头 |
| 2 | ftruncate(fd, size) |
设置大小(新建的共享内存大小为 0,必须设置) |
| 3 | mmap(..., MAP_SHARED, fd, 0) |
把它映射到本进程地址空间,返回一个指针 |
| 4 | 读写指针指向的内存 | |
| 5 | munmap(ptr, size) |
解除映射 |
| 6 | shm_unlink(name) |
删除共享内存对象(不删会残留在 /dev/shm) |
线程同步与互斥
第一种就是互斥锁 mutex,最基础的用法还可以配合 lock_guard 和 unique_lock 这种 RAII 封装。
第二种是 condition_variable 条件变量,通过 notify_one、notify_all,让一个线程达到条件之后通知其他线程。最常见的就是生产者—消费者模型。
第三种是原子操作,通过 CPU 提供的原子指令和 atomic 原子变量,在不使用普通锁的情况下保证单次操作的原子性。
第四种是使用 future 和 promise,异步地获取和传递任务结果。还有 C++20 引入的,信号量、屏障之类的我没用过不太了解。
锁
最基础的就是互斥锁,保证多线程情况下同一时间只有一个线程可以获得锁。其他线程如果尝试获取这把锁,就会被阻塞,直到持有锁的线程释放。合适较复杂、较长临界区的情况(锁的持有时间较长)。
第二种就是自旋锁,通过持续检查锁的状态,直到锁被释放以后立即尝试获得。在 C++ 中可以用一个循环不断检查原子变量,标准库里可以使用 atomic_flag 这类原子类型实现。但是自旋时间太长会一直占用 CPU,所以更适合锁持有时间很短的场景。
第三种是读写锁。读锁可以让多个线程同时读取,但是写操作在同一时间只能由一个线程进行。C++ 中可以使用 shared_mutex 和 shared_lock 来实现共享读,再使用独占锁进行写。
然后还有递归锁,通过一个计数器让同一线程可以多次对同一资源加锁,而不会因为再次加锁直接造成死锁。只有相同次数的解锁完成后,锁才真正释放。
还有两种锁的概念,就是悲观锁和乐观锁。悲观锁是在每次获取资源前都直接加锁;乐观锁一般是先尝试修改资源,如果多线程之间发生冲突,就回退或者重新尝试。一般可以用版本号或者原子 CAS 操作来实现乐观控制。
死锁四大条件和避免方式:
| 条件 | 简约描述 | 解决方法 |
|---|---|---|
| 互斥 | 资源一次只能被一个线程持有。 | 允许资源共享(如使用读写锁),但通常此条件难以破坏。 |
| 持有并等待 | 线程持有一个资源,同时又在等待另一个。 | 要求线程一次性申请所有需要的资源。 |
| 不可抢占 | 资源不能被强行从持有者那里夺走。 | 允许系统在必要时抢占资源(实现复杂),或让线程主动释放。 |
| 循环等待 | 多个线程形成一个环形资源等待链。 | 按固定顺序申请资源 |
4
- 熟悉常用数据结构,数组、链表、栈、队列、二叉树等;熟悉 STL 常用容器 vector、list、map 等;
5
熟悉 H.264 编解码、 NALU 及 I/B/P 帧等相关概念;了解 FFmpeg 框架;
播放器起播流程(整合 FFmpeg 实战) / 音视频编解码基础
6
具备 Qt 桌面开发经验,熟悉信号槽机制、元对象系统、MVC/MVVM 设计架构;
Qt 机制 / Qt 事件与消息循环 / MVC 对比 MVVM 架构
7
- 熟悉 VS/Qt Creator/VS Code、Codex 等 IDE、CLI,熟悉 AI 编程辅助;熟悉 GDB,Git 等开发与调试工具;
开发与调试工具
GDB
编译时加 -g(带调试信息),修改优化参数。
| 命令 | 作用 |
|---|---|
gdb ./app / gdb ./app core |
启动 / 分析 core 文件 |
run |
运行 |
break 函数名 或 break 文件:行号 |
设断点(可加 if 条件) |
next / step / continue / finish |
单步不进函数 / 单步进函数 / 继续 / 跑完当前函数 |
print 变量 |
打印变量 |
bt |
看调用栈 |
frame N |
切换栈帧 |
watch 变量 |
变量被修改时停下 |
info threads / thread apply all bt |
看所有线程的栈 |
- 程序崩溃(段错误):开 core dump(
ulimit -c unlimited),gdb ./app core,bt看崩溃时的调用栈,切到崩溃那一帧看变量,通常能发现空指针或野指针。 - 程序卡死:
gdb -p 进程号attach 上去,thread apply all bt看每个线程停在哪,如果几个线程都卡在等锁,就是死锁,对照栈找出锁的获取顺序。 - 某个变量被莫名改掉:用
watch设观察点,变量一变就停下,bt看是谁改的。
Git
三个区域:工作区 → 暂存区(git add)→ 本地仓库(git commit)→ 远程仓库(git push)
**merge 与 rebase**(必问)
| merge | rebase | |
|---|---|---|
| 做法 | 把两个分支合并,产生一个新的合并提交 | 把我的提交”挪”到目标分支最新提交之后 |
| 历史 | 保留真实分叉历史,图上有分叉 | 变成一条直线,干净 |
| 风险 | 安全 | 会改写提交(hash 变化) |
原则:自己的本地/个人分支可以 rebase 整理历史;**已经推送、别人在用的公共分支不要 rebase**。
Sanitizer
- 了解 OpenCV、OpenGL/GLSL、Direct3D11 图像处理及图形学基础;
- 具备 ONNX Runtime 模型部署经验,Stable Diffusion(WebUI、ComfyUI) AIGC 经验。

