2024最新大厂C++面试真题合集,大厂面试百日冲刺 bay8

腾讯视频

C++多态性如何体现?模版怎么实现的多态?

C++中的多态性主要通过两种方式体现:虚函数(动态多态性)和模板(静态多态性)。

  1. 虚函数实现的多态性:通过基类指针或者引用,调用由不同的派生类重载的虚函数,实现在运行时根据对象的实际类型调用相应的函数处理,这被称为动态多态。
  2. 模板实现的多态性:模板通过允许函数或类操作泛型类型,实现在编译时根据使用的实际类型生成具体的函数或类实例,这被称为静态多态。模板多态不依赖于继承体系,因此提供了一种更为灵活的多态实现方式。

在C++的多继承里面,怎么知道调用的方法和属性属于哪一个父类?

如果需要指定调用特定父类的方法或属性,应在方法或属性名称前加上相应父类的名称,使用作用域解析操作符(::)。

C++指针和智能指针的区别?智能指针的实现原理是什么?指针和函数指针的区别?

  1. 指针和智能指针的区别:指针是C++的核心特性,用于存储对象的内存地址。使用时需要手动分配和释放内存,否则会导致内存泄漏等问题。智能指针则是一种对象,它像常规指针一样,可以指向另一个对象,但它包含更加完善的内存管理机制,可以自动释放它所指向的资源,减少内存泄漏和其他相关错误。
  2. 智能指针的实现原理:智能指针的实现主要基于RAII(Resource Acquisition Is Initialization)概念。其主要含义是,将一个对象的生命周期(从初始化到销毁)与某项资源的获取和释放(如内存、文件描述符、锁等)绑定起来。当对象销毁时,其对应的资源也随之被释放。例如,shared_ptr智能指针通过引用计数来管理资源,当没有任何指针引用该资源时,资源会被自动释放。
  3. 指针和函数指针的区别:指针是指向一个内存地址的变量,用于存储和访问对象的地址。函数指针则是指向函数的指针,它可以用于调用函数或传递函数作为参数。

C++什么时候可能出现内存泄漏?如何定位到内存泄漏?

C++可能出现内存泄漏的情况包括但不限于:

  1. 动态分配的内存没有被正确释放,例如使用new分配内存后没有使用delete释放;
  2. 资源分配和释放之间程序提前退出(如异常抛出)导致释放代码未执行;
  3. 指针变量被覆盖,导致原有的内存地址丢失而无法释放;
  4. 在程序数据结构中的循环引用,特别是在智能指针未正确使用时。

定位内存泄漏的方法有:

  1. 利用C++标准库中的<memory>头文件,使用智能指针如std::unique_ptrstd::shared_ptr以帮助自动管理内存;
  2. 使用内存分析工具,如Valgrind、AddressSanitizer等,它们可以在程序运行时检测内存泄漏;
  3. 在代码中使用断言和监测,追踪动态内存的分配和释放;

析构函数必须是虚函数吗?

析构函数不是必须声明为虚函数,但如果你的类是多态基类,即预期会被其他类继承,并且通过基类指针或引用来删除派生类对象,那么你应该将析构函数声明为虚函数。这确保了对象的正确析构,触发动态绑定,以调用适当的派生类析构函数。如果不这么做,在通过基类指针删除派生类对象时,可能会导致资源泄漏或未定义的行为。

new和malloc申请内存有什么差别?

  1. new是C++中的操作符,而malloc是C中的函数。
  2. new不仅分配内存,还会创建对象(即会调用构造函数),而malloc只分配内存,不会初始化内存。
  3. new直接返回指定类型的正确指针,无需类型转换,而malloc返回void*,在使用前需要转换为正确的类型。
  4. 与new配套的是delete ,与malloc配套的是free。
  5. 如果内存分配失败,new会抛出bad_alloc异常,而malloc返回NULL。
  6. 可以重载new和delete,但不能重载malloc和free。

讲一下平衡二叉搜索树,删除非叶子节点后如何调整?

平衡二叉搜索树(比如AVL树或红黑树)是一种在每次插入和删除后能够自平衡的二叉搜索树,以保证树的高度尽可能低,从而保证操作的性能。

删除非叶子节点后的调整步骤大致如下:

  1. 查找替代节点:被删除节点如果有两个子节点,一般用它的中序前驱(左子树中的最大值)或中序后继(右子树中的最小值)来替代该节点。
  2. 替换&删除:将查找到的替代节点的值复制到被删除节点(不实际移动节点),然后删除那个替代节点(这个替代节点最多只有一个子节点)。
  3. 平衡调整:删除替代节点可能破坏了树的平衡,所以需要从替代节点原本的位置向上回溯并调整树的平衡。调整包含旋转操作(单旋转或双旋转)以及可能的颜色变更(对于红黑树)。

