目录
一、友塔一面游戏开发面经解析
(一)职业选择相关问题
- 看你的项目和游戏没什么太多关系,为什么想要投递我们的游戏岗位?
- 答案:可以表达对游戏行业的热爱和兴趣,提及游戏的魅力和影响力。同时,可以强调自己对游戏开发技术的好奇和学习的热情,以及认为自己的技能和能力可以在游戏开发中得到应用和发展。
(二)项目相关问题
- 你在里面主要负责什么工作?
- 答案:详细介绍自己在项目中的具体职责,包括技术选型、系统设计、开发实现、测试调试等方面的工作。
- 给我介绍一下你这个系统大概用到什么样的技术?
- 答案:介绍项目中使用的主要技术,包括编程语言、框架、数据库、中间件等。同时,可以提及一些技术的特点和优势,以及在项目中的应用场景。
(三)多线程相关问题
- 项目中什么情况下会使用多线程?
- 答案:多线程可以在以下情况下使用:需要同时处理多个任务时,如同时进行网络通信、文件读写、计算等;提高程序的响应速度,避免单线程阻塞导致用户界面卡顿;充分利用多核处理器的性能,提高程序的执行效率。
- 你们的队列是用锁实现的,如果我想做一个无锁的队列要怎么做呢?
- 答案:无锁队列可以通过原子操作和内存屏障来实现。例如,可以使用 CAS(Compare and Swap)操作来实现队列的入队和出队操作,避免使用锁。同时,可以使用内存屏障来保证线程之间的内存可见性。
- C++ 有线程安全的队列吗?
- 答案:C++ 标准库中没有提供线程安全的队列,但是可以使用第三方库,如 Boost 中的线程安全队列。也可以自己实现线程安全的队列,使用锁、原子操作和条件变量等机制来保证线程安全。
(四)哈希 map 相关问题
- 为什么你们用哈希 map?
- 答案:哈希 map 具有快速的查找、插入和删除操作,可以在平均 O (1) 的时间复杂度内完成这些操作。在项目中,如果需要快速查找和存储键值对数据,可以选择使用哈希 map。
(五)多线程和多进程相关问题
- 多线程和多进程有什么区别?
- 答案:多线程和多进程的区别主要包括以下几个方面:
- 资源共享:多线程共享进程的内存空间和系统资源,多进程拥有独立的内存空间和系统资源。
- 开销:多线程的创建和切换开销相对较小,多进程的创建和切换开销较大。
- 通信方式:多线程可以直接访问共享的内存空间进行通信,多进程需要使用进程间通信机制,如管道、消息队列、共享内存等。
- 稳定性:多进程相对更加稳定,一个进程的崩溃不会影响其他进程,多线程中一个线程的崩溃可能会导致整个进程崩溃。
- 答案:多线程和多进程的区别主要包括以下几个方面:
(六)父子进程相关问题
- 如果有一个父进程一个子进程,父进程奔溃了,子进程会怎么样?
- 答案:在大多数操作系统中,父进程崩溃不会直接导致子进程崩溃。子进程会继续运行,但是可能会受到一些影响,如失去父进程的控制、无法获取父进程的资源等。具体的行为取决于操作系统的实现和子进程的设置。
(七)Linux 系统相关问题
- linux 系统,想知道某个进程有多少连接,怎么看?
- 答案:可以使用一些工具来查看进程的连接数量,如 netstat、lsof 等。这些工具可以显示进程的网络连接信息,包括连接的状态、协议、端口等。通过分析这些信息,可以统计出进程的连接数量。
(八)C++ 面向对象相关问题
- C++ 面向对象有什么概念?
- 答案:C++ 面向对象的概念包括封装、继承、多态。封装是将数据和操作封装在类中,通过访问修饰符控制对类成员的访问,提高代码的安全性和可维护性。继承是子类继承父类的属性和方法,实现代码的复用,同时可以在子类中进行扩展和重写。多态是同一操作作用于不同的对象可以有不同的表现形式,通过方法重写和方法重载实现。
- C++ 的参数列表是什么?
- 答案:C++ 的参数列表是函数或方法的参数声明列表,用于指定函数或方法接受的参数类型和名称。参数列表可以包含不同类型的参数,如基本数据类型、指针、引用、自定义类型等。
(九)STL 相关问题
- STL 主要用过哪些数据结构?
- 答案:STL(Standard Template Library)提供了多种数据结构,如 vector、list、deque、set、map、unordered_set、unordered_map 等。可以根据自己的项目经验,介绍使用过的 STL 数据结构,并说明其应用场景和优点。
- STL 里有个叫 allocator 的东西,就是分配器,我们一般分配内存有 malloc,有 new,为什么 STL 有自己的一个 allocator 这个东西?
- 答案:STL 的分配器(allocator)是为了提供一种灵活的内存分配方式,可以根据不同的需求定制内存分配策略。与 malloc 和 new 相比,STL 的分配器可以更好地管理内存,提高内存的利用率和性能。同时,分配器可以与容器紧密结合,实现高效的内存管理和容器的扩展。
- malloc 和 new 有什么区别?
- 答案:malloc 和 new 的区别主要包括以下几个方面:
- 功能:malloc 是 C 语言中的内存分配函数,只负责分配内存,不进行对象的构造。new 是 C++ 中的运算符,不仅负责分配内存,还会调用对象的构造函数进行对象的初始化。
- 返回值:malloc 返回 void* 类型的指针,需要进行类型转换。new 返回对象的指针,不需要进行类型转换。
- 异常处理:malloc 分配内存失败时返回 NULL,可以通过判断返回值来处理内存分配失败的情况。new 在分配内存失败时会抛出 std::bad_alloc 异常,可以通过捕获异常来处理内存分配失败的情况。
- 答案:malloc 和 new 的区别主要包括以下几个方面:
(十)内存相关问题
- 一个进程用 malloc 申请了一块内存,这个对进程的地址空间有什么影响?(想听我说的越多越好)
- 答案:当一个进程用 malloc 申请了一块内存时,会对进程的地址空间产生以下影响:
- 增加进程的虚拟内存大小:malloc 分配的内存是在进程的虚拟地址空间中,申请内存会增加进程的虚拟内存大小。
- 可能导致内存碎片:如果频繁地申请和释放内存,可能会导致内存碎片的产生,降低内存的利用率。
- 影响内存管理:进程需要管理分配的内存,包括记录内存的使用情况、释放不再使用的内存等。如果管理不当,可能会导致内存泄漏或错误的内存访问。
- 答案:当一个进程用 malloc 申请了一块内存时,会对进程的地址空间产生以下影响:
(十一)进程间通信相关问题
- 进程间通信的方法?
- 答案:进程间通信的方法包括管道、消息队列、共享内存、信号量、套接字等。可以介绍这些方法的特点和应用场景。
- 匿名管道和命名管道在使用上的区别?
- 答案:匿名管道和命名管道的区别主要包括以下几个方面:
- 命名方式:匿名管道没有名称,只能在父子进程或有亲缘关系的进程之间使用。命名管道有名称,可以在不同的进程之间使用。
- 通信方式:匿名管道是单向通信,只能在一个方向上传输数据。命名管道可以是双向通信,也可以是单向通信。
- 创建方式:匿名管道由系统自动创建,不需要显式地创建。命名管道需要显式地创建,可以使用 mkfifo 命令或系统调用创建。
- 答案:匿名管道和命名管道的区别主要包括以下几个方面:
- 在 linux 里,管道实际上是一个什么东西?
- 答案:在 Linux 中,管道实际上是一种特殊的文件,它在内核中实现了一种先进先出(FIFO)的队列数据结构。管道可以在两个进程之间传递数据,一个进程向管道写入数据,另一个进程从管道读取数据。
(十二)MySQL 相关问题
- 在建一张 mysql 表的时候,你会关注哪些问题?
- 答案:在建一张 MySQL 表时,需要关注以下问题:
- 表的结构设计:包括字段的类型、长度、约束等。要根据实际需求选择合适的数据类型,避免浪费空间或数据类型不匹配的问题。同时,要设置合理的约束,如主键、唯一键、外键等,保证数据的完整性和一致性。
- 索引的设计:根据查询需求设计合适的索引,提高查询性能。要选择合适的字段作为索引,避免过多的索引导致插入和更新操作变慢。
- 数据的存储引擎:MySQL 支持多种存储引擎,如 InnoDB、MyISAM 等。要根据实际需求选择合适的存储引擎,考虑数据的存储方式、事务支持、并发性能等因素。
- 答案:在建一张 MySQL 表时,需要关注以下问题:
- 怎么让联合索引更有效率?
- 答案:要让联合索引更有效率,可以考虑以下几点:
- 选择合适的字段:选择经常用于查询条件的字段作为联合索引的字段,并且字段的顺序要根据查询的频率和选择性来确定。一般来说,选择性高的字段放在前面,可以提高索引的过滤效果。
- 避免索引覆盖:如果查询只需要联合索引中的字段,就可以避免回表操作,提高查询性能。可以通过合理的设计表结构和查询语句,尽量让查询只使用联合索引中的字段。
- 避免索引失效:在使用联合索引时,要注意查询条件的写法,避免索引失效。例如,不要在索引字段上进行函数操作、类型转换、范围查询等,这些操作可能会导致索引失效。
- 答案:要让联合索引更有效率,可以考虑以下几点:
- 比如现在有三个字段,一个玩家的服务器 ID,一个是玩家的年龄,第三个就是玩家的名字,三个字段我可能要建一个联合索引,这三个词段怎么建一个联合索引,你觉得它是比较合理的?
- 答案:可以根据查询的需求来确定联合索引的字段顺序。如果经常根据服务器 ID 进行查询,可以将服务器 ID 放在联合索引的最左边;如果经常根据年龄进行查询,可以将年龄放在联合索引的中间;如果经常根据名字进行查询,可以将名字放在联合索引的最右边。同时,要考虑字段的选择性和查询的频率,选择最适合的字段顺序。
(十三)TCP 相关问题
- tcp 四次挥手讲一下?
- 答案:TCP 四次挥手的过程如下:
- 第一次挥手:客户端发送一个 FIN 报文,请求关闭连接,此时客户端进入 FIN_WAIT_1 状态。
- 第二次挥手:服务器收到 FIN 报文后,发送一个 ACK 报文,确认收到客户端的关闭请求,此时服务器进入 CLOSE_WAIT 状态。客户端收到 ACK 报文后,进入 FIN_WAIT_2 状态。
- 第三次挥手:服务器发送一个 FIN 报文,请求关闭连接,此时服务器进入 LAST_ACK 状态。
- 第四次挥手:客户端收到 FIN 报文后,发送一个 ACK 报文,确认收到服务器的关闭请求,此时客户端进入 TIME_WAIT 状态。服务器收到 ACK 报文后,连接关闭。客户端在 TIME_WAIT 状态等待一段时间后,连接也关闭。
- 答案:TCP 四次挥手的过程如下:
- 一个 http 连接是怎么保持长连接的?
- 答案:在 HTTP 1.1 中,可以通过设置 Connection 头部字段为 keep-alive 来保持长连接。当客户端和服务器建立连接后,如果双方都支持长连接,就可以在一个连接上发送多个请求和响应,避免频繁地建立和关闭连接,提高性能。
- 我们游戏基本都是长连接,我们怎么维护 tcp 的长连接呢?
- 答案:维护 TCP 长连接可以采取以下措施:
- 心跳机制:客户端和服务器定期发送心跳包,保持连接的活跃状态。如果在一定时间内没有收到对方的心跳包,就认为连接已经断开,需要重新建立连接。
- 超时重连:如果连接断开,客户端可以自动尝试重新建立连接,直到连接成功。
- 异常处理:在连接过程中,如果出现网络异常、服务器崩溃等情况,需要及时处理,避免影响游戏的正常运行。
- 答案:维护 TCP 长连接可以采取以下措施:
- tcp 保活的探测报文是服务端发送的还是客户端发送的?
- 答案:TCP 保活的探测报文可以由客户端或服务器发送,具体取决于实现。一般来说,服务器会定期发送探测报文,检查客户端的连接状态。如果客户端在一定时间内没有响应,服务器就认为连接已经断开,关闭连接。
- tcp 在哪一层?
- 答案:TCP(Transmission Control Protocol)位于传输层,它提供了可靠的数据传输服务,确保数据的顺序、完整性和可靠性。
(十四)应用层协议相关问题
- 如果我们要基于 tcp 自己实现一个应用层的协议,我们可能会考虑哪些事情?这个你随便你说了,看你对这个东西了解多少了?
- 答案:基于 TCP 实现一个应用层协议需要考虑以下几个方面:
- 协议的功能和需求:确定协议的目的和功能,例如数据传输、命令控制、状态同步等。根据需求设计协议的消息格式和通信流程。
- 消息格式:设计协议的消息格式,包括消息头和消息体。消息头可以包含消息的类型、长度、校验和等信息,消息体可以包含具体的数据内容。
- 错误处理:考虑协议在传输过程中可能出现的错误情况,如数据丢失、重复、顺序错误等。设计相应的错误处理机制,如重传、校验和验证、序号管理等。
- 性能优化:考虑协议的性能问题,如减少消息的大小、提高传输效率、降低延迟等。可以采用压缩、缓存、批量处理等技术来优化协议的性能。
- 安全性:如果协议需要保证数据的安全性,可以考虑加密、认证、授权等安全机制。
- 兼容性:考虑协议的兼容性问题,确保不同版本的协议能够相互通信。可以采用版本号管理、向后兼容等技术来保证协议的兼容性。
- 答案:基于 TCP 实现一个应用层协议需要考虑以下几个方面:
(十五)HTTPS 相关问题
- https 的加密是对称还是非对称的?
- 答案:HTTPS(Hypertext Transfer Protocol Secure)的加密是同时使用对称加密和非对称加密。在建立连接时,使用非对称加密算法(如 RSA)交换对称加密的密钥,然后使用对称加密算法(如 AES)进行数据的加密传输。这样可以结合非对称加密的安全性和对称加密的高效性。
(十六)职业选择相关问题
- 你为什么想要进入游戏行业?因为其实游戏行业比较封闭,选择了这个行业就比较难进别的行业。
- 答案:可以表达对游戏行业的热爱和兴趣,提及游戏的魅力和影响力。同时,可以强调自己对游戏开发技术的好奇和学习的热情,以及认为自己在游戏行业中可以发挥自己的技能和能力,实现个人价值。对于游戏行业的封闭性,可以表示自己已经充分考虑了这个问题,并且愿意在游戏行业中不断学习和成长,适应行业的发展和变化。
二、网易游戏引擎组外包技术一面面经解析
(一)项目细节追问
根据自己的项目经验,详细回答面试官的问题,包括项目的技术选型、系统设计、开发实现、测试调试等方面的工作。
(二)面向对象相关问题
- 封装继承多态,分别的含义,追问面向对象和面向过程的区别,保护继承的用法。
- 答案:
- 封装:将数据和操作封装在类中,通过访问修饰符控制对类成员的访问,提高代码的安全性和可维护性。
- 继承:子类继承父类的属性和方法,实现代码的复用,同时可以在子类中进行扩展和重写。
- 多态:同一操作作用于不同的对象可以有不同的表现形式,通过方法重写和方法重载实现。
- 面向对象和面向过程的区别:面向对象编程强调将数据和操作封装在对象中,通过对象之间的交互来完成任务。面向过程编程强调按照步骤和流程来完成任务,数据和操作通常是分离的。面向对象编程具有更高的可维护性、可扩展性和可复用性,但是性能可能相对较低。面向过程编程性能较高,但是可维护性、可扩展性和可复用性相对较低。
- 保护继承的用法:保护继承是一种继承方式,子类可以访问父类的保护成员,但是不能访问父类的私有成员。保护继承可以在一定程度上保护父类的实现细节,同时允许子类进行扩展和重写。
- 答案:
(三)友元相关问题
- 外部对象如何访问类内的保护和私有成员 --- 友元。
- 答案:在 C++ 中,可以使用友元函数和友元类来让外部对象访问类内的保护和私有成员。友元函数是在类中声明的非成员函数,可以访问类的私有和保护成员。友元类是在类中声明的另一个类,可以访问类的私有和保护成员。但是,使用友元会破坏类的封装性,应该谨慎使用。
(四)多继承相关问题
- C++ 支持多继承吗,菱形继承的问题,如何处理,虚继承后对基类的变量初始化几次?
- 答案:C++ 支持多继承。菱形继承是指一个子类继承自两个或多个父类,而这些父类又有共同的基类,可能导致基类成员在子类中出现多份副本的问题。
- 处理菱形继承的方法是使用虚继承。虚继承可以确保子类中只有一份基类的副本。在使用虚继承时,基类的构造函数会在最终的子类中被调用一次,以确保基类的成员只被初始化一次。
(五)多态相关问题
- 对多态的理解。
- 答案:多态是指同一操作作用于不同的对象可以有不同的表现形式。在 C++ 中,多态主要通过虚函数实现。通过在基类中声明虚函数,在派生类中重写虚函数,然后通过基类指针或引用调用虚函数时,实际调用的是派生类中的重写函数,从而实现了多态。多态可以提高代码的可扩展性和可维护性,使得程序更加灵活。
(六)虚函数相关问题
-
基类可以不实现虚函数吗?
- 答案:基类可以不实现虚函数。如果基类中的虚函数没有被实现,那么在派生类中必须实现该虚函数,否则在调用该虚函数时会出现未定义的行为。
-
虚函数的底层原理,多个派生类实例,它的虚函数表是一样的吗,为什么一样?
- 答案:虚函数的底层原理是通过在类中添加一个虚函数表(vtable)来实现的。每个包含虚函数的类都有一个虚函数表,其中存储了该类的虚函数地址。当通过基类指针或引用调用虚函数时,程序会根据对象的实际类型在虚函数表中查找对应的函数地址并调用。多个派生类实例的虚函数表不一定完全一样,因为派生类可以重写基类的虚函数,所以派生类的虚函数表中可能会有不同的函数地址。但是,如果派生类没有重写基类的虚函数,那么它们的虚函数表中对应位置的函数地址是相同的。
(七)构造函数和析构函数相关问题
-
构造函数、析构函数能不能设为虚函数?
- 答案:构造函数不能设为虚函数。因为在构造函数执行时,对象的类型还没有完全确定,无法进行虚函数调用。析构函数可以设为虚函数,特别是当基类的指针或引用指向派生类对象时,通过将基类的析构函数设为虚函数,可以确保在删除对象时正确地调用派生类的析构函数,防止内存泄漏。
-
析构函数和构造函数的调用顺序;
- 答案:在创建对象时,先调用基类的构造函数,然后按照继承层次依次调用派生类的构造函数。在销毁对象时,先调用派生类的析构函数,然后按照继承层次依次调用基类的析构函数。
(八)场景题相关问题
- 场景题,基类的析构函数中调用了虚函数(printA),派生类重写了这个虚函数(printB),派生类的析构也调用了这个虚函数;对象发生了析构,调用的顺序是怎样的,会如何打印?
- 答案:当对象发生析构时,先调用派生类的析构函数,在派生类的析构函数中会先调用派生类重写的虚函数(printB),然后再按照继承层次依次调用基类的析构函数。在基类的析构函数中又会调用虚函数,由于此时对象的类型已经确定为基类,所以会调用基类中的虚函数(printA)。具体的打印顺序取决于虚函数的实现。
(九)静态和动态相关问题
- 静态和动态的区别。
- 答案:静态和动态在不同的上下文中有不同的含义。在 C++ 中,静态通常指静态成员变量和静态成员函数。静态成员变量属于类而不属于类的任何对象,在整个程序的生命周期内只有一份副本。静态成员函数可以直接通过类名调用,不需要通过对象调用。动态通常指通过指针或引用来操作对象,在运行时确定对象的类型和行为。动态绑定是多态的实现基础,通过虚函数实现。
(十)函数重载相关问题
- 函数重载时,(int a = 3, int b), (int b) 这样可以形成函数重载吗 -- 不行。(int a), (const int a) 可以形成重载吗 -- 可以。
- 答案:函数重载是指在同一个作用域内,可以有多个函数具有相同的函数名,但参数列表不同。对于 (int a = 3, int b) 和 (int b),它们的参数列表实际上是相同的,因为第一个函数中的默认参数可以在调用时不提供,所以这两个函数不能形成重载。对于 (int a) 和 (const int a),它们的参数类型不同,一个是普通的 int 类型,一个是常量 int 类型,所以可以形成重载。
(十一)容器相关问题
-
vector 和 list 的区别。
- 答案:vector 和 list 都是 C++ 标准库中的容器。它们的主要区别如下:
- 内存分配方式:vector 采用连续的内存空间存储元素,当需要增加容量时,可能需要重新分配内存并复制元素。list 采用链表结构存储元素,内存分配比较灵活,不需要连续的内存空间。
- 随机访问性能:vector 支持随机访问,可以通过下标在常量时间内访问元素。list 不支持随机访问,只能通过迭代器依次访问元素。
- 插入和删除操作性能:在 vector 的中间位置进行插入和删除操作可能需要移动大量元素,时间复杂度较高。在 list 的任意位置进行插入和删除操作只需要修改指针,时间复杂度为常量。
- 答案:vector 和 list 都是 C++ 标准库中的容器。它们的主要区别如下:
-
vector 在头部插入和尾部插入删除的时间复杂度,list 呢?
- 答案:vector 在尾部插入和删除元素的时间复杂度通常为 amortized 常量时间,但在头部插入和删除元素可能需要移动大量元素,时间复杂度为线性。list 在任意位置插入和删除元素的时间复杂度为常量。
-
vector 容器是线程安全的吗,为什么,一个线程在遍历 vector,另一个线程在删除其中的元素,会出现问题吗?
- 答案:vector 容器不是线程安全的。多个线程同时访问和修改 vector 可能会导致数据不一致或未定义的行为。如果一个线程在遍历 vector,另一个线程在删除其中的元素,可能会导致迭代器失效、访问非法内存等问题。
(十二)链表相关问题
- 如何找出一个链表有环?
- 答案:可以使用快慢指针的方法来找出链表是否有环。设置两个指针,一个慢指针每次移动一步,一个快指针每次移动两步。如果链表有环,那么快指针一定会在某个时刻追上慢指针。如果快指针到达链表末尾仍然没有追上慢指针,说明链表没有环。
(十三)容器相关问题
- unordered_map 和 map 的区别。
- 答案:unordered_map 和 map 都是 C++ 标准库中的关联容器,用于存储键值对。它们的主要区别如下:
- 内部实现:map 通常基于红黑树实现,保证了元素的有序性。unordered_map 基于哈希表实现,不保证元素的有序性。
- 查找性能:在一般情况下,unordered_map 的查找性能比 map 更好,因为哈希表可以在平均常量时间内完成查找操作,而红黑树的查找时间复杂度为对数级别。但是,在某些情况下,由于哈希冲突等原因,unordered_map 的性能可能会下降。
- 内存占用:unordered_map 通常需要更多的内存空间,因为哈希表需要存储额外的哈希值和链表指针等信息。
- 答案:unordered_map 和 map 都是 C++ 标准库中的关联容器,用于存储键值对。它们的主要区别如下:
(十四)红黑树相关问题
- 红黑树的实现原理,和平衡二叉树的区别。
- 答案:红黑树是一种自平衡的二叉查找树,它通过对节点颜色的约束来保证树的平衡。红黑树的实现原理包括以下几个方面:
- 节点颜色:每个节点都有一个颜色属性,要么是红色,要么是黑色。
- 规则约束:红黑树有五条规则,分别是:每个节点要么是红色,要么是黑色;根节点是黑色;叶子节点(NIL 节点)是黑色;如果一个节点是红色,那么它的两个子节点都是黑色;从任意一个节点到其每个叶子节点的所有路径上都包含相同数量的黑色节点。
- 红黑树和平衡二叉树的区别:
- 平衡方式:平衡二叉树通过旋转操作来保持平衡,而红黑树通过对节点颜色的调整来保持平衡。
- 时间复杂度:在一般情况下,红黑树和平衡二叉树的查找、插入和删除操作的时间复杂度都是 O (log n),但红黑树的性能更加稳定,因为它不需要频繁地进行旋转操作。
- 内存占用:红黑树通常需要更多的内存空间,因为它需要存储节点的颜色信息。
- 答案:红黑树是一种自平衡的二叉查找树,它通过对节点颜色的约束来保证树的平衡。红黑树的实现原理包括以下几个方面:
(十五)智能指针相关问题
-
常见的智能指针,shared_ptr 底层是如何实现的,weak_ptr 的用法。
- 答案:常见的智能指针有 shared_ptr、unique_ptr 和 weak_ptr。shared_ptr 是一种共享所有权的智能指针,它通过引用计数来管理所指向的对象。当一个 shared_ptr 被创建时,它会将引用计数加一。当一个 shared_ptr 被销毁或赋值给另一个 shared_ptr 时,它会将引用计数减一。当引用计数为零时,所指向的对象会被自动删除。
- shared_ptr 的底层实现通常使用原子操作来实现引用计数的增减。它还可能使用控制块来存储引用计数、弱引用计数和其他管理信息。
- weak_ptr 是一种弱引用的智能指针,它不会增加所指向对象的引用计数。它通常用于解决循环引用的问题,当一个对象被多个 shared_ptr 指向,并且其中一个 shared_ptr 又指向了另一个对象,而另一个对象又指向了第一个对象时,就会形成循环引用,导致对象无法被正确释放。使用 weak_ptr 可以打破循环引用,当所有的 shared_ptr 都被销毁时,所指向的对象会被自动删除。
-
weak_ptr 能不能知道 shared_ptr 被析构?
- 答案:weak_ptr 可以通过调用 expired () 函数来检查所指向的 shared_ptr 是否已经被析构。如果返回 true,表示 shared_ptr 已经被析构;如果返回 false,表示 shared_ptr 仍然存在。
(十六)左值和右值相关问题
- 左值和右值的概念,右值引用,移动构造函数如何声明。
- 答案:左值是可以取地址的表达式,通常表示一个具有持久状态的对象或变量。右值是不能取地址的表达式,通常表示一个临时对象或值。右值引用是对右值的引用,使用 && 表示。右值引用可以延长右值的生命周期,并且可以用于实现移动语义。
- 移动构造函数是一种特殊的构造函数,用于从一个右值对象初始化一个新的对象。移动构造函数的声明通常如下:
ClassName(ClassName&& other) noexcept;
- 在移动构造函数中,可以将右值对象的资源 “窃取” 过来,而不是进行深拷贝,从而提高性能。
-
调用移动构造时传入一个左值会发生什么?
- 答案:当调用移动构造函数时传入一个左值,通常会发生编译错误。因为移动构造函数是为了处理右值而设计的,如果传入左值,编译器会尝试调用拷贝构造函数而不是移动构造函数。但是,可以使用 std::move () 函数将左值转换为右值引用,然后再传入移动构造函数。
-
完美转发的概念。
- 答案:完美转发是指在函数模板中,将参数原封不动地转发给另一个函数,保持参数的左值 / 右值属性和 const/volatile 修饰符不变。完美转发可以避免不必要的拷贝和类型转换,提高函数模板的通用性和性能。在 C++ 中,可以使用 std::forward () 函数来实现完美转发。
三、总结
通过对这些面经的分析,可以看出游戏开发及引擎相关岗位的面试涵盖了广泛的 C++ 知识,包括面向对象编程、多线程、容器、智能指针、内存管理等方面。在准备面试时,需要深入理解这些知识点,并能够结合实际项目经验进行回答。同时,要注意代码的规范性和性能优化,以及对常见问题的解决方法和技巧的掌握。希望本文对求职者有所帮助。