【游戏客户端面试题干货】--2021年最新游戏客户端面试干货(lua篇)

2023-11-14

  【游戏客户端面试题干货】-- 2021年度最新游戏客户端面试干货(lua篇)

 

  大家好,我是Lampard~~

  经过春招一番艰苦奋战之后,我终于是进入了心仪的公司。

  今天给大家分享一下我在之前精心准备的一套面试知识。

 

  今天和大家分享的是lua的面试题

  本人亲测80%的引擎相关题目都是围绕着我总结出来的知识点提出的 。

  配合我博客里面的lua系列一起食用会更有效果哟~~~

 

  

   PS:本博客知识参考资料为:《Lua程序设计第四版》

  该书由Lua的创始人2018年所编著,所以大家可以放心去吸收知识

 

一.Lua的8种数据类型

在Lua中有8种基本类型,分别是:nil--空,boolean--布尔,number--数值,string--字符串,userdata--用户数据,function--函数,thread--线程(注意这里的线程和操作系统的线程完全不同,lua和c/c++进行交互的lua_Stack就是一种llua的线程类型),和table--表。

我们可以通过调用print(type(变量))来查看变量的数据类型。

(1) nil 类型

nil是一种只有一个nil值的类型,它的主要作用是与其他所有值进行区分。Lua语言使用nil值来表示没有有用的值的情况。全局变量第一次被赋值前的默认值就是nil,将nil赋值给全局变量相当于将其删除。

 

(2) boolean类型

boolean类型具有两个值,true和false,他们分别代表了传统的布尔值。敲黑板:

不过在Lua中,任何值都能表示条件Lua定义除了false和nil的值为假之外,所有的值都为真,包括0和空字符串。

 提到布尔值就不得不提一下逻辑运算符:and,or,not 他们都遵循着短路求值。

举个栗子:

 首先,对于and来说,如果第一个值为假,则返回第一个值,否则返回第二个值 

  对于or来说,如果第一个值为真,则返回第一个值,否则返回第二个值

 对于not来说,返回值永远为Boolean

通过上述对逻辑运算符的理解,我们用这种写法来代替简单的ifelse,让代码变得更优雅

if a + b > 0 then
    c = 1
else
    c = 10
end

-------------- 等同于 ---------------

c = a + b > 0 and 1 or 10

 

  (3) number类型

 在Lua5.2之前所有的数值都是双精度的浮点型,在Lua5.3之后引入了整形integer。整形的引入是Lua5.3的一个重要标志

整形与浮点型的区别:

整形:类似1,2,3,4,5.....在其身后不带小数和指数

浮点型:类似1.2,3.5555,4.57e-3......在其身后带小数点或指数符号(e)的数字

我们使用type(3) 和type(3.5)返回的都是num

但是如果我们调用math库里面的math.type(3)返回的是integer, math.type(3.5)返回的是float

参与游戏开发一年,对num类型的使用无非是以下的状况:

对于加+,减-,乘*来说:

int对int型进行运算,则得到的结果都是int型,但只要两个数字中有一个是float型,则得出来的结果都是float型

对于除/来说:

无论是什么数字,得到的结果永远都是float类型

那如果我硬是想要直接一步到位,除出来的结果也要是整形怎么办?

双除法 // :

得到的是一个整值,若结果存在小数,则向负无穷截断

除了加减乘除之外,使用得最多的就是取整和取随机数了

取整:

  1. floor()--向负无穷取整
  2. ceil() -- 向正无穷取整
  3. modf()--向0取整
  4. floor(x+0.5)--四舍五入

取随机数:

 产生随机数(math.random()):

   Lua中产生随机数的三种方式:

  1. math.random()-- 产生(0~1)的随机值
  2. math.random(n)-- 产生(1~n)的随机值
  3. math.random(m,n)-- 产生(m~n)的随机值

想看对这三种类型更具体的介绍,可以看我写的这篇博客:【Lua基础系列】之类型与值(nil,number,boolean)详细版

 

 (4) function类型

在Lua语言中,函数(Function)是对语句和表达式进行抽象的一种方式。函数调用时都需要使用一对圆括号把参数列表括起来。几时被调用的函数不需要参数,也需要一堆空括号()。

举个例子:

function add(a)               -- 声明add这个函数
    local sum = 0             -- 实现序列a的求和
    for i=1, #a do            -- 循环体
        sum = sum + a[i]
    end
    return sum                -- 返回值
end

这是我们类C的写法,function 函数名 小括号 参数但其实我们还有另外一种写法,把函数当成一个对象去定义:

local add = function (a)      -- 声明add这个函数
    local sum = 0             -- 实现序列a的求和
    for i=1, #a do            -- 循环体
        sum = sum + a[i]
    end
    return sum                -- 返回值
end

两种方式都可以声明一个函数,至于使用哪一种方式,就根据贵公司项目而定了。

lua的函数类型除了可以把它当成对象这样定义之外,还有两个特性:可变长参数,以及多返回值

(1) 多返回值

Lua语言中一种与众不同但又非常有用的特性是允许一个函数返回多个结果只需要在return关键字后列出所有要返回的值即可。

例如一个用于查找序列中最大元素的函数可以同时返回最大值以及该元素的位置

(2) 可变长参数

Lua语言中的函数可以是可变长参数函数(variadic),即可以支持数量可变的参数, 只需要在函数声明的时候参数项用...代替即可。