讲一下程序从源代码到可执行程序经过了哪些步骤?每个步骤都做了什么事?

  1. 预处理(Preprocessing)
    • 处理源码文件中的预处理指令,比如#include#define
    • 展开宏定义,删除注释,添加编译器需要的特定预处理文件。
  2. 编译(Compilation)
    • 将预处理后的源代码转换成中间代码(通常是汇编语言)。
    • 进行语法分析、语义分析、优化等处理。
  3. 汇编(Assembly)
    • 将汇编语言转换为机器码,也就是具体的指令,生成目标文件(.obj或.o文件)。
  4. 链接(Linking)
    • 解决目标文件之间的引用,将多个目标文件与库文件合并成一个可执行文件。
    • 处理外部依赖和函数库的调用问题,分配内存地址给各个函数和变量。

C++多线程了解吗?如何定义多线程?如何让他们跑起来?

在C++中,可以通过several ways来定义并运行多线程。以下是使用pthread库和C++11标准中的线程库的基本概述:

使用pthread库:

  1. 定义线程 :通过pthread_create函数创建一个新线程,需要传递一个指向函数的指针,该函数是线程将要执行的代码。
  2. 启动线程 :调用pthread_create后,线程就开始运行了。
  3. 等待线程 :通过pthread_join方法可以等待一个特定线程执行完毕。

使用C++11线程库:

  1. 定义线程:通过std::thread类创建一个新线程。
  2. 启动线程:线程对象在构建时就会开始执行。
  3. 等待线程 :通过成员函数join可以确保主线程等待其他线程完成执行。

有三个线程ABC,C必须在A和B运行完之后才能运行,应该怎样实现?

为了确保线程C在A和B之后运行,可以使用同步机制,例如条件变量、事件或者期物(futures)和承诺(promises)在C++11中。以下是一个使用C++11线程库和std::promisestd::future实现的例子:

  1. A和B线程在执行结束时向相应的std::promise对象设置一个值,从而修改关联的std::future对象的状态。
  2. C线程在开始执行工作前等待两个std::future对象,这样就能确保A和B线程完成后C线程才开始运行。

DNS解析的过程?系统DNS查询时可能存在什么缺陷?

DNS解析的过程包括以下几个步骤:

  1. 本地缓存查询:检查是否有该域名的解析结果在本地缓存中。
  2. 递归查询:如果本地缓存没有,向配置的DNS服务器发送查询请求,DNS服务器将代表用户进行查询。
  3. 根域服务器查询:如果DNS服务器没有缓存,它会查询根域服务器。
  4. 顶级域(TLD)服务器查询:根域服务器会返回负责该顶级域(如.com、.org)的服务器地址。
  5. 权威域名服务器查询:顶级域服务器将返回负责该域名的权威DNS服务器地址。
  6. 获取记录:权威DNS服务器返回请求的域名的IP地址。
  7. 缓存结果:接收方DNS服务器缓存解析结果,并将结果返回给用户,用户的操作系统也可能会缓存结果。

系统DNS查询可能存在的缺陷:

  1. 缓存污染:攻击者可能通过各种方式污染DNS缓存,导致用户被引导至恶意网站。
  2. 查询延迟:解析过程可能需要多次网络请求,如果服务器响应慢或者网络状况不好,会导致解析延迟,影响用户体验。
  3. 单点故障:如果使用的DNS服务器出现故障,可能会导致无法解析域名。
  4. 隐私泄露:DNS请求暴露了用户的访问网站信息,不加密的DNS查询可以被第三方监听,造成隐私泄露。
  5. DNS劫持:运营商或者攻击者可以修改DNS解析结果,将用户导向其他网站。

DNS用的什么协议?

DNS基本上使用的是UDP协议,端口号为53。对于那些超过512字节的DNS响应或者需要可靠连接的情况(如区域传输),则会使用TCP协议。

详细说一下TCP三次握手过程。第一、二、三次握手失败后分别会做什么事?序号和确认号怎么变的?

  1. 第一次握手(SYN):客户端发送一个SYN(同步)报文到服务器,并在报文中指定客户端的初始序列号(Client ISN)。此时客户端进入SYN_SENT状态。
  2. 第二次握手(SYN-ACK):服务器收到客户端的SYN报文后,会应答一个SYN-ACK报文。该报文中包含服务器的初始序列号(Server ISN)和确认号(即客户端的初始序列号加1)。此时服务器进入SYN_RCVD状态。
  3. 第三次握手(ACK):客户端收到服务器的SYN-ACK报文后,发送一个ACK(确认)报文,确认号为服务器的初始序列号加1。此时连接建立成功,客户端和服务器进入ESTABLISHED状态。

