一些知识点的复习
雄关漫道真如铁,而今迈步从头越。
经过了漫长的休息期后(其实也就休息了一周左右),我终于再次开始了学习。
今天先大致复习一下过往学习的内容,对在笔试面试中遇到的问题进行一个梳理和总结。
C++
字符串的内存分区是怎样的?
我们都知道如果是字符串常量,那么会存储在常量区,当然如果你是局部变量就在栈上,是动态分配的内存就在堆上,然而其实还有一个考虑:我们会把较短的字符串放在栈上,把较长的字符串放在堆上。它能显著提升性能,避免对短字符串进行昂贵的堆内存分配和释放操作,减少内存碎片,同时提高CPU缓存命中率,使内存访问速度更快。
一些常见的字符串操作函数:
我们在这里介绍一下strlen(),strcpy(),strcmp(),strcat()。
strlen
用于计算字符串长度(不包括结尾的 '\0'
),strcpy
将源字符串(包括 '\0'
)复制到目标空间,strcat
将源字符串连接到目标字符串的末尾(覆盖目标串的 '\0'
),strcmp
按ASCII码值逐字符比较两个字符串,返回零表示相等,非零值表示大小关系。
使用时务必确保目标空间足够大且可写,并且源字符串必须以 '\0'
结尾,否则可能导致缓冲区溢出或未定义行为。
如何理解string中的\0?
std::string
是一个更为现代的字符串类型,它不依赖 '\0'
来判断字符串的结束。它通过一个内部的成员变量(通常是 size
或 length
)来明确知道字符串的长度。因此,std::string
甚至可以包含中间带 '\0'
的字符序列,而不会被截断。
然而,为了与大量现有的 C 语言库函数(如 strcpy
, printf
等)进行交互,这些函数都要求传入的字符数组必须以 '\0'
结尾,std::string
提供了 c_str()
和 data()
(C++11 起) 方法。这两个方法返回的指针指向一个保证以 '\0'
结尾的字符数组。这就是它内部存储末尾需要那个 '\0'
的主要原因。
new分配内存的大小总是等同于sizeof的结果吗?
new分配内存的大小确实是基于sizeof的返回值的,但是事实上一般new会比sizeof的返回值稍微大一些,主要是因为内存分配器需要额外的空间来存储管理这块内存所需的元信息(例如分配块的大小),并且为了满足系统的内存对齐要求,分配器也可能略微多分配一些空间。这些额外的开销对程序员是透明的,但确保了内存管理的正确性和高效性。
什么是WebSocket?
WebSocket 是一种网络通信协议,它通过在单个TCP连接上建立持久化的全双工通道,实现了服务器与客户端之间的真正双向实时通信。这使得服务器可以主动、即时地向客户端推送数据,彻底摆脱了传统HTTP协议“请求-响应”模式带来的延迟和高开销,非常适合构建聊天应用、实时数据看板、在线游戏等需要高实时性的场景。
PS:WebSocket是应用层协议。
有哪些常见的I/O方式?
这其中第四种IO信号驱动IO的应用场景有限,我们主要针对其他四种IO进行介绍,这四者的差异基本体现在数据准备阶段和数据拷贝阶段。
红黑树是什么?具有哪些性质?
红黑树(Red-Black Tree)是一种自平衡的二叉查找树(Binary Search Tree)。它通过在节点中增加一个存储位(表示颜色,红或黑)和遵循特定的规则,来确保树在插入和删除元素后能通过旋转和变色快速恢复平衡,从而保证查找、插入、删除等操作在最坏情况下仍能保持较高的效率(时间复杂度为 O(log n))。
对于有n个节点的红黑树,对于有 n 个节点的红黑树,其高度 h ≤ 2log₂(n+1),大于理论最小值 log₂n。
红黑树在插入和删除操作时所需的旋转次数更少(插入最多2次旋转,删除最多3次旋转),且调整频率较低。这是因为红黑树的平衡要求更为宽松,允许暂时的“近似平衡”。AVL树为了维持其严格的平衡(左右子树高度差不超过1),在插入和删除操作后可能需要进行更多的旋转操作,并需要维护从被删节点到根节点路径上所有节点的平衡,因此在这些写操作上开销通常更大。
因此,红黑树在插入和删除(写操作)上通常比AVL树更高效。
什么是图的最小生成树?有哪些相关算法?
最小生成树是指在一个带权的、连通的、无向图中,找到一个边的子集,使得这个子集能够连接图中的所有顶点,并且不形成任何环,同时满足所有边的权重之和最小。
求解最小生成树的两个经典算法是 Kruskal算法 和 Prim算法,它们都基于贪心算法的策略,即在每一步选择中都采取当前看来最优的选择。
Kruskal算法:采用 “加边法”,全局地将所有边按权值从小到大排序,然后依次选择不会形成环的边加入生成树(常用并查集判环),适合稀疏图(边少);Prim算法:采用 “加点法”,从某顶点开始,逐步选择与当前生成树相连的最小权值边来扩展生成树(常用优先队列维护候选边),适合稠密图(边多)。
当图中存在权重相同的边时,最小生成树(MST)的结果可能不唯一,这意味着你可能会得到多棵结构不同但总权重相同的生成树。
C++中支持对nullptr调用delete吗?
在 C++ 中,使用 delete
或 delete[]
释放一个 nullptr
是完全安全的。语言标准明确规定了这一行为,它不会执行任何操作,也不会导致运行时错误。delete
操作符的内部实现包含了对空指针的检查,因此,如果 ptr
是 nullptr
,检查条件失败,不会进行任何操作。
可以访问union中的非活跃成员吗?
在 union
中,所有成员共享同一块内存空间。在任意时刻,只有一个成员是“活跃”的(即其值是被最后写入的)。通过其他(非活跃)成员来读取这块内存,属于未定义行为。
可以分别介绍一下工厂模式,装饰器模式,观察者模式和单例模式吗?
单例模式确保一个类只有一个实例并提供全局访问点,用于控制资源共享;工厂模式将对象创建逻辑封装起来,使客户端无需关心具体实现,实现了创建与使用的分离;装饰器模式通过包装原有对象来动态添加新功能,提供了比继承更灵活的扩展方式;观察者模式则定义了一种一对多的依赖关系,当一个对象状态改变时,所有依赖它的对象都会自动得到通知和更新。
可以用shared_ptr来管理this指针吗?如何解决问题?
std::shared_ptr
的机制是为其管理的每个对象创建一个控制块(存储引用计数等信息)。如果你直接使用 this
指针创建新的 shared_ptr
,即使对象已被另一个 shared_ptr
管理,新创建的 shared_ptr
无法感知到已有的控制块,它会为自己创建一个全新的、独立的控制块。
要安全地解决这个问题,应让你的类公有继承 std::enable_shared_from_this<T>
(其中 T
是你的类名),然后在成员函数中通过 shared_from_this()
方法来获取指向自身的 shared_ptr
。关键在于:std::enable_shared_from_this
内部维护了一个 std::weak_ptr
,它会在对象被首个 shared_ptr
管理时(例如通过 std::make_shared<T>()
或 std::shared_ptr<T>(new T)
)自动关联到该 shared_ptr
的控制块;此后调用 shared_from_this()
会通过这个 weak_ptr
安全地升级为一个新的 shared_ptr
,并与所有现有 shared_ptr
共享同一个控制块和引用计数,从而避免重复释放。
Unity
Unity有哪些IO操作?
Unity主要提供了针对文件,资源和网络的IO操作。
如何理解C#中的Attribute特性?
C# 中的 Attribute(特性)是一种强大的声明性标签,用于为代码元素(如类、方法、属性、参数等)附加元数据(metadata)。这些元数据可以在编译时或运行时被读取,从而影响程序的编译过程、运行时行为,或提供额外的信息。
常见的 Attribute 及其用途如下:
C#的unsafe是什么?有何作用?
C# 中的 unsafe
上下文就像一把打开底层操作权限的“钥匙”。通过在代码块或方法外使用 unsafe
关键字,你可以在其内部直接使用指针进行内存地址的读写和算术运算,从而绕过 CLR 的安全检查来提升性能或与非托管代码交互。
但这不仅仅是“能用指针”,它还解锁了 fixed
(固定托管对象防GC移动)、stackalloc
(在栈上分配临时内存)等关键操作。然而,这是一把双刃剑,它要求开发者自行承担内存安全的责任,如避免越界访问和内存泄漏,因此应谨慎使用,并优先考虑 Span<T>
等安全替代方案。
如何理解Unity中的Sprite和Texture?
在 Unity 中,Texture(纹理) 和 Sprite(精灵) 是处理图像资源的两个核心概念,它们既有区别又紧密联系。
Texture 是原始的图像数据,本质上是存储在 GPU 内存中的一张图片,它主要负责定义物体表面的颜色和细节信息,通常作为 3D 模型的贴图,需要通过与材质球(Material)关联并赋予网格模型来使用。Sprite 则可以理解为基于 Texture 的、专为 2D 和 UI 设计的一种资源类型,它不仅包含了纹理数据,还是一个可定位、可移动的 2D 对象,拥有位置、旋转、缩放等属性,可以直接被 Unity 的 UI 系统(如 Image
组件)或 2D 渲染系统(如 Sprite Renderer
)使用,并支持动画、碰撞检测等功能。
它们的联系在于,Sprite 通常是从 Texture 中“切割”或定义出来的一个部分(或者直接使用整张 Texture),一个 Texture 可以通过 Sprite Editor 设置为多个 Sprite,形成图集(Atlas),从而优化渲染性能。
区别则在于用途和功能:Texture 是基础的数据层,而 Sprite 是基于 Texture 的应用层对象,专为 2D 交互和显示设计。简单来说,所有 Sprite 都依赖 Texture 提供图像源,但并非所有 Texture 都需要用作 Sprite(例如,有些 Texture 仅作为 3D 模型的贴图)。
Unity的材质球是什么?
可以将 材质球 (Material Asset) 理解为一张“设计图纸”,而 材质实例 (Material Instance) 则是根据这张图纸批量生产出的“产品”。修改图纸,所有产品都会变;但修改某一个产品,不会影响图纸和其他产品。
在 Unity 中,你首先需要自定义一个着色器(使用 Shader Graph 或编写 ShaderLab/CG/HLSL 代码),这定义了物体表面的渲染逻辑和可调参数。接着,在编辑器里创建一个材质球(.mat 文件),并将你写好的着色器赋给它,此时材质球就成了一个可配置的着色器实例。然后,将这个材质球拖拽或指定给场景中的模型(Mesh),模型的渲染器(Renderer)会使用它。默认情况下,所有使用该材质球的模型会共享同一份材质数据;但当你通过代码 (renderer.material
) 访问或修改其属性时,Unity 会自动为该模型创建一个独立的材质实例,后续的所有修改都只影响这个实例,而不会影响原始的材质球资源。
关于Resources,AB包和Addressable?
Unity 中的 Resources、AssetBundles (AB 包) 和 Addressables 是三种主流的资源管理方式,它们在存储、加载和卸载行为上各有特点。
AssetBundle(AB 包)和 Addressables 的核心区别在于自动化程度和易用性。AB 包是一个相对底层的系统,需要开发者手动处理依赖关系(如通过 AssetBundleManifest
递归加载依赖包)、自行实现引用计数机制来管理内存释放,并完全手动编写热更新流程(包括版本对比、差分下载和资源替换)。而 Addressables 是在 AB 包之上构建的高阶抽象层,它通过一个中心的 catalog
文件自动管理所有依赖,加载资源时开发者无需关心内部依赖链;它内置了基于引用计数的内存管理,通过 Release
方法自动跟踪和卸载不再使用的资源,极大降低了内存泄漏风险;其热更新流程也更高效,通过对比 catalog
文件可实现增量更新,仅下载变化的资源包。简而言之,Addressables 通过自动化解决了 AB 包在依赖、内存和更新三大方面的繁琐管理问题,显著提升了开发效率和可靠性。
Unity中的层级系统有哪些?
Hierarchy 窗口是 Unity 编辑器中的一个核心面板,它以树形结构直观地展示了当前场景中的所有游戏对象 (GameObject);Layer 是分配给单个游戏对象的一种标签或分类,主要用于过滤;Sorting Layer这套系统专门用于控制 2D 精灵 (Sprites) 和 UI 元素的渲染顺序(谁在前谁在后)。
父类的transform变化,可见性都会影响子类,同时父类和子类之间的组件可以互相获取。
Unity提供的PlayerPrefs是什么?有何作用?
Unity 中的 PlayerPrefs
是一个用于在玩家设备本地存储和读取简单数据的键值对存储系统。它本质上是 Unity 引擎提供的一个静态类,核心作用是实现数据的持久化,让你能够在游戏会话之间保存和加载一些基础信息。
它的工作原理很简单:你给每份数据一个唯一的键(Key,字符串类型),并为其赋值(Value,仅支持三种基本类型)。这些数据会被写入设备本地特定的存储位置(如 Windows 的注册表、Android 的 XML 文件等)。
.Net和Mono的关系是什么?
.NET 是一个由微软主导的跨语言、跨平台的开发者平台和生态标准,它定义了一套通用的API规范(.NET Standard)和运行环境(CLR);而 Mono 则是 .NET 标准的一个开源、跨平台的具体实现,它最早由社区开发,旨在让 .NET 应用程序(尤其是使用 C# 编写的程序)能够运行在 Linux、macOS、Android 等非 Windows 操作系统上,并成为 Unity 游戏引擎长期使用的脚本运行时环境。
Unity协程的底层原理是什么?
Unity 协程的底层原理是:C# 编译器将你的协程方法编译成一个状态机,Unity 引擎的调度器在主线程的每一帧中根据条件驱动这些状态机的执行。它提供了一种在单线程内优雅处理异步和延时操作的强大模式。
Unity 协程的底层原理是 C# 编译器将包含 yield return
的协程方法编译成一个实现了 IEnumerator
接口的状态机类,这个类通过一个状态变量记录执行位置,并通过 MoveNext()
方法推进;Unity 引擎的 C++ 层则提供了一个协程调度器,它在主线程每帧的特定阶段(如 Update 后)遍历所有活跃协程,检查其 yield return
返回的条件(如等待时间是否结束、条件是否满足),若条件满足则调用该状态机的 MoveNext()
方法,使协程从上次暂停的位置继续执行,直到协程结束或再次被挂起。整个过程完全在主线程中进行,本质是分帧的条件调度,而非多线程并发。
Unity实现跨平台的原理?
简而言之,Unity 就像一位全能的“翻译官”和“总指挥”。它先让你的代码变成一种通用语言(IL),然后根据要去的“国家”(目标平台),要么派出一位即时翻译(Mono虚拟机),要么直接生成一份当地语言的剧本(IL2CPP生成原生码)。同时,它还把不同地方的硬件和系统功能(如图形、输入、文件系统)都包装成统一的工具给你用(平台抽象层),只有在遇到极其特殊的地方法规时,才需要请本地专家(原生适配模块)来处理。
物理
什么是SAT分离轴定理?
SAT的核心思想基于一个直观的几何观察:若两个凸图形在所有可能的轴上投影均重叠,则它们相交;反之,若存在一条轴使其投影不重叠,则它们分离。
GJK算法又是什么?
GJK算法通过迭代方式构建一个称为单纯形的几何结构,并判断该结构是否包含原点,从而确定两个凸体是否相交。
UGUI
我们来聊聊UGUI的底层原理:
当你点击屏幕时,EventSystem 中的 Input Module(如 StandaloneInputModule
)会捕获输入信息并生成一个 PointerEventData
数据结构,然后由 Raycaster(如 GraphicRaycaster
用于 UI)从点击位置发射射线,检测其路径上与所有启用了 Raycast Target
的 UI 元素的矩形区域是否发生碰撞,并通过深度排序确定最终命中的最上层元素,最后通过 ExecuteEvents
这个“调度中心”将事件(如 IPointerClickHandler
)派发给目标对象上实现了相应接口的组件(如 Button
)来触发响应。
UGUI的运作流程?如何优化呢?
UGUI通过为每个UI元素创建网格和材质球来渲染,但为了优化性能(降低drawcall),它会将同层级、同材质的元素合并成一个大网格并使用同一张图集(共享材质球)。这种合并在UI静止时效率极高,但任何元素的移动或属性改变都会触发网格的拆分与重建,消耗大量CPU资源。因此,优化UGUI性能的关键在于减少此类动态变化,以最小化网格的拆分合并操作。
什么是material和shared material?有何区别?
首先会有一个纹理和一个着色器,然后可以根据这两个东西生成一个材质球,然后在场景中具体某个物体是由网格和材质组成的,这里的材质就是我们去材质球处获取的材质,material和shared material是两种从材质球处获取材质的方式 ,前者是获取一个材质实例,是一个独立副本,而后者是直接获取原始材质球的材质,如果修改前者不会影响原材质球而后者会。
如何在不同分辨率下保持UI的一致性?
有两种方法:一种是针对UI的RectTransform,另一种是Canvas Scaler中的Scale With Screen Size,这两种方法都可以帮助我们在不同分辨率下实现一致的UI。
Canvas上有哪些渲染模式?
overlay是无视所有摄像机而直接将UI绘制在屏幕最上面,camera space则是会在摄像机前方绘制,world space则是把UI作为世界中一个物体来处理。
Graphic Raycaster是什么?有何作用?
Graphic Raycaster 是 Unity UGUI 系统中一个处理 UI 交互的核心组件。它的主要作用是向 UI 元素发射射线,检测点击、触摸等输入事件,并判断这些事件是否落在了某个 UI 元素上。
什么是合批?什么是静态合批和动态合批?有何差异?
合批(Batching)是 Unity 中的一项核心渲染优化技术,其根本目的是通过减少 CPU 向 GPU 发送的绘制命令(Draw Call)次数来提升性能。它主要分为两种方式:静态合批(Static Batching) 和 动态合批(Dynamic Batching)。
静态合批在运行前(如打包时)进行,它将场景中标记为 Static、使用相同材质的静态物体的网格数据预先合并成一个大的网格。其优点是运行时几乎没有CPU开销,并能利用视锥剔除提升GPU效率,但代价是会增加包体大小和内存占用,因为合并后的网格需要常驻内存。
而动态合批则在运行时每帧进行,它自动将使用相同材质、且满足特定条件(如顶点数通常不超过900个顶点属性)的移动物体的顶点数据进行动态合并。其优点是不会增加包体和内存负担,物体可以移动,但缺点是会给CPU带来持续的计算开销,且条件苛刻,容易因顶点数超限、材质实例或属性不同等原因导致合批失败。
简而言之,两者最核心的差异在于:静态合批是“以空间换时间”,通过预消耗内存来避免运行时开销,适用于静止物体;动态合批是“以计算换效率”,通过CPU的实时计算来减少Draw Call,适用于小型可移动物体。在实际项目中,它们常根据物体的静态属性和性能需求结合使用。
如何理解UI的动静分离?
UGUI的渲染基于网格(Mesh)。一个Canvas下的所有UI元素,最终会被合并成一个或几个大网格提交给GPU渲染,这个过程称为合批(Batching),目的是减少Draw Call。
然而,一旦某个UI元素的属性(如位置、颜色、纹理等)发生变化,UGUI就必须重新计算该Canvas内所有UI元素的顶点数据,并重新合批网格,这个过程称为网格重构(Rebuild)。如果动静UI混杂在同一个Canvas中,一个血条的微小移动就可能触发整个界面(包括复杂的静态背景)的网格重构,造成巨大的CPU浪费。
UI动静分离是UGUI性能优化中最核心、最有效的策略之一。它的核心思想就是将频繁变化的UI元素与静态UI元素放置在不同的Canvas下,以限制网格重构的范围,从而降低CPU开销。
如何拆分过大的UI?
在 Unity UGUI 中,一个复杂的 UI 界面(如 RPG 游戏界面)如果不进行合理拆分,任何一个部分的微小变化(如血条更新)都会导致整个 Canvas 的网格重建(Rebuild),造成巨大的 CPU 性能开销。
将频繁变化的“动态”UI元素(如血条、技能图标、计时器)和永不变化的“静态”UI元素(如背景、边框)分离开,放置到不同的 Canvas 中。这样做可以严格限制重建操作的影响范围,动态UI的变化只会触发其所在Canvas的重建,而不会波及到静态UI所在的Canvas。同时,在实际项目中需要权衡利弊:虽然拆分 Canvas 可能会轻微增加 Draw Call,但因此带来的重建开销的显著降低,其收益远大于代价,是提升UI性能的关键实践。
为什么要预加载UI?如何预加载?
UGUI 界面实例化时,涉及 Prefab 实例化、Mesh 合并、组件初始化、渲染初始化等操作,CPU 开销集中。若在玩家点击打开界面的瞬间进行这些操作,极易引起明显卡顿,破坏游戏体验。预加载的核心思想就是“提前做功”,将这部分消耗转移到玩家不敏感的时间段(如游戏启动时、过场加载时)。
具体如何预加载,对于使用 Resources
目录的资源,可以使用异步加载在后台进行。对于更复杂的项目,通常使用 AssetBundle 或 Addressables 资源管理系统进行异步加载,原理类似。如果还是太慢的话,在加载场景或空闲时,实例化UI并完成初始化,然后立即禁用(SetActive(false)
)。需要显示时,只需启用它,几乎无开销。
Sprite Atlas是什么?有何作用?
Sprite Atlas 是 Unity 优化 2D 和 UI 渲染性能的利器。它通过将小纹理合并成大纹理来减少 Draw Call,优化内存使用,并提升渲染效率。要有效使用它,你需要:合理地规划和划分图集内容、根据项目类型(UGUI 或 2D Sprite)配置合适的打包参数、以及在运行时选择正确的加载方式(如 AssetBundle 或后期绑定)。
Mask是什么?有何作用?对性能有何影响?
在 Unity UGUI 中,Mask(遮罩)组件是一种利用 GPU 的模板缓冲区(Stencil Buffer) 来限制其子元素可见区域的工具。它的作用是只允许子元素在 Mask 定义的形状区域内显示,区域外的部分会被裁剪掉,常用于实现圆形头像、滚动视图或复杂 UI 的局部显示。
然而,Mask 会带来显著的性能开销:
- 它需要两次渲染操作(先写模板缓冲,再渲染子元素),会增加 Draw Call。
- 它会中断 UI 元素的合批(Batching),导致更多的绘制调用。
- 频繁的模板缓冲操作会增加 GPU 负载,尤其在低端设备上可能引发卡顿。
因此,对于矩形裁剪需求,应优先使用性能更优的RectMask2D
组件(它基于屏幕坐标裁剪,无模板缓冲开销);并尽量避免多层 Mask 嵌套,以减轻性能负担。
为什么RectMask2D
组件性能更优?
RectMask2D 性能更优的核心在于其简单、前置的裁剪机制。它在 CPU 端就直接对子元素的顶点数据进行计算和修改,完全丢弃超出矩形范围的顶点,使得传递给 GPU 渲染的数据本身就是“纯净”且可见的。这避免了像传统 Mask 那样,需要将大量最终不可见的像素送入 GPU 渲染管线,并在模板测试阶段才丢弃所造成的 GPU 填充率浪费和额外的 Draw Call。因此,对于矩形裁剪需求,RectMask2D 是一种开销更小、效率更高的解决方案。
Lua
ProtoBuf
这里专门开一个专题来介绍ProtoBuf:
ProtoBuf是什么?
ProtoBuf(Protocol Buffers)是 Google 开发的一种语言无关、平台无关、可扩展的结构化数据序列化机制。它通过预定义的 .proto
文件描述数据结构,并利用专用编译器生成不同编程语言的源代码,实现高效、紧凑的数据序列化与反序列化,特别适用于高性能网络通信(如 gRPC)和数据存储场景。
ProtoBuf相比起其他序列化方法来说,有哪些优点?
ProtoBuf比起其他协议的优点主要体现在高效的数据序列化,跨语言跨平台的特性以及优秀的向后向前兼容性。具体来说,ProtoBuf序列化后的数据体积更小,支持多种语言和平台使用,且允许删除旧字段或者添加新字段。
如何实现高效的数据序列化?
要实现高效的数据序列化,TLV(Tag-Length-Value)结构、Varints 编码和 Zigzag 编码正是其中的核心技术。它们通过紧凑的数据组织、灵活的整数编码和高效的负数处理,共同构成了像 Protocol Buffers (Protobuf) 这样的高效序列化方案的基石。
采用 TLV(Tag-Length-Value) 结构存储数据,其中 Tag 包含了字段的唯一编号和数据类型信息,这使得序列化时无需传输字段名,极大减少了数据体积。对整数类型,它使用 Varints 编码,动态根据整数大小分配存储字节,让小整数占用空间更少。同时,为高效处理负数,引入了 Zigzag 编码,先将有符号整数映射为无符号整数,再进行 Varints 编码,避免了直接编码负数时效率低下的问题。此外,Protocol Buffers 的序列化过程还会利用预编译的代码和零拷贝等技术来优化性能。
其实就是我们每个字段都会包含一个Tag,这个Tag中包含编号和数据类型,然后可以根据数据类型来最小化字段所需要的空间体积,比如一个int一般都要占据4个字节,但是我们在确定具体传输的内容后,比如有一个int值为1,那我们只用一个字节就可以存储这个1,而不是传统的存储int用4个字节。
然后在这个基础之上,我还想更广泛地学习一些其实比较常见但是目前还没有掌握的知识。
多线程
什么是自旋锁?
自旋锁(SpinLock)是一种用于保护共享资源的轻量级同步机制,主要用于多线程/多处理器环境。它的核心特点是:当线程尝试获取锁失败时,不会进入睡眠状态,而是会在一个循环中“忙等”(自旋),不断检查锁是否被释放,直到成功获取锁。
原子操作可以避免部分锁的开销吗?
原子操作通过利用硬件指令在用户态完成同步,避免了传统锁机制中陷入内核、线程挂起和上下文切换的主要开销。因此,在保护简单变量、短临界区且低竞争的场景下,原子操作的性能通常远高于互斥锁。
Linux相关
select和epoll是什么?
select和epoll都是Linux系统中用于I/O多路复用的机制,它们允许单个进程或线程同时监视多个文件描述符(如网络连接),并在这些描述符可读、可写或出现异常时通知应用程序进行处理,从而高效地管理大量并发连接。
它们的核心区别在于:select 采用轮询方式,每次调用都需要将全部监控的文件描述符从用户空间拷贝到内核空间,并由内核线性遍历所有描述符(时间复杂度 O(n)),且存在连接数限制(通常 1024);而 epoll 采用事件驱动机制,通过回调函数只处理活跃的描述符(时间复杂度 O(1)),无连接数限制,且通过内核与用户空间共享内存避免了重复拷贝,因此能高效处理海量并发连接,但缺点是仅限 Linux 平台使用。
数据库
什么是关系型数据库?
关系型数据库是一种基于关系模型来组织和管理数据的数据库系统。它使用二维表格(称为“关系”)来存储数据,表格由行和列组成,能够清晰地表示数据之间的关系。
一般关系型数据库的组成部分如下:
关系型数据库包含以下特性:采用二维表格来结构化地存储数据,通过主键、外键等约束维护数据的完整性和表之间的关联,并严格遵循 ACID 原则(原子性、一致性、隔离性、持久性)来保证事务的可靠性,同时使用标准化的SQL语言作为数据操作和查询的统一接口,从而确保了数据的一致性、安全性和强大的复杂查询能力。
如何理解遵循范式?
数据库设计中的“遵循范式”,指的是在构建关系型数据库时,遵守一系列预先定义的规范化设计原则。这些原则旨在通过减少数据冗余、避免数据操作异常(如插入、更新和删除异常)来提升数据的完整性和一致性。