大湾区国创中心面经C++

张开发
2026/4/11 23:41:08 15 分钟阅读

分享文章

大湾区国创中心面经C++
这次的面试比上次好多了没那么僵硬可能是因为面试官没露脸又比较好说话过程首先是固有的自我介绍环节这次我事先准备了把项目大概的东西说了一遍还有获奖啥的。问题大部分应该是以下这些目录1.vector和list的区别和使用场景2.lambda表达式你有用到吗捕获列表有哪些3.虚函数表的使用原理4.智能指针有哪几种有什么区别5.weak_ptr主要用于解决什么问题6.有遇到过git的冲突问题吗7.Qt的自定义控件自定义界面8.互斥锁和读写锁区别9.展开说一下读锁和写锁10.有用过哪些设计模式11.单例模式有了解吗12.快速查找未读消息用的什么数据结构13.用python做过什么项目吗14.后面的职业规划是怎么样的1.vector和list的区别和使用场景我简单地回答了一下内存和操作开销方面的区别参考优先选vector的情况(默认首选)需要频繁读取数据比如通过下标访问元素。数据量波动不大或者主要在末尾添加数据。关注性能由于内存连续vector对 CPU 缓存非常友好实际运行速度通常比list快得多。优先选list的情况需要在中间频繁插入/删除比如实现一个撤销操作的历史记录。元素非常大且复杂移动元素的成本极高而list只需改指针。需要保证迭代器不失效即使删除其他节点指向某个节点的指针依然有效。2.lambda表达式你有用到吗捕获列表有哪些用了说了下在项目中的使用参考在 C 开发中Lambda 表达式几乎是必用的特别是在配合 STL 算法如sort,find_if和异步回调时它能让代码简洁得多不用专门去写个函数对象Functor。捕获形式含义[]不捕获。函数体内不能使用外部变量。[a]值捕获a。拷贝一份a进来内部修改不影响外部且默认是const的。[a]引用捕获a。内部修改直接影响外部性能高不拷贝。[]隐式值捕获所有。自动按值捕获函数体用到的所有局部变量。[]隐式引用捕获所有。自动按引用捕获所有用到的局部变量。[this]捕获当前类对象的指针。在成员函数内部使用可以访问成员变量和函数。3.虚函数表的使用原理调用虚函数时不直接看指针类型而是通过对象的 vptr 找到 vtable再查表调用对应函数 → 实现多态。参考1. 核心组成虚函数表 (vtable)编译器为每个拥有虚函数的类创建一个隐藏的静态数组。里面存放的是该类所有虚函数的地址。虚表指针 (vptr)编译器在每个对象的内存空间头部通常是前 8 字节偷偷插入一个指针指向该类对应的虚函数表。2. 运行时的“找路”过程当你通过基类指针调用虚函数时程序的执行逻辑如下找到对象顺着指针找到内存中的对象实例。找到虚表指针 (vptr)读取对象开头的地址。跳转到虚表 (vtable)通过 vptr 找到该类对应的函数地址表。取出函数地址根据函数在类中声明的顺序索引取出目标函数的真实地址。执行代码跳转到该地址执行。这就是“动态绑定”编译器在编译时不知道ptr-func()指向谁只知道去 vtable 的第 N 个槽位取地址。3. 继承与覆盖Override发生了什么基类虚表里存的是基类的函数地址 A、B、C。派生类没重写派生类的虚表会拷贝基类的地址。派生类重写了 funcA派生类虚表的第一个槽位会被替换成派生类自己的函数地址 D。因此当你用基类指针指向派生类对象时它拿到的vptr指向的是派生类的表自然就调用到了重写后的函数。4. 性能与内存代价内存开销每个类多了一张表通常很小。每个对象多了一个指针64位系统下占 8 字节这在创建海量小对象时会显著增加内存占用。时间开销多了一次间接寻址查表比直接调用普通函数慢一点点。无法被编译器内联Inline因为编译器在编译期定不下调用哪个函数。5. 一个直观的避坑点为什么构造函数不能是虚函数因为在构造函数执行时对象的vptr可能还没初始化好或者派生类部分还没构造查表没有意义。为什么析构函数必须是虚函数如果基类析构不是虚的通过基类指针delete派生类对象时程序只会调用基类的析构导致派生类特有的资源如vector或堆内存泄露。4.智能指针有哪几种有什么区别unique_ptr,shared_ptr,weak_ptr参考1.std::unique_ptr独占式核心特性它是独占的同一时间只能有一个unique_ptr指向该内存。区别不支持拷贝Copy只能移动Move。场景最常用。当你确定一个资源只有一个主人时如函数内部局部变量、类的成员变量优先选它。它几乎没有额外性能开销。2.std::shared_ptr共享式核心特性它是共享的内部维护一个引用计数Reference Count。区别支持拷贝。每多一个指针指向该内存计数加 1当计数减到 0 时自动释放内存。场景用于多个对象需要共同管理同一块内存的场景如复杂的图结构或多线程任务分发。代价由于要维护原子性的计数器性能比unique_ptr略低。3.std::weak_ptr辅助型核心特性它是shared_ptr的“旁观者”不增加引用计数。区别它不能直接操作内存必须先lock()转换成shared_ptr才能使用。它能感知指向的对象是否已被销毁。场景专门用来解决shared_ptr的循环引用死锁问题比如 A 指向 BB 也指向 A导致计数永远不归零。简单总结对比指针类型拥有权拷贝/移动引用计数常用场景unique_ptr独占仅移动无默认首选唯一控制权shared_ptr共享拷贝移动有资源需要被多处共享weak_ptr无拷贝移动不增加计数解决循环引用观察者5.weak_ptr主要用于解决什么问题shared_ptr循环引用参考1. 什么是循环引用想象两个类A和B互相持有对方的shared_ptr对象 A 内部有一个shared_ptr指向对象 B。对象 B 内部有一个shared_ptr指向对象 A。后果当外部指向 A 和 B 的指针都销毁时A 等待 B 释放才能销毁而 B 又等待 A 释放才能销毁。两者的引用计数永远保持为1析构函数永远不会被调用。2.weak_ptr是如何解决的weak_ptr是一种“不控制对象生命周期的智能指针”它指向一个由shared_ptr管理的对象但不增加引用计数。因为它不增加计数所以不会阻止对象被销毁。只要把其中一个类的成员变量改为weak_ptr循环引用就被打破了。3. 代码示例对比会导致泄漏的情况struct B; struct A { shared_ptrB ptrB; }; struct B { shared_ptrA ptrA; };使用weak_ptr解决struct B; struct A { shared_ptrB ptrB; }; struct B { weak_ptrA ptrA; }; // 改为 weak_ptr4. 另一个重要用途安全观测除了解决循环引用weak_ptr还常用于观察者模式或缓存你想知道一个对象是否还活着但你不想延长它的寿命。通过wp.expired()可以检查对象是否已销毁。通过wp.lock()可以临时获取一个shared_ptr来安全地访问对象如果对象已死返回空指针。6.有遇到过git的冲突问题吗没有拉取新内容本地修改直接推送参考太常遇到了在多人协作或者自己开多分支开发时Git 冲突Conflict几乎是日常。简单来说冲突发生的根本原因是两个人或两个分支修改了同一个文件的同一行代码Git 不知道该听谁的。什么时候会触发冲突Merge/Pull想把别人的代码合并进来时。Rebase想把自己的提交“嫁接”到新的基点时。Stash pop弹出之前暂存的代码发现现在的代码已经变了。后面是一些项目问题7.Qt的自定义控件自定义界面不太记得了瞎说参考在 Qt 开发中自定义控件是进阶的必经之路。当标准库里的QPushButton或QProgressBar满足不了视觉UI或逻辑要求时我们通常有三种实现方案。根据你的需求深度可以从这三个方向入手1. 样式表优化最快换皮如果只是想改颜色、边框、圆角不需要改变控件行为直接用QSS (Qt Style Sheets)。做法类似 CSS写QPushButton { background-color: red; border-radius: 5px; }。适用简单的皮肤更换。2. 提升/继承现有控件最常用增强如果你想在现有控件基础上加点功能比如让QLineEdit只能输入 IP 地址或者给QLabel增加点击事件。做法新建一个类继承自QPushButton或其他。重写Override事件处理函数如mousePressEvent、paintEvent。UI 界面关联在 Qt Designer 里右键点击原始控件选择“提升为... (Promote to...)”填入你的自定义类名。适用逻辑增强外观微调。3. 完全从零绘制最强换骨如果你要写一个非常酷炫的仪表盘、环形进度条或动态波形图就需要重写paintEvent。4. 自定义复杂界面组合控件如果你想把一堆按钮、输入框封装成一个独立的组件比如一个带搜索框的侧边栏。做法新建一个Qt Designer Form Class带.ui文件。在里面摆放好子控件。对外暴露信号Signal和槽Slot隐藏内部实现细节8.互斥锁和读写锁区别互斥锁Mutex和读写锁Read-Write Lock的核心区别在于对共享资源访问控制的粒度。简单来说互斥锁是“绝对独占”而读写锁是“读共享、写独占”。1. 核心区别对比特性互斥锁 (Mutex)读写锁 (RWLock / Shared Mutex)访问规则独占访问同一时刻只允许一个线程访问无论是读还是写。读写分离允许多个读者同时访问但写者必须独占访问。并发性低读操作也会互相阻塞。高在读多写少的场景下大幅提升吞吐量。性能开销低算法简单加锁/解锁速度极快。高需要维护读者计数逻辑更复杂单次加锁比互斥锁慢。潜在问题竞争剧烈时可能导致严重的线程排队。可能存在写者饥饿读者太多导致写者一直等不到锁。2. 使用场景建议优先使用 互斥锁 (std::mutex)写操作频繁如果读写比例接近如 1:1或写多读少互斥锁的低开销更占优势。临界区极短如果只是保护一个简单的变量赋值互斥锁通常比读写锁更快。追求稳定性互斥锁逻辑简单不容易出现死锁或饥饿问题。优先使用 读写锁 (std::shared_mutex)读多写少典型 10:1 以上例如配置文件的读取、数据库缓存查找。读操作耗时较长当多个线程需要同时进行耗时的读操作时读写锁能让它们并行运行不至于互相阻塞。3. C 对应实现互斥锁使用std::mutex配合std::lock_guard或std::unique_lock。读写锁C17 引入了std::shared_mutex。读模式使用std::shared_lockstd::shared_mutex。写模式使用std::unique_lockstd::shared_mutex。9.展开说一下读锁和写锁1. 读锁 (Shared Lock / 共享锁)读锁的核心逻辑是“既然大家都不修改数据那就一起看。”共享性当一个线程拿到了读锁其他线程也可以同时拿到读锁。此时多个线程可以并行读取数据互不阻塞。排他性如果此时有线程想申请“写锁”它必须挂起等待直到所有持有读锁的线程都释放为止。代码实现在 C 中使用std::shared_lockstd::shared_mutex lock(mutex);。2. 写锁 (Unique Lock / 排他锁)写锁的核心逻辑是“我要改数据谁都别靠近。”完全排他一旦写锁被占用任何其他线程无论是想读还是想写都无法获取锁必须全部排队等待。唯一性同一时刻写锁只能被一个线程持有。代码实现在 C 中使用std::unique_lockstd::shared_mutex lock(mutex);。3. 它们之间的“相处规则”当前状态申请读锁申请写锁无锁成功成功已持有读锁成功(共享)失败(等待所有读锁释放)已持有写锁失败(等待写锁释放)失败(等待写锁释放)4. 关键特性写者优先 vs 读者优先这是读写锁最容易出坑的地方。想象一个极端场景读操作非常多一个接一个读锁从未被完全释放过。此时一个写操作过来了。读者优先只要有读的需求就一直给读锁。结果写操作可能被“饿死”永远拿不到锁。写者优先现代操作系统的常见实现当有写操作在排队时后续新来的读请求会被阻塞直到写操作完成。这样能保证数据被及时更新。5. 什么时候该换用它们读锁的优势提高并发度。例如在一个“配置中心”里100个线程都在频繁读配置只有1个线程偶尔改配置用读锁能让这100个线程像没加锁一样飞快。写锁的优势保证原子性和数据一致性防止“一边改一边读”导致的数据损坏。10.有用过哪些设计模式MVC、工厂模式在 C 和 Qt 开发中设计模式不是为了套公式而是为了解决耦合和代码复用问题。我常用的有以下几种它们在实际工程中非常经典1. 单例模式 (Singleton)场景全局唯一的资源管理如数据库连接池、配置管理器或 Qt 中的日志模块。实现要点在 C11 以后最推荐使用局部静态变量Meyers Singleton既简洁又是线程安全的。static ConfigManager getInstance() { static ConfigManager instance; return instance; }2. 观察者模式 (Observer)场景数据更新通知。当底层数据变化时UI 界面自动刷新。Qt 里的实现信号与槽Signals Slots本质上就是一种高度封装、松耦合的观察者模式。原生 C通常维护一个listObserver*遍历通知update()。3. 工厂模式 (Factory)场景当你需要根据不同条件创建不同的控件时。比如根据配置文件加载不同的“皮肤主题”或“通信协议TCP/UDP”。优势解耦了对象的使用和创建新增一种类型时不需要修改调用方的代码。4. 适配器模式 (Adapter)场景旧代码复用。比如你有一个旧的算法库接口但现在的项目定义了一套新的接口标准。做法写一个包装类Wrapper把旧接口“翻译”成新接口。在对接第三方库如 OpenCV 或 FFMpeg到 Qt 界面时经常用到。5. 策略模式 (Strategy)场景算法切换。比如你的程序支持多种排序方式或者多种文件导出格式PDF, CSV, Excel。优势避免了冗长的if-else或switch-case将每种算法封装成独立的类运行时动态切换。6. Pimpl 模式Private Implementation场景这是C 特有的设计模式在 Qt 源码D-Pointer中大量使用。作用将类的私有数据成员放到一个单独的实现类中头文件只包含指针。好处二进制兼容修改私有成员不需要重新编译调用方的代码。编译加速减少了头文件包含。11.单例模式有了解吗用过但是不熟12.快速查找未读消息用的什么数据结构只记得用过哈希表了1. 追求“查找速度”和“展示顺序”std::liststd::unordered_map这是最经典的组合类似 LRU Cache 的思路list(双向链表)按时间顺序存放未读消息对象。新消息push_back读取消息时直接从链表中删除。unordered_mapMessageID, list::iterator哈希表里存消息 ID 到链表节点的指针/迭代器。优势查找通过 ID 瞬时定位消息是否未读。展示遍历 list 即可得到按时间排序的未读列表。2. 追求“范围查找”如某时间段内的未读std::set或std::map原理底层是红黑树按消息时间戳Timestamp排序。优势可以非常快地找到“最早的一条未读”或“某个时间段的所有未读”。3. 追求“极小空间开销”std::vector 位图Bitset原理如果消息 ID 是连续的整数可以用一个巨大的std::bitset。1代表未读0代表已读。优势内存占用极低判断某条消息状态极快。但缺点是不方便存储消息的具体内容只适合做“状态标记”。4. 社交软件常见场景基于“已读回执”的偏移量Offset在像微信或钉钉这样的系统中通常不会为每条消息存一个“是否已读”的状态而是数据结构每个会话存一个last_read_message_id最后一条已读 ID。逻辑所有ID last_read_message_id的消息即为未读。优势大幅减少数据库存储压力查找未读只需一条范围查询语句。5. 客户端 UI 实时统计哈希计数Unordered Map数据结构unordered_mapChatID, int unread_counts;场景主界面左侧侧边栏显示的红色数字如张三 [5]。逻辑新消息来时count点开会话count 0。13.用python做过什么项目吗14.后面的职业规划是怎么样的

更多文章