如果在这个过程中出现失败:

  • 第一次握手失败:如果客户端的SYN报文在网络中丢失,客户端会超时重传SYN报文,直到接收到服务器的SYN-ACK报文为止。
  • 第二次握手失败:如果服务器的SYN-ACK报文在网络中丢失,服务器会超时重传SYN-ACK报文。同时客户端在一定时间未收到SYN-ACK报文后,也会重传SYN报文。
  • 第三次握手失败:如果客户端的ACK报文在网络中丢失,服务器将不能进入ESTABLISHED状态,而是会等待一段时间并超时重传SYN-ACK报文。客户端在发送ACK报文后如果直接开始数据传输,在服务器收到数据之后也会自动确认ACK,从而避免握手失败影响连接。

关于序号和确认号的变化:

  • 序号(Sequence Number):用来标识从TCP源端向目的端发送的字节流,每个字节都按传输顺序编号。
  • 确认号(Acknowledgment Number):期望收到对方下一个报文段的第一个数据字节的序号。实际上是对方发送的数据序号加1。

创建表名的SQL语句是什么?

sql 复制代码
CREATE TABLE 表名 (
    列名1 数据类型,
    列名2 数据类型,
    列名3 数据类型,
    ...
);

往表中插入一条数据的SQL语句是什么?如果我要一次插入很多数据用什么方法?

插入单条数据的SQL语句格式如下:

sql 复制代码
INSERT INTO 表名 (列名1, 列名2, 列名3, ...)
VALUES (值1, 值2, 值3, ...);

要一次插入多条数据,可以使用单个INSERT语句,后跟多组值,每组值代表一条记录,格式如下:

sql 复制代码
INSERT INTO 表名 (列名1, 列名2, 列名3, ...)
VALUES 
(值1a, 值2a, 值3a, ...),
(值1b, 值2b, 值3b, ...),
(...);

使用事务处理这些插入操作,以确保要么所有的插入都成功,要么在遇到错误时全部撤销,格式如下:

sql 复制代码
START TRANSACTION;
INSERT INTO 表名 (列名1, 列名2, 列名3, ...) VALUES (值1, 值2, 值3, ...);
INSERT INTO 表名 (列名1, 列名2, 列名3, ...) VALUES (值1, 值2, 值3, ...);
...
COMMIT;

如果中途出现错误,可以使用ROLLBACK来回滚到事务开始前的状态。

腾讯天美游戏客户端

什么是面向对象编程

面向对象编程是一种编程范式,它使用"对象"来设计软件。对象是包含数据(通常称为属性或字段)和能够执行操作的函数(通常称为方法)的实体。OOP的核心概念包括封装、继承和多态:

  • 封装:包装代码和数据到单一的单元中,隐藏内部实现细节,通过公有接口进行交互。
  • 继承:允许新创建的对象继承现有对象的属性和方法。
  • 多态:允许不同的对象以适合它们自己的方式响应同一消息(或方法调用)。

说一下C++多态

C++中的多态性指的是用一个接口来引用多种不同类型的对象,而具体的实现取决于对象的类型。在C++中,多态性主要有两种形式:

  1. 编译时多态(静态多态):这是通过函数重载或运算符重载实现的,编译器根据函数参数的类型和数量在编译时确定要调用的具体函数。
  2. 运行时多态(动态多态):这是通过虚函数和继承实现的。当派生类覆盖基类中的虚函数时,可以通过基类的指针或引用调用派生类的方法

虚函数的原理

虚函数的原理基于一个称为虚函数表的机制。当一个类中声明了虚函数时,编译器会为该类创建一个虚函数表。这个表是一个包含指向类中所有虚函数的指针的数组。每个对象都包含一个指向其类的虚函数表的指针。

当调用一个对象的虚函数时,程序运行时会使用对象的虚函数表指针去查找对应的函数实现。这就允许在派生类中覆盖基类的虚函数,并且即使使用基类指针或引用也能调用到派生类的实现,实现运行时多态。

说一下虚构造函数

构造函数不能被声明为虚函数。构造函数的任务是初始化对象的成员变量和设置对象的初始状态,而虚函数机制是用来实现运行时多态的,即在基类的指针或引用调用派生类的方法。因为在构造对象时,对象的类型是已知的,所以不需要虚构造函数来实现多态。

