游戏客户端与服务器面试题-- 2022年最新游戏客户端与服务器面试(lua篇持续更新)
Posted 森明帮大于黑虎帮
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了游戏客户端与服务器面试题-- 2022年最新游戏客户端与服务器面试(lua篇持续更新)相关的知识,希望对你有一定的参考价值。
【游戏客户端与服务器面试题干货】-- 2022年度最新游戏客户端面试干货(lua篇)
文章目录
- 一、Lua的8种数据类型
- 二、pairs和ipairs的区别
- 三、lua表常用方式(插入,删除,移动,排序)
- 四、如何实现继承关系(__index)
- 五、__newindex元方法
- 六、实现一个常量表
- 七、__call元方法
- 八、__tostring元方法
- 九、lua元方法
- 十 、lua闭包
- 十一、类使用:和.的区别
- 十二、require,loadfile和dofile的区别
- 十三、Lua的热更新原理
- 十四、Lua协同程序
- 十五、Lua垃圾回收机制
- 十六、Lua和C相互调用
- 十七、Lua的一些实例测试
- 十八、lua的命名规范以及注释方法
- 十九、lua条件与循环
- 二十、lua代码优化,别再依赖if..else了
- 二十一、lua数值型for和泛型for
- 二十二、lua模式匹配
- 二十三、lua模式匹配练习
- 二十四、lua之数据结构(多维数组,链表,队列)
- 二十五、rawset & rawget方法
- 二十六、lua环境ENV
一、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:
通过上述对逻辑运算符的理解,我们用这种写法来代替简单的if else,让代码变得更优雅
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类型的使用无非是以下的状况, Lua语言还提供了除了加减乘除之外,向下取整除法(floor除法),取模和指数运算。
1.加+,减-,乘*:
int对int型进行运算,则得到的结果都是int型,但只要两个数字中有一个是float型,则得出来的结果都是float型。
2.除/:
无论是什么数字,得到的结果永远都是float类型。
那如果我硬是想要直接一步到位,除出来的结果也要是整形怎么办?
3.双除法 // :
得到的是一个整值,若结果存在小数,则向负无穷截断。
除了加减乘除之外,使用得最多的就是取整和取随机数了。
4.取整:
- floor()–向负无穷取整
- ceil() – 向正无穷取整
- modf()–向0取整
- floor(x+0.5)–四舍五入
number类型的取整,返回结果为整形值:
- (1)floor()–向负无穷取整:floor(1.5)=1
- (2)ceil() – 向正无穷取整:ceil(1.5)=2
- (3)modf()–向0取整:modf(1.5)=1.modf(-1.5)=-1
- (4)floor(x+0.5)–四舍五入
5.强制类型转换
number类型的取整以及强制转换。
- 整数类型转化成浮点型:+0.0
- 浮点类型转化成整形:math.tointeger()
6.取随机数:
产生随机数(math.random()):
- Lua中产生随机数的三种方式:
- math.random()-- 产生(0~1)的随机值
- math.random(n)-- 产生(1~n)的随机值
- math.random(m,n)-- 产生(m~n)的随机值
7.表示范围
最大值math.maxinteger和最小值math.mininteger。
lua中int型和float型都是使用8个字节来存储的,所以他们有最大值和最小值存在。
当对int最大值加整数1时,会发生回滚,如:
math.maxinteger+1=math.mininteger
math.mininteger-1=math.maxinteger
但是如果当他们加的是浮点型数字时,就不会发生回滚,而是取近似值。
math.maxinteger+1.0=math.maxinteger
math.mininteger-1.0=math.mininteger
(4) function类型
在Lua语言中,函数(Function)是对语句和表达式进行抽象的一种方式。函数调用时都需要使用一对圆括号把参数列表括起来。几时被调用的函数不需要参数,也需要一堆空括号()。唯一的例外是,当函数只有一个参数且该参数是字符串常量或表构造器时,括号是可选的。
print "Hello World" -- 相等于print(“Hello World”)
type -- 相等于type()
正如我们已经在其他示例中看到的一样,Lua语言中的函数定义的常见语法格式如下,举个例子:
function add(a) -- 声明add这个函数
local sum = 0 -- 实现序列a的求和
for i=1, #a do -- 循环体
sum = sum + a[i]
end
return sum -- 返回值
end
在这种语法中,一个函数定义具有一个函数名(name,本例中的add),一个参数组成的列表和由一组语句组成的函数体。参数的行为与局部变量的行为完全一致,相当于一个用函数调用时进行初始化的局部变量。
调用函数时,使用的参数个数与定义函数时使用的参数不一致。Lua语言会通过抛弃多余的参数以及将不足的参数设为nil的方式来调整参数的个数。
这是我们类C的写法,function 函数名 小括号 参数, 但其实我们还有另外一种写法,把函数当成一个对象去定义:
两种方式都可以声明一个函数,至于使用哪一种方式,就根据贵公司项目而定了。
lua的函数类型除了可以把它当成对象这样定义之外,还有两个特性:可变长参数,以及多返回值。
1.多返回值
- Lua语言中一种与众不同但又非常有用的特性是允许一个函数返回多个结果,只需要在return关键字后列出所有要返回的值即可。
- 例如一个用于查找序列中最大元素的函数可以同时返回最大值以及该元素的位置:
当函数作为 一条单独语句使用时,其所有值均会被抛弃。当函数被作为 表达式(例如加法操作数)调用时,将 只保留第一个返回值。
function foo ()
return "a","b"
end
x,y = foo() -- x="a",y="b"
x = foo() -- x="a"
x,y,z=foo() -- x="a",y="b",z=nil
2.可变长参数
- Lua语言中的函数可以是可变长参数函数(variadic),即可以支持数量可变的参数, 只需要在函数声明的时候参数项用…代替即可。
- 下面是一个简单的示例,该函数返回所有参数的总和:
参数列表中的三个点表示该函数的参数是可变长的。当这个函数被调用时,Lua内容会把它的所有参数收集起来,三个点是作为一个表达式来使用的。在上例中,表达式…的结果是一个由所有可变长参数组成的列表,该函数会遍历该列表来累加。
-- 我们可以通过以下这几种方式进行对变化参数的调用
local Array... -- 把它变成一个表
#... -- 取得当前变化参数的个数
selecti,... -- 通过select方法取得第i个变化参数的值
(5) string类型
Lua中的字符串是不可变值(immutable value)。我们不可以像在C语言中那样直接改变某个字符串中的某个字符。但是我们可以创建另外一个新字符串的方式来达到修改的目的。
可以使用来获取字符串的长度。
我们也可以用连接操作符(…)来拼接两个字符串)。但是由于Lua的字符串是不可变的,所以得到的是一个新的字符串。
1.字符串常量
我们可以使用双引号或者单引号来声明字符串常量。
a = "a line"
b = ‘another line’
那么如果在字符串内容中出现双引号或者单引号怎么办呢?老司机们可能就会脱口而出:用转义字符’'啊。
没错使用转义字符确实能够解决问题,但是如果是在双引号定义的字符串中出现单引号,或者单引号字符串中出现双引号则不需要使用转义字符。
- 使用双引号声明的字符串中出现单引号时,不需要转义。
- 同理,使用单引号声明的字符串出现双引号时,不需要转义。
2.长字符串/多行字符串
为了方便缩进排版,所以Lua定义了用户可以使用一对方括号 [[]] 来声明长字符串。被方括号扩起来的内容可以由很多行,并且内容中的转义序列不会被转义。
同时,为了避免出现像这种情况:
array[b[10]] -- 出现了两个]]
我们还可以在声明长字符串时在两个中括号之间加入等量的=号,如:
array[==[
123456 -- 这样lua也会自动识别它是一个长的字符串
]==]
3.类型强制转换
当Lua语言发现在需要字符串的地方出现数字时,它会自动把数值转换为字符串。
但是假如我们需要 1 … 2 想输出“12”的化话,那么数字和…连接符之间需要带空格,避免系统把它当成小数点。
当在算数运算中发现字符串时,它会转化为浮点型数值再进行计算,要注意在比较操作中不会默认转化。比如下图中的a和b是字符串,但是相加的时候则转化成数字:
当然我们也可以显式的把字符串和数值相互转换:tostring()-- 返回字符串/ tonumber () --返回整形或浮点型数值。
4.字符串常用操作
- (1) 字符串拼接: …(两个点)
a = “hello”
b = "world"
c = a..b -- 此时c等于hello world
- (2) 取字符串长度
c = “hello world”
print (#c) -- 此时输出11
5.字符串标准库
Lua本身对字符串的处理十分有限,仅能够创建,拼接,取长度和比较字符串。
所以Lua处理字符串的完整能力来自字符串的标准库。
诶!怎么没有得到想要的结果呢?原来是忘记了Lua中字符串是不可变的这定义。所以我们要看到改变后的后果,可以用一个新的字符串接住它。
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()。
(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”
a = -- 创建了一个空表
k = “x”
a[k] = 10 -- 这句话的意思是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()。
1.lua中深拷贝与浅拷贝
lua中我们使用 = 来进行浅拷贝,使用函数clone() 来进行深拷贝。
如果拷贝的对象里的元素只有值(string、number、boolean等基础类型 ),那浅拷贝和深拷贝没有差别,都会将原有对象复制一份,产生一个新对象。
如果是一个表 的话,则浅拷贝拷贝出来的对象和拷贝前的实际上是同一个对象,占同一个内存,而深拷贝才创建出一个新的内存,一个新的对象。
2.lua中深拷贝与浅拷贝源码
function clone(object)
local lookup_table =
local function _copy(object)
if type(object) ~= "table" then
return object
elseif lookup_table[object] then
return lookup_table[object]
end
local new_table =
lookup_table[object] = new_table
for key, value in pairs(object) do
new_table[_copy(key)] = _copy(value)
end
return setmetatable(new_table, getmetatable(object))
end
return _copy(object) -- 返回clone出来的object表指针/地址
end
lua中clone的源代码十分简短,但是如果第一次看的话还是比较容易看懵。
我们如果传进去的对象不是表类型的话,那么我们就会直接把这个值return出去,然后再利用=号进行一次浅拷贝,上文提过如果是数值类型的话,浅拷贝也会生成一个对象。那么如果如果传的object是一个表类型的话,则递归去把object中的key, value复制到一个新创建的表中,最后再把object的元表设置成新表的元表。这样就完成了整个深克隆的过程了。
3.表索引
同一个表中存储的值可以有不同的类型索引:既不同类型的键。未经初始化的表元素为nil。
当把表当做结构体使用时,可以把索引当做成员名称使用。
对于Lua语言而言,这两种形式是等价的。但是对于阅读程序的人而言,这两种形式分别代表了两种意图:当你用a.name来赋值时,清晰地说明了把表当做结构体使用,此时的标识由预先定义的键组成的集合。而使用a【“name”】来赋值,则说明了表可以使用任意字符串当做键。
4.表构造器
除了使用空构造器构造表之外我们还可以这样做:
注意:Lua中默认值是从1开始。
days = “Monday”,“Tuesday”,“Wednesday”,“Thursday”,“Friday”,“Saturday”,“Sunday”
--[[ 此时days[1]到days[7]被默认定义为“Monday”~“Sunday” ]]
Lua语言还提供了一种初始化记录式表的特殊语法:
a = x = 10 , y = 20
-- 上面的写法等价于 a["x"]=10,a["y"]=20
在同一个构造器中,可以混用记录式和列表式写法。
polyLine =
color = "blue",
thickness = 2,
npoints = 4,
x=0,y=0, --[[ 类似二维数组,此时polyLine[1]=x=0,y=0
x=-10,y=1, polyLine[2]=x=-10,y=1
x=0,y=1 polyLine[3]=x=0,y=1] ]]
5.数组,列表和序列
如果想表示常见的数组或者列表,那么只需要使用整形作为索引的表即可。当该表不存在空洞,既表中的所有数据都不为nil时,则成这个表为序列(sequence)。
Lua语言提供了获取序列长度的操作符#。正如我们之前所看到,对于字符串而言,该操作符会统计字符串的字节数。对于表而言,则会返回序列的大小。
因而,当我们想在序列后增加元素时则可以使用语句 a[#a+1]=new
6.遍历表
我们可以使用pairs迭代器遍历表中的键值对。遍历过程中元素出现的顺序可能是随机的,相同的程序在每次运行时也可能产生不同的顺序。唯一可以确定的是,在遍历的过程中每个元素会且只会出现一次。
对于序列而言我们可以使用ipairs迭代器:此时Lua确保是按顺序进行的。
7.表标准库
表标准库提供了操作列表和序列的一些常用函数。
今天简单介绍增加(table.insert),删除(table.remove),移动(table.move)以及排序(table.sort)。
- table.insert ()
- insert()有两种格式,一种是两个参数,insert(tableName,元素),这种情况下就会默认插到末尾。
- 另一种是三个参数(tableName,位置,元素),则可以按照自己的想法插入元素。
- table.remove ()
- 删除指定位置的元素,并把后面的元素往前移动填充删除所造成的空缺。。
- table.move(tableA,起始索引,终止索引,tableB)
- 它的作用时把表A中从起始索引到终止索引的值移动到表B中。
- table.sort()
- 这个就是单纯的排序方法。
(7) userdata类型
userdata是用户自定义的数据类型,lua只提供了一块原始的内存区域,用于存储任何东西, 在Lua中userdata没有任何预定义操作。
因为lua只是一个两三万行代码的一个脚本语言,有很多功能都是依靠c给它提供,所以userdata在实际中它代指了那些使用c/c++语言给lua提供的函数模块。
1.实例lua调用capi
今天是要和大家分享关于luaDebug库的一些内容,但是我在研究luaDebug库的时候,发现它调用了许多的luaAPI,对于没有研究过lua与c/c++交互的我可以说是看到满头大汉,一脸懵逼。所以我就决定从最原始入手,研究lua和c/c++是如 以上是关于游戏客户端与服务器面试题-- 2022年最新游戏客户端与服务器面试(lua篇持续更新)的主要内容,如果未能解决你的问题,请参考以下文章