美团移动端 C++开发 二面 面经
美团移动端二面(60分钟)
1. 说说进程和线程的区别,以及线程间通信的方式有哪些?
参考答案:
进程是资源分配的基本单位,线程是CPU调度的基本单位。
- 资源开销:进程拥有独立的地址空间、代码段、数据段、堆栈,创建和切换开销大,线程共享进程的地址空间和资源,只有独立的栈和寄存器,创建和切换开销小
- 通信方式:进程间通信需要IPC机制如管道、消息队列、共享内存、信号量,线程间通信更简单可以直接访问共享变量
- 安全性:进程间相互独立一个进程崩溃不影响其他进程,线程共享地址空间一个线程崩溃可能导致整个进程崩溃
- 并发性:多进程可以在多核CPU上真正并行执行,多线程也可以并行但需要注意数据竞争和同步问题
线程间通信方式:
- 共享内存:多个线程直接访问同一块内存区域,需要用互斥锁或信号量保护临界区,效率最高但需要处理同步问题
- 互斥锁(Mutex):保护共享资源,同一时刻只有一个线程可以访问临界区,防止数据竞争
- 条件变量(Condition Variable):配合互斥锁使用,线程可以等待某个条件满足,其他线程通过notify唤醒等待的线程
- 信号量(Semaphore):控制对共享资源的访问数量,可以实现生产者消费者模型
- 消息队列:线程通过队列传递消息,发送线程将消息放入队列,接收线程从队列取出消息
- 原子操作:使用atomic类型进行无锁编程,适用于简单的计数器或标志位
2. 什么是死锁?死锁的四个必要条件是什么?如何避免死锁?
参考答案:
死锁是指两个或多个线程互相持有对方需要的资源,导致所有线程都无法继续执行的状态。
死锁的四个必要条件:
- 互斥条件:资源不能被多个线程同时使用,一个线程占用资源时其他线程必须等待
- 请求与保持条件:线程已经持有至少一个资源,同时又请求其他线程持有的资源
- 不可剥夺条件:线程已获得的资源在使用完之前不能被强制剥夺,只能由线程自己释放
- 循环等待条件:存在一个线程资源的循环等待链,每个线程都在等待下一个线程持有的资源
避免死锁的方法:
- 破坏互斥条件:尽量使用无锁数据结构或读写锁,允许多个线程同时访问资源
- 破坏请求与保持条件:一次性申请所有需要的资源,要么全部获得要么全部不获得
- 破坏不可剥夺条件:如果线程请求资源失败,释放已持有的所有资源,稍后重试
- 破坏循环等待条件:对所有资源进行编号,线程必须按照固定顺序申请资源,这是最常用的方法
- 使用超时机制:线程在等待资源时设置超时时间,超时后释放已持有的资源
- 死锁检测与恢复:定期检测系统是否存在死锁,发现死锁后终止某些线程或回滚操作
3. 说说C++的内存布局,栈、堆、全局区、常量区、代码段分别存储什么?
参考答案:
C++程序的内存布局从低地址到高地址依次是代码段、数据段、BSS段、堆、栈。
- 代码段(.text):存储程序的机器指令,只读可执行,所有函数的代码都在这里,多个进程可以共享同一份代码段
- 数据段(.data):存储已初始化的全局变量和静态变量,程序启动时从可执行文件加载,可读可写
- BSS段(.bss):存储未初始化的全局变量和静态变量,程序启动时自动初始化为0,不占用可执行文件空间
- 堆(Heap):动态分配的内存区域,通过new/malloc分配,delete/free释放,从低地址向高地址增长,由程序员手动管理容易出现内存泄漏
- 栈(Stack):存储局部变量、函数参数、返回地址等,从高地址向低地址增长,由编译器自动管理,作用域结束自动释放,空间有限通常几MB
- 常量区:存储字符串常量和const修饰的全局变量,只读不可修改,通常在数据段或代码段附近
内存增长方向:堆向上增长地址增大,栈向下增长地址减小,当堆和栈相遇时会发生栈溢出或堆溢出。
4. 什么是内存泄漏?如何检测和避免内存泄漏?
参考答案:
内存泄漏是指程序动态分配的内存没有被释放,导致可用内存逐渐减少,最终可能导致程序崩溃或系统资源耗尽。
常见的内存泄漏场景:
- new了对象但忘记delete,或者delete之前程序异常退出
- 循环中重复分配内存但没有释放
- 智能指针的循环引用,两个shared_ptr互相引用导致引用计数永远不为0
- 容器中存储指针但没有释放指针指向的内存
- 单例模式中动态分配的资源没有释放
检测内存泄漏的方法:
- Valgrind:Linux下强大的内存检测工具,可以检测内存泄漏、越界访问、使用未初始化内存等问题
- AddressSanitizer(ASan):编译器内置的内存检测工具,编译时加上-fsanitize=address选项
- Visual Studio的内存泄漏检测:Windows下使用_CrtDumpMemoryLeaks函数
- 智能指针的use_count:检查shared_ptr的引用计数是否符合预期
- 自定义内存分配器:重载new/delete运算符记录内存分配和释放,程序结束时检查是否有未释放的内存
避免内存泄漏的方法:
- 使用智能指针:用unique_ptr和shared_ptr代替裸指针,自动管理内存
- RAII原则:资源获取即初始化,在构造函数中分配资源,在析构函数中释放资源
- 使用容器:用vector、string等容器代替手动管理的数组
- 避免循环引用:shared_ptr循环引用时用weak_ptr打破循环
- 异常安全:使用智能指针或RAII确保异常发生时资源也能正确释放
- Code Review:团队成员互相审查代码,发现潜在的内存泄漏问题
5. 说说C++的多态是如何实现的?静态多态和动态多态有什么区别?
参考答案:
C++支持两种多态:静态多态(编译期多态)和动态多态(运行期多态)。
静态多态:
- 函数重载:同一作用域内多个同名函数,参数列表不同,编译器根据参数类型和数量选择调用哪个函数
- 模板:通过模板参数实例化不同的函数或类,编译期确定具体类型
- 运算符重载:为自定义类型定义运算符的行为
- 优点:编译期确定调用关系,没有运行时开销,性能高
- 缺点:缺乏灵活性,无法根据运行时类型动态选择
动态多态:
- 虚函数:基类声明虚函数,派生类重写虚函数,通过基类指针或引用调用时根据对象实际类型调用对应的函数
- 实现机制:编译器为每个包含虚函数的类生成虚函数表,对象中有vptr指向虚函数表,调用虚函数时通过vptr查表找到实际函数地址
- 优点:灵活性高,可以根据运行时类型动态选择函数,支持接口和抽象类
- 缺点:有运行时开销,需要查虚函数表,对象大小增加(vptr),不能内联优化
区别总结:
- 绑定时机:静态多态在编译期绑定,动态多态在运行期绑定
- 性能:静态多态性能高无运行时开销,动态多态有虚函数表查找开销
- 灵活性:静态多态灵活性低编译期确定,动态多态灵活性高运行时确定
- 使用场景:静态多态适合性能敏感的场景,动态多态适合需要运行时多态的场景如插件系统、框架设计
6. 说说智能指针的实现原理,shared_ptr是线程安全的吗?
参考答案:
智能指针通过RAII原则自动管理内存,在构造函数中获取资源,在析构函数中释放资源。
unique_ptr实现原理:
- 独占所有权,不允许拷贝只允许移动
- 内部持有一个裸指针,析构时delete该指针
- 移动构造和移动赋值时转移所有权,原指针置为nullptr
- 开销最小,几乎等同于裸指针
shared_ptr实现原理:
- 共享所有权,使用引用计数管理资源
- 内部有两个指针:一个指向管理的对象,一个指向控制块(包含引用计数、弱引用计数、删除器等)
- 拷贝构造和拷贝赋值时引用计数加1
- 析构时引用计数减1,当引用计数为0时释放资源
- 控制块单独分配,可能导致两次内存分配,使用make_shared可以优化为一次分配
weak_ptr实现原理:
- 弱引用,不增加引用计数,用于打破shared_ptr的循环引用
- 不能直接访问对象,需要通过lock()转换为shared_ptr
- 可以判断对象是否还存在,expired()返回true表示对象已被释放
shared_ptr的线程安全性:
- 引用计数的增减是线程安全的,使用原子操作保证
- 多个线程可以安全地拷贝和销毁shared_ptr
- 但是对shared_ptr管理的对象的访问不是线程安全的,需要额外的同步机制
- 对同一个shared_ptr对象的读写操作不是线程安全的,一个线程读另一个线程写会有数据竞争
- 总结:引用计数操作是线程安全的,但对象访问和shared_ptr本身的修改不是线程安全的
7. 说说左值和右值的区别,什么是移动语义?为什么需要移动语义?
参考答案:
左值和右值是C++中表达式的分类。
左值和右值的区别:
- 左值:有明确内存地址的对象,可以取地址,可以出现在赋值语句的左边,如变量、数组元素、返回左值引用的函数调用
- 右值:临时对象或字面量,没有明确的内存地址,不能取地址,只能出现在赋值语句的右边,如字面量、临时对象、返回值类型的函数调用
- 左值引用:用&声明,只能绑定到左值,const左值引用可以绑定到右值
- 右值引用:用&&声明,只能绑定到右值,用于实现移动语义和完美转发
移动语义:
- 定义:将资源的所有权从一个对象转移到另一个对象,而不是拷贝资源
- 实现:通过移动构造函数和移动赋值运算符实现,参数是右值引用
- 过程:将源对象的资源指针转移给目标对象,将源对象的指针置为nullptr,避免了深拷贝
- std::move:将左值转换为右值引用,强制使用移动语义
为什么需要移动语义:
- 性能优化:避免不必要的深拷贝,特别是对于管理大量资源的对象如容器、字符串
- 临时对象优化:函数返回对象时,使用移动语义避免拷贝,编译器会自动优化
- 资源管理:对于不可拷贝的资源如文件句柄、互斥锁,只能通过移动转移所有权
- 容器性能:vector扩容时使用移动语义可以大幅提升性能,避免拷贝所有元素
使用场景:
- 函数返回大对象时,编译器自动使用移动语义
- 容器插入临时对象时,使用移动避免拷贝
- 智能指针转移所有权时,unique_ptr只能移动不能拷贝
- 交换两个对象时,使用std::move提高效率
8. 说说HTTP和HTTPS的区别,HTTPS的性能开销在哪里?
参考答案:
HTTP和HTTPS的主要区别在于安全性和传输方式。
HTTP和HTTPS的区别:
- 安全性:HTTP是明文传输数据容易被窃听和篡改,HTTPS使用TLS/SSL加密传输数据安全性高
- 端口:HTTP默认使用80端口,HTTPS默认使用443端口
- 证书:HTTPS需要CA颁发的数字证书,HTTP不需要
- 连接建立:HTTP直接建立TCP连接,HTTPS需要先建立TCP连
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
本专栏系统梳理C++技术面试核心考点,涵盖语言基础、面向对象、内存管理、STL容器、模板编程及经典算法。从引用指针、虚函数表、智能指针等底层原理,到继承多态、运算符重载等OOP特性从const、static、inline等关键字辨析,到动态规划、KMP算法、并查集等手写实现。每个知识点以面试答题形式呈现,注重原理阐述而非冗长代码,帮助你快速构建完整知识体系,从容应对面试官提问,顺利拿下offer。
查看9道真题和解析