影石 音视频开发-C++ 二面
1. 自我介绍
2. 如何设计一个类只能在栈上创建对象
答案:如果想让一个类只能在栈上创建,核心就是禁止堆分配。最常见的做法是把 operator new 和 operator delete 声明为删除,或者放到 private 里不让外部调用。这样外部就不能通过 new 在堆上申请对象了,只能直接定义局部对象。不过这种方式只能限制常规 new,如果有人用 placement new 或者其他非常规手段,还得结合接口设计一起控制。
代码:
#include <iostream>
using namespace std;
class StackOnly {
public:
StackOnly() = default;
~StackOnly() = default;
void* operator new(size_t) = delete;
void operator delete(void*) = delete;
};
int main() {
StackOnly obj; // 可以
// StackOnly* p = new StackOnly; // 编译报错
return 0;
}
3. 如何设计一个类只能在堆上创建对象
答案:如果想让一个类只能在堆上创建,通常会把析构函数设为 private 或 protected,然后提供静态工厂函数返回对象指针。这样外部就没法直接定义栈对象,因为栈对象离开作用域时编译器需要调用析构函数,而析构函数不可访问。更稳妥的写法一般是返回智能指针,并且把删除逻辑也封装进去,避免外部误删或忘记释放。
代码:
#include <iostream>
#include <memory>
using namespace std;
class HeapOnly {
public:
static shared_ptr<HeapOnly> create() {
return shared_ptr<HeapOnly>(new HeapOnly());
}
void hello() {
cout << "heap only\n";
}
private:
HeapOnly() = default;
~HeapOnly() = default;
};
int main() {
auto p = HeapOnly::create();
p->hello();
return 0;
}
4. 进程 IPC 通信里最关键的问题是什么,共享内存怎么做同步
答案:IPC 真正难的通常不是“能不能传数据”,而是如何保证一致性、同步和可恢复性。共享内存是最快的一类 IPC,因为它避免了多次数据拷贝,但它本身只提供共享数据区域,不提供同步语义,所以必须额外配合互斥锁、信号量、原子变量或者事件通知机制。如果多个进程同时读写共享内存,没有同步保护,就很容易出现竞争条件、读到中间状态、数据覆盖这些问题。实际工程里,如果是固定格式的小块共享数据,常见是“共享内存 + 进程间互斥锁/信号量”;如果是高吞吐消息传递,通常会进一步做成环形缓冲区或者无锁队列模型。
5. 在共享内存上直接放 std::vector 会有什么问题
答案:直接把普通 std::vector 放到共享内存里,最大的问题是它内部维护的指针通常指向当前进程地址空间里的堆内存,而不是共享内存本身。这意味着即使 vector 对象放进了共享内存,它内部的数据区也未必是共享的,而且不同进程的虚拟地址空间不一样,指针值本身也不具备跨进程可移植性。另外 std::vector 的扩容、分配器、迭代器失效这些行为,默认也都是为单进程场景设计的。如果真要在共享内存里用类似容器,一般要么自己实现基于偏移量的容器,要么使用支持自定义分配器和共享内存语义的库,比如 Boost.Interprocess。
6. 用事件驱动模型实现一个定时器,通常怎么做
答案:如果是事件驱动模型,定时器通常不会单独开线程死等,而是把“定时到期”也变成事件源,统一交给事件循环处理。Linux 下比较常见的做法是 timerfd + epoll,把定时器文件描述符注册到 epoll,到期后和普通 IO 事件一样被统一派发。如果是自己实现,也可以用最小堆、时间轮这类结构来管理超时任务,再结合事件循环在合适时机触发回调。小规模定时任务最小堆够用,超大规模、高频定时任务更常见的是时间轮,因为插入删除和推进时钟的成本更稳定。
代码:
#include <sys/timerfd.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <iostream>
#include <cstring>
using namespace std;
int main() {
int tfd = timerfd_create(CLOCK_MONOTONIC, 0);
itimerspec its{};
its.it_value.tv_sec = 1;
its.it_interval.tv_sec = 1;
timerfd_settime(tfd, 0, &its, nullptr);
int epfd = epoll_create1(0);
epoll_event ev{};
ev.events = EPOLLIN;
ev.data.fd = tfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, tfd, &ev);
epoll_event events[10];
int n = epoll_wait(epfd, events, 10, -1);
if (n > 0) {
uint64_t exp;
read(tfd, &exp, sizeof(exp));
cout << "timer fired: " << exp << endl;
}
close(tfd);
close(epfd);
return 0;
}
7. 同步、异步、阻塞、非阻塞 IO 怎么区分
答案:这几个概念很容易混,但其实是两个维度。阻塞和非阻塞描述的是调用线程在等结果时会不会被挂起;同步和异步描述的是结果完成后由谁来拿结果、什么时候拿结果。比如同步阻塞 IO,调用后线程一直等到数据准备好;同步非阻塞 IO,线程不会一直卡住,但要自己反复轮询;异步 IO 则通常是内核在数据准备好之后主动通知应用,应用不需要自己去等。很多网络框架里常说的 epoll 本质上更接近“同步 IO 多路复用”,不是严格意义上的异步 IO。
8. 常见的锁有哪些,怎么选
答案:常见的锁有 mutex、recursive_mutex、timed_mutex、shared_mutex,再往下还有自旋锁、读写锁、
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
本专栏系统梳理C++方向, 大中厂高频高频面试考点 , 内容皆来自真实面试经历,从基础语法、内存管理、STL与设计模式,到操作系统与项目实战,结合真实面试题深度解析,帮助开发者高效查漏补缺,提升技术理解与面试通过率,打造扎实的C++工程能力.

查看5道真题和解析