三七互娱 游戏开发-C++ 一面

1. 自我介绍

2. 这两个项目是你自己写的吗?

3. 指针和引用的区别是啥?

核心区别

(1)引用底层实现

语法层面引用是“变量别名”,底层编译器通过指针实现:

  • 编译时会为引用分配与指针相同的内存(如64位系统8字节),存储绑定变量的地址;
  • 运行时访问引用等同于通过指针解引用访问原变量(如int &a = b; a = 10;等价于int *p = &b; *p = 10;);
  • 区别仅在于编译器做了语法约束(如强制初始化、禁止修改指向),无额外性能开销。

(2)引用作为函数返回值的风险

风险点:

  1. 返回局部变量的引用:局部变量存在栈上,函数结束后栈帧销毁,引用指向非法内存(野引用);示例:int& func() { int a = 10; return a; } → 调用func()后引用指向已释放的栈空间;
  2. 返回临时对象的非const引用:临时对象生命周期仅在表达式内,引用悬空。

正确场景:

  • 返回类成员变量的引用(如std::string& operator[](int idx));
  • 返回全局/静态变量的引用。

4. 栈内存和堆内存的分配和释放

核心区别

(1)堆申请空间是真实内存还是虚拟内存

堆申请的是虚拟内存:

  • 操作系统为进程分配虚拟地址空间(如64位系统48位虚拟地址),new/malloc仅在虚拟地址空间中标记一段地址为“已分配”;
  • 只有当程序首次访问该虚拟地址时,操作系统才会通过MMU(内存管理单元)将虚拟地址映射到物理内存(真实内存),触发“缺页中断”;
  • 虚拟内存的优势:进程间地址隔离、突破物理内存大小限制。

(2)new和malloc的区别

(3)堆内存碎片化的原因及解决方案

碎片化原因:

  • 频繁申请/释放不同大小的堆内存块,导致内存空间被分割为大量不连续的小空闲块(外部碎片);
  • 内部碎片:分配的内存块大于实际需求(如malloc按8字节对齐,申请1字节会分配8字节)。

解决方案:

  1. 内存池:预分配大块连续堆内存,划分为固定大小的内存块,统一管理申请/释放,减少系统调用和碎片;
  2. 内存对齐:按固定大小(如16字节)分配,减少内部碎片;
  3. 碎片整理:空闲时合并相邻空闲块(内存池的“内存合并”逻辑);
  4. 使用智能指针:减少手动释放导致的内存块零散问题。

5. 怎么避免内存泄漏

核心方法

  1. 使用智能指针:替代裸指针管理堆内存,自动释放;
  2. 遵循RAII原则:将资源(内存/文件句柄/锁)封装到类中,构造时申请,析构时释放;
  3. 工具检测:用Valgrind、AddressSanitizer、Visual Leak Detector排查泄漏;
  4. 规范编码:配对使用new/delete、new[]/delete[],避免重复释放/悬空指针;
  5. 内存池:统一管理堆内存申请/释放,减少零散内存块。

(1)RAII思想 -> 智能指针

  • RAII(资源获取即初始化)核心:将资源的生命周期与对象绑定,对象创建时获取资源,对象销毁时自动释放资源;
  • 智能指针是RAII的典型实现:封装裸指针,析构函数中调用delete释放内存,无需手动管理。
① 智能指针有几种,有什么特点
② shared_ptr的循环引用是怎么回事
  • 循环引用场景:A的shared_ptr指向B,B的shared_ptr指向A → 双方引用计数互锁(均为1),析构时计数无法归0,对象无法释放,导致内存泄漏;
  • 解决方法:将其中一方的shared_ptr改为weak_ptr(如B中用weak_ptr<A>指向A),weak_ptr不增加引用计数,外部释放后计数可归0。
③ 智能指针和裸指针有什么优缺点
④weak_ptr的lock()方法返回空的场景及处理
  • lock()返回空的场景:weak_ptr观察的对象已被销毁(shared_ptr引用计数为0);
  • 处理方式:调用lock()前先通过expired()判断对象是否存活;lock()返回shared_ptr后判空,再访问对象,避免空指针访问:
std::weak_ptr<Obj> wp = ...;
if (auto sp = wp.lock()) { // lock()返回shared_ptr,非空则对象存活
    sp->do_something();
} else {
    // 处理对象已销毁的逻辑
}

6. C++的多态是怎么实现的

C++多态分为静态多态(编译期) 和动态多态(运行期):

(1)模板+函数重载(静态多态)

  • 函数重载:同一作用域内函数名相同、参数列表不同,编译器根据实参类型在编译期绑定函数;
  • 模板:template <typename T> T add(T a, T b),编译器根据传入类型实例化不同版本,编译期确定调用;
  • 核心:编译期确定调用的函数,无运行时开销。

(2)继承+虚函数

  • 实现步骤:父类声明虚函数(virtual void func());子类重写虚函数(void func() override);父类指针/引用指向子类对象,运行期调用子类重写的函数;
  • 底层原理:编译器为含虚函数的类生成虚表(vtable)(存储所有虚函数地址);类对象包含虚表指针(vptr),指向所属类的虚表;运行期通过vptr找到对应类的vtable,调用重写后的虚函数。

