滴滴 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_ptrshared_ptrweak_ptrunique_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

答案:selectpoll 本质上都需要用户态把关注的 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 真正的区别是什么

答案:常见方法有 GETPOSTPUTDELETEPATCHHEADOPTIONSGET 更偏获取资源,语义上应该是幂等的;POST 更偏提交数据、触发服务端状态变化,不强调幂等。很多人容易把“GET 放 URL、POST 放 body”当成核心区别,其实那只是常见用法,不是语义本质。真正重要的是资源操作语义、缓存行为、幂等性以及接口设计规范。另外,安全性和 GET/POST 本身没有直接因果,是否安全主要取决于是否走 HTTPS、是否做鉴权和敏感数据保护。

12. C++ 里内存管理方式有哪些

答案:最基础的是自动存储期对象,也就是栈上对象,生命周期由作用域决定。再往下是动态内存管理,常见有 new/deletenew[]/delete[],更推荐的是容器、智能指针和 RAII 包装,尽量减少手写裸资源管理。如果项目性能要求更高,还可能接触对象池、内存池、jemalloc/tcmalloc 这类分配器优化。所以这题如果想答得更像工程实践,不要只停在 newmalloc 两个关键词上。

13. new/deletemalloc/free 的区别

答案:malloc/free 是 C 的库函数,只负责按字节申请和释放原始内存,不会调用构造和析构。new/del

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

C++ 常考面试题总结 文章被收录于专栏

本专栏系统梳理C++方向, 大中厂高频高频面试考点 , 内容皆来自真实面试经历,从基础语法、内存管理、STL与设计模式,到操作系统与项目实战,结合真实面试题深度解析,帮助开发者高效查漏补缺,提升技术理解与面试通过率,打造扎实的C++工程能力.

全部评论

相关推荐

04-17 23:48
西北大学 Java
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务