文章目录
2020.11.11更新
一、语言基础
1、面向对象的基础概念
- 封装
一层含义是把对象的属性和行为看成一个密不可分的整体,另一层含义指“信息隐藏”,把不需要让外界知道的信息隐藏起来。 - 继承
继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。 - 多态
同一个行为具有多个不同表现形式或形态的能力。
2、结构体和类的区别
- 类是引用类型,而结构体是值类型
- 所有结构成员默认都是Public,而类的变量和常量数则默认位Private
- 结构不允许声明析构函数(Destructor),类则无此限制
- 结构体会内存对齐
3、引用和指针、值类型和引用类型
- 引用和指针
指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;
而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。 - 值类型和引用类型
值类型的变量直接存储数据,而引用类型的变量持有的是数据的引用,数据存储在数据堆中。
4、协程
- 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态。
- 协同方法的执行是不用等协同方法执行完再执行调用之前原来方法的代码,而是两者异步执行。
- 协程有极高的执行效率:因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
- 不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
5、委托、事件和回调函数
- 委托可以理解为一种协议。委托,是什么意思呢?举个例子,你碰到一件事,你需要让别人来帮你做(可能你还有别的事情要做),这就是委托,把你现在不能做的事让别人去做。
- 回调函数就是把一个方法的传给另外一个方法去执行。
- 在某件事情发生时,一个对象可以通过事件通知另一个对象。比如,前台界面一个求和按钮被点击了,他通知你,可以把a和b这两个数相加了。这就是一个事件。可以看出事件是在一个时间节点去触发另外一件事情,而另外一件事情怎么去做,他不会关心。就事件来说,关键点就是什么时候,让谁去做。
6、接口和抽象类
-
抽象类
1抽象类
(1) 抽象方法只作声明,而不包含实现,可以看成是没有实现体的虚方法
(2) 抽象类不能被实例化
(3) 抽象类可以但不是必须有抽象属性和抽象方法,但是一旦有了抽象方法,就一定要把这个类声明为抽象类
(4) 具体派生类必须覆盖基类的抽象方法
(5) 抽象派生类可以覆盖基类的抽象方法,也可以不覆盖。如果不覆盖,则其具体派生类必须覆盖它们。 -
接口
(1) 接口不能被实例化
(2) 接口只能包含方法声明
(3) 接口的成员包括方法、属性、索引器、事件
(4) 接口中不能包含常量、字段(域)、构造函数、析构函数、静态成员。
(5) 接口中的所有成员默认为public,因此接口中不能有private修饰符
(6) 派生类必须实现接口的所有成员
(7) 一个类可以直接实现多个接口,接口之间用逗号隔开
(8) 一个接口可以有多个父接口,实现该接口的类必须实现所有父接口中的所有成员 -
抽象类和接口
相同点:
(1) 都可以被继承
(2) 都不能被实例化
(3) 都可以包含方法声明
(4) 派生类必须实现未实现的方法
区别:
(1) 抽象基类可以定义字段、属性、方法实现。接口只能定义属性、索引器、事件、和方法声明,不能包含字段。
(2) 抽象类是一个不完整的类,需要进一步细化,而接口是一个行为规范。微软的自定义接口总是后带able字段,证明其是表述一类“我能做。。。”
(3) 接口可以被多重实现,抽象类只能被单一继承
(4) 抽象类更多的是定义在一系列紧密相关的类间,而接口大多数是关系疏松但都实现某一功能的类中
(5) 抽象类是从一系列相关对象中抽象出来的概念, 因此反映的是事物的内部共性;接口是为了满足外部调用而定义的一个功能约定, 因此反映的是事物的外部特性
(6) 接口基本上不具备继承的任何具体特点,它仅仅承诺了能够调用的方法
(7) 接口可以用于支持回调,而继承并不具备这个特点
(8) 抽象类实现的具体方法默认为虚的,但实现接口的类中的接口方法却默认为非虚的,当然您也可以声明为虚的
(9) 如果抽象类实现接口,则可以把接口中方法映射到抽象类中作为抽象方法而不必实现,而在抽象类的子类中实现接口中方法
7、内存相关
8、C#的反射机制
二、数据结构
1、对数据结构的理解和应用场景
2、红黑树
3、A*算法
4、四叉树、八叉树的应用
-
应用:
(1)碰撞检测的广阶段 (2)地形渲染剔除机制 (3)根据四叉树只绘制玩家周围物体,或者用LoD技术 (4)动态载入游戏对象
5、排序算法的稳定性
- 哪些排序算法是稳定的
三、unity相关
1、生命周期内的几个函数
- Unity常用的生命周期函数
- Awake和Start函数的区别
- Awake()函数在脚本实例被载入时就被调用,不管游戏对象是否被激活都会被调用,在整个游戏当中只会被调用一次和构造函数差不多,运行于Start()函数之前,Awake()主要被用于初始化游戏状态或变量。
- 在第一次调用Update方法之前时,在这一帧之前调用Start()。
与Awake函数一样,Start在脚本的生命周期中只调用一次。但是,无论脚本是否已启用,都会在初始化脚本对象时调用Awake。如果在初始化时未启用脚本,则可能无法调用Start()函数 。
2、GUI相关
3、shader
4、本地数据持久化
- Unity自身提供的 PlayerPrefs
- BinaryFormatter 二进制序列化
- 保存为json格式的文本文件,使用 Unity 自身API JsonUtility
- XmlSerializer进行串行化
四、图形学
1、矩阵、欧拉角、四元数
2、点乘和叉乘的意义
3、法线和法线贴图
4、渲染管线
- 渲染管线入门
- 渲染管线(Graphics pipeline/rendering pipeline),其实贴切的翻译应该是渲染流水线。渲染流水线就是将数据从3D场景转换成2D图像,最终在屏幕上显示出来的总过程。它分为几个阶段:应用阶段、几何阶段和光栅阶段。
- 应用阶段:主要是CPU与内存打交道,例如碰撞检测,计算好的数据(顶点坐标、法向量、纹理坐标、纹理)就会通过数据总线传给图形硬件 。
- 几何阶段:为了从3D场景转换到2D,场景中的所有物体都需要转换到几个空间。每个空间都有自己的坐标系。这些转换是通过一个空间的顶点转换到另一个空间的顶点来实现的。
- 光栅阶段:是矢量图形转化成像素点的过程。
五、设计模式
1、常用的设计模式
- 单例模式、工厂模式
2、单例模式是怎么实现的,在多线程情况下如何实现
六、为什么要在游戏领域使用LUA
- 学习门槛低,快速上手
- 开发成本低,可维护性强
- 动态语言,灵活性高
- 相对于C/C++这类高复杂性、高风险的编译型语言来说,Lua脚本做为一种轻量级的动态语言,简单的语言特性,精简的核心和基础库,使得语言的学习门槛大大的降低,即使是没有任何游戏经验的人都能快速上手,开发游戏功能。实际上游戏设计是一种十分繁杂的工作,C/C++虽然给我们带来极大的高效性,但同时也不能忽视其复杂性,极易产生BUG,而且对于开发人员的要求非常高。从语言的的抽象层面来说C/C++的抽象低更加适合于底层逻辑的支持,而Lua脚本抽象层次高,更加适合游戏逻辑的实现。脚本语言运行在虚拟机之上,而虚拟机运行在游戏逻辑之上,作为一种解释型语言,我们可以随时修改并及时体现在游戏之中,快速完成开发。C/C++却做不到,对一个巨大的游戏工程,每次修改都需要重新编译,成本很高。设想一下,如果所有的功能都是使用C/C++实现的话,那么对开发人员来说简直是一场灾难。
2020.10.20更新
计算机网络
计算机网络每层都有什么协议
TCP和UDP
三次握手、四次挥手
面试官不要再问我三次四次啦
三次握手
1.客户端发送 SYN(SEQ=x)报文给服务器端,进入 SYN_SEND 状态。
2.服务器端收到 SYN 报文,回应一个 SYN (SEQ=y)ACK(ACK=x+1)报文,进入 SYN_RECV 状态。
3.客户端收到服务器端的 SYN 报文,回应一个 ACK(ACK=y+1)报文,进入 Established 状态。
四次挥手
1.某个应用进程首先调用 close,称该端执行“主动关闭”(active close)。该端的 TCP 于是发送一个 FIN 分节,表示数据发送完毕。
2.接收到这个 FIN 的对端执行 “被动关闭”(passive close),这个 FIN 由 TCP 确认。
注意:FIN
的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程,放在已排队等候该应用进程接收的任何其他数据之后,因为,FIN
的接收意味着接收端应用进程在相应连接上再无额外数据可接收。
3.一段时间后,接收到这个文件结束符的应用进程将调用 close 关闭它的套接字。这导致它的 TCP 也发送一个 FIN。
4.接收这个最终FIN的原发送端 TCP(即执行主动关闭的那一端)确认这个 FIN。
既然每个方向都需要一个 FIN 和一个 ACK,因此通常需要4个分节。
TCP拥塞控制
四种方法
- 慢开始
假设当前发送方拥塞窗口cwnd的值为1,而发送窗口swnd等于拥塞窗口cwnd,因此发送方当前只能发送一个数据报文段(拥塞窗口cwnd的值是几,就能发送几个数据报文段),接收方收到该数据报文段后,给发送方回复一个确认报文段,发送方收到该确认报文后,将拥塞窗口的值变为2,当前的拥塞窗口cwnd的值已经等于慢开始门限值,之后改用拥塞避免算法。 - 拥塞避免
也就是每个传输轮次,拥塞窗口cwnd只能线性加一,而不是像慢开始算法时,每个传输轮次,拥塞窗口cwnd按指数增长。同理,16+1……直至到达24,假设24个报文段在传输过程中丢失4个,接收方只收到20个报文段,给发送方依次回复20个确认报文段,一段时间后,丢失的4个报文段的重传计时器超时了,发送发判断可能出现拥塞,更改cwnd和ssthresh.并重新开始慢开始算法 - 快重传
发送方发送1号数据报文段,接收方收到1号报文段后给发送方发回对1号报文段的确认,在1号报文段到达发送方之前,发送方还可以将发送窗口内的2号数据报文段发送出去,接收方收到2号报文段后给发送方发回对2号报文段的确认,在2号报文段到达发送方之前,发送方还可以将发送窗口内的3号数据报文段发送出去,此时,发送方收到了累计3个连续的针对2号报文段的重复确认,立即重传3号报文段,接收方收到后,给发送方发回针对6号报文的确认,表明,序号到6为至的报文都收到了,这样就不会造成发送方对3号报文的超时重传,而是提早收到了重传。 - 快恢复
当发送方连续收到三个重复确认,执行乘法减小,ssthresh减半
操作系统
进程与线程
进程间通讯方式
- 共享内存
共享内存是最快的进程间通讯的方式
原因:相对于其他几种方式,共享内存直接在进程的虚拟地址空间进行操作,不再通过执行进入内核的系统调用来传递彼此的数据 - 信号
信号量和以前的IPC通信方式不同,信号量的本质是计数器,用于多进程对共享数据对象的访问。
在进程访问临界资源之前,需要测试信号量,如果为正数,则信号量-1并且进程可以进入临界区,若为非正数,则进程挂起放入等待队列,直至有进程退出临界区,释放资源并+1信号量,此时唤醒等待队列的进程。
信号量本身就是临界资源,所以必须是原子操作。 - 管道
单向,一端输入,另一端输出,先进先出FIFO。管道也是文件。管道大小4096字节。
特点:管道满时,写阻塞;空时,读阻塞。
分类:普通管道(仅父子进程间通信)位于内存;命名管道位于文件系统,没有亲缘关系管道只要知道管道名也可以通讯。
管道是由内核管理的一个缓冲区(buffer),相当于我们放入内存中的一个纸条。管道的一端连接一个进程的输出。这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。一个缓冲区不需要很大,它被设计成为环形的数据结构,以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失 - 消息队列
消息队列是消息的链表,包括Posix消息队列和system v消息队列(Posix常用于线程,system常用于进程),有权限的进程可以向消息队列中添加消息,有读权限的进程可以读走消息队列的消息。
消息队列克服了信号承载信息量少,管道只能承载无格式字节流及缓冲区大小受限等缺陷。
死锁的必要条件
- 什么是死锁
死锁是指两个或两个以上的进程(线程)在运行过程中因争夺资源而造成的一种僵局(Deadly-Embrace) ) ,若无外力作用,这些进程(线程)都将无法向前推进。 - 死锁产生的原因
- 竞争不可抢占资源引起死锁
- 竞争可消耗资源引起死锁
- 进程推进顺序不当引起死锁
- 产生死锁的必要条件
- 互斥条件
进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。 - 不可剥夺条件
进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。 - 请求与保持条件
进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。 - 循环等待条件
存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。
- 互斥条件
数据结构
map和hashmap的区别
Map集合的特点:
1、Map集合一次存储两个对象,一个键对象,一个值对象
2、键对象在集合中是唯一的,可以通过键来查找值
HashMap特点:
1、使用哈希算法对键去重复,效率高,但无序
2、HashMap是Map接口的主要实现类
hashset和dictionary的区别
1.HashTable
哈希表(HashTable)表示键/值对的集合。在.NET Framework中,Hashtable是System.Collections命名空间提供的一个容器,用于处理和表现类似key-value的键值对,其中key通常可用来快速查找,同时key是区分大小写;value用于存储对应于key的值。Hashtable中key-value键值对均为object类型,所以Hashtable可以支持任何类型的keyvalue键值对,任何非 null 对象都可以用作键或值。
在哈希表中添加一个key/键值对:HashtableObject.Add(key,);
在哈希表中去除某个key/键值对:HashtableObject.Remove(key);
从哈希表中移除所有元素: HashtableObject.Clear();
判断哈希表是否包含特定键key: HashtableObject.Contains(key);
2.HashSet
HashSet类主要是设计用来做高性能集运算的,例如对两个集合求交集、并集、差集等。集合中包含一组不重复出现且无特性顺序的元素,HashSet拒绝接受重复的对象。
HashSet的一些特性如下:
a. HashSet中的值不能重复且没有顺序。
b. HashSet的容量会按需自动添加。
3.Dictionary
Dictionary表示键和值的集合。
Dictionary<string, string>是一个泛型
他本身有集合的功能有时候可以把它看成数组
他的结构是这样的:Dictionary<[key], [value]>
他的特点是存入对象是需要与[key]值一一对应的存入该泛型
通过某一个一定的[key]去找到对应的值
4.HashTable和Dictionary的区别:
(1).HashTable不支持泛型,而Dictionary支持泛型。
(2). Hashtable 的元素属于 Object 类型,所以在存储或检索值类型时通常发生装箱和拆箱的操作,所以你可能需要进行一些类型转换的操作,而且对于int,float这些值类型还需要进行装箱等操作,非常耗时。
(3).单线程程序中推荐使用 Dictionary, 有泛型优势, 且读取速度较快, 容量利用更充分。多线程程序中推荐使用 Hashtable, 默认的 Hashtable 允许单线程写入, 多线程读取, 对 Hashtable 进一步调用 Synchronized() 方法可以获得完全线程安全的类型. 而 Dictionary 非线程安全, 必须人为使用 lock 语句进行保护, 效率大减。
(4)在通过代码测试的时候发现key是整数型Dictionary的效率比Hashtable快,如果key是字符串型,Dictionary的效率没有Hashtable快。
C#
GC——垃圾回收
.Net类型分为两大类,一个就是值类型,另一个就是引用类型。前者是分配在栈上,并不需要GC回收;后者是分配在堆上,因此它的内存释放和回收需要通过GC来完成。
只要判定此对象或者其包含的子对象没有任何引用是有效的,那么系统就认为它是垃圾。
对于内存中的垃圾分为两种,一种是需要调用对象的析构函数,另一种是不需要调用的。GC对于前者的回收需要通过两步完成,第一步是调用对象的析构函数,第二步是回收内存,但是要注意这两步不是在GC一次轮循完成,即需要两次轮循;相对于后者,则只是回收内存而已。
如何使用协程?协程和线程区别?
-
如何使用协程
-
开启协程的两种方式
1、StartCoroutine(string methodName)
注意:
(1)、参数是方法名(字符串类型),此方法可以包含一个参数
(2)、形参方法可以有返回值
2、StartCoroutine(IEnumerator method)
注意:
(1)、参数是方法名(TestMethod()),方法中可以包含多个参数
(2)、IEnumrator 类型的方法不能含有ref或者out 类型的参数,但可以含有被传递的引用
(3)、必须有有返回值,且返回值类型为IEnumrator,返回值使用(yield retuen +表达式或者值,或者 yield break)语句
终止协程的两种方式:
StopCoroutine (string methodName),只能终止指定的协程
StopAllCoroutine(),终止所有协程
常量 静态字段 实例字段
用修饰符static声明的字段为静态字段。不管包含该静态字段的类生成多少个对象或根本无对象,该字段都只有一个实例,静态字段不能被撤销。
如果类中定义的字段不使用修饰符static,该字段为实例字段,每创建该类的一个对象,在对象内创建一个该字段实例,创建它的对象被撤销,该字段对象也被撤销
用const修饰符声明的字段为常量,常量只能在声明中初始化,以后不能再修改。