说一下虚析构函数

虚析构函数用于确保当删除一个指向派生类对象的基类指针时,能够正确地调用派生类的析构函数。如果基类的析构函数被声明为虚函数,当通过基类指针删除派生类对象时,会首先调用派生类的析构函数,然后是基类的析构函数,从而保证对象的资源被适当释放。这是实现多态性资源管理的重要机制。

说一下内存泄露

内存泄露是指程序在申请内存后,由于疏忽或错误导致没有正确释放,使得操作系统无法回收该部分内存。长期或频繁的内存泄露会导致系统可用内存减少,最终可能引起程序或系统运行缓慢甚至崩溃。

内存泄漏和访问冲突的关系

内存泄漏和访问冲突都是程序中常见的错误类型,但它们指向不同的问题。

内存泄漏指的是程序未能释放不再使用的内存,导致内存逐渐耗尽。

访问冲突(也称为竞态条件)指的是两个或多个进程或线程在没有适当同步的情况下同时访问某些共享资源,导致结果不确定或出错。

它们有关联:一个程序如果有访问冲突问题,可能导致某些内存区域异常,这可能间接导致内存泄漏,例如,不一致的锁行为导致未能正确释放内存。但通常这两个问题是独立的,需要分别处理。

new 和 melloc的区别

  1. 类型安全new 自动计算分配对象所需的内存大小,而 malloc 需要手动指定字节数。
  2. 构造/析构函数new 会调用对象的构造函数初始化对象,delete 调用析构函数。mallocfree 不会调用构造函数和析构函数。
  3. 返回类型new 返回的是指定类型的指针,不需要类型转换;malloc 返回 void*,使用时通常需要类型转换。
  4. 错误处理 :分配内存失败时,new 抛出 bad_alloc 异常(或返回空指针,视new的版本而定),而malloc 返回 NULL
  5. 空间分配newdelete 配对使用,mallocfree 配对使用,不能混用。

介绍一下const,你是怎么用const的

const 是一个关键字,用于声明一个常量或者一个不能修改的变量。

以下是一些常见的使用方式:

  1. 常量const int age = 20; 这里,变量 age 被设定为常量,不能再次修改。
  2. 指向常量的指针const int *p; 这里,p 是一个指向常整数的指针,通过 p 不能改变其指向的值,但可以改变 p 的指向。
  3. 常指针int * const p; 这里,p 是一个指针常量,p 的指向不能改变,但可以改变 p 所指向的值。
  4. 常指针指向常量const int * const p; 这里,既不能改变指针 p 的指向,也不能通过 p 来改变其指向的值。
  5. 函数中的只读参数和返回值:你可以在函数的参数声明以及返回值中使用 const,使得函数在调用过程中不会改变参数的值,也使得返回对象不能被修改。
  6. 类成员函数 :在类的成员函数后面声明 const,比如 void display() const;表示该成员函数内不能修改类的任何成员变量。

说一下静态成员变量

静态成员变量是类的一个成员,它被所有对象共享,不属于任何单独实例。静态成员变量有以下特点:

  • 在类的所有对象之间共享。
  • 即使没有创建类的对象,静态成员变量也存在。
  • 必须在类外部进行初始化(通常在类的实现文件中)。
  • 可以通过类名加作用域解析运算符(::)来访问,无需对象实例。

静态成员变量在什么时候初始化

静态成员变量在程序开始时,即在 main() 函数执行之前就由运行时系统初始化。它们通常在类的实现文件中进行初始化,只初始化一次。

说一下平衡二叉树

平衡二叉树是一种特殊的二叉搜索树,每个节点的左子树和右子树的高度差最多为1。这种平衡条件确保树的高度保持对数级,从而在最坏情况下也能保证基本的动态集合操作(如查找、插入、删除)具有O(log n)的时间复杂度。 AVL树是平衡二叉树的一种常见实现。

二叉树有哪些搜索方法

二叉树的搜索方法主要有三种:前序遍历、中序遍历和后序遍历。

  • 前序遍历:先访问根节点,然后递归地对左子树进行前序遍历,接着递归地对右子树进行前序遍历。
  • 中序遍历:先递归地对左子树进行中序遍历,然后访问根节点,最后递归地对右子树进行中序遍历。
  • 后序遍历:先递归地对左子树进行后序遍历,然后递归地对右子树进行后序遍历,最后访问根节点。

说一下堆排序