下面是一个简单的示例,该函数返回所有参数的总和:

参数列表中的三个点表示该函数的参数是可变长的。当这个函数被调用时,Lua内容会把它的所有参数收集起来,三个点是作为一个表达式来使用的。在上例中,表达式{...}的结果是一个由所有可变长参数组成的列表,该函数会遍历该列表来累加。

-- 我们可以通过以下这几种方式进行对变化参数的调用
 
local Array{...} -- 把它变成一个表
 
#{...} -- 取得当前变化参数的个数
 
select{i,...} -- 通过select方法取得第i个变化参数的值 

想看lua函数类型更具体的介绍,可以看我写的这篇博客:【Lua基础系列】之类型与值 (函数Function)

 

 (5) string类型

Lua中的字符串是不可变值(immutable value)。我们不可以像在C语言中那样直接改变某个字符串中的某个字符。但是我们可以创建另外一个新字符串的方式来达到修改的目的。

定义:

我们可以使用双引号或者单引号来声明字符串常量。

a = "a line"
b = ‘another line’

 那么如果在字符串内容中出现双引号或者单引号怎么办呢?老司机们可能就会脱口而出:用转义字符'\'啊。

 没错使用转义字符确实能够解决问题,但是如果是在双引号定义的字符串中出现单引号,或者单引号字符串中出现双引号则不需要使用转义字符。

长字符串/多行字符串

为了方便缩进排版,所以Lua定义了用户可以使用一对方括号 [[]] 来声明长字符串。被方括号扩起来的内容可以由很多行,并且内容中的转义序列不会被转义。

类型强制转换

当在算数运算发现字符串时,它会转化为浮点型数值再进行计算,要注意在比较操作中不会默认转化。比如下图中的a和b是字符串,但是相加的时候则转化成数字

当然我们也可以显式的把字符串和数值相互转换tostring()-- 返回字符串/ tonumber () --返回整形或浮点型数值。

字符串的常用方式:

(1) 字符串拼接: ..(两个点)

a = “hello”

b = "world"

c = a..b    -- 此时c等于hello world

(2) 取字符串长度

c = “hello world”

