基础知识总结
语言
C++
1、vector
的注意事项:
- 删除一个元素:remove and erase
- 释放vector申请的内存:与临时构建的vector交换(swap)
vector<bool>
不是STL容器,它储存的元素也不是bool
。vector<bool>
存储bool
的方式是一个bit一个bit的存,而不是一个byte一个byte的存,通过下标访问元素时得到的是vector<bool>::reference
而不是bool&
clear()
只是清除原本数组中的元素,即size() == 0
,但是capacity() != 0
- 如果
vector
中的元素是指针,先遍历每个元素delete
,否则将造成内存泄漏
2、STL的哈希表(unordered_map
):
- 底层容器:哈希桶(bucket),连续的一段内存,capacity是离数据大小最近的质数
- 解决冲突:开链法,转换后key相同的话就用链表接在一起(内存不连续)
3、C++面向对象的三个特性: 继承、封装、多态
4、多态分类:
- 静态多态:模板、重载
- 动态多态:虚函数
5、C++编译过程,以及生成的文件:
- 预处理:删除
#define
,展开宏定义;处理条件预编译指令,如#if
、#endif
等;处理#include
预编译指令,将文件内容替换到它的位置;删除注释。预处理后生成.i
文件 - 编译:将预处理后的文件转换成汇编代码,生成汇编文件(
.s
文件) - 汇编:把汇编语言翻译成目标机器指令的过程,生成目标文件(
.o
文件) - 链接:将多个目标文件即所需要的库链接成最终的可执行目标文件,生成可执行文件(
.exe
文件)
6、虚函数表和虚表指针:
- 虚函数表是属于类的,C++中虚函数表位于只读数据段(
.rodata
),也就是C++内存模型中的常量区,可以看作一个存放虚函数指针的数组 - 虚表指针属于每个实例化的对象(如果类中有虚函数),存放在对象的内存首部,在类初始化之前就被创建了,它的大小就是一根指针的大小(32位4字节,64位8字节)
- 调用虚函数时,先通过对象的this指针去找到虚表指针,然后再通过虚表指针去找虚函数表中对应的虚函数
7、C++ 11新特性:
- auto类型推导
- lambda表达式:
[ capture ] ( parameter ) optional -> return { body; }
- 右值引用和移动语义
- 智能指针
nullptr
代替NULL
constexpr
常量表达式- 基于范围循环
- 初始化列表
8、deque
的底层实现:deque 容器存储数据的空间是由一段一段等长的连续空间构成,各段空间之间并不一定是连续的,用一个数组存储各个连续空间的首地址
9、C++构造和析构顺序:
- 构造:先基类后派生类
- 析构:先派生类后基类
10、C++类成员初始化顺序:
- 成员变量使用初始化列表初始化时,顺序与变量的声明顺序有关,与初始化列表顺序无关
- 使用构造函数初始化时与赋值的顺序有关
- 类中const成员常量必须在构造函数初始化列表中初始化
- 类中static成员变量,必须在类外初始化
- 常规初始化顺序:基类的静态变量或全局变量、派生类的静态变量或全局变量、基类的成员变量、派生类的成员变量
11、struct
和union
的区别:
struct
会给每个定义的变量分配空间,union
只会给最后一个赋值的变量分配空间- 对于
Union
的不同成员赋值,将会对其他成员重写,原来成员的值就不存在了,而对于struct
的不同成员赋值 是互不影响的
12、虚函数可以是内联函数(inline
和virtual
可以一起用):
inline
和virtual
一起用不会报语法错误- 内联是在发生在编译期间,编译器会自主选择内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。说人话:指针调用不可inline
inline virtual
唯一可以内联的时候是:编译器知道所调用的对象是哪个类,这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。说人话:对象调用可以inline
13、static
的用法:
(1)修饰局部变量,延长局部变量的生命周期,在程序运行结束之后才会释放。static
修饰的局部变量存放在全局数据区的静态变量区,初始化时自动初始化为0
(2)修饰全局变量,限制全局变量智能在本文件中调用,不能在其他文件中访问,即使使用extern
外部声明
(3)修饰函数,限制函数只能在当前文件中调用,不能被其他文件调用
(4)修饰类成员的数据成员:
- 静态数据成员可以实现多个对象之间的数据共享,它是类的所有对象的共享成员,它在内存中只占一份空间,如果改变它的值,则各对象中这个数据成员的值都被改变
- 静态数据成员是在程序开始运行时被分配空间,到程序结束之后才释放,只要类中指定了静态数据成员,即使不定义对象,也会为静态数据成员分配空间
- 静态数据成员可以被初始化,但是只能在类体外进行初始化,若未对静态数据成员赋初值,则编译器会自动为其初始化为0
- 静态数据成员既可以通过对象名引用,也可以通过类名引用
(5)修饰类成员的函数成员:
- 静态成员函数和静态数据成员一样,他们都属于类的静态成员,而不是对象成员
- 非静态成员函数有this指针,而静态成员函数没有this指针
- 静态成员函数只能访问静态数据成员而不能访问非静态成员
(6)static
不可修饰虚函数
14、const
的用法:
(1)修饰一般常量及数组:表示该变量是不可修改的
(2)修饰指针:
const
在星号左边(const int* p
)表示常量指针(底层const),指针所指向的变量是一个常量,不能改变所指变量的值
int a = 0;
const int* p = &a;
*p = 10; // error
const
在星号右边(int* const p
)表示指针常量(顶层const),指针本身是一个常量,不能改变指针的指向
int a = 0;
int b = 1;
int* const p = &a;
p = &b; // error
(3)修饰函数:
- 修饰函数返回值:返回值不可修改,归根究底就是使得函数调用表达式不能作为左值,但是不代表函数的返回值是一个常量
const int foo() {
return 1;
}
int main() {
auto a = foo(); // a是int
return 0;
}
- 修饰函数参数:
- 修饰普通类型时,表示这是一个常量,不可修改
- 修饰指针时,与上面(2)的作用一样,主要用于防止修改指针所指的值(底层const)
- 修饰引用时,防止函数修改所引用的值(相当于底层const)
- 修饰成员函数时:
const
放在参数表右边,作用是防止成员函数修改类成员的值(除非变量用mutable
修饰)
class Test {
int a = 0;
public:
void Change() const {
a = 10; // error
}
};
16、四种cast
:
-
const_cast
:将const type
转换为type
,将const type&
转换为type&
-
static_cast
:强制类型转换- 任何隐式类型转换都可以用
static_cast
显式进行 - 使用
static_cast
可以找回存放在void*
指针中的值 static_cast
也可以用在于基类与派生类指针或引用类型之间的转换,向上转换是安全的,向下转换不安全,因为它不做运行时类型检查
- 任何隐式类型转换都可以用
-
dynamic_cast
:用于类层次间的上行转换和下行转换,要使用dynamic_cast
进行下行转换,要保证基类中有虚函数,否则无法过编译。而上行转换则无要求 -
reinterpret_cast
:转换时,执行的是逐个比特复制的操作
17、内存对齐: 现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定变量的时候经常在特定的内存地址访问,这就需要各类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。
为什么需要内存对齐:CPU读取内存时是按照每N个byte(N=2^n,n≥0)去读的,N称之为内存存取粒度,32位系统默认对齐系数是4,64位系统默认对齐系数是8。现在考虑4字节存取粒度的处理器取int
类型变量,处理器从0地址开始读取。假设该int
类型变量存在[1,4]地址中,那么处理器要读取两次内存才能完整读到该变量(第一次读[0,3]区间,变量在4地址还有未读的数据,需要处理器第二次读[4,7]区间的内存)。有了对齐后,该变量只能存在[0,3]区间,处理器一次读取操作就能完成。
内存对齐规则:每个特定平台上的编译器都有自己的默认对齐系数,可以通过预编译命令来改变这一系数。
#pragma pack(n) // n = 1,2,4,8,16
struck {
// ...
}
#pragma pack() // 取消当前对齐系数,恢复默认
给定的对齐系数和结构体中最长数据类型长度中较小的那个称之为有效对其值(对齐单位)。对齐规则如下
- 结构体第一个成员的偏移量(offset)为0,以后每个成员相对于结构体首地址的 offset 都是该成员大小与有效对齐值中较小那个的整数倍,如有需要编译器会在成员之间加上填充字节
- 结构体的总大小为有效对齐值的整数倍,如有需要编译器会在最末一个成员之后加上填充字节
18、内存模型:
- 栈区,由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等
- 堆区,由malloc等分配的内存块,用free来结束自己的生命
- 全局静态区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。全局静态区又分为数据段和BSS段,已初始化的静态变量存放在数据段,未初始化的静态变量存放在BSS段
- 常量区,就是存放常量的区,字符串字面值也存在这,它们是只读的,不允许修改
- 代码区,存放函数体的二进制代码
19、C++14新特性:
- 泛型lambda函数
- lambda捕获表达式
- 函数返回值类型推导
decltype(auto)
- 放松的
constexpr
限制
C#
1、C#的GC是怎么实现的:
Mark-Compact标记压缩算法:
步骤为:线程挂起、确定root、创建reachable object graph、对象回收、heap压缩、指针修复。
线程挂起,通过一个 root object,遍历这个该对象引用的变量,并且标记为reachable object(主要是已经初始化的静态变量和线程仍在使用的对象),从root出发可以创建reachable object graph,没有标记的对象都被回收了。对象被回收之后内存空间变得不连续,在heap中移动这些对象,让它们重新从heap的基址开始连续排列,类似磁盘空间的碎片整理。因为对象被移动了,地址发生了变化,所以要修复所有的引用指针,让它们指到正确的位置。结束GC后线程重新激活。
2、确定GC的root对象:GC搜索roots的地方包括全局对象、静态变量、局部对象、函数调用参数、当前CPU寄存器中的对象指针(还有finalization queue)等。主要可以归为2种类型:已经初始化了的静态变量、线程仍在使用的对象(栈+CPU寄存器)
3、装箱与拆箱:
- 装箱指的是将值类型转为引用类型
- 拆箱指的是将引用类型转为值类型
- 值类型的字段是在栈上分配内存的,而引用类型的字段则是在托管堆上分配内存,然后栈上保存一个指向托管堆的指针
- 装箱操作先在托管堆中分配内存,分配的内存量是值类型各字段所需的内存量,还要加上类型对象指针和同步块索引所需的内存量。然后把栈上的值复制到新分配的堆里,栈上的数据出栈。最后返回对象地址,把地址入栈
- 拆箱操作先获取已装箱对象的各个字段地址,然后把值拷贝到栈上的实例中
- 因为在装箱时有内存分配操作,有可能引发GC,拆箱之后,内存也不是马上释放,后续有GC风险。所以写代码时候要尽量避免装箱拆箱
- 比较直接的解决方案,把
struct
改为class
,引用类型不会引发装箱拆箱。enum
改不了,可以用List<>
去包装,List<>
包装的方法适用所有值类型
操作系统
1、线程与进程的区别:
- 进程是分配资源的最小单位,线程是执行程序的最小单位(资源调度的最小单位)
- 一个程序可以对应多个进程,一个进程对应一个程序,一个进程可以有多个线程
2、线程同步:
- 互斥对象:只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程同时访问。当前拥有互斥对象的线程处理完任务后必须将互斥对象交出,以便其他线程访问该资源
- 信号量:它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。每增加一个线程对共享资源的访问,当前可用资源计数就会减1 ,只要当前可用资源计数是大于0 的,就可以发出信号量信号
- 事件对象: 通过通知操作的方式来保持线程的同步,还可以方便实现对多个线程的优先级比较的操作
3、同一进程中线程共享的资源:
- 堆
- 全局变量
- 静态变量
- 文件等公共资源
4、同一进程中线程独享的资源:
- 栈
- 寄存器
5、死锁(两个或多个进程无限期的阻塞、相互等待的一种状态。)产生的四个条件(缺一不可):
- 互斥条件:一个资源一次只能被一个进程使用
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得资源保持不放
- 不剥夺条件:进程获得的资源,在未完全使用完之前被强行剥夺
- 循环等待条件:若干进程之间形成一种头尾相接的环形等待资源关系
6、处理死锁:
- 预防死锁:去破坏产生死锁的四个必要条件中的一个或者几个,来预防发生死锁,但是会严重地损害系统性能
- 资源一次性分配:一次性分配所有资源,这样就不会再有请求了(破坏请求条件)
- 只要有一个资源得不到分配,也不给这个进程分配其他的资源(破坏请保持条件)
- 当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
- 系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)
- 避免死锁:允许进程动态地申请资源,系统在进行资源分配之前预先计算资源分配的安全性。若此次分配不会导致死锁,则将资源分配给进程;否则,进程等待。代表算法:银行家算法
- 死锁检测和解除:先检测是否发生死锁(从一个节点出发进行深度优先搜索,对访问过的节点进行标记,如果访问了已经标记的节点,就表示有向图存在环,也就是检测到死锁的发生),若发生死锁,采取适当措施解除死锁(资源剥夺法、进程回退法、进程撤销法、系统重启法)
7、分页和分段:
区别:
- 页是信息的物理单位,分页是为实现离散分配方式,以消减内存的外零头,提高内存的利用率,分页是出于系统管理的需要
- 段是信息的逻辑单位,它含有一组其意义相对完整的信息,它是根据用户的需要划分的
- 页的大小固定,由操作系统决定;段的大小不固定,由用户编写的程序决定
- 页向用户提供的是一维地址空间;段向用户提供二维地址空间
8、寄存器:是CPU的组成部分,是有限存贮容量的高速存贮部件,它们可用来暂存指令、数据和位址
9、虚拟内存:
- 内存不够用的时候,把硬盘中的一部分匀出来分给内存使用,这部分就是虚拟内存,它在硬盘中是一个较大的文件
- 我们程序所使用的内存地址叫做虚拟内存地址(Virtual Memory Address)
- 实际存在硬件里面的空间地址叫物理内存地址(Physical Memory Address)
- 虚拟内存的优点:将逻辑内存和物理内存分开。虚拟内存允许文件和内存通过共享页而为两个或多个进程所共享。每个进程都被分配4GB虚拟内存,可使用比实际物理内存更大的地址空间
- 缺页中断:当软件试图访问已映射在虚拟地址空间中,但是并未被加载在物理内存中的一个分页时,由中央处理器的内存管理单元所发出的中断
- 缺页中断在指令执行期间产生和处理中断信号,而一般中断在一条指令执行完成后检查和处理中断信号
- 缺页中断返回到该指令的开始重新执行该指令,而一般中断返回回到该指令的下一个指令执行
10、用户态:用户态也称为目态,用于运行用户程序。当进程运行在用户态时,CPU堆栈指针寄存器指向的是用户堆栈地址,使用的是用户堆栈
11、内核态:系统态也称为管态、核心态、内核态,用于运行操作系统程序。当进程运行在内核态时,CPU堆栈指针寄存器指向的是内核堆栈地址,使用的是内核堆栈
12、特权指令:在实际运行过程中,处理机会在系统态和用户态间切换。相应地,现代多数操作系统将 CPU 的指令集分为特权指令和非特权指令
- 特权指令
- 在系统态时运行的指令
- 能访问所有的内存空间和对象,不仅能访问用户存储空间,也能访问系统存储空间
- 其所占有的处理机是不可被抢占的
- 特权指令只允许操作系统使用,不允许应用程序使用,否则会引起系统混乱
- 包括启动I/O,内存清零,允许禁止中断,设置时钟,停机等
- 非特权指令
- 一般应用程序所使用的都是非特权指令,它只能完成一般性的操作和任务
- 访问的内存空间和对象受到限制,其所占有的处理机是可被抢占的
- 不能对系统中的硬件和软件直接进行访问,其对内存的访问范围也局限于用户空间
13、两个状态的切换:
- 用户态切换到内核态的唯一途径——中断、异常、陷入
- 内核态切换到用户态的途径——设置程序状态字
14、并发与并行:多个程序、交替执行的思想,就有 CPU 管理多个进程的初步想法;虽然单核的 CPU 在某一个瞬间,只能运行一个进程。但在 1 秒钟期间,它可能会运行多个进程,这样就产生并行的错觉,实际上这是并发
15、同步与异步:并发进程或线程在一些关键点上可能需要互相等待与互通消息,这种相互制约的等待与互通信息称为进程或线程同步,指需要阻塞等待事件的完成才能进行下一步操作。异步是指不需要等待事件完成,可以在这期间做点别的事件,即非阻塞
16、进程:我们编写的代码只是一个存储在硬盘的静态文件,通过编译后就会生成二进制可执行文件,当我们运行这个可执行文件后,它会被装载到内存中,接着 CPU 会执行程序中的每一条指令,那么这个运行中的程序,就被称为进程(Process)。进程是计算机中的程序关于某数据集合上的一次运行活动。
一个进程的活动期间至少具备三种基本状态,即运行状态、就绪状态、阻塞状态
- 运行状态(Running):该时刻进程占用 CPU
- 就绪状态(Ready):可运行,但因为其他进程正在运行而暂停停止
- 阻塞状态(Blocked):该进程正在等待某一事件发生(如等待输入/输出操作的完成)而暂时停止运行,这时,即使给它CPU控制权,它也无法运行
17、进程控制块:在操作系统中,是用进程控制块(process control block,PCB)数据结构来描述进程的。PCB是进程存在的唯一标志。
18、线程:线程(Thread)是进程当中的一条执行流程,同一个进程内多个线程之间可以共享代码段、数据段、打开的文件等资源,但每个线程都有独立一套的寄存器和栈,这样可以确保线程的控制流是相对独立的。
线程的状态:
- 一个进程中可以同时存在多个线程
- 各个线程之间可以并发执行
- 各个线程之间可以共享地址空间和文件等资源
- 当进程中的一个线程崩溃时,会导致其所属进程的所有线程崩溃
19、线程的实现:
- 用户线程(User Thread):在用户空间实现的线程,不是由内核管理的线程,是由用户态的线程库来完成线程的管理
- 内核线程(Kernel Thread):在内核中实现的线程,是由内核管理的线程
- 轻量级进程(Light Weight Process):在内核中来支持用户线程
20、进程上下文切换:进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。同一进程的线程的上下文切换只需要切换线程的独立一套的寄存器和栈不共享的数据。
21、线程的上下文切换:
- 当两个线程不是属于同一个进程,则切换的过程就跟进程上下文切换一样
- 当两个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据
所以,线程的上下文切换相比进程,开销要小很多
22、线程安全:在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况
- 给共享的资源加把锁,保证每个资源变量每时每刻至多被一个线程占用
- 让线程也拥有资源,不用去共享进程中的资源。如: 使用threadlocal可以为每个线程的维护一个私有的本地变量
- 让共享资源只能看,不能改
线程不安全就是两条都满足:共享和可变,只要打破其中一条即可
23、进程调度
- 非抢占式:当进程正在运行时,它就会一直运行,直到该进程完成或发生某个事件而被阻塞时,才会把 CPU 让给其他进程
- 先来先服务调度算法FCFS,对短作业不利(等前面长作业)
- 短作业优先SJF,对长作业不利(等短作业)
- 高响应比优先(Highest Response Ratio Next, HRRN),优先权=(等待时间+要求服务时间)/要求服务时间,兼顾到了长短作业
- 抢占式:进程正在运行的时,可以被打断,使其把 CPU 让给其他进程
- 时间片轮转(Round Robin, RR)
- 如果时间片用完,进程还在运行,那么将会把此进程从 CPU 释放出来,并把 CPU 分配另外一个进程
- 如果该进程在时间片结束前阻塞或结束,则 CPU 立即进行切换
- 最高优先级(Highest Priority First,HPF)
- 静态优先级:创建进程时候,就已经确定了优先级了,然后整个运行时间优先级都不会变化
- 动态优先级:根据进程的动态变化调整优先级
- 时间片轮转(Round Robin, RR)
24、大端模式和小端模式
- 大端模式:高字节存储在低地址中,低字节存放在高地址中。符合人正常逻辑(TCP/IP协议规定为大端模式)
- 小端模式:与大端相反,符合计算机逻辑
大小端模式各有优势:
- 小端模式强制转换类型时不需要调整字节内容,直接截取低字节即可
- 大端模式由于符号位为第一个字节,很方便判断正负
计网
1、TCP协议:
TCP(Transmission Control Protocol,传输控制协议)是面向连接的协议,在收发数据前,必须和对方建立可靠的连接:三次握手、四次挥手
序列号:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就累加一次该数据字节数的大小。用来解决网络包乱序问题。
确认应答号:指下一次期望收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决丢包的问题。
三次握手:
- 主机A通过向主机B发送一个含有同步序列号(SYN)的标志位的数据段,向B请求建立连接,通过这个数据段, A告诉B两件事:我想要和你通信;你可以用这个序列号作为起始数据段来回应我。主机A进入SYN_SENT状态,SYN置1
- B收到A的请求后,用一个带有确认应答(ACK)和同步序列号(SYN)标志位的数据段响应A,也告诉A两件事:我已收到请求了,你可以传输数据了;你要用这个序列号作为起始数据段来回应我。主机B进入SYN_RECV状态,SYN、ACK置1
- A收到这个数据段后,再发送一个确认应答(ACK),确认已收到B的数据段:已收到回复,现在要开始传输实际数据了,这样3次握手就完成了,主机A和主机B就可以传输数据了。主机A和主机B进入ESTABLISHED状态,ACK置1
- 第三次握手是可以携带数据的,前两次握手是不可以携带数据的
四次挥手:
- 当主机A完成数据传输后,将控制位FIN置1,提出停止TCP连接的请求。主机A进入FIN-WAIT-1(终止等待1)状态
- 主机B收到FIN后对其作出响应,确认这一方向上的TCP连接将关闭,将ACK置1。主机B就进入了CLOSE-WAIT(关闭等待)状态。主机A到了主机B的数据之后进入FIN-WAIT-2(终止等待2)状态
- 由主机B端再提出反方向的关闭请求,将FIN置1,主机B就进入了LAST-ACK(最后确认)状态
- 主机A对主机B的请求进行确认,将ACK置1,双方向的关闭结束,主机A就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2MSL(最长报文段寿命)的时间后,当主机A撤销相应的TCB后,才进入CLOSED状态
- 主动关闭连接的,才有 TIME_WAIT 状态
2、UDP协议:
UDP(User Datagram Protocol)是面向报文的协议,只在 IP 的数据报服务之上增加了复用和分用的功能以及查错检测的功能
- 目标和源端口:主要是告诉 UDP 协议应该把报文发给哪个进程
- 包长度:该字段保存了 UDP 首部的长度跟数据的长度之和
- 校验和:校验和是为了提供可靠的 UDP 首部和数据而设计
UDP的特点:
- UDP 是无连接的,即发送数据之前不需要建立连接(发送数据结束时也没有连接可释放),减少了开销和发送数据之前的时延
- UDP 使用尽最大努力交付,即不保证可靠交付,主机不需要维持复杂的连接状态表
- UDP 没有拥塞控制,网络出现的拥塞不会使源主机的发送速率降低。这对某些实时应用是很重要的
- UDP 支持一对一、一对多、多对一和多对多的交互通信
如何实现UDP保证有序传输:
- 发送:包的分片、包确认、包的重发
- 接收:包的调序、包的序号确认
建立缓冲区,由一个线程专门接受数据并且重排,但是这个是在UDP的基础上自己实现部分简单的TCP。即收到数据包检查无误后返回一个应答。如果发送端在一定时间内没有收到应答,就自动重发
3、TCP与UDP对比:
- 相同点:TCP和UDP都是在传输层,都是传输层协议,都能都是保护网络层的传输,双方的通信都需要开放端口,没有应用层的数据
- TCP 面向连接,需要三次握手,四次挥手;UDP 是无连接的,即发送数据之前不需要建立连接,直接丢过去
- TCP是点对点的服务;UDP支持一对一、一对多、多对多
- TCP 提供可靠的服务,通过 TCP 连接传送的数据,无差错、不丢失、不重复,且按序到达,如文件传输(FTP协议)、超文本传输(HTTP协议、HTTPS协议);UDP 尽最大努力交付,即不保证可靠交付,如视频、音频等多媒体通信、广播通信
- TCP 有拥塞控制和流量控制机制,保证数据传输的安全性;UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率
- TCP 首部长度较长,会有一定的开销,首部在没有使用选项字段时是 20 个字节,如果使用了选项字段则会变长的;UDP 首部只有 8 个字节,并且是固定不变的,开销较小
- TCP 是字节流式传输,没有边界,但保证顺序和可靠,同时对重复的报文会自动丢弃;UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序
- TCP 的数据大小如果大于 MSS(最大报文段) 大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装 TCP 数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片;UDP 的数据大小如果大于 MTU (最大传输单元)大小,则会在 IP 层进行分片,目标主机收到后,在 IP 层组装完数据,接着再传给传输层,但是如果中途丢了一个分片,则就需要重传所有的数据包,这样传输效率非常差,所以通常 UDP 的报文应该小于 MTU
4、网络模型:
- ICMP用于告知网络包传送过程中产生的错误以及各种控制信息
- ARP用于根据IP地址查询相应的以太网MAC地址
IP层(网络层)是不可靠的,它不保证网络包的交付、不保证网络包的按序交付、也不保证网络包中的数据的完整性。如果需要保障网络数据包的可靠性,那么就需要由上层(传输层)的 TCP协议来负责。
5、DNS:在域名没有被发明之前,人们访问网站都是通过IP地址。但是IP地址不直观,而且用户记忆十分不方便,于是人们又发明了另一套字符型的地址方案,即所谓的域名地址。为了让域名地址和IP地址一一对应,DNS(Domain Name System)就出现了,域名地址和IP地址的对应关系就放在DNS内
计组
1、机器码:机器码分为原码,反码,补码, 0只有在补码中表示形式才是唯一的
- 真值->原码:转换为二进制,加符号位(纯小数的符号位为小数点左边原个位),0为正,1为负
- 真值->反码:先转换为原码,正数反码=原码,负数反码=原码除符号位取反
- 真值->补码:正数补码=原码,负数补码=反码+1
通过将符号位也参与运算的方法。我们知道,根据运算法则减去一个正数等于加上一个负数,即:1-1 = 1 + (-1) = 0, 所以机器可以只有加法而没有减法,这样计算机运算的设计就更简单了。为了解决原码做减法的问题, 出现了反码。补码的出现,解决了0的符号问题以及0的两个编码问题。
数据结构和算法
1、单链表原地反转:
无言投下
2、图论:
- 最短路径,Dijkstra是怎么实现的:贪心找到下一个节点的最短距离,与之前的距离相加后对比直接到该节点的距离,短就保留,长就选直接到该节点的路径,继续贪心寻找。在寻找最短路径的时候可以用小根堆加速这个找的过程
- 图的基本知识
3、堆:
- STL的
priority_queue
是怎么实现的:底层容器,上浮下沉 - 堆排序
4、二叉树:
- 手算平衡二叉树
- 如何实现平衡:左旋右旋
- 红黑树的基础知识
- 二叉树的五个性质
5、寻路算法:
(1)A*寻路
关键公式:F = G + H
- G值(耗费值):指从起点走到该点要耗费的值
- H值(预测值):指从该点走到终点的预测的值(从该点到终点无视障碍物情况下预测要耗费的值,也可理解成该点到终点的直线距离的值)
- F值(优先级值):从起点经过该点再到达终点的预测总耗费值
从起点开始往四周各个方向搜索,计算出每个方向对应点的F、G、H值后,还需要给这些点记录当前点的值(回溯时可以通过这个值知道上次经过哪个点),然后将这些点放入openList(开启列表:用于存放可以搜索的点),然后再将当前点放入closeList(关闭列表:用于存放已经搜索过的点,避免重复搜索同一个点),然后再从openList取出一个F值最小(最优先方向)的点,进行上述同样的搜索。在搜索过程中,如果搜索方向上的点是障碍物或者关闭列表里的点,跳过。通过递归式的搜索,多次搜索后,最终搜到了终点。搜到终点后,然后通过前一个点的指针值,我们便能从终点一步步回溯通过的路径点。
对朴素A*的优化:用最小堆来存openList中的点,每次取最小值操作是常数级时间复杂度。
A*的优化版本:JSP和JSP+
JPS(Jump Point Search)算法改进的地方在于减少了openList的点(原来的搜索方案会导致很多无关的点都加入到openList中),并产生了两个关键的概念:强迫邻居(Forced Neighbour)和跳点(Jump Point)。
强迫邻居(Forced Neighbour):个人理解为点x附近8个点内有障碍物,障碍物朝前进方向的前一格为强迫邻居。
跳点(Jump Point):当前点 x 满足以下三个条件之一
- 节点 x 是起点或终点
- 节点 x 至少有一个强迫邻居
- 如果父节点在斜方向(意味着这是斜向搜索),节点x的水平或垂直方向上有满足条件1、2的点
JPS 算法步骤如下: - openlist取一个权值最低的节点,然后开始搜索(这些和A*是一样的)
- 搜索时,先进行直线搜索(4个方向,沿直线方向一直搜下去,直到搜到跳点或者障碍或边界),然后再斜向搜索(4个方向,只搜索一步)。如果期间某个方向搜索到跳点或者碰到障碍(或边界),则当前方向完成搜索,若有搜到跳点就添加进openlist
- 若斜方向没完成搜索,则斜方向前进一步,重复上述过程
- 若所有方向已完成搜索,则认为当前节点搜索完毕,将当前节点移除于openList,加入closeList
- 重复取openList权值最低节点搜索,直到openList为空或者找到终点
JPS+ 本质上也是 JPS寻路,只是加上了预处理来改进,从而使寻路更加快速。
(2)Dijkstra
6、斐波那契数列的实现方法:
- 递归函数
int RecursionFibonacci(int n) {
if (n == 1 || n == 2) {
return 1;
}
else {
return RecursionFibonacci(n - 1) + RecursionFibonacci(n - 2);
}
}
- 循环
int IterationFibonacci(int n) {
if (n <= 2) {
return 1;
}
int pre = 1, curr = 1, temp = 0;
for (int i = 2; i < n; i++)
{
temp = pre + curr;
pre = curr;
curr = temp;
}
return temp;
}
- 模板元
template<int N>
class TemplateMetaFibonacci {
public:
constexpr static int value = TemplateMetaFibonacci<N - 2>::value + TemplateMetaFibonacci<N - 1>::value;
};
template<>
class TemplateMetaFibonacci<0> {
public:
constexpr static int value = 0;
};
template<>
class TemplateMetaFibonacci<1> {
public:
constexpr static int value = 1;
};
template<>
class TemplateMetaFibonacci<2> {
public:
constexpr static int value = 1;
};
设计模式
1、单例模式:饿汉式、懒汉式
- 饿汉式:程序运行时就创建单例,是线程安全的,空间换时间
- 懒汉式:等到程序访问到的时候再创建单例,多线程访问要加锁(双检锁),时间换空间
避免使用大量的单例,单例是static
的,在实例化之后一直到程序结束才释放内存。可以设计一套分层的单例,当前层次的单例会有更高一层的单例控制生命周期,类似UE的Subsystem
2、MVC和MVVM:
MVC(Model-View-Controller)是常见的UI设计模式,把数据、面板和逻辑控制分开三个脚本去写。MVVM(Model-View-ViewModel)是对MVC的改进,在MVC中,Controller是每帧去跑更新逻辑,MVVM则是用回调函数来更新,ViewModel中定义各类回调函数,当Model中的数据变化时调用这些回调
项目
1、项目中遇到过什么困难,如何解决
2、状态机与行为树的差别:
- 状态机是朴素的AI设计,包含状态和转换条件,是一种网状结构
- 行为树是树状结构,父节点是行为分支,叶子节点是行为
- 状态机直观,容易实现,但是耦合高,代码复用性差,状态多的情况下逻辑混乱。行为树逻辑和状态数据分开,复用性高,但是每一帧都从root节点开始,消耗更多性能
3、同步策略:
- 帧同步:帧同步是指客户端把操作上传服务端,服务端不模拟操作,把操作转发给所有的客户端
- 状态同步:状态同步是指客户端把操作上传服务端,服务端模拟操作,然后把模拟的结果转发给所有客户端
- 策略选择:
- 在对准确性要求高,实时性要求不高的地方采用状态同步
- 在对准确性要求不高,实时性要求高的地方采用帧同步