(3)虚函数存放在哪里

  • 虚函数的代码存放在代码区(text段)(与普通函数一致);
  • 虚函数的地址存放在虚表(vtable) 中,虚表属于类级别的数据,存放在全局/静态区(data段);
  • 虚表指针(vptr)存放在对象的内存布局中(通常是对象的第一个成员)。

(4)虚析构函数的底层原理及不写虚析构的坑

  • 底层原理:析构函数声明为virtual后,会被加入虚表;子类重写析构函数(编译器自动处理),运行期通过虚表调用子类析构函数,保证“先析构子类、再析构父类”。
  • 不写虚析构的坑:父类指针指向子类对象时,析构仅调用父类析构函数,子类析构函数不执行 → 子类资源泄漏(如子类堆内存未释放)
class Base { ~Base() {} }; // 非虚析构
class Derived : public Base { int *p = new int; ~Derived() { delete p; } };
Base *ptr = new Derived();
delete ptr; // 仅调用Base::~Base(),Derived的p未释放,内存泄漏

7. 用过什么排序或者你最常用的排序是什么

(推荐回答:快速排序,兼顾性能和实用性)我最常用的是快速排序,其次是归并排序和插入排序(小规模数据)。

(1)快速排序的原理是什么,简要说明一下

核心是“分治思想”,步骤:

  1. 选基准:从数组中选一个元素作为基准(pivot,通常选首/尾/中间元素);
  2. 分区:遍历数组,将小于基准的元素放到左区间,大于基准的放到右区间,基准归位;
  3. 递归:对左右区间重复上述步骤,直到区间长度为1(有序)。

(2)快速排序的最坏时间复杂度及优化方案

  • 最坏时间复杂度:​(如数组已排序,选首元素为基准,分区后左区间为空,右区间n-1个元素);
  • 优化方案:基准优化:选“首+中+尾”三个元素的中位数作为基准;小规模数据优化:区间长度小于10时,改用插入排序(避免递归开销);三路快排:将数组分为“小于基准、等于基准、大于基准”三部分,避免重复元素导致的性能退化;随机选基准:随机选取基准元素,降低最坏情况概率。

8. 设计模式用过哪些?

(推荐回答:创建型+结构型各1-2个,突出落地场景)我用过创建型的单例模式、简单工厂模式,结构型的适配器模式:

  • 单例模式:用于内存池、日志器(全局唯一实例);
  • 简单工厂模式:用于TCP服务器的客户端连接处理(根据协议类型创建不同的处理类);
  • 适配器模式:适配不同版本的第三方库接口。

(1)单例模式是怎么实现的

单例模式核心是“保证类仅有一个实例,提供全局访问点”,主要有两种实现:

  1. 饿汉模式:
class Singleton {
private:
    static Singleton instance; // 程序启动时初始化(全局区)
    Singleton() = default; // 私有化构造
    Singleton(const Singleton&) = delete; // 禁用拷贝
    Singleton& operator=(const Singleton&) = delete; // 禁用赋值
public:
    static Singleton& getInstance() { return instance; }
};
Singleton Singleton::instance; // 类外初始化

2. 懒汉模式:

class Singleton {
private:
    static std::unique_ptr<Singleton> instance;
    static std::mutex mtx;
    Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
public:
    static Singleton& getInstance() {
        if (!instance) { // 第一次检查(减少锁

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

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

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

全部评论
感谢分享,沾点喜气!
点赞 回复 分享
发布于 昨天 21:36 四川
虚表我记得存储在只读数据段吧
点赞 回复 分享
发布于 昨天 20:23 重庆

相关推荐

GGGGGGG,难死我得了,继续沉淀pulsar是什么模式的?怎么实现高性能的pulsar怎么保证消息不丢失的?消息积压怎么处理?怎么保证能榨干pulsar的性能?怎么保证消费的平衡?怎么通过并发去压榨pulsar的性能?拒绝策略怎么定义的,参数怎么去设置的?你知道并发和并行的区别吗?java中哪些工具是并发,哪些是并行的呢?有没有哪种是非阻塞的保证线程安全的?kafka是什么模式?了解事件驱动吗?不清楚是不是这个问题了io多路复用有了解吗?怎么实现高性能的?如果调用第三方网络超时了应该怎么处理?请求之后超时了你怎么确定你这次请求有没有改成功呢?重复请求你又怎么去保证数据的幂等性,防止幂等问题?有一个协议可以解决这个问题,你知道是什么协议吗?(TCP)当时脑子卡住了,没想起来,我是傻逼如果请求服务端出现大量的close_wait是什么原因?linux什么命令可以排查大量close_wait是什么导致的netty有了解过吗?不了解数据库查询很慢,你对索引分片等都已经做了优化,但还是很慢,怎么排查?数据库连接有调优过吗?redis分布式锁怎么实现的原理是什么?看门狗机制是什么?看门狗什么时候会失效?Redisession&nbsp;底层怎么实现的分布式锁?xxl-job和???定时有什么区别,了解底层调度原理吗?时间轮算法有了解吗?内存溢出怎么排查?第三方包的升级你知道升级了什么吗?怎么优化这个问题的?堆外内存溢出怎么排查是什么问题呢?ThreadLocal没有remove为什么会产生内存泄漏sharding&nbsp;的分库分表是出于什么原因要分库分表?分片键是什么?如果一个公司占用了90%的资源,那分库分表还有意义吗?怎么解决?没有反问&nbsp;G
点赞 评论 收藏
分享
评论
点赞
1
分享

创作者周榜

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