print (#c)    -- 此时输出11

(3)字符串标准库

string.gsub(stringName,"字符串一","字符串二")--把字符串一改成字符串二

string.sub(stringName,起始位置,终止位置) -- 返回从起始位置到终止位置的字符串

string.char(num) -- 把数字通过ascall译码转化为字符

string.byte(stringName) -- 把字符通过ascall译码转化为数字

string.reverse(stringName) -- 把字符串翻转

string.rep(stringName, 重复的次数) -- 把字符串重复N遍

string.upper(stringName) -- 字符串大写

string.lower(stringName) -- 字符串小写

示例图:

最后要给大家介绍介绍string.format(),它适用于进行字符串格式化和将数值输出为字符串的强大工具。

有点类似C中的printf()。

想看lua字符串类型更具体的介绍,可以看我写的这篇博客:【Lua基础系列】之类型与值 (字符串String)

 

 (6) table类型

表是Lua语言中最强大也是唯一的数据结构。使用表,Lua语言可以以一种简单,统一且高效的方式表示数组,集合,记录和其他很多的数据结构。

Lua语言中的表本质是一种辅助数组,这种数组不仅可以通过数字下标作为索引,也可以通过使用字符串或其他任意类型的值来映射相对应的值(键值对)。

在我看来,当lua是使用连续的数字下标作为索引的时候,它就是c++中的数组,当是使用键值对方式映射,用字符串作为索引的时候,因为其无序且键值唯一,它就很像c++中的unorder_map

构造:

a = {} -- 创建了一个空表
 
a[“x”] = 10 -- 这句话的键是“x”,值是10,此时我们可以通过a.x和a["x"]访问到10
 
a[10] = "Hello Table" --这句话的意思是,索引是10,值是字符串“Hello Table”

表永远是匿名的,表本身和保存表的变量之间没有固定的关系。当没有变量指向表的时候,Lua会对其进行自动回收

a = {}                       -- a指向一个空表
a["x"] = 10                  -- a的"x"键赋值为10
b = a                        -- b指向a这个表
print(b["x"])                -- 此时答案为10
b["x"] = 20                    
print(a["x"])                -- 此时答案为20
                             -- 说明a和b指向的是同一张表,并没有进行深拷贝
a=nil                        -- 只剩下b指向这张表
b=nil                        -- Lua自动回收

解释一下上面的b = a,此时a和b其实是同一张表,b只不过是a表的一个别名,这有点像c++中的引用&,大家是同一个内存地址,所以修改b的时候,a也会被修改。这是浅拷贝,若想完全复制一个互相不影响的表,我们需要使用clone()函数.

比如b = a:clone(),更多深浅拷贝相关知识可以看一看我的这一篇博客:【Lua进阶系列】之深拷贝与浅拷贝

除了使用空构造器{}构造表之外我们还可以这样做:注意:Lua中默认值是从1开始

days = {“Monday”,“Tuesday”,“Wednesday”,“Thursday”,“Friday”,“Saturday”,“Sunday”}
       --[[ 此时days[1]到days[7]被默认定义为“Monday”~“Sunday” ]]
a = {x = 10 , y = 20}
-- 上面的写法等价于 a["x"]=10,a["y"]=20

因为表是lua最最重要的内容,所以我决定把它的知识拆分成小点在下文展示,或者有兴趣的同学可以到这篇博客中看详细的表的介绍:【Lua基础系列】之类型与值 (表Table)

 

 (7) userdata类型

userdata是用户自定义的数据类型,lua只提供了一块原始的内存区域,用于存储任何东西,在Lua中userdata没有任何预定义操作

因为lua只是一个两三万行代码的一个脚本语言,有很多功能都是依靠c给它提供,所以userdata在实际中它代指了那些使用c/c++语言给lua提供的函数模块

 

 (8) thread类型

再三强调,lua的线程并不是操作系统中的线程!!!它是lua和c/c++进行交互的一个数据结构lua_stack,lua通过这个数据结构和c进行交互,来调用上文中的那些库函数

对于lua_Stack的研究同样太长,只需要记住它是c和lua之间交互的堆栈即可,我另外写了一篇博客进行深入研究,大家有兴趣可以前往看看: 【Lua进阶系列】实例lua调用capi

 

二.pairs和ipairs的区别

在项目的研发中,我们经常需要遍历表中的所有元素,此时我们就可以通过pairs和ipairs进行遍历

pairs迭代映射+数组,能返回表中所有的键值对但是无序,上文中说lua中存放键值对的表像c++中的无需图unorder_map也是基于这个原因,因为普通的map是用红黑树做底层,使用迭代器输出所有的键值都是有序的。

Ipairs迭代的是数组,遇到空值会停止,但是输出的是有序的

对于上述序列例子我们可以用for循环方式来代替

for i = 1, #a do
    print(a[i])
end

顺便说一下上面的for循环例子,i = 1, i < #a其实它是隐藏了一个参数,默认i = i + 1,如果我们不想加1,想要加2怎么办?那么只需要加上这个参数即可

for i = 1, #a, 2 do
    print(a[i])
end

 

三.lua表常用方式(插入,删除,移动,排序)

表标准库提供了操作列表和序列的一些常用函数。简单介绍增加(insert),删除(remove),移动move(),以及排序(sort)

table.insert ()

insert()有两种格式,一种是两个参数,insert(tableName,元素),这种情况下就会默认插到末尾

另一种是三个参数(tableName,位置,元素),则可以按照自己的想法插入元素。

table.remove()

删除指定位置的元素,并把后面的元素往前移动填充删除所造成的空洞

28

table.move(tableA, 表A起始索引,表A终止索引,表B元素安防位置,tableB)

它的作用时把表A中从起始索引到终止索引的值移动到表B中

table.sort () 排序

如果我们仅仅想把它们的值给排序一遍,则只需要table.sort(表名)即可。

但是假如我们的值不是单纯的数字,而是一个表。也就是说我们的数组是存放了一个个表,我们想要根据表中的某一个元素作为标准进行排序,我们可以再sort参数中放入一个函数

比如下图中,我想对cnt字段大的排在前面

 

四.如何实现继承关系(__index)

从c++,java这些高级语言走过来的我们,肯定对类的继承十分熟悉,甚至没了它还十分不习惯。其实lua也是可以实现继承的,这要利用到它的元方法_index:

local parent = {}
parent["a"] = 111
parent.__index = parent         // 把parent表的__index字段仍然设置为parent
 
local child = {}
setmetatable(child, parent)     // 把parent表设置为child表的原表 

print(child.a)

这时候我就出现了疑惑:既然说把parent设置为child的原表,那child没有的属性就可以在parent中寻找了呀,设置parent.__index是什么东西?

而这个理解是完全错误的,实际上,即使将child的元表设置为parent,而且parent中也确实有这个成员,但是parent的__index元方法没有赋值为本身,返回结果仍然会是nil!!!

实际上拥有了元表等于告诉了Lua:在A表找不到数据时,我们有解决方法;而元表中的__index则是告诉Lua:你从我的__index中找去吧。所以说parent的__index字段设置成本身相当于告诉lua,没有的话就从parent表中查找吧。

元表的__index字段不一定为自身的表,也可以指向其他表效果一样

元表的__index字段还可以是一个函数,当在表中找不到这个值时,会调用元表中的__index函数,然后拿去返回值(若无返回值则为nil)

 

五.__newindex

 如果说__index字段是在访问表中不存在的值(get)是执行的操作的话

 那么__nexindex字段则是在对表中不存在的值进行赋值(set)时候执行的操作(记住i是小写)

 在这个时候可能有人吐槽:纳尼!!我天天给表创建新字段,咋不见得有执行什么__newindex呢?

确实,如果没有元表,或者元表中没有__newindex字段,那给表新建一个字段则不会执行其他多余的操作

若存在元表且元表中存在着__newindex字段,那么和__index一样,会存在两种情况

(1)__nexindex指向一个函数

如果__newindex字段指向一个函数,则给表创建一个新字段的时候,则会执行该函数,且对本表创建不成功

(2)__nexindex指向一个表

如果__newindex字段指向一个表,那么就会对该表创建这个字段,且对本表创建不成功

我们可以看到,当我们输出myTable.c时,lua是找不到这个值的,因为实际上是给__nexindex指向的yourTable给赋值。那为什么还是nil呢?从结果我们可以看到,其实这个3我们是赋值给了yourTable.c, 虽然yourTbale已经被赋值,但是访问是__index字段的事,myTable并不能访问得到yourTable的值。

总结来说,就是只要存在__nexindex字段,那么就不会对本表新建值

那么这个__newindex字段有何作用呢?其实它可以起到一个很好的限制筛选作用。可以防止表被赋值,加入些杂七杂八的元素。有时候一表多用可能会导致些lua中的垃圾回收相关的问题。

 

六.实现一个常量表

我们可以通过对表设置__index和__newindex字段来把一个表定义成常量表

 

七.__call

__call元方法比较好玩,比如说我们上述例子中的myTable是外部引用的一个表。那如果我把它当成一个函数使用会怎么样呢

print(myTable(1, 2))

毫无疑问是会报错的哈,但是__call方法能够帮助我们实现解决这个问题

比如说我们的myTable和yourTable都是一个序列(num类型的),我想求出这两个序列的总和

这个时候可能有同学会问:搞那么复杂干嘛咧,我们直接先遍历一遍myTable,再遍历一遍yourTable不就好了吗?或者说我直接在myTable中添加一个新的函数字段,实现同样的功能不也一样吗?

诚然功能确实是一样的,但是如果我们需要频繁的利用这个外表(myTable)去生成或取得某一些内容时,利用__call方法会简便许多你说是myTable(XX)方便还是myTable.函数名(XX)方便?

 

八.__string

__tostring 元方法用于修改表的输出行为 ,如果我们直接print()一个表,那么我们返回得到的是一个地址

而如果我们通过设置其元表的__tostring字段,那么返回的就是__tostring指向的结果。

比如上面的例子就是输出自己的序列和,记住__tostring返回的是一个字符串,不然会报错。

 

九.lua闭包

简单来说就是:对于一个函数,能够访问到外部函数的非全局变量的一种机制。

什么是闭包?说起来很绕,我们看一个栗子

function func1 ()
    local x = 1
    -- 定义一个内部函数
    function func2 ()
        print(x)
    end
    -- 执行这个内部函数
    func2()
end

func1()

这个例子就是在外部调用了func1函数,而func1中定义了一个func2函数并调用了它。我们可以看到,func2访问了属于func1的local变量x,并且访问成功了。按道理来讲,x并不是全局函数,也不是func2的局部函数,应该是访问不到的。而lua却做到了,lua把实现这个功能的方式定义为闭包

所以从理论上来讲,lua只有闭包没有函数,函数只是不需要调用外部变量的一个闭包的特例

那么lua是怎么实现闭包的呢?大家可以参考一下这篇博客:【深入理解Lua的闭包】以下是对该博客的一些总结归纳

闭包的实现方式:

1.当Lua编译一个函数时,它会生成一个原型(prototype),原型中包括函数的虚拟机指令、函数中的常量(数值和字符串等)和一些调试信息

2.每个闭包都有一个相应函数原型的引用以及一个数组,数组中每个元素都是一个对upvalue的引用,可以通过该数组来访问外部的局部变量

上文提到函数是闭包的一部分,那么简而言之,如果访问到外部的非全局变量,那么数组则不为空。若没有访问到非全局变量(普通函数),那么闭包中的数组就为空。而且该数组对于这些非全局变量会复制在upValue中,因此闭包与闭包之间是的非全局遍历不会相互影响

举个例子:

 

十.类使用:和.的区别

一个类调用方法时,可以使用类名A.方法名(A, 参数)

也可以使用语法糖,类名 A:方法名(参数)

简而言之就是使用冒号符的时候默认把自身的table传进去函数中,举个例子:

 

十一.require,loadfile和dofile的区别

在lua中我们引用其他模块的时候,可以使用三种方法:requireloadfiledofile。那么它们有什么区别呢?

首先区分loadfile,loadfile只负责编译并不会执行模块代码,而require和dofile都会编译且执行

举个例子:弄三个文件moduleA,moduleB,moduleC

然后我们用一个测试文件测试一下这三种不同的引入方式:

那么dofile和require的区别在哪里呢?

主要是在dofile每次调用的时候都会编译且执行,而require会先在 package.loaded中寻找是否存在该模块,若存在则直接返回该模块而不执行,否不存在则编译执行一遍,并把路径记录在package.loaded中。简单来说就是dofile执行多遍,require执行一次

 

十二.Lua的热更新原理

什么是热更新?

热更新也叫不停机更新,是在游戏服务器运行期间对游戏进行更新。实现不停机修正bug、修改游戏数据等操作。

从上文中我们知道,Lua想要调用其他文件中的函数时,需要使用require方法

此时require会把函数及其内容缓存到packet.loaded【modelname】,而我们想改变数据的话,只需要把缓存区中的内容进行更改就可以实现了

怎么对packet.loaded的内容进行修改?

第一种:简单粗暴型

把packet.loaded【modelname】的内容直接值为空,然后重新require,但是这样子会有问题,导致旧数据无法得到更新 

function reloadUp(module_name)  
    package.loaded[modulename] = nil  
    require(modulename)  
end  

第二种:递归更新型

function reloadUp(module_name)  
    local old_module = _G[module_name]  
  
    package.loaded[module_name] = nil  
    require (module_name)  
  
    local new_module = _G[module_name]  
    for k, v in pairs(new_module) do  
        old_module[k] = v  
    end  
  
    package.loaded[module_name] = old_module  
end  

 

十三.Lua协同程序

Lua 协同程序(coroutine)与线程(这里的线程指的是操作系统的线程)比较类似:拥有独立的堆栈,独立的局部变量,独立的指令指针,同时又与其它协同程序共享全局变量和其它大部分东西。

一个多线程程序可以同时运行几个线程(并发执行、抢占),而协程却需要彼此协作地运行,并非真正的多线程,即一个多协程程序在同一时间只能运行一个协程,并且正在执行的协程只会在其显式地要求挂起(suspend)时,它的执行才会暂停(无抢占、无并发)。协同程序有点类似同步的多线程,在等待同一个线程锁的几个线程有点类似协同程序。

协程的用法:

coroutine.running就可以看出来,coroutine在底层实现就是一个线程,当create一个coroutine的时候就是在新线程中注册了一个事件。

resume和yeildr的协作是Lua协程的核心

举一个经典生产者消费者例子:创建一个生产工厂,让它生产20件产品,每生产一件就把协程挂起,等待客户下一次提交需求的时候才重新resume唤醒

local newProductor

function productor()
    local i = 0
    while true do
        i = i + 1
        send(i)     -- 将生产的物品发送给消费者
    end
end

function consumer()
    local i = receive()
    while i < 20 do
        print(i)
        i = receive()
    end
end

function receive()
    -- 唤醒程序
    local status, value = coroutine.resume(newProductor)
    return value
end

function send(x)
    coroutine.yield(x)     -- x表示需要发送的值,值返回以后,就挂起该协同程序
end

-- 创建生产工厂
newProductor = coroutine.create(productor)
consumer()

测试结果:

协程的作用:

我作为客户端,其实一直都是单线程开发的,对于多线程,协程这些为何存在一直不太理解,知道查阅了这篇博客稍微的了解一些:协程的好处是什么?

一开始大家想要同一时间执行那么三五个程序,大家能一块跑一跑。特别是UI什么的,别一上计算量比较大的玩意就跟死机一样。于是就有了并发,从程序员的角度可以看成是多个独立的逻辑流。内部可以是多cpu并行,也可以是单cpu时间分片,能快速的切换逻辑流,看起来像是大家一块跑的就行。

但是一块跑就有问题了。我计算到一半,刚把多次方程解到最后一步,你突然插进来,我的中间状态咋办,我用来储存的内存被你覆盖了咋办?所以跑在一个cpu里面的并发都需要处理上下文切换的问题。进程就是这样抽象出来个一个概念,搭配虚拟内存、进程表之类的东西,用来管理独立的程序运行、切换。

后来一电脑上有了好几个cpu,好咧,大家都别闲着,一人跑一进程。就是所谓的并行

因为程序的使用涉及大量的计算机资源配置,把这活随意的交给用户程序,非常容易让整个系统分分钟被搞跪。所以核心的操作需要陷入内核(kernel),切换到操作系统,让老大帮你来做。

有的时候碰着I/O访问,阻塞了后面所有的计算。空着也是空着,老大就直接把CPU切换到其他进程,让人家先用着。当然除了I\O阻塞,还有时钟阻塞等等。一开始大家都这样弄,后来发现不成,太慢了。为啥呀,一切换进程得反复进入内核,置换掉一大堆状态。进程数一高,大部分系统资源就被进程切换给吃掉了。后来搞出线程的概念,大致意思就是,这个地方阻塞了,但我还有其他地方的逻辑流可以计算,这些逻辑流是共享一个地址空间的,不用特别麻烦的切换页表、刷新TLB,只要把寄存器刷新一遍就行,能比切换进程开销少点。

如果我们不要这些功能了,我自己在进程里面写一个逻辑流调度的东西,碰着i\o我就用非阻塞式的。那么我们即可以利用到并发优势,又可以避免反复系统调用,还有进程切换造成的开销,分分钟给你上几千个逻辑流不费力。这就是协程。

本质上协程就是用户空间下的线程。

 

十四.Lua垃圾回收机制

在 Lua 中,一共只有8种数据类型,分别为 nil 、boolean 、userdata 、number 、string 、 table 、 function 、 userdata 和 thread 。其中,只有 string table function thread 四种是以引用方式共享,是需要被 GC 管理回收的对象。

Lua采用的是Mark-sweep算法:

mark阶段
这个阶段叫做扫描阶段。简单来讲,就是对于现在lua用到的所有对象进行扫描一次。如果某个对象当前跟别的对象有引用关系,那么说明他还在用;如果某个对象跟其他任何对象都没有引用关系了,说明这个对象已经没有用了。这个阶段做完,就可以知道哪些对象还有用,哪些对象不再使用了,下面就交给下一个阶段,sweep阶段。
cleaning阶段

这个阶段lua会出里对象的析构和弱引用表,它会遍历标记需要析构的对象,以及遍历弱引用表将要移除的键或者值

sweep阶段
这个阶段做的事情其实很少,关键步骤在前一个阶段做完了。这个阶段根据前一个扫描阶段得到的结果,遍历一遍所有对象。如果这个对象已经标记为不再使用了,就会被清理掉,释放掉所在的内存;如果这个对象还在使用,那么就处理一下状态,等待下一次gc在处理。

finalization析构

对标记需要析构的对象进行析构

这里添加一下对弱表的介绍:若一个存放在表中,那么哪怕这个对象没有被任何地方引用,但是也不会被清除,因为此时这个对象就正在被这个表引用。为了解决这个问题可以在表中的__mode字段来定义该表是一个弱表,那么在GC的时候才会把它给回收掉

__mode = "k"  -- 代表这个表中的键是弱引用的弱引用表

__mode = “v”  -- 代表这个表中的值是弱引用的弱引用表

__mode = “kv” -- 代表这个表中的键值都是弱引用的弱引用表

无论哪一种情况,只要其中一个键或者值被回收了,那么整个键值对就会被回收,这和我们把变量置位nil其实是将它 删除的原理是一样的

 

缺陷:

在lua5.0之前,早期的 Lua GC 采用的是 stop the world 的实现。一旦发生 gc 就需要等待整个 gc 流程走完。(STW: 在垃圾回收期间除了垃圾回收器线程,其他线程都会被挂起)

如果mark阶段一次性把所有节点都扫描,再一次性清理完,那么这两个步骤就都很简单了。但是,这样就有效率问题,一次性要把所有对象处理一遍,在大工程里面就绝对是一个瓶颈。

所以,lua5.0以后就把gc改成了增量式的gc,主要是把标记扩展成了三种颜色,下面详细介绍一下。

我们可以将所有对象分成三个状态:
White状态,也就是待访问状态。表示对象还没有被垃圾回收的标记过程访问到。
Gray状态,也就是待扫描状态。表示对象已经被垃圾回收访问到了,但是对象本身对于其他对象的引用还没有进行遍历访问。
Black状态,也就是已扫描状态。表示对象已经被访问到了,并且也已经遍历了对象本身对其他对象的引用。

将root集合引用到的对象从White设置成Gray,并放到Gray集合中;

while(Gray集合不为空,并且没有超过本次计算量的上限)
{
    从Gray集合中移除一个对象O,并将O设置成Black状态;
    for(O中每一个引用到的对象O1) {
        if(O1在White状态) {
            将O1从White设置成Gray,并放到到Gray集合中;
        }
     }
}
for(任意一个对象O){
    if(O在White状态)
        销毁对象O;
    else
        将O设置成White状态;
}

但是由于垃圾回收的过程变成分步的话,那么我们之前已经标注到的状态就可能会发生改变,此时lua提供了屏障barrier在程序正常运行过程中,监控所有的引用改变,然后更换对象的状态。

 

十五.Lua和C相互调用

lua和c/c++之间是通过lua_Stack 进行交互的,大家可以看一下我写的这篇文章,里面通过举出两个例子分别从lua调用c,然后从c调用lua,然后再通过例子来引出讲解lua_Stack中的全局状态机global_stack,数据栈,调用栈等知识

【Lua进阶系列】实例lua调用capi

 

十六.Lua的一些实例测试

(1) 字符串把“abc”,换成“bcd”

local str ="abcdefgh";
b = string.gsub(str, "%abc", "ddc");
str = b;
print(str);

(2) if的使用方法

If  条件    then
     语句1
Else
     语句2
end

(3) 如何对表中元素排序后输出?

从小到大
Table.sort(表名)

For k,v inipairs(table)
     Print(k,v)
End

从大到小
Table.Sort(a)

For i=1,#a,1 do
  b[i] =a[#a-i+1]
end

(4) 写一个返回多值的函数

function foo2 () 
         return 'a','b'
end

返回的值之间用逗号隔开

(5) 写一个可变参数得函数

Function foo(…)
      Retrun #{…}
End

…是可变参数的意思,上述函数等价于select(#,…)
我们还可以通过select(n,。。。)找到第n个参数

 

OK,以上就是我呕心沥血总结出来的引擎常见的面试题

如果对大家有帮助记得点赞呀,谢谢大家 !

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【游戏客户端面试题干货】--2021年最新游戏客户端面试干货(lua篇) 的相关文章

  • Lua:“拖动”数组中的元素序列

    我正在尝试创建一个函数 将连续数量的元素 拖动 到数组中的新位置 并限制为数组的当前大小 其他项目应该围绕 拖动 的项目晃动 例如 如果我的数组有 7 个元素 并且我想拖动中间的三个 1 2 3 4 5 6 7 lt keys a b C
  • 如何更新lua中的时间以反映执行过程中系统时区的变化?

    Problem 我想修改awful widget textclockAwesome wm 中的小部件可以立即反映系统时区的更改 这个小部件和所有的 Awesome wm 配置都是用 lua 编写的 目前 如果系统时区发生更改 小部件将继续根
  • 了解静态链接嵌入式lua环境中lua扩展dll的构建/加载

    我有一个相对复杂的 lua 环境 我试图了解以下内容如何工作 起始设置包括以下两个模块 主要应用 无lua环境 DLL 静态链接到lua lib 包括解释器 该 dll 被加载到主应用程序中 并运行 lua 控制台解释器和可从控制台访问的
  • Lua 中的内联条件(a == b ? "yes" : "no")?

    无论如何 Lua 中可以使用内联条件吗 Such as print blah a true blah nahblah Sure print blah a and blah or nahblah
  • 为什么 LuaJIT 和 Lua 中的数字舍入格式不同?

    Using string format 据说遵循 Csprintf 在 LuaJIT 轮次中格式化数字与我尝试过的所有其他 Lua 解释器不同 lua v Lua 5 4 1 Copyright C 1994 2020 Lua org PU
  • 用于嵌入式服务器的 Web 技术

    我最近开始了一个针对嵌入式设备的新 Web 开发项目 并希望征求一些有关使用技术的建议 该设备将提供 HTML 页面 其中包括用于从 JSON 服务器检索数据的 AJAX 代码 我们暂时使用 Cherokee 作为 Web 服务器 但我们并
  • Lua-迭代嵌套表

    我已经学习 Lua 几个星期了 这一次又一次成为我的症结所在 我尝试阅读有关该主题的帖子和书籍 我使用 Lua 查询软件监控系统 Nimsoft 我的数据以表格形式返回给我 我不会发布整个输出 但这里有一个我认为可以描述结构的片段 表参考是
  • 什么时候适合使用Lua这样的嵌入式脚本语言

    我玩 魔兽世界 大约有两年了 我对用来编写插件的 Lua 很好奇 由于到目前为止我读到的有关 Lua 的内容都是 快 轻 和 这太棒了 所以我想知道如何以及何时使用它 您需要在系统中嵌入像 Lua 这样的脚本语言的典型情况是什么 当您需要最
  • 如何在 Lua 中实现 OO?

    Lua 没有内置对 OO 的支持 但它允许您自己构建它 您能否分享一些实现面向对象的方法 请为每个答案写一个例子 如果您有更多示例 请发布另一个答案 我喜欢将 OOP 视为容器 对象 内的数据封装以及可以使用该数据完成的操作子集 还有很多内
  • Lua userdata:无法同时进行数组访问和方法

    我遇到了这个人的问题 Lua userdata数组访问及方法 https stackoverflow com questions 26970316 lua userdata array access and methods 其中 当我设置用
  • 尝试将 nil 与数字堆栈回溯进行比较?

    我正在通过以下链接玩 Lua https www lua org pil 4 2 html https www lua org pil 4 2 html并对某一点感到困惑 Lua 5 2 4 Copyright C 1994 2015 Lu
  • 为什么 LuaJIT 这么好?

    编辑 不幸的是 LuaJIT 已从下面链接的比较中删除 This 比较 http shootout alioth debian org u64 which programming languages are fastest php编程语言的
  • VB6 - Lua 集成

    我想知道是否有人有任何集成 Lua 和 VB6 的技巧 我正在运行一个小型在线角色扮演游戏 添加一些脚本会很棒 嗯 这是可行的 我曾经为 Lua 5 0 2 做过 但找不到文件 在您拥有的选项中 您可以 将 Lua 封装在公开 Lua AP
  • 安装Lua套接字库

    要么我太累了 要么我瞎了 我想学习 Lua 网络 因此我必须安装socketlib 所以我可以轻松地要求它 但我不知道我应该 要求 哪些文件 例子说 local socket require socket 但正如我所说 如果我使用 我不知道
  • 检查多个位置的值并仅在源唯一时返回匹配项

    假设我有一个清单Vendors 阿斯达 乐购 Spar 我有一个清单Sources 或者这个类比中的供应商 家乐氏 Kellogg 吉百利 Cadbury 雀巢 Nestle 强生 Johnsons 帮宝适 Pampers Simple 等
  • 如何在 emacs lua-mode 中配置缩进?

    完整的 emacs 新手在这里 我在 Ubuntu 上使用 emacs 23 1 1emacs 入门套件 https github com technomancy emacs starter kit 我主要在 lua 模式下工作 安装了pa
  • 在Luasocket中,在什么条件下,即使在select告诉它可以安全读取之后,accept调用也可以阻塞?

    卢阿索基特select http w3 impa br diego software luasocket socket html select函数应该告诉何时可以在不阻塞的情况下读取套接字 它显然也可以用来告诉服务器套接字何时准备好接受新连
  • 如何终止Lua脚本?

    如何终止 Lua 脚本 现在我在 exit 方面遇到问题 我不知道为什么 这更像是一个 Minecraft ComputerCraft 问题 因为它使用了包含的 API 这是我的代码 while true do if turtle dete
  • 如何在 Lua - Lightroom 插件中使用 HMAC

    首先我要提的是我对 Lua 真的很陌生 如果你认为我的问题太愚蠢 请耐心等待 这是我的要求 我需要使用 HMAC sha256 进行 Lightroom 插件开发 因为我使用它是为了安全 我试图使用这个但没有运气https code goo
  • Lua 的标准(或最好支持的)大数(任意精度)库是什么?

    我正在处理大量无法四舍五入的数字 使用 Lua 的标准数学库 似乎没有方便的方法来保持精度超过某些内部限制 我还看到有几个库可以加载以处理大数字 http oss digirati com br luabignum http oss dig

随机推荐

  • 毕业设计 单片机LSRB算法的走迷宫小车 - 嵌入式 stm32

    文章目录 0 前言 1 简介 2 主要器件 3 实现效果 4 硬件设计 马达驱动器 L298N Mpu 6050 60 RPM 直流电机 红外传感器 Arduino Pro mini 5 软件说明 LSRB 算法 6 最后 0 前言 这两年
  • JavaSE进阶

    1 使用集成开发工具eclipse 1 1 java的集成开发工具很多 包括 eclipse Intellij IDEA netbeans eclipse IBM开发的 eclipse翻译为 日食 寓意吞并SUN公司 SUN是太阳 最终没有
  • 坐标转换、地球转火星、百度转火星(python版)

    一 坐标介绍 1 地球坐标 GPS WGS84 地理坐标系统 2 火星坐标 GCJ 02 投影坐标系统 中国自己在WGS84基础上加密而成 3 地球坐标 BD 09 投影坐标系统 百度地图使用 二 坐标转换 import math pi 3
  • 掌握MySQL分库分表(二)Mysql数据库垂直分库分表、水平分库分表

    文章目录 垂直分表 拆分方法 举例 垂直分库 水平分表 水平分库 小结 垂直角度 表结构不一样 水平角度 表结构一样 垂直分表 需求 商品表字段太多 每个字段访问频次不 样 浪费了IO资源 需要进行优化 也就是 大表拆小表 基于列字段进行的
  • Mongodb创建用户角色

    文章目录 一 Mongodb数据库用户角色 二 创建用户 其权限有哪些 1 创建用户语法格式 2 字段解析 三 创建用户实列 总结 一 Mongodb数据库用户角色 MongoDB采用基于角色的访问控制 RBAC 来确定用户的访问 授予用户
  • 博弈论——组合游戏(Bash和nim)

    博弈论1 组合游戏 特征 两个玩家 一个状态集合 游戏规则是指明玩家从一个状态可以移动到哪些状态 玩家轮流进行移动 如果当前处于的状态无法移动 则说明游戏结束 大部分时候 无论玩家如何选择 游戏会在有限步操作内结束 通常的解题步骤 首先设置
  • easyx图形库制作新年烟花(附图片资源)

    目录 先看效果 代码 先看效果 map 看吧是不是非常好看 哈哈 接下来就直接上代码 这个是图形库的坐标图 代码 include
  • kali linux 压缩文件解压缩命令(包含7z)

    kali linux 压缩文件解压缩命令 包含7z tar 解包 tar xvf FileName tar 打包 tar cvf FileName tar DirName 注 tar是打包 不是压缩 gz 解压1 gunzip FileNa
  • DBT乳腺切片投影及重建(MATLAB版)

    采用RadiAnt DICOM Viewer可以轻松读入图像 能读取理想的WW和WL值 衰减系数转HU的程序 water atten 0 150 自己设定的 f f water atten water atten 1000 HU转衰减系数的
  • 吴恩达机器学习笔记之神经网络参数的反向传播算法

    代价函数 回顾Logistic Regression中的代价函数为 神经网络的代价函数的基本思想与逻辑回归是一样的 但是形式上有一些差别 L表示神经网络的层数 sl表示l层神经网中的神经元的个数 K表示输出层的神经元的个数 正则项的计算包含
  • php实时股票,php股票数据分析源码

    1 股票指标源码 别名 彩色棒状线 所属类别 线型描述 参数数量 0 以零轴为中心画彩色棒状线 零轴下为阴线颜色 零轴上为阳线颜色 例如 CLOSE OPEN COLORSTICK 别名 分笔数量 所属类别 行情函数 参数数量 0 取得该周
  • 15 openEuler使用DNF管理软件包

    文章目录 15 1 搜索软件包 15 2 列出软件包清单 15 3 显示RPM包信息 15 4 安装RPM包 15 5 下载软件包 15 6 删除软件包 DNF是一款Linux软件包管理工具 用于管理RPM软件包 DNF可以查询软件包信息
  • 【八】springboot整合AOP实现日志操作(超详细)

    springboot篇章整体栏目 一 springboot整合swagger 超详细 二 springboot整合swagger 自定义 超详细 三 springboot整合token 超详细 四 springboot整合mybatis p
  • python机器人开发教程:学习如何使用Python编程创建机器人

    前言 我们学习一些如何使用 ChatterBot 库在 Python 中创建聊天机器人 该库实现了各种机器学习算法来生成响应对话 还是挺不错的 什么是聊天机器人 聊天机器人也称为聊天机器人 机器人 人工代理等 基本上是由人工智能驱动的软件程
  • DSA算法

    DSA 本文主要叙述在CTF中的DSA 根据我自己的理解重述一遍CTF wiki对DSA的描述 公私钥的生成 选择一个哈希函数 H H H 一般选作SHA1 选择比特数为
  • linux diff 补丁文件夹,diff打补丁详解

    对于开源源码修改过程中的必经阶段 对源码打补丁 总是不够精通 搜索了补丁的原理的详细过程 如下 在移植或版本升级过程中 手动比对 用比对工具 转换是很费力的事情 特别是发生变化的文件非常多的情况下 制作补丁 打补丁 可以简化这个过程 主要用
  • Diffie-Hellman(迪菲-赫尔曼)秘钥交换

    Diffie Hellman算法是Whitefield Diffie和Martin Hellman在1976年公布的一种秘钥交换算法 它是一种建立秘钥的方法 而不是加密方法 所以秘钥必须和其他一种加密算法结合使用 这种秘钥交换技术的目的在于
  • Centos7 配置zookeeper 开机自启动

    昨天参考网上的相关教程 Centos7安装Zookeeper3 4 12并实现开机自动 今天早上开启虚拟机 查看zookeeper 的服务状态 切换至zookeeper 的安装目录 cd usr local zookeeper zookee
  • HTML <template> 标签

    实例 使用
  • 【游戏客户端面试题干货】--2021年最新游戏客户端面试干货(lua篇)

    游戏客户端面试题干货 2021年度最新游戏客户端面试干货 lua篇 大家好 我是Lampard 经过春招一番艰苦奋战之后 我终于是进入了心仪的公司 今天给大家分享一下我在之前精心准备的一套面试知识 今天和大家分享的是lua的面试题 本人亲测