堆排序是一种基于比较的排序算法,使用二叉堆(binary heap)数据结构来实现。它包括两个主要步骤:

  1. 构建堆:将无序数组构建成一个最大堆(或最小堆),确保所有非叶子节点都遵循堆的性质。
  2. 排序:依次删除堆顶元素(最大元素或最小元素),并将其移到数组的末尾,然后调整剩余元素以保持堆的性质,直到堆为空。

堆排序的时间复杂度为O(n log n),不是稳定排序。

还有哪些排序算法

排序算法主要有以下几种类型:

  1. 冒泡排序:通过重复交换相邻逆序的元素,使得较小(或较大)的元素逐步浮到顶端。
  2. 选择排序:逐个找出未排序部分的最小(或最大)元素,放到已排序序列的末尾。
  3. 插入排序:取未排序区间中的元素,在已排序序列中从后向前扫描找到相应位置并插入。
  4. 快速排序:选取基准值,将数组分为大于和小于基准值的两部分,递归地对这两部分进行快速排序。
  5. 归并排序:将数组分成两半,对每一半递归地进行归并排序,然后将两个有序的部分合并成一个。
  6. 希尔排序:是插入排序的一种更高效的改进版本,通过比较距离较远的元素来减少元素的移动次数。
  7. 计数排序:利用数组下标来统计元素的出现次数,适用于一定范围内的整数排序。
  8. 基数排序:根据数字的有效位或基数将整数分布到桶中,集合各个桶的内容得到有序序列,适用于非负整数。
  9. 桶排序:将数组分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。

说一下快速排序

快速排序是一种分治策略的排序算法。它的基本过程如下:

  1. 选择基准值:从数组中选择一个元素作为基准值(pivot)。
  2. 分区操作:重新排列数组,所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准的后面(相等的数可以到任一边)。在这个分区结束之后,对基准值的排序就已经完成。
  3. 递归排序:递归地将小于基准值的子数组和大于基准值的子数组排序。

快速排序的平均时间复杂度为O(n log n),不过最坏情况下会退化为O(n^2)。

快速排序稳定吗

快速排序不是稳定的排序算法,因为在分区过程中,相等的元素可能会因为分区操作而改变相对顺序。

进程和线程的区别

进程拥有独立的地址空间,而多个线程共享所属进程的地址空间和资源。线程之间的通信和切换开销小于进程,效率更高。

UE中有哪些线程

主要有以下几种线程:

  1. 主线程:处理所有非异步的游戏逻辑,包括输入处理、动画更新、游戏状态的维护等。
  2. 渲染线程:主要负责将预计算的图形数据提交给GPU。UE使用多线程渲染技术,渲染线程在主线程之后运行,以异步的方式更新渲染场景。
  3. 工作线程:用于执行分配给他们的任务。它们通常用于计算密集型工作,以便在主线程和渲染线程之间分摊负担。
  4. 子线程(或后台线程):执行耗时较长的任务,例如网络通信、AI计算、物理计算等,通常用于碎片化计算任务,使其不会影响到主线程的运行。

UE提供了哪些容器

常见的容器类型:

  1. TArray:动态数组,可以存储任意类型的元素,支持快速随机访问。
  2. TMap:键值对集合,提供快速查找功能。
  3. TSet:无序集合,元素唯一,提供快速查找和高效的集合运算。
  4. FString:字符串类型,用于存储和操作文本数据。
  5. TQueue:队列容器,实现先进先出(FIFO)的数据结构。
  6. TStack:栈容器,实现后进先出(LIFO)的数据结构。

说一下UE的智能指针

  1. TSharedPtr:共享智能指针,可以有多个TSharedPtr指向同一个对象,对象会在最后一个TSharedPtr被销毁时自动删除。
  2. TWeakPtr:弱智能指针,它不拥有对象,可以从TSharedPtr或其他TWeakPtr构造而来,用于解决循环引用问题。
  3. TUniquePtr:独占智能指针,保证只有一个TUniquePtr可以指向一个特定的对象,对象会在TUniquePtr被销毁时自动删除。

actor pawn character的区别

  1. Actor:是所有游戏对象的基类,能够在场景中被放置和移动。它可以代表任何事物,包括静态物体、动态物体、摄像机等。
  2. Pawn:是可以由玩家或AI控制的Actor。它是一个中间层次的类,提供了输入处理的基础。
  3. Character:是专门为有需求的移动和碰撞检测等功能的Pawn。通常用于代表有复杂移动机制的玩家或AI控制的角色,例如能够行走、跳跃的角色。Character还集成了一个CharacterMovementComponent,以提供典型的角色移动逻辑。

