滴滴 C++开发 一面
1. 自我介绍
2. 进程和线程的区别
答案:进程是资源分配的基本单位,线程是 CPU 调度的基本单位。一个进程里可以有多个线程,这些线程共享地址空间、文件描述符、堆和全局变量,但每个线程有自己的栈、寄存器上下文和线程局部存储。进程隔离性更强,一个进程崩溃通常不会直接把别的进程带崩;线程切换开销通常比进程切换小,因为同一进程内线程切换不一定需要切页表,但线程间共享资源多,也更容易带来竞态、死锁和可见性问题。实际工程里,进程更适合隔离模块、容灾和权限控制,线程更适合细粒度并发。
3. 进程调度和线程调度的效率大致是什么样的
答案:一般来说,线程切换比进程切换轻一些,但也不能简单理解成“线程一定很便宜”。线程切换时主要保存和恢复寄存器、程序计数器、栈指针、调度状态等;如果是同一进程里的线程切换,地址空间可能不用切,代价相对小。进程切换除了这些,还可能涉及页表切换、TLB 失效以及缓存命中下降,所以开销通常更大。但在真实服务里,真正吃性能的往往不是“切换动作本身”,而是频繁上下文切换导致的缓存抖动、锁竞争和调度延迟。所以很多高性能服务会控制线程数,或者改用事件驱动、协程等模型。
4. 你怎么理解虚拟内存
答案:虚拟内存的核心是给每个进程提供一个看起来连续、独立的地址空间,让程序不需要直接面对物理内存布局。CPU 访问的是虚拟地址,经过页表映射后才落到物理页框上。这样可以实现进程隔离、按需分配、页级保护、内存共享和把一部分不活跃数据换到磁盘。从工程角度看,虚拟内存带来的好处不仅是“内存变大了”,更重要的是统一了内存视图,支持 mmap、共享内存、写时复制、动态库加载这些机制。如果继续往下问,通常就会延伸到页表、多级页表、TLB、缺页异常和大页。
5. 缺页异常是怎么发生的,和页换入换出有什么关系
答案:当进程访问某个虚拟地址时,如果页表项显示这个页当前不在内存,或者权限不满足,CPU 就会触发异常,交给内核处理。如果这个地址本来就是合法映射,只是页面还没装入内存,那就是缺页异常,内核会找到对应页并把它换入;如果内存不够,还可能挑一个旧页换出。所以“缺页”不等于“出错”,很多时候它是虚拟内存按需调页的正常行为。真正麻烦的是频繁缺页,尤其是工作集和物理内存不匹配时,会导致抖动,程序大部分时间都在换页而不是干活。
6. 指针和引用的区别
答案:引用本质上更像一个别名,定义时必须绑定对象,语义上默认有效,而且一般不能改绑到别的对象;指针本身是一个变量,存的是地址,可以为空,也可以随时改指向。从接口设计角度看,引用通常表达“这个参数一定存在”,指针更适合表达“这个对象可能为空”或者“需要手动处理所有权”。面试里这题如果答得更深入一点,可以顺带提到常量引用能绑定右值、指针有多级间接访问、引用不是对象这一层语义区别。
代码:
#include <iostream>
using namespace std;
void add1(int& x) { x += 1; }
void add2(int* x) { if (x) *x += 2; }
int main() {
int a = 10;
add1(a);
add2(&a);
cout << a << endl;
return 0;
}
7. RAII 机制怎么理解
答案:RAII 的核心思想是把资源的申请和对象生命周期绑定。对象构造时拿资源,对象析构时放资源,这样即使函数中间 return、抛异常,也不会忘记释放。C++ 里很多标准库设施都是按 RAII 思想设计的,比如 lock_guard 管锁,unique_ptr 管堆内存,文件流对象管理文件句柄。这套思想的价值不只是“少写 delete”,而是让资源所有权变得清晰,减少异常路径上的泄漏和状态不一致。
代码:
#include <iostream>
#include <mutex>
using namespace std;
mutex mtx;
void work() {
lock_guard<mutex> lk(mtx);
cout << "critical section" << endl;
}
8. 智能指针有哪几种,循环引用问题怎么解决
答案:常见的有 unique_ptr、shared_ptr、weak_ptr。unique_ptr 表示独占所有权,开销小,语义清晰;shared_ptr 表示共享所有权,通过控制块维护引用计数;weak_ptr 不拥有对象,只是弱引用,用来观察对象生命周期。循环引用一般发生在两个对象互相持有对方的 shared_ptr,导致强引用计数始终降不到 0。解决办法通常是把其中一边改成 weak_ptr,把“拥有关系”和“观察关系”分开。如果继续追问,还可以展开控制块、删除器、线程安全边界和 enable_shared_from_this。
代码:
#include <memory>
using namespace std;
struct B;
struct A {
shared_ptr<B> b;
};
struct B {
weak_ptr<A> a;
};
int main() {
auto pa = make_shared<A>();
auto pb = make_shared<B>();
pa->b = pb;
pb->a = pa;
return 0;
}
9. epoll、select、poll 的区别,为什么高并发场景更常用 epoll
答案:select 和 poll 本质上都需要用户态把关注的 fd 集合交给内核,再由内核遍历检查哪些就绪;fd 多时,这个线性扫描成本会很明显。epoll 把“注册感兴趣的 fd”和“等待活跃事件”拆开了,注册后由内核维护就绪队列,等待时只返回活跃 fd,所以在大量连接场景下更高效。另外 epoll 支持边缘触发和水平触发,配合非阻塞 IO 更适合做事件驱动服务器。如果继续深挖,通常会问红黑树、就绪链表、ET 模式下为什么必须把数据读到 EAGAIN。
10. 多态和虚函数底层是怎么工作的
答案:运行时多态通常依赖虚函数机制。当类里有虚函数时,对象内部一般会有一个虚表指针,指向对应类型的虚函数表。通过基类指针或引用调用虚函数时,程序会先找到对象实际类型对应的虚表,再间接调用函数地址,所以运行时能表现出“同一接口,不同实现”。代价是对象通常会多一个虚表指针,调用多一次间接寻址,而且会影响对象布局。如果问得更深入,可以继续讲虚析构、纯虚函数、对象切片和构造析构阶段调用虚函数的行为。
代码:
#include <iostream>
using namespace std;
class Base {
public:
virtual void print() { cout << "Base\n"; }
virtual ~Base() = default;
};
class Derived : public Base {
public:
void print() override { cout << "Derived\n"; }
};
int main() {
Base* p = new Derived();
p->print();
delete p;
return 0;
}
11. HTTP 里常见请求方法有哪些,GET 和 POST 真正的区别是什么
答案:常见方法有 GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS。GET 更偏获取资源,语义上应该是幂等的;POST 更偏提交数据、触发服务端状态变化,不强调幂等。很多人容易把“GET 放 URL、POST 放 body”当成核心区别,其实那只是常见用法,不是语义本质。真正重要的是资源操作语义、缓存行为、幂等性以及接口设计规范。另外,安全性和 GET/POST 本身没有直接因果,是否安全主要取决于是否走 HTTPS、是否做鉴权和敏感数据保护。
12. C++ 里内存管理方式有哪些
答案:最基础的是自动存储期对象,也就是栈上对象,生命周期由作用域决定。再往下是动态内存管理,常见有 new/delete、new[]/delete[],更推荐的是容器、智能指针和 RAII 包装,尽量减少手写裸资源管理。如果项目性能要求更高,还可能接触对象池、内存池、jemalloc/tcmalloc 这类分配器优化。所以这题如果想答得更像工程实践,不要只停在 new 和 malloc 两个关键词上。
13. new/delete 和 malloc/free 的区别
答案:malloc/free 是 C 的库函数,只负责按字节申请和释放原始内存,不会调用构造和析构。new/del
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
本专栏系统梳理C++方向, 大中厂高频高频面试考点 , 内容皆来自真实面试经历,从基础语法、内存管理、STL与设计模式,到操作系统与项目实战,结合真实面试题深度解析,帮助开发者高效查漏补缺,提升技术理解与面试通过率,打造扎实的C++工程能力.
查看14道真题和解析