哔哩哔哩C++
RTSP、RTMP、HLS的区别
- RTSP(Real Time Streaming Protocol):
- 用途: 主要用于视频监控和视频会议系统。
- 特点: 支持暂停、播放、快进等操作,实现了对流媒体的实时控制。
- 延迟: 低至几百毫秒,适合实时交互。
- RTMP(Real Time Messaging Protocol):
- 用途: 初期主要用于Adobe Flash播放器,现在用于直播。
- 特点: 在传输过程中可以加密,更加安全。
- 延迟: 较低,适合直播。
- HLS(HTTP Live Streaming):
- 用途: 主要用于在线视频平台和OTT(Over The Top)流媒体。
- 特点: 基于HTTP传输,易于跨平台,且便于跨防火墙和代理服务器传输。
- 延迟: 较高,通常在几秒到十几秒,但最新的技术进展已能显著减低延迟。
视频编码
常见的视频编码标准包括H.264、H.265、VP9等。
影响编码效率的因素
- 编码算法:不同编码标准如H.264、H.265的算法复杂性不同,影响效率。
- 分辨率和帧率:高分辨率和高帧率视频需要更多的数据处理。
- 码率:码率高时数据量大,编码负荷更重。
- 内容复杂度:场景复杂多变的视频比静态或重复场景的视频编码难度大。
- 颜色深度:颜色深度大的视频如10bit比8bit的视频编码更费时。
- 并发编码算法:硬件加速和多线程技术可以提高编码效率。
- 压缩方式:使用更高级的压缩技术(如CABAC)可以提升编码效率。
视频延迟来自于哪些方面
- 采集延迟:摄像头和麦克风捕捉数据的时间。
- 编码延迟:将原始视频和音频数据编码成数字流的处理时间。
- 处理延迟:视频图像的预处理、滤镜应用等额外处理步骤。
- 封装延迟:将编码后的数据打包成特定格式的时间。
- 网络传输延迟:数据包通过网络从发送者到接收者的时间,包括传播、排队、处理和解包时间。
- 缓冲延迟:为了平滑网络抖动,在客户端进行的数据缓冲。
- 解码延迟:客户端将接收的数据流解码为可播放的视频和音频的时间。
- 播放延迟:视频渲染和播放的等待时间。
开源流媒体服务器了解吗
- **SRS ** - 简单高效的RTMP/HLS直播服务器。
- Nginx-RTMP - 基于Nginx开发的RTMP流媒体服务器。
- Red5 - 使用Java开发的流媒体服务器,支持多种流媒体协议。
- MediaSoup - 针对WebRTC的高性能SFU服务器。
- Janus - 实时通信服务器,支持WebRTC等多种协议。
编码的参数有哪些
- 比特率(Bitrate):编码时的数据传输速率,直接影响视频和音频质量。
- 帧率(Frame Rate):每秒显示的图片数,影响视频流畅度。
- 分辨率(Resolution):视频的宽度和高度,影响视频清晰度。
- 编码格式(Codec):影响文件兼容性,如H.264, H.265等。
- GOP(Group of Pictures):影响I帧(关键帧)的频率,进而影响视频可寻址性和压缩效果。
I帧、P帧、B帧
I帧:关键帧,独立编码,不依赖其他帧。
P帧:向前预测帧,依赖前面的I帧或P帧进行编码。
B帧:双向预测帧,依赖前后帧进行编码,压缩率最高。
多态
C++的多态是通过指向基类的指针,来调用实际子类的方法,让不同的子类对象可以执行各自的操作。多态主要包括虚函数(动态多态)和函数重载(静态多态)两种形式。
手撕大小端转换
- 掩码并取得各个字节。
- 通过移位把字节放到相反位置。
- 组合这些字节得到最终结果。
这是一个32位整数大小端转换的典型代码片段:
uint32_t swapEndian(uint32_t value) {
return ((value & 0x000000FF) << 24) |
((value & 0x0000FF00) << 8) |
((value & 0x00FF0000) >> 8) |
((value & 0xFF000000) >> 24);
}
手撕合并区间
- 对区间按照起始位置进行排序。
- 遍历排序后的区间列表,逐一判断当前区间是否可以与结果数组的最后一个区间合并:
- 如果可以合并(当前区间的起始位置小于或等于结果数组最后一个区间的结束位置),则更新结果数组最后一个区间的结束位置为两个区间结束位置的较大值。
- 如果不可以合并,直接将当前区间加入到结果数组中。
代码示例:
#include <vector>
#include <algorithm>
std::vector<std::vector<int>> mergeIntervals(std::vector<std::vector<int>>& intervals) {
if (intervals.empty()) return {};
std::sort(intervals.begin(), intervals.end()); // 按区间起始位置排序
std::vector<std::vector<int>> merged;
for (const auto& interval : intervals) {
// 如果结果数组为空,或当前区间与结果数组最后一个区间不重叠,直接添加
if (merged.empty() || merged.back()[1] < interval[0]) {
merged.push_back(interval);
}
// 否则,合并当前区间与结果数组最后一个区间
else {
merged.back()[1] = std::max(merged.back()[1], interval[1]);
}
}
return merged;
}
腾讯光子工作室游戏客户端
说一下 unity dots 的 ecs
Unity的DOTS是Unity的一种高性能编程模式,核心是ECS 。ECS主要包括三个基本概念:实体(Entity)、组件和系统。
- 实体:代表游戏世界中的对象,比如玩家、敌人或任何想要表示的事物。在ECS中,实体本身几乎不包含任何数据或行为,它更像是一个标识符。
- 组件:包含数据的容器,用于描述实体的特征或状态。例如,一个位置组件可能包含X、Y和Z坐标。
- 系统:包含逻辑和行为,用于操作具有特定组件的实体集合。系统会执行具体的任务,如移动所有具有位置和速度组件的实体。
baker 过程主要是输出了什么
产生的输出通常包括:
- 贴图(比如法线贴图、位移贴图、环境光遮蔽贴图等),用于提高渲染效率,同时保持视觉上的复杂度和细节。
- 静态光照信息,将光照信息嵌入到贴图中,减少实时光照计算的需要。
说一下 archetype 的概念,它的用处主要是做什么的
Archetype的概念源自于Unity的ECS 中,指的是一种具有一组特定组件配置的实体模板。每个Archetype定义了一组实体共享的组件类型,这使得具有相同组件集合的实体可以高效地存储和访问。
Archetype的主要用处是优化数据的存储和访问。通过将相同Archetype的实体数据紧凑地存储在一起,Unity可以更高效地遍历这些数据,从而提高了缓存的命中率和性能。这种数据组织方式使得针对大量相似实体进行操作时,如同批处理一样高效。
entity 上面挂载三个 component,数据是分开存的还是存在一起的
实体上挂载的组件数据是分开存储的
uitookit 和 ugui 区别
- UI Toolkit:是一种新的UI构建方式,它使用CSS样式表进行样式定义,支持视觉树,旨在提高UI开发的效率和性能,主要应用于编辑器扩展开发,但也支持运行时UI开发。
- UGUI(Unity GUI):是Unity的早期UI系统,基于游戏对象和组件,通过拖放和配置这些组件在场景中创建UI。它非常直观,易于入门,适用于游戏内UI开发。
说一下水位线对象池
水位线对象池是一种内存管理策略,它通过预先分配一定数量的对象,并在这个基础上动态地根据需要创建或回收对象来管理内存使用。"水位线"指的是对象池中对象的数量阈值:低水位线和高水位线。
- 当对象池中的可用对象数量下降到低水位线以下时,对象池会预先创建更多的对象,以避免运行时创建对象的开销。
- 当对象池中的对象数量上升到高水位线以上时,多余的对象会被销毁或回收,以释放内存。
uitoolkit 里面怎么去实现自定义事件
- 创建一个新的事件类,继承自
EventBase<T>
,其中T
是你的新事件类名。 - 在你的自定义事件类中,添加所需的属性和方法。
- 使用
RegisterCallback<T>()
方法在感兴趣的UI元素上注册事件监听器,T
是你的自定义事件类。 - 使用
visualElement.SendEvent()
方法触发自定义事件。
c++ 虚函数实现原理,继承的时候虚函数表会发生什么
C++的虚函数是通过一个称为虚函数表的机制实现的。每个包含虚函数的类或者结构体都有一个相应的虚函数表。
虚函数表是一个存储指向虚函数的指针数组。每个类或者结构体的对象都有一个指向其虚函数表的指针,称为虚指针。
当我们声明一个基类指针指向派生类对象并通过该指针调用虚函数时,编译器会首先查找基类的虚函数表,找到虚函数的索引,然后利用这个索引到派生类的虚函数表中找到并调用对应的派生类函数。这个过程就是多态的实现原理。
当我们进行继承时,派生类会继承基类的虚函数表,并根据需要在其中添加新的虚函数指针,或者替换原有虚函数指针,因为派生类可以重写基类的虚函数。这使得基类指针可以调用派生类重写的虚函数,实现多态。
c++ 如何调用 c 函数
确保C函数的声明被放置在extern "C"
的代码块中,这样可以告诉C++编译器这些函数按照C语言的方式进行链接。
extern "C" {
// C函数声明
void c_function();
}
正常调用C函数,就像调用C++函数一样。
int main() {
c_function(); // 调用C函数
return 0;
}
使用extern "C"
防止C++编译器改变函数名(即名称修饰),这样链接器就能正确识别和链接C函数。
字节对齐的概念和意义
字节对齐是计算机内存管理中的一种策略,它要求数据的起始地址按照某个数布的倍数进行对齐。
实现字节对齐的意义有两点:
- 更快读取速度:在许多系统中,如果数据落在自然边界上(即地址是某个值(如4或8)的倍数),硬件可以在一个操作中取得数据。如果没有对齐,可能需要进行多次内存访问才能获取或存储同样的数据。
- 硬件要求:许多硬件平台只能在特定地址处访问特定类型的数据,对齐可以确保满足这些硬件平台的数据访问规定。例如,某些硬件平台不允许跨越字边界来存取数据,或者在这种系统中这样做会严重影响性能。
float 变量如何和 0 比较
使用一个很小的正数作为容差来判断浮点数是否“等于”0。例如:
const float EPSILON = 1e-6; // 定义一个很小的数
float x = ...; // 浮点数变量
if (abs(x) < EPSILON) {
// x “等于” 0
}
这种方法通过检查x
是否在0
的一个很小的区间内来判断x
是否“等于”0。
协程算是异步吗
协程是一种异步编程的模型。协程允许异步执行操作如输入/输出操作、耗时计算等,而不会阻塞程序的执行。这样,可以在一个线程内实现多个操作的并发执行,提高程序的效率和响应速度。
unity 协程是在主线程还是子线程,靠什么调度的
Unity中的协程运行在主线程,它们由Unity的游戏循环调度。协程通过使用yield
语句暂停执行,并将控制权返回给游戏循环,随后在合适的时机继续从暂停的地方继续执行。这允许进行延时操作和等待Unity的其他事件,而不会阻塞主线程。
gameobject 上面的协程什么时候销毁
Gameobject上面的协程会在该Gameobject被销毁时结束。
美团到家后端
c++的多态是怎么实现的
C++中的多态主要通过虚函数实现。当一个类中的函数被声明为虚函数时,它可以在该类的派生类中被重写,实现运行时多态。在调用时,会根据对象的实际类型来确定调用哪个版本的函数,这个决定是在运行时做出的。这种机制是通过虚函数表来实现的,每个有虚函数的类都有一个对应的虚函数表,表中存储了指向虚函数的指针。通过对象指针或引用调用虚函数时,会根据这个表来动态确定调用哪个函数。
什么是虚函数
虚函数是在基类中使用关键字 virtual
声明的函数,它允许在派生类中被重写,以实现多态性。这意味着当你通过基类指针或引用调用一个虚函数时,实际调用的是对象的动态类型(派生类类型)中对应的函数实现,而非指针或引用的静态类型(基类类型)中的实现。这个机制允许在运行时根据对象的实际类型调用正确的函数版本,从而实现运行时的多态。
什么是进程?什么是线程?两者的区别?
进程是操作系统分配资源和调度的基本单位,它包含了运行程序所需的代码、数据以及其他系统资源。线程是进程中的执行流,它是CPU调度和执行的最小单位。
两者的主要区别在于:
- 进程拥有独立的地址空间,而同一进程下的线程共享地址空间。
- 进程间切换开销较大,线程间切换开销较小。
- 进程间通信(IPC)方式多样但相对复杂,线程间可以直接通过读写共享内存来通信,更简单高效。
什么是死锁?怎么避免死锁?
死锁是指多个进程或线程在运行过程中,因争夺资源而造成的一种僵持状态,每个进程都在等待其他进程释放资源,但没有任何进程先行释放资源,导致所有进程都无法向前推进。
避免死锁的策略包括:
- 资源分配顺序:确保所有进程以相同的顺序请求资源。
- 资源一次性分配:要求进程启动时一次性申请所需的所有资源。
- 资源使用限制:限制同一时刻请求资源的进程数量,或通过预先分析确定资源的最大需求量。
- 使用锁超时:进程尝试锁定资源时,加入超时机制,超时未锁定则释放已占有的资源并重试。
操作系统的段和页?
- 段:段是一种逻辑地址空间的划分方式,以便将程序分成多个相对独立的部分,例如代码段、数据段等。每个段在逻辑内存中是连续的,但在物理内存中可能不连续。段的大小不固定,由程序所需要的内存量决定。
- 页:页是一种物理地址空间的划分方式,将物理内存和逻辑内存都分割成固定大小的块。物理内存的页被称为页框,逻辑内存的页被称为页。通过页表,操作系统可以将逻辑内存的页映射到物理内存的页框,实现内存的虚拟化。
段和页的主要区别在于,段是以逻辑的方式进行内存划分
MySQL中什么是索引?
索引是一种数据结构,用于提高数据检索效率,类似于书的目录,可以快速定位到数据的位置。
建立索引的原则是什么?
- 选择性高的字段:字段的唯一值越多,索引的效果越好。高选择性意味着查询可以从大量数据中快速过滤出少量结果。
- 常用作查询条件的字段:经常出现在WHERE子句中的字段是建立索引的好候选。
- 排序、分组字段:经常用于ORDER BY、GROUP BY子句的字段,建立索引可以提高排序和分组的效率。
- 连接字段:在多表JOIN操作中作为连接条件的字段,索引可以显著加快连接查询的速度。
- 避免过宽字段:过宽的字段意味着索引空间更大,维护成本更高,应优先考虑短索引。
- 避免频繁修改的字段:字段数据频繁变动会导致索引频繁重建,影响性能。
- 考虑创建组合索引:多个字段经常一起作为查询条件时,考虑创建组合索引,但需注意字段顺序。
- 权衡索引成本:虽然索引可以提高查询效率,但也会增加写操作(INSERT、UPDATE、DELETE)的成本和占用额外的存储空间。合理构建和维护索引以平衡这种权衡。
索引是怎么实现的?
索引通常通过数据结构如B树(尤其是B+树)和哈希表来实现。
讲讲为什么使用B+树?
- B+树是一种平衡多叉树,保证了所有叶子节点都在同一层,查询路径长度一致,确保了查询性能的稳定性。
- B+树的叶子节点按键值有序链接,便于实现范围查询。
- 相对于二叉树等其他平衡树,B+树有更高的分支因子,减少了磁盘I/O操作次数。
- B+树的设计充分利用了磁盘预读原理,减少了页的分裂概率,提高了空间和时间局部性。
- B+树只在叶子节点保存数据的指针或实际数据,在内部节点仅存储键值,提高了节点存储效率。
什么是聚簇索引和非聚簇索引?
聚簇索引:直接按照一定顺序存储数据记录的物理形式。一个表只能有一个聚簇索引,因为数据只能按照一种顺序排列。
非聚簇索引:不改变数据实际存储顺序,而是创建一个单独的索引结构来指向数据记录的位置。一个表可以有多个非聚簇索引。
B+树是什么?
B+树是一种自平衡的树数据结构,它维护排序数据以允许搜索、顺序访问、插入和删除操作在对数时间内进行。B+树特别适合用于读写较大数据块的存储系统。主要特点包括:
- 所有的叶子节点都位于同一层,并且包含数据及其对应的键值。
- 所有的非叶子节点都可以作为索引部分,只包含其子节点中的最大(或最小)键值。
- 叶子节点通过指针相连,这为遍历提供了高效的顺序访问路径。
什么叫慢查询
慢查询是指在数据库中执行时间过长,响应速度慢的查询操作。具体的时间阈值可以根据系统的具体需求进行定义,例如,如果一条查询语句执行时间超过了设定的阈值(比如1秒),那么就可以将这条查询语句标记为慢查询。慢查询的原因可能包括数据量过大、数据库设计不合理、索引问题等。了解和优化慢查询对于提升数据库和整个系统的性能有重要作用。
什么是事务?
事务是数据库管理系统执行过程中的一个操作序列单元,它是一个不可分割的工作单位。事务具备以下四个标准属性,通常被称为ACID属性:
- 原子性:事务内的所有操作要么全部成功,要么全部失败回滚。
- 一致性:事务必须保证数据库从一个一致性状态转移到另一个一致性状态。
- 隔离性:并发执行的事务之间要相互隔离,防止互相影响。
- 持久性:一旦事务提交,其结果就永久地保存在数据库中。
讲讲什么是动态规划以及他的主要思想。
动态规划(DP)是一种算法设计技术,主要用来解决特定类型的优化问题。它能够将复杂问题分解为更简单的子问题,并且存储这些子问题的解,避免了重复计算,极大地提高了计算效率。
动态规划的主要思想基于两个核心要素:
- 最优子结构:
关键在于识别出给定问题的解可以通过其子问题的最优解有效构造出来。换言之,原问题的最优解包含了其子问题的最优解,这样就可以逐步构建出整个问题的最优解。 - 重叠子问题:
动态规划适用于子问题重叠的情况,即不同的问题部分包含了相同的子问题。为了提高效率,动态规划会保存这些子问题的解,这样当再次遇到相同子问题时,就可以直接使用已经计算过的结果。
以下是实施动态规划的基本步骤:
- 定义状态:分析问题性质,定义合适的状态表示解的各个方面。
- 状态转移方程:根据问题的规则,确定状态之间的转移方程,这是动态规划的核心部分。
- 初始化条件:确定初始状态值,以正确开始状态转移的过程。
- 计算顺序:确定计算状态值的顺序,通常是一种基于之前计算结果的迭代过程。
- 构造最终解:利用计算出的状态值构造问题的最终解。
动态规划一般可以解决什么样子的实际问题?
- 计算最优解:比如路径寻找(最短路径问题)、资源分配(背包问题)。
- 决策制定:如工程项目的调度、库存管理。
- 序列:比如字符串比对(编辑距离问题)、最长递增子序列。
- 组合:比如子集划分(划分问题)、不同路径的计算问题。
最长公共子序列,只用说出思路,然后问最长公共子序列的现实价值
在C++中实现最长公共子序列(LCS)通常使用动态规划。创建一个二维数组dp
,其中dp[i][j]
代表第一个序列前i
个元素和第二个序列前j
个元素的LCS长度。遍历两个序列,当遇到相同元素时,dp[i][j] = dp[i-1][j-1] + 1
;否则,dp[i][j] = max(dp[i-1][j], dp[i][j-1])
。最终dp[m][n]
(m和n分别是两个序列的长度)即为所求的LCS长度。
最长公共子序列在实际中的应用包括:
在版本控制和代码审查中用来识别文件之间的差异。
在数据比对和数据同步中确定相似度高的记录。
在撤销功能的实现中记录和比较文本的历史更改。
一道sql题目:给了一个表,查询每个用户最近一天登录的日子(提示了使用Max函数)
思路:
- 定义数据结构:使用
std::unordered_map<std::string, std::string>
存储用户ID和其最后登录日期的映射。这里假定用户ID和日期都是字符串格式。 - 读入数据:遍历给定的表中的每一行记录,对于每条记录,提取用户ID和日期。
- 更新记录:对于每条记录,首先检查是否已经在
std::unordered_map
中记录了该用户。如果记录了,比较现有日期和表中的日期,只保留较新的那一个;如果还未记录,直接插入该用户ID和日期的键值对。
#include <iostream>
#include <unordered_map>
#include <vector>
#include <algorithm> // 用于std::max
// 假设LoginRecord是从某处得到的数据结构,存储了用户ID和登录日期
struct LoginRecord {
std::string userId;
std::string loginDate;
};
std::unordered_map<std::string, std::string> findLatestLogin(const std::vector<LoginRecord>& records) {
std::unordered_map<std::string, std::string> userLatestLogin;
for (const auto& record: records) {
auto& recordedDate = userLatestLogin[record.userId];
recordedDate = std::max(recordedDate, record.loginDate);
}
return userLatestLogin;
}
int main() {
// 示例:假设records是从某处读入的登录记录
std::vector<LoginRecord> records = {
{"user1", "2024-04-01"},
{"user2", "2024-04-02"},
{"user1", "2024-04-03"},
// 添加更多记录...
};
auto latestLogins = findLatestLogin(records);
// 打印结果
for (const auto& pair : latestLogins) {
std::cout << "User: " << pair.first << ", Latest Login: " << pair.second << std::endl;
}
return 0;
}
lc二叉树层序遍历
思路:
- 如果根节点为空,则返回空列表。
- 创建一个队列,将根节点加入队列。
- 当队列不为空的时候,进行以下操作:
- 计算当前队列的长度,这将是这一层的节点数。
- 为当前层创建一个空列表。
- 根据这一层的节点数,依次从队列中取出节点,并将其子节点(非空的左和右子节点)加入队列。
- 将取出节点的值加入当前层的列表中。
- 将每一层的节点值列表加入到最终结果中。
- 当队列为空时,返回最终结果。
#include <iostream>
#include <queue>
#include <vector>
// Definition for a binary tree node.
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
std::vector<std::vector<int>> levelOrder(TreeNode* root) {
std::vector<std::vector<int>> levels;
if (!root) return levels;
std::queue<TreeNode*> queue;
queue.push(root);
while (!queue.empty()) {
int levelLength = queue.size();
std::vector<int> currentLevel;
for (int i = 0; i < levelLength; ++i) {
TreeNode* node = queue.front();
queue.pop();
// Add the current node's value to the current level
currentLevel.push_back(node->val);
// Insert the children of current node in the queue
if (node->left) queue.push(node->left);
if (node->right) queue.push(node->right);
}
// Add the current level's values to the output list
levels.push_back(currentLevel);
}
return levels;
}
int main() {
// Example usage:
// Construct binary tree here and call levelOrder with root node
return 0;
}
收集整理了一份2024年最新C++开发学习资料,既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C++开发知识点,真正体系化!
包含大厂面经、学习笔记、实战项目、大纲路线、讲解视频 领取地址:
https://docs.qq.com/doc/DR2N4d25LRG1leU9Q
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。