如果不想一个actor被GC回收,该怎么做

为避免一个Actor被垃圾回收(GC)系统回收,可以将其设置为Root,使用AddToRoot()方法。这会将Actor标记为永久对象,直到调用RemoveFromRoot()之前不会被GC系统回收。这个方法主要用于确保关键对象在游戏运行期间始终可用,但使用时需要小心,因为过度使用可能会导致内存泄露。

快手------游戏客户端

怎么理解C++的封装继承多态

C++的封装、继承和多态是面向对象编程(OOP)的三大基石:

  1. 封装:隐藏对象的具体实现细节,只暴露必要的操作接口。这有助于减少系统的复杂度和增加安全性。
  2. 继承:允许创建新的类(派生类)来继承一个或多个现有类(基类)的属性和方法。继承支持代码复用和逻辑复用。
  3. 多态:指派生类可以定义自己的行为,同时还能共享父类的接口。在运行时,可以根据对象的实际类型来调用相应的方法,即故一个接口,多种实现。这增加了程序的灵活性和可扩展性。

多重继承,如果不用虚继承,能用其他方式解决这个二义性问题?显示调用?

如果不使用虚继承来解决多重继承中的二义性问题,可以通过明确指定父类的作用域来显式调用特定的成员。这种方式称为作用域解析。在成员访问时,需要前缀类名和作用域解析运算符(::)来指明从哪个基类继承的成员被访问。这样可以明确地区分同名成员属于哪个基类,从而避免二义性。

动态多态,讲一下具体的动态多态

具体的动态多态在C++中通常通过虚函数(virtual functions)实现。当一个类中声明了虚函数,任何派生类都可以重写(override)这个函数来提供特定的实现。在运行时,根据对象的实际类型来决定调用哪个版本的函数,而不是在编译时。

静态多态怎么通过不同类型实现多态

静态多态在C++中主要通过函数重载和模板实现。

  1. 函数重载:在同一作用域中,可以声明多个具有相同名字但是参数类型或个数不同的函数。编译器根据函数调用时传入的参数类型和个数来决定调用哪个函数。
  2. 模板:使用模板(template)可以创建泛型类或函数,它们能够用任何类型工作。编译器根据传入的具体类型生成相应的代码,实现多态性。这包括函数模板和类模板。

虚函数和纯虚函数的区别?

虚函数是在基类中声明的,可以在派生类中被重写的函数。它允许派生类根据需要提供自己的实现。

纯虚函数是一种特殊的虚函数,它在基类中没有具体的实现,并且要求任何派生类必须提供该函数的实现。纯虚函数通过在函数声明的末尾加上= 0的语法来表示。

具有纯虚函数的类称为抽象类,不能直接实例化对象。

析构函数为什么一般都是虚函数

析构函数通常声明为虚函数是为了确保通过基类指针删除派生类对象时,能够正确地调用派生类的析构函数,从而实现资源的适当释放。这防止了内存泄露和资源未正确释放的问题,保证了当对象被删除时,整个对象的析构过程是从派生类到基类的正确顺序进行的。

什么情况下,基类会析构,派生类不会析构,哪种写法会造成这种情况?

当通过基类指针删除指向派生类对象的指针,而基类的析构函数不是虚函数时,会发生这种情况。因为此时运行时不会调用派生类的析构函数,只会调用基类的析构函数,导致派生类可能分配的资源没有被适当释放。

纯虚函数存在什么地方?

纯虚函数存在于抽象类中,用于定义接口规范,它在基类中没有实现(即没有函数体),但必须在派生类中被实现,除非派生类也是抽象类。纯虚函数的声明在基类中以= 0结尾,表示该函数为纯虚函数。

析构函数在什么时候调用呢?

析构函数在对象生命周期结束时被调用,以进行资源释放和清理工作。这通常发生在以下几种情况:

  1. 当一个局部非静态对象的作用域结束时(例如,函数返回时)。
  2. 当一个对象被delete表达式显式删除时。
  3. 当一个临时对象的初始化表达式结束时。
  4. 对于全局或静态对象,析构函数在main()函数执行完成后,程序退出前调用。

如果是new,超出作用域范围内,析构函数会调用吗?如果会析构,delete调用的时候还会调用吗?

如果是通过new创建的对象,当超出作用域时,析构函数不会自动调用。必须显式使用delete来删除动态分配的对象,这时才会调用析构函数。如果对象已经被delete,则其析构函数不会再次被调用,重复调用delete会导致未定义行为。

new一个类,在new的过程中,new做了哪些操作?new什么时候分配内存?

new操作符在创建类的对象时主要执行以下操作:

  1. 分配内存:new首先分配足够存储对象的内存空间,通常是通过调用内存分配函数operator new进行分配。
  2. 构造对象:在分配的内存空间上,new调用类的构造函数来初始化对象。

如果频繁的new和delete?new的时候从堆上面分配内存?delete的时候会将内存还回去?操作系统为什么会帮我们回收内存?

频繁地使用newdelete时:

  • new会从堆上分配内存给对象。
  • delete会将对象所占用的内存还回堆,并调用对象的析构函数。

操作系统帮助回收内存是因为它负责管理计算机的资源,包括内存。这样做可以防止内存泄漏,确保内存可以被重新分配和使用,维护系统的稳定性和性能。

STL容器?如果我比较频繁的增插数据,并且寻找索引,选用什么容器?

STL容器包括:

  • 序列容器(如std::vectorstd::dequestd::list):线性存储,支持顺序访问。
  • 关联容器(如std::setstd::multisetstd::mapstd::multimap):基于键值对存储,支持快速查找。
  • 无序关联容器(如std::unordered_setstd::unordered_multisetstd::unordered_mapstd::unordered_multimap):基于哈希表,支持高效查找和访问。
  • 容器适配器(如std::stackstd::queuestd::priority_queue):通过某个容器提供特定的数据结构接口。

需要频繁地插入数据并且根据索引快速访问元素,应该使用std::vector。向std::vector的末尾添加数据很高效,并且它提供了通过索引快速访问元素的能力。然而,如果要频繁在非尾部位置插入或删除元素,可能要考虑使用std::liststd::deque,但这些容器不支持直接通过索引访问。如需在频繁插入的同时保持排序,则可能考虑使用std::setstd::multiset,但它们不支持直接索引访问,而是提供基于键的查找。

讲讲TCP/UDP

TCP(传输控制协议)和UDP(用户数据报协议)是两种主要的互联网传输层协议。

  • TCP提供一种面向连接的、可靠的字节流服务,当消息被TCP传送时,TCP会确保消息正确无误地到达目的地。如果出现错误,TCP会自动处理重发操作。然而,这种可靠性导致TCP比其他协议消耗更多的资源和带宽。
  • UDP是一种无连接的协议,信息在网络中以独立的包的形式进行传输,这意味着消息可能会乱序到达,也可能丢失。但是,UDP协议的开销小,工作效率较高,对于不需要可靠性,或者可由应用程序自行处理可靠性的应用来说,采用UDP是一种更好的选择。

三次握手,服务器没有收到客户端的第三次握手怎么办?

如果服务器没有收到客户端的第三次握手确认消息(ACK),它将会在一段时间后重发它的第二次握手消息(SYN-ACK)。如果经过多次重试后仍然没有收到第三次握手的确认,服务器最终会超时放弃连接。客户端如果期间已经发送了数据,由于没有建立起一个稳定的连接,数据会被丢弃。客户端如果没有收到服务器对第三次握手的确认,可能会重新尝试建立连接。

四次挥手,第二次我没有收到服务器的ACK,怎么办?

如果你在四次挥手中没有收到服务器的ACK,你的TCP层会重新发送FIN报文,并等待服务器的ACK应答。TCP有自己的重试机制,如果在一定的重试次数或时间后仍未收到应答,连接会超时关闭。

TCP的报文头有什么内容?TCP怎么保证传输的内容没有被修改?CRC?CRC是怎么做的?除了CRC还有什么检验方法?

TCP报文头包含的内容有:源端口号、目的端口号、序列号、确认号、数据偏移、保留位、控制位(如SYN、ACK、FIN等)、窗口大小、校验和、紧急指针以及选项和填充。

TCP保证内容没有被修改主要是通过校验和(Checksum)机制,而非CRC。校验和是对报文段中的所有16位字进行二进制反码求和,然后再进行一次反码操作得到的。接收方会对接收到的数据执行相同的计算过程,比较计算出的校验和与报文中的校验和是否一致,以此验证数据的完整性。

除了校验和,其他检验方法包括:循环冗余检验(CRC)、消息摘要(如MD5、SHA系列)以及更高级的错误检测和纠正代码(如汉明码)。

四次挥手,能不能三次挥手?

TCP四次挥手过程中,必须要做四次挥手来结束连接,不能只做三次。

这是因为TCP的半关闭状态设计。即,发送FIN报文的一方在发送完FIN后,就进入了FIN-WAIT-1状态,但是此时仍可以接收对方的数据。这就说明了为什么不能三次挥手结束。因为当一方发送FIN,并且对方返回ACK和FIN时,虽然这一方能确认对方不再发送数据了,但是对方并不能确认这一方不再发送数据,没有达到"四次挥手"正常断开连接的目的。因此,仍然需要四次挥手才能断开连接。

TCP怎么判断丢包?

TCP判断丢包主要依靠超时重传和冗余ACK机制。当发送的数据包超出设定的超时时间后,还没有收到ACK确认,发送方就会认为该包已丢失,进行重传。若发送方连续收到三个或以上的重复ACK,也会认为相应序列号的包已丢失,并触发快速重传机制。

HTTP和HTTPS的区别?传输协议使用什么协议,TCP?UDP?HTTPS的数据加密过程怎么做的?

HTTP和HTTPS的主要区别在于安全性。HTTP不提供数据加密,传输的内容可能被窃听或者篡改。而HTTPS提供了SSL/TLS加密层,确保了数据的安全传输。

HTTP和HTTPS都使用TCP协议进行传输,不使用UDP协议,因为TCP提供了可靠的、面向连接的数据传输服务。

HTTPS的数据加密过程涉及对称加密和非对称加密的结合使用。首先,通过非对称加密协商生成一个会话密钥,接下来的通信都使用这个对称会话密钥进行加密,确保了数据的机密性和完整性。

讲讲帧同步和状态同步的概念

帧同步是指在游戏或应用中,所有客户端在同一时间内都执行相同的"帧"操作,通过发送用户的输入而不是游戏状态来保持所有玩家的游戏状态同步。

状态同步则指直接同步所有玩家的游戏状态,如玩家位置、分数等信息,通常需要更频繁地数据传输,以确保所有玩家看到的游戏世界状态一致。

一场游戏里面有十个敌人,这个场景可以用状态同步做吗?怎么做呢?

AOI的常用的实现方式?

AOI的常用实现方式主要有以下几种:

  1. 格子法(Grid-Based):将整个游戏场景划分为多个格子,每个格子存储当前该格子内的角色,角色移动或者做出行为时,只对相邻格子的角色进行消息通知和状态更新。
  2. 九宫格法:也是格子法的一种,将每个格子的周围8个格子以及自己称为九宫格,只向九宫格内的角色发送消息。
  3. 四叉树/八叉树(Quad/Octree):将空间按四叉树(二维)或八叉树(三维)进行划分,每个节点包含当前区域内的对象,更适合处理不均匀分布的场景。

我的游戏由某个客户端组转发数据,那么是什么同步?

客户端主导的同步P2P(Peer-to-Peer)同步

帧同步分为锁帧同步和什么?

帧同步分为锁帧同步和非锁帧同步。

寻路算法?

常用的寻路算法有:

  1. A*(A星算法)
  2. Dijkstra算法
  3. 广度优先搜索(BFS)
  4. 深度优先搜索(DFS)
  5. 贪婪最佳优先搜索(Greedy Best-First Search)
  6. 跳点搜索(JPS,Jump Point Search)

收集整理了一份2024年最新C++开发学习资料,既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C++开发知识点,真正体系化!

包含大厂面经、学习笔记、实战项目、大纲路线、讲解视频 领取 君羊739729163 或者
https://docs.qq.com/doc/DR2N4d25LRG1leU9Q

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

相关推荐
码农爱java2 小时前
Spring Boot 中的监视器是什么?有什么作用?
java·spring boot·后端·面试·monitor·监视器
爱上电路设计3 小时前
有趣的算法
开发语言·c++·算法
窜天遁地大吗喽3 小时前
每日一题~ (判断是否是合法的出栈序列)
c++
skyshandianxia4 小时前
Java面试八股之如何提高MySQL的insert性能
java·mysql·面试
yachihaoteng5 小时前
Studying-代码随想录训练营day27| 贪心算法理论基础、455.分发饼干、376.摆动序列、53.最大子序和
c++·算法·leetcode·贪心算法
逸群不凡5 小时前
C++|哈希应用->布隆过滤器
开发语言·数据结构·c++·算法·哈希算法
从后端到QT6 小时前
Qt 基础组件速学 鼠标和键盘事件
c++·qt
quaer6 小时前
使用引用返回类对象本身
开发语言·c++·算法
w_outlier6 小时前
gcc/g++的四步编译
linux·c++·gcc·g++
Navigator_Z6 小时前
C++ //练习 14.39 修改上一题的程序令其报告长度在1至9之间的单词有多少个、长度在10以上的单词又有多少个。
开发语言·c++·算法