【游戏客户端与服务器面试题】-- 2022年最新游戏客户端与服务器面试(lua篇
【游戏客户端与服务器面试题干货】-- 2022年度最新游戏客户端面试干货(lua篇)文章目录
[*]一、Lua的8种数据类型
[*]
[*]
[*](1) nil 类型
[*](2) boolean类型
[*](3) number类型
[*]
[*]1.加+,减-,乘*:
[*]2.除/:
[*]3.双除法 // :
[*]4.取整:
[*]5.强制类型转换
[*]6.取随机数:
[*]7.表示范围
[*](4) function类型
[*]
[*]1.多返回值
[*]2.可变长参数
[*](5) string类型
[*]
[*]1.字符串常量
[*]2.长字符串/多行字符串
[*]3.类型强制转换
[*]4.字符串常用操作
[*]5.字符串标准库
[*](6) table类型
[*]
[*]1.lua中深拷贝与浅拷贝
[*]2.lua中深拷贝与浅拷贝源码
[*]3.表索引
[*]4.表构造器
[*]5.数组,列表和序列
[*]6.遍历表
[*]7.表标准库
[*](7) userdata类型
[*]
[*]1.实例lua调用capi
[*]
[*]1.看两个小栗子
[*]
[*]1.创建lua_Stack
[*]2.第一个栗子:c++调用lua的函数
[*]3.第二个栗子:lua调用c++的函数
[*]1.分析这两个栗子
[*](8) thread类型
[*]
[*]1.C/C++与lua的交互方式
[*]2.lua_Stack究竟由什么组成
[*]
[*]1.CommonHeader -- GC的通用头
[*]2. nci -- 记录调用栈item个数的变量
[*]3.l_G -- 全局状态机,维护全局字符串表、内存管理函数、gc等信息
[*]
[*]1.什么是全局状态机global_State
[*]2.全局状态机global_State的组成
[*]3.全局状态机global_State初始化过程
[*]4.StkId -- 数据栈:
[*]5. CallInfo -- 调用栈
[*]6. HOOK 相关-- 服务于debug模块
[*]
[*]1.首先什么是debug库
[*]2.自省(introspective)函数
[*]3.introspective自省debug.getinfo函数
[*]3.1.栈级别(stack level)
[*]3.2.提高调用getInfo的效率
[*]4.introspective自省debug.getlocal函数
[*]5.Hooks钩子
[*]7.GC 垃圾回收
[*]二、pairs和ipairs的区别
[*]三、lua表常用方式(插入,删除,移动,排序)
[*]四、如何实现继承关系(__index)
[*]
[*]
[*]1.面向对象编程--语法糖
[*]2.面向对象编程--类的实现与继承
[*]
[*]1.定义一个类:
[*]2.继承
[*]3.__index元方法
[*]五、__newindex元方法
[*]
[*]
[*]1.__nexindex指向一个函数
[*]2.__nexindex指向一个表
[*]六、实现一个常量表
[*]七、__call元方法
[*]八、__tostring元方法
[*]九、lua元方法
[*]
[*]
[*]1.__index元方法
[*]2.__newindex元方法
[*]
[*]1.__nexindex指向一个函数
[*]2.__nexindex指向一个表
[*]3.__call元方法
[*]4.__tostring元方法
[*]5.__le,__eq, __lt,__add, __pow,__mod
[*]十 、lua闭包
[*]
[*]
[*]1.闭包的概念
[*]2.闭包的应用
[*]3.闭包的实现原理
[*]十一、类使用:和.的区别
[*]十二、require,loadfile和dofile的区别
[*]十三、Lua的热更新原理
[*]
[*]
[*]1.什么是热更新
[*]2.热更新机制应运而生,两种实现方式
[*]
[*]1.简单版但是有缺陷
[*]2.复杂版但是很有用
[*]2.hotfix 实现了Lua热更新,仅对函数作替换,保留旧数据
[*]3.热更新进一步介绍
[*]
[*]1.原理
[*]2.要点分析
[*]3.Lua热更新的实现
[*]4._ENV 环境变量
[*]5.上值 upvalue
[*]6.热更新函数Lua的require函数
[*]7.热更新函数Lua的setenv函数
[*]8.热更新函数Lua的debug库函数
[*]9.深度递归替换所有的upvalue
[*]10.实例分析
[*]4.Lua脚本热更新方案
[*]5.lua热更新
[*]
[*]1.什么是热更新
[*]2.热更新原理第一种:
[*]3.热更新原理第二种:
[*]十四、Lua协同程序
[*]
[*]
[*]1.lua协同程序初阶
[*]2.lua协同程序进阶
[*]
[*]1.什么是协程
[*]2.协程&线程的作用
[*]3.lua中协程的调用
[*]
[*]1.coroutine.create()创建协程
[*]2.coroutine.status()协程状态
[*]3.resume和yeild
[*]4.通过协程实现异步I/O
[*]十五、Lua垃圾回收机制
[*]
[*]
[*]1.mark阶段
[*]2.cleaning阶段
[*]3.sweep阶段
[*]4.finalization析构
[*]5.缺陷
[*]十六、Lua和C相互调用
[*]
[*]
[*]1.实例lua调用capi
[*]
[*]1.看两个小栗子
[*]
[*]1.创建lua_Stack
[*]2.第一个栗子:c++调用lua的函数
[*]3.第二个栗子:lua调用c++的函数
[*]1.分析这两个栗子
[*]十七、Lua的一些实例测试
[*]
[*]
[*](1) 字符串把“abc”,换成“bcd”
[*](2) if的使用方法
[*](3) 如何对表中元素排序后输出
[*](4) 写一个返回多值的函数
[*](5) 写一个可变参数得函数
[*]十八、lua的命名规范以及注释方法
[*]
[*]
[*]1.lua命名规范
[*]2.lua注释方式
[*]十九、lua条件与循环
[*]
[*]
[*]1.条件选择语句if
[*]2. while循环语句
[*]3. repeat..until循环语句
[*]4. for循环语句
[*]5.提前结束循环
[*]二十、lua代码优化,别再依赖if..else了
[*]二十一、lua数值型for和泛型for
[*]
[*]
[*]1.数值型for
[*]2.泛型for
[*]二十二、lua模式匹配
[*]
[*]
[*]1.string.find()
[*]2.string.match()
[*]3.string.gmatch()
[*]4.string.gsub()
[*]二十三、lua模式匹配练习
[*]二十四、lua之数据结构(多维数组,链表,队列)
[*]
[*]
[*]1.数组
[*]
[*]1.一维数组
[*]2.二维数组
[*]2.链表
[*]3.栈和队列
[*]二十五、rawset & rawget方法
[*]
[*]
[*]1.什么是rawset & rawget
[*]二十六、lua环境ENV
[*]
[*]
[*]1.全局环境_G
[*]2.非全局环境_ENV
一、Lua的8种数据类型
在Lua中有8种基本类型,分别是:nil–空,boolean–布尔,number–数值,string–字符串,userdata–用户数据,function–函数,thread–线程(注意这里的线程和操作系统的线程完全不同,lua和c/c++进行交互的lua_Stack就是一种llua的线程类型),和table–表。
我们可以通过调用print(type(变量))来查看变量的数据类型。
https://img-blog.csdnimg.cn/c70d97f5bc5844e49485d902361c8e94.png
(1) nil 类型
nil是一种只有一个nil值的类型,它的主要作用是与其他所有值进行区分。Lua语言使用nil值来表示没有有用的值的情况。全局变量第一次被赋值前的默认值就是nil,将nil赋值给全局变量相当于将其删除。
(2) boolean类型
boolean类型具有两个值,true和false,他们分别代表了传统的布尔值。敲黑板:
不过在Lua中,任何值都能表示条件:Lua定义除了false和nil的值为假之外,所有的值都为真,包括0和空字符串。
提到布尔值就不得不提一下逻辑运算符:and,or,not 他们都遵循着短路求值。
举个栗子:
首先,对于and来说,如果第一个值为假,则返回第一个值,否则返回第二个值:
https://img-blog.csdnimg.cn/0ec6eb05eba34d75b7bd92cc207c8721.png
对于or来说,如果第一个值为真,则返回第一个值,否则返回第二个值:
https://img-blog.csdnimg.cn/12bfb994315c4ba1bf0332a5190c12b9.png
对于not来说,返回值永远为Boolean:
https://img-blog.csdnimg.cn/4e2c8d567d8f466888d8d58007519d93.png
通过上述对逻辑运算符的理解,我们用这种写法来代替简单的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型。
https://img-blog.csdnimg.cn/fcf354f0f8bb498ba4109c80c25987bd.png
2.除/:
无论是什么数字,得到的结果永远都是float类型。
https://img-blog.csdnimg.cn/21b9b32ae37043999a94f631b0129e2c.png
那如果我硬是想要直接一步到位,除出来的结果也要是整形怎么办?
3.双除法 // :
得到的是一个整值,若结果存在小数,则向负无穷截断。
https://img-blog.csdnimg.cn/43fd053e9e6e4036a7a9c4a2535844c0.png
除了加减乘除之外,使用得最多的就是取整和取随机数了。
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
end
return sum -- 返回值
end 在这种语法中,一个函数定义具有一个函数名(name,本例中的add),一个参数组成的列表和由一组语句组成的函数体。参数的行为与局部变量的行为完全一致,相当于一个用函数调用时进行初始化的局部变量。
调用函数时,使用的参数个数与定义函数时使用的参数不一致。Lua语言会通过抛弃多余的参数以及将不足的参数设为nil的方式来调整参数的个数。
https://img-blog.csdnimg.cn/2394aea6a4a04b38a61be4b392c4743d.png
这是我们类C的写法,function 函数名 小括号 参数, 但其实我们还有另外一种写法,把函数当成一个对象去定义:
两种方式都可以声明一个函数,至于使用哪一种方式,就根据贵公司项目而定了。
lua的函数类型除了可以把它当成对象这样定义之外,还有两个特性:可变长参数,以及多返回值。
1.多返回值
[*]Lua语言中一种与众不同但又非常有用的特性是允许一个函数返回多个结果,只需要在return关键字后列出所有要返回的值即可。
[*]例如一个用于查找序列中最大元素的函数可以同时返回最大值以及该元素的位置:
https://img-blog.csdnimg.cn/5912767cbf594613850cf71373b3ae05.png
当函数作为 一条单独语句使用时,其所有值均会被抛弃。当函数被作为 表达式(例如加法操作数)调用时,将 只保留第一个返回值。
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),即可以支持数量可变的参数, 只需要在函数声明的时候参数项用…代替即可。
[*]下面是一个简单的示例,该函数返回所有参数的总和:
https://img-blog.csdnimg.cn/6876f87b282b4fd596617cea8c8cfac8.png
参数列表中的三个点表示该函数的参数是可变长的。当这个函数被调用时,Lua内容会把它的所有参数收集起来,三个点是作为一个表达式来使用的。在上例中,表达式{…}的结果是一个由所有可变长参数组成的列表,该函数会遍历该列表来累加。
-- 我们可以通过以下这几种方式进行对变化参数的调用
local Array{...} -- 把它变成一个表
#{...} -- 取得当前变化参数的个数
select{i,...} -- 通过select方法取得第i个变化参数的值 (5) string类型
Lua中的字符串是不可变值(immutable value)。我们不可以像在C语言中那样直接改变某个字符串中的某个字符。但是我们可以创建另外一个新字符串的方式来达到修改的目的。
https://img-blog.csdnimg.cn/04c7c32829c84dcaad8639a9c277b3c9.png
可以使用来获取字符串的长度。
https://img-blog.csdnimg.cn/ec198a44ea1a46fabaf9081cbaad30f3.png
我们也可以用连接操作符(…)来拼接两个字符串)。但是由于Lua的字符串是不可变的,所以得到的是一个新的字符串。
https://img-blog.csdnimg.cn/be8f61163b2c4f14a45498aee1fde9d4.png
1.字符串常量
我们可以使用双引号或者单引号来声明字符串常量。
a = "a line"
b = ‘another line’ 那么如果在字符串内容中出现双引号或者单引号怎么办呢?老司机们可能就会脱口而出:用转义字符’'啊。
没错使用转义字符确实能够解决问题,但是如果是在双引号定义的字符串中出现单引号,或者单引号字符串中出现双引号则不需要使用转义字符。
[*]使用双引号声明的字符串中出现单引号时,不需要转义。
[*]同理,使用单引号声明的字符串出现双引号时,不需要转义。
https://img-blog.csdnimg.cn/9b069e254902499286543d51892d56cf.png
2.长字符串/多行字符串
为了方便缩进排版,所以Lua定义了用户可以使用一对方括号 [[]] 来声明长字符串。被方括号扩起来的内容可以由很多行,并且内容中的转义序列不会被转义。
https://img-blog.csdnimg.cn/9b537025ec364670813cac7511682082.png
同时,为了避免出现像这种情况:
array] -- 出现了两个]] 我们还可以在声明长字符串时在两个中括号之间加入等量的=号,如:
array[==[
123456 -- 这样lua也会自动识别它是一个长的字符串
]==] 3.类型强制转换
当Lua语言发现在需要字符串的地方出现数字时,它会自动把数值转换为字符串。
https://img-blog.csdnimg.cn/d06c483f469b4a1e9755e84edacbe2f4.png
但是假如我们需要 1 … 2 想输出“12”的化话,那么数字和…连接符之间需要带空格,避免系统把它当成小数点。
当在算数运算中发现字符串时,它会转化为浮点型数值再进行计算,要注意在比较操作中不会默认转化。比如下图中的a和b是字符串,但是相加的时候则转化成数字:
https://img-blog.csdnimg.cn/187c07e7f3084c488542204888895008.png
当然我们也可以显式的把字符串和数值相互转换:tostring()-- 返回字符串/ tonumber () --返回整形或浮点型数值。
https://img-blog.csdnimg.cn/9c5451827bfd4ccba53994567d29882e.png
4.字符串常用操作
[*] (1) 字符串拼接: …(两个点)
a = “hello”
b = "world"
c = a..b -- 此时c等于hello world
[*] (2) 取字符串长度
c = “hello world”
print (#c) -- 此时输出11 5.字符串标准库
Lua本身对字符串的处理十分有限,仅能够创建,拼接,取长度和比较字符串。
所以Lua处理字符串的完整能力来自字符串的标准库。
https://img-blog.csdnimg.cn/2cf7c4e630e946379e187821fd852165.png
诶!怎么没有得到想要的结果呢?原来是忘记了Lua中字符串是不可变的这定义。所以我们要看到改变后的后果,可以用一个新的字符串接住它。
https://img-blog.csdnimg.cn/8e3b07f019f443969381662b00638011.png
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) – 字符串小写
示例图:
https://img-blog.csdnimg.cn/f54ce310d18342b083fd85e03259dba5.png
最后要给大家介绍介绍string.format(),它适用于进行字符串格式化和将数值输出为字符串的强大工具。
有点类似C中的printf()。
https://img-blog.csdnimg.cn/cae63eefd92f4e7da801fd3dbd0020cc.png
(6) table类型
表是Lua语言中最强大也是唯一的数据结构。使用表,Lua语言可以以一种简单,统一且高效的方式表示数组,集合,记录和其他很多的数据结构。
Lua语言中的表本质是一种辅助数组,这种数组不仅可以通过数字下标作为索引,也可以通过使用字符串或其他任意类型的值来映射相对应的值(键值对)。
在我看来,当lua是使用连续的数字下标作为索引的时候,它就是c++中的数组,当是使用键值对方式映射,用字符串作为索引的时候,因为其无序且键值唯一,它就很像c++中的unorder_map。
我们使用构造器表达式创建表,其最简单的形式是{}
构造:
a = {} -- 创建了一个空表
a[“x”] = 10 -- 这句话的键是“x”,值是10,此时我们可以通过a.x和a["x"]访问到10
a = "Hello Table" --这句话的意思是,索引是10,值是字符串“Hello Table” a = {} -- 创建了一个空表
k = “x”
a = 10 -- 这句话的意思是a["x"]=10,键是“x”,值是10,此时我们可以通过a.x和a["x"]访问到10
a = "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 then
return lookup_table
end
local new_table = {}
lookup_table = new_table
for key, value in pairs(object) do
new_table = _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。
当把表当做结构体使用时,可以把索引当做成员名称使用。
https://img-blog.csdnimg.cn/2aa5bff4ef3c4a39b2efefba60c3c5ce.png
对于Lua语言而言,这两种形式是等价的。但是对于阅读程序的人而言,这两种形式分别代表了两种意图:当你用a.name来赋值时,清晰地说明了把表当做结构体使用,此时的标识由预先定义的键组成的集合。而使用a【“name”】来赋值,则说明了表可以使用任意字符串当做键。
4.表构造器
除了使用空构造器{}构造表之外我们还可以这样做:
注意:Lua中默认值是从1开始。
days = {“Monday”,“Tuesday”,“Wednesday”,“Thursday”,“Friday”,“Saturday”,“Sunday”}
--[[ 此时days到days被默认定义为“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={x=0,y=0}
{x=-10,y=1}, polyLine={x=-10,y=1}
{x=0,y=1} polyLine={x=0,y=1] ]]
} 5.数组,列表和序列
如果想表示常见的数组或者列表,那么只需要使用整形作为索引的表即可。当该表不存在空洞,既表中的所有数据都不为nil时,则成这个表为序列(sequence)。
Lua语言提供了获取序列长度的操作符#。正如我们之前所看到,对于字符串而言,该操作符会统计字符串的字节数。对于表而言,则会返回序列的大小。
https://img-blog.csdnimg.cn/0e711d6fcf9141c494f005b197d10262.png
因而,当我们想在序列后增加元素时则可以使用语句 a[#a+1]=new
https://img-blog.csdnimg.cn/bf7b660bfb0c4fc0aa6bc0cf42ad1d16.png
6.遍历表
我们可以使用pairs迭代器遍历表中的键值对。遍历过程中元素出现的顺序可能是随机的,相同的程序在每次运行时也可能产生不同的顺序。唯一可以确定的是,在遍历的过程中每个元素会且只会出现一次。
https://img-blog.csdnimg.cn/123bde92ed574b47903a0e6085ba1c54.png
对于序列而言我们可以使用ipairs迭代器:此时Lua确保是按顺序进行的。
https://img-blog.csdnimg.cn/d4d377f7c06e42d9bae19a39ab271a6c.png
7.表标准库
表标准库提供了操作列表和序列的一些常用函数。
今天简单介绍增加(table.insert),删除(table.remove),移动(table.move)以及排序(table.sort)。
[*] table.insert ()
[*]insert()有两种格式,一种是两个参数,insert(tableName,元素),这种情况下就会默认插到末尾。
[*]另一种是三个参数(tableName,位置,元素),则可以按照自己的想法插入元素。
https://img-blog.csdnimg.cn/e112764c5e9b40aaba17de7d8a518cb7.png
[*] table.remove ()
[*]删除指定位置的元素,并把后面的元素往前移动填充删除所造成的空缺。。
https://img-blog.csdnimg.cn/b7f5e1e40e11462a9f63138fadc6d14e.png
[*] table.move(tableA,起始索引,终止索引,tableB)
[*]它的作用时把表A中从起始索引到终止索引的值移动到表B中。
[*] table.sort()
[*]这个就是单纯的排序方法。
https://img-blog.csdnimg.cn/b5f355e22f454943b9118db854c10c0a.png
(7) userdata类型
userdata是用户自定义的数据类型,lua只提供了一块原始的内存区域,用于存储任何东西, 在Lua中userdata没有任何预定义操作。
因为lua只是一个两三万行代码的一个脚本语言,有很多功能都是依靠c给它提供,所以userdata在实际中它代指了那些使用c/c++语言给lua提供的函数模块。
https://img-blog.csdnimg.cn/dd320df1ff814dd7b0de1f0dd8e0b05e.png
1.实例lua调用capi
今天是要和大家分享关于luaDebug库的一些内容,但是我在研究luaDebug库的时候,发现它调用了许多的luaAPI,对于没有研究过lua与c/c++交互的我可以说是看到满头大汉,一脸懵逼。所以我就决定从最原始入手,研究lua和c/c++是如何相互调用。今天分享的流程主要是通过举两个c++和lua相互调用的栗子,然后研究底层的实现,紧接着对我们的lua_debug库进行介绍,最后再尝试打印一些堆栈信息。
https://img-blog.csdnimg.cn/cd2bb6e005ec4f66b3bdbc2a7ff54b76.png
大家都知道,lua和c/c++之间是通过一个lua_Stack进行交互的,关于lua_Stack,网上对它的叫法有很多,有的说它是一个lua的堆栈,有的说它是lua状态机,也有的将它叫做lua的线程(注意这里的thread是lua的一种数据类型,与操作系统的线程需要区分开),我们可以简单的把lua_Stack当作一个翻译官,负责在c/c++与lua之间翻译,把正确的信息保存并传达给对方。
1.看两个小栗子
要让lua文件与c/c++文件进行交互有两种方式:
[*]其一是把我们的CAPI给打包成一个动态链接库dll,然后在运行的时候再加载这些函数。
[*]其二是把CAPI给编译到exe文件中。为了方便,以下是测试例子使用的是编译成一个exe文件的方式,准备步骤分三步:
[*]新建一个c++控制台项目。
[*]下载lua源码,把src目录下的所有文件拷贝到新建的c++目录下。
[*]include需要用到的lua库函数,生成解决方案即可。
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
} 需要注意的是,因为我们创建的是c++的程序(cocos,u3d,ue4的底层都是c++代码),但是lua的库函数中使用的是纯c的接口,所以我们要extern "C"让编译器帮我们修改一下函数的编译和连接规约。
extern关键字:
my.cpp文件
//
#include "my.h"
CMyWinApp theApp; // 声明和定义了一个全局变量
//------------------------------------------------------------------
// main
//------------------------------------------------------------------
int main()
{
CWinApp* pApp = AfxGetApp();
return 0;
}
//------------------------------------------------------------------ MFC.cpp
#include "my.h"// it should be mfc.h, but for CMyWinApp definition, so...
extern CMyWinApp theApp; // 提示编译器此变量定义在其他文件中,遇到这个变量时到其他模块中去寻找
CWinApp* AfxGetApp()
{
return theApp.m_pCurrentWinApp;
} extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。
extern C用法:
典型的,一个C++程序包含其它语言编写的部分代码。类似的,C++编写的代码片段可能被使用在其它语言编写的代码中。不同语言编写的代码互相调用是困难的,甚至是同一种编写的代码但不同的编译器编译的代码。例如,不同语言和同种语言的不同实现可能会在注册变量保持参数和参数在栈上的布局,这个方面不一样。
为了使它们遵守统一规则,可以使用extern指定一个编译和连接规约。例如,声明C和C++标准库函数strcyp(),并指定它应该根据C的编译和连接规约来链接:
extern "C" char* strcpy(char*,const char*); extern "C"指令中的C,表示的一种编译和连接规约,而不是一种语言。C表示符合C语言的编译和连接规约的任何语言,如Fortran、assembler等。
还有要说明的是,extern "C"指令仅指定编译和连接规约,但不影响语义。例如在函数声明中,指定了extern “C”,仍然要遵守C++的类型检测、参数转换规则。
如果你有很多语言要加上extern “C”,你可以将它们放到extern “C”{ }中。
extern "C"{
typedef int (*CFT) (const void*,const void*);//style of C
void qsort(void* p,size_t n,size_t sz,CFT cmp);//style of C
} extern "C"的真实目的是实现类C和C++的混合编程。在C++源文件中的语句前面加上extern “C”,表明它按照类C的编译和连接规约来编译和连接,而不是C++的编译的连接规约。这样在类C的代码中就可以调用C++的函数or变量等。(注:我在这里所说的类C,代表的是跟C语言的编译和连接方式一致的所有语言)
1.创建lua_Stack
前文提及lua_Stack是c/c++与lua的翻译官,所以在它们交互之前我们首先需要生成一个lua_Stack:
lua_State *L = luaL_newstate(); 然后我们需要打开lua给我们提供的标准库:
luaL_openlibs(L); https://img-blog.csdnimg.cn/f789c17804f14be1a03fd49b43469e16.png
其实lua早已经在我们不经意间调用了c的api。
2.第一个栗子:c++调用lua的函数
我们首先需要新建一个lua文件,名称随意我这里使用的是luafile.lua。然后我们在lua文件中定义一个function,举一个最简单的减法吧。
https://img-blog.csdnimg.cn/ff70b8ddbf0f4982b0d063d9b54b026c.png
然后就是使用luaL_dofile方法让我们的lua_Stack编译并执行这个文件,我们在打lua引用其他文件的时候知道loadfile是只编译,dofile是编译且每次执行,require是在package.loaded中查找此模块是否存在,不存在才执行,否则返回该模块。luaL_dofile和luaL_loadfile和上述原理相似,luaL_loadfile是仅编译,luaL_dofile是编译且执行。
然后通过lua_getglobal方法可以通过lua的全局表拿到lua的全局函数,并将它压入栈底(我们可以把lua_Stack的存储结构理解为下图的样子,实际上肯定没有那么简单,我们往下看)。
lua数据栈的抽象图:
我们可以通过两种索引来获取lua_Stack的调用栈所指向的数据:
static TValue *index2addr (lua_State *L, int idx) {
CallInfo *ci = L->ci;
if (idx > 0) {
TValue *o = ci->func + idx;
api_check(L, idx <= ci->top - (ci->func + 1), "unacceptable index");
if (o >= L->top) return NONVALIDVALUE;
else return o;
}
else if (!ispseudo(idx)) {/* negative index */
api_check(L, idx != 0 && -idx <= L->top - (ci->func + 1), "invalid index");
return L->top + idx;
}
else if (idx == LUA_REGISTRYINDEX)
return &G(L)->l_registry;
else {/* upvalues */
idx = LUA_REGISTRYINDEX - idx;
api_check(L, idx <= MAXUPVAL + 1, "upvalue index too large");
if (ttislcf(ci->func))/* light C function? */
return NONVALIDVALUE;/* it has no upvalues */
else {
CClosure *func = clCvalue(ci->func);
return (idx <= func->nupvalues) ? &func->upvalue : NONVALIDVALUE;
}
}
} https://img-blog.csdnimg.cn/ad40b4a9d5cf4bdfb32118bd5c8a889c.png
然后把两个参数按顺序压入栈中(不同类型压栈的函数接口大家可以查阅文档),然后调用pcall函数执行即可:
/* c++调用lua函数 */
luaL_dofile(L, "luafile.lua");
lua_getglobal(L, "l_sub");
lua_pushnumber(L, 1);
lua_pushnumber(L, 2);
lua_pcall(L, 2, 1, 0);
cout << lua_tostring(L, 1) << endl;
[*] lua_newstate主要做了3件事情:
[*]新建一个global_state和一个lua_State。
[*]初始化默认值,创建全局表等。
[*]调用f_luaopen函数,初始化栈、字符串结构、元方法、保留字、注册表等重要部件。
[*] 全局状态机global_state:
[*]global_State 里面有对主线程的引用,有注册表管理所有全局数据,有全局字符串表,有内存管理函数,有GC 需要的把所有对象串联起来的相关信息,以及一切 lua 在工作时需要的工作内存。我们以为的是c/c++ 和 lua之间只通过一个翻译官lua_Stack,但其实还有一个负责数据存放,回收的翻译公司global_State,客户只需要直接和翻译官打交道,但是一些翻译档案还是要翻译公司存放管理。
[*] lua线程lua_State:
[*] lua_State是暴露给用户的数据类型,是一个lua程序的执行状态,也是lua的一个线程thread。大致分为4个主要模块,分别是独立的数据栈StkId,数据调用栈CallInfo ,独立的调试钩子以及错误处理机制。而在调用栈中我们就可以通过func域获得所在函数的源文件名,行号等诸多调试信息。
[*] f_luaopen函数:
[*]f_luaopen函数,非常重要,主要作用:初始化栈、初始化字符串结构、初始化原方法、初始化保留字实现、初始化注册表等。
static int stackDump(lua_State *L)
{
int i = 0;
int top = lua_gettop(L); // 获取栈中元素个数。
cout << "当前栈的数量:" << top << endl;
for (i = 1; i <= top; ++i) // 遍历栈中每个元素。
{
int t = lua_type(L, i); // 获取元素的类型。
switch (t)
{
case LUA_TSTRING: // strings
cout << "参数" << i << " :" << lua_tostring(L, i);
break;
case LUA_TBOOLEAN: // bool
cout << "参数" << i << " :" << lua_toboolean(L, i) ? "true" : "false";
break;
case LUA_TNUMBER: // number
cout << "参数" << i << " :" << lua_tonumber(L, i);
break;
default: // other values
cout << "参数" << i << " :" << lua_typename(L, t);
break;
}
cout << " ";
}
cout << endl;
return 1;
} 可以先看看注册表是怎么样初始化的:会把当前的线程设置为注册表的第一个元素,全局表设置位第二个元素。
void lua_register (lua_State *L, const char *name, lua_CFunction f); 在得到一个初始化后的lua_Stack之后,要想lua能拿到CAPI,我们会对c/c++的函数进行注册。
static int c_add(lua_State *L)
{
stackDump(L);
double arg1 = luaL_checknumber(L, 1);
double arg2 = luaL_checknumber(L, 2);
lua_pushnumber(L, arg1 + arg2);
return 1;
}
...
int main() {
...
LUA_API void lua_setglobal (lua_State *L, const char *name) {
Table *reg = hvalue(&G(L)->l_registry);
lua_lock(L);/* unlock done in 'auxsetstr' */
// LUA_RIDX_GLOBALS是全局环境在注册表中的索引
auxsetstr(L, luaH_getint(reg, LUA_RIDX_GLOBALS), name);
}
} 那么我们继续往下看看究竟这个函数做了什么。
LUALIB_API lua_State *luaL_newstate (void) {
lua_State *L = lua_newstate(l_alloc, NULL);
if (L) lua_atpanic(L, &panic);
return L;
} 分成两部分:首先把c/c++的函数弄成一个闭包push到lua_Stack数据栈中,判断是否溢出并对栈顶元素自增,然后就是把这个函数给注册在注册表中。
LUA_API lua_State *lua_newstate (lua_Alloc f, void *ud) {
int i;
lua_State *L;
global_State *g;
/* 分配一块lua_State结构的内容块 */
LG *l = cast(LG *, (*f)(ud, NULL, LUA_TTHREAD, sizeof(LG)));
if (l == NULL) return NULL;
L = &l->l.l;
g = &l->g;
L->next = NULL;
L->tt = LUA_TTHREAD;
g->currentwhite = bitmask(WHITE0BIT);
L->marked = luaC_white(g);
/* 初始化一个线程的栈结构数据 */
preinit_thread(L, g);
g->frealloc = f;
g->ud = ud;
g->mainthread = L;
g->seed = makeseed(L);
g->gcrunning = 0;/* no GC while building state */
g->GCestimate = 0;
g->strt.size = g->strt.nuse = 0;
g->strt.hash = NULL;
setnilvalue(&g->l_registry);
g->panic = NULL;
g->version = NULL;
g->gcstate = GCSpause;
g->gckind = KGC_NORMAL;
g->allgc = g->finobj = g->tobefnz = g->fixedgc = NULL;
g->sweepgc = NULL;
g->gray = g->grayagain = NULL;
g->weak = g->ephemeron = g->allweak = NULL;
g->twups = NULL;
g->totalbytes = sizeof(LG);
g->GCdebt = 0;
g->gcfinnum = 0;
g->gcpause = LUAI_GCPAUSE;
g->gcstepmul = LUAI_GCMUL;
for (i=0; i < LUA_NUMTAGS; i++) g->mt = NULL;
if (luaD_rawrunprotected(L, f_luaopen, NULL) != LUA_OK) { //f_luaopen函数中调用了 stack_init 函数
/* memory allocation error: free partial state */
close_state(L);
L = NULL;
}
return L;
} 我们知道lua把所有的全局表里存放在一个_G的表中,而LUA_RIDX_GLOBALS就是全局环境在注册表中的索引。至此我们就把我们的c/c++的API注册在lua的全局表中,所以lua文件中就能访问到该函数了。
(8) thread类型
再三强调,lua的线程并不是操作系统中的线程!!!它是lua和c/c++进行交互的一个数据结构lua_stack,lua通过这个数据结构和c进行交互,来调用上文中的那些库函数。
https://img-blog.csdnimg.cn/7dd2d5ddb120479195bf381a34667465.png
1.C/C++与lua的交互方式
https://img-blog.csdnimg.cn/cab4919be8644ae2b6da02d7f41c2be6.png
讲的是如何在lua中如何实现与C/C++的交互:
谜底直接告诉大家,它们是通过一个虚拟,强大的栈来进行交互。
这个栈是由lua实现的。C方面只是通过取栈得到想要的数据,然后再通过往这个栈中压入元素,从而实现向Lua那边更新数据。这个强大的栈长这样:
https://img-blog.csdnimg.cn/a482242246ba48e0943c2d43cc201492.png
TValue结构对应于lua中的所有数据类型, 是一个{值, 类型} 结构, 这就lua中动态类型的实现, 它把值和类型绑在一起, 用tt记录value的类型, value是一个联合结构, 由Value定义, 可以看到这个联合有四个域, 先说明简单的:
[*]p – 可以存一个指针, 实际上是lua中的light userdata结构。
[*]n – 所有的数值存在这里, 不过是int , 还是float。
[*]b – Boolean值存在这里, 注意, lua_pushinteger不是存在这里, 而是存在n中, b只存布尔。
[*]gc – 其他诸如table, thread, closure, string需要内存管理垃圾回收的类型都存在这里。
[*]gc是一个指针, 它可以指向的类型由联合体GCObject定义, 从图中可以看出, 有string, userdata, closure, table, proto, upvalue, thread。
[*] 从上面的图可以的得出如下结论:
[*]1. lua中, number, boolean, nil, light userdata四种类型的值是直接存在栈上元素里的, 和垃圾回收无关。
[*]2. lua中, string, table, closure, userdata, thread存在栈上元素里的只是指针, 他们都会在生命周期结束后被垃圾回收。
好,当我们有了这个强大的栈之后,究竟要如何使用他呢?假设我们有一个lua的文件如下:
static void f_luaopen (lua_State *L, void *ud) {
global_State *g = G(L);
UNUSED(ud);
stack_init(L, L);/* init stack */
init_registry(L, g); //初始化注册表
luaS_init(L); //字符串结构初始化
luaT_init(L); //元方法初始化
luaX_init(L); //保留字实现
g->gcrunning = 1;/* allow gc */
g->version = lua_version(NULL);
luai_userstateopen(L); 现在向大家展示怎么去调用它:
static void init_registry (lua_State *L, global_State *g) {
TValue temp;
/*创建注册表,初始化注册表数组部分大小为LUA_RIDX_LAST*/
Table *registry = luaH_new(L);
sethvalue(L, &g->l_registry, registry);
luaH_resize(L, registry, LUA_RIDX_LAST, 0);
/*把这个注册表的数组部分的第一个元素赋值为主线程的状态机L*/
setthvalue(L, &temp, L);/* temp = L */
luaH_setint(L, registry, LUA_RIDX_MAINTHREAD, &temp);
/*把注册表的数组部分的第二个元素赋值为全局表,即registry = table of globals */
sethvalue(L, &temp, luaH_new(L));/* temp = new table (global table) */
luaH_setint(L, registry, LUA_RIDX_GLOBALS, &temp);
}
[*] lua_newstate主要做了3件事情:
[*]新建一个global_state和一个lua_State。
[*]初始化默认值,创建全局表等。
[*]调用f_luaopen函数,初始化栈、字符串结构、元方法、保留字、注册表等重要部件。
[*] 全局状态机global_state:
[*]global_State 里面有对主线程的引用,有注册表管理所有全局数据,有全局字符串表,有内存管理函数,有GC 需要的把所有对象串联起来的相关信息,以及一切 lua 在工作时需要的工作内存。我们以为的是c/c++ 和 lua之间只通过一个翻译官lua_Stack,但其实还有一个负责数据存放,回收的翻译公司global_State,客户只需要直接和翻译官打交道,但是一些翻译档案还是要翻译公司存放管理。
[*] lua线程lua_State:
[*] lua_State是暴露给用户的数据类型,是一个lua程序的执行状态,也是lua的一个线程thread。大致分为4个主要模块,分别是独立的数据栈StkId,数据调用栈CallInfo ,独立的调试钩子以及错误处理机制。而在调用栈中我们就可以通过func域获得所在函数的源文件名,行号等诸多调试信息。
[*] f_luaopen函数:
[*]f_luaopen函数,非常重要,主要作用:初始化栈、初始化字符串结构、初始化原方法、初始化保留字实现、初始化注册表等。
static int stackDump(lua_State *L)
{
int i = 0;
int top = lua_gettop(L); // 获取栈中元素个数。
cout << "当前栈的数量:" << top << endl;
for (i = 1; i <= top; ++i) // 遍历栈中每个元素。
{
int t = lua_type(L, i); // 获取元素的类型。
switch (t)
{
case LUA_TSTRING: // strings
cout << "参数" << i << " :" << lua_tostring(L, i);
break;
case LUA_TBOOLEAN: // bool
cout << "参数" << i << " :" << lua_toboolean(L, i) ? "true" : "false";
break;
case LUA_TNUMBER: // number
cout << "参数" << i << " :" << lua_tonumber(L, i);
break;
default: // other values
cout << "参数" << i << " :" << lua_typename(L, t);
break;
}
cout << " ";
}
cout << endl;
return 1;
} 可以先看看注册表是怎么样初始化的:会把当前的线程设置为注册表的第一个元素,全局表设置位第二个元素。
void lua_register (lua_State *L, const char *name, lua_CFunction f); 在得到一个初始化后的lua_Stack之后,要想lua能拿到CAPI,我们会对c/c++的函数进行注册。
static int c_add(lua_State *L)
{
stackDump(L);
double arg1 = luaL_checknumber(L, 1);
double arg2 = luaL_checknumber(L, 2);
lua_pushnumber(L, arg1 + arg2);
return 1;
}
...
int main() {
...
LUA_API void lua_setglobal (lua_State *L, const char *name) {
Table *reg = hvalue(&G(L)->l_registry);
lua_lock(L);/* unlock done in 'auxsetstr' */
// LUA_RIDX_GLOBALS是全局环境在注册表中的索引
auxsetstr(L, luaH_getint(reg, LUA_RIDX_GLOBALS), name);
}
} 那么我们继续往下看看究竟这个函数做了什么。
LUALIB_API lua_State *luaL_newstate (void) {
lua_State *L = lua_newstate(l_alloc, NULL);
if (L) lua_atpanic(L, &panic);
return L;
} 分成两部分:首先把c/c++的函数弄成一个闭包push到lua_Stack数据栈中,判断是否溢出并对栈顶元素自增,然后就是把这个函数给注册在注册表中。
LUA_API lua_State *lua_newstate (lua_Alloc f, void *ud) {
int i;
lua_State *L;
global_State *g;
/* 分配一块lua_State结构的内容块 */
LG *l = cast(LG *, (*f)(ud, NULL, LUA_TTHREAD, sizeof(LG)));
if (l == NULL) return NULL;
L = &l->l.l;
g = &l->g;
L->next = NULL;
L->tt = LUA_TTHREAD;
g->currentwhite = bitmask(WHITE0BIT);
L->marked = luaC_white(g);
/* 初始化一个线程的栈结构数据 */
preinit_thread(L, g);
g->frealloc = f;
g->ud = ud;
g->mainthread = L;
g->seed = makeseed(L);
g->gcrunning = 0;/* no GC while building state */
g->GCestimate = 0;
g->strt.size = g->strt.nuse = 0;
g->strt.hash = NULL;
setnilvalue(&g->l_registry);
g->panic = NULL;
g->version = NULL;
g->gcstate = GCSpause;
g->gckind = KGC_NORMAL;
g->allgc = g->finobj = g->tobefnz = g->fixedgc = NULL;
g->sweepgc = NULL;
g->gray = g->grayagain = NULL;
g->weak = g->ephemeron = g->allweak = NULL;
g->twups = NULL;
g->totalbytes = sizeof(LG);
g->GCdebt = 0;
g->gcfinnum = 0;
g->gcpause = LUAI_GCPAUSE;
g->gcstepmul = LUAI_GCMUL;
for (i=0; i < LUA_NUMTAGS; i++) g->mt = NULL;
if (luaD_rawrunprotected(L, f_luaopen, NULL) != LUA_OK) { //f_luaopen函数中调用了 stack_init 函数
/* memory allocation error: free partial state */
close_state(L);
L = NULL;
}
return L;
} 我们知道lua把所有的全局表里存放在一个_G的表中,而LUA_RIDX_GLOBALS就是全局环境在注册表中的索引。至此我们就把我们的c/c++的API注册在lua的全局表中,所以lua文件中就能访问到该函数了。
十七、Lua的一些实例测试
(1) 字符串把“abc”,换成“bcd”
struct lua_State {
CommonHeader;
unsigned short nci;/* 存储一共多少个CallInfo number of items in 'ci' list */
lu_byte status;
StkId top;/* 指向栈的顶部,压入数据,都通过移动栈顶指针来实现。 first free slot in the stack */
global_State *l_G;
CallInfo *ci;/* 当前运行函数信息 call info for current function */
const Instruction *oldpc;/* last pc traced */
StkId stack_last;/* 指向栈的底部,但是会预留空间作宝物处理 last free slot in the stack */
StkId stack;/* 指向栈的底部 stack base */
UpVal *openupval;/* list of open upvalues in this stack */
GCObject *gclist;
struct lua_State *twups;/* list of threads with open upvalues */
struct lua_longjmp *errorJmp;/* current error recover point */
CallInfo base_ci;/* 调用栈的头部指针 CallInfo for first level (C calling Lua) */
volatile lua_Hook hook;
ptrdiff_t errfunc;/* current error handling function (stack index) */
int stacksize; /* 栈的大小 */
int basehookcount;
int hookcount;
unsigned short nny;/* number of non-yieldable calls in stack */
unsigned short nCcalls;/* number of nested C calls */
l_signalT hookmask;
lu_byte allowhook;
}; (2) if的使用方法
#define CommonHeader GCObject *next; lu_byte tt; lu_byte marked (3) 如何对表中元素排序后输出
/* thread status */
#define LUA_OK 0
#define LUA_YIELD 1
#define LUA_ERRRUN 2
#define LUA_ERRSYNTAX 3
#define LUA_ERRMEM 4
#define LUA_ERRGCMM 5
#define LUA_ERRERR 6 (4) 写一个返回多值的函数
typedef struct global_State {
/* 版本号*/
const lua_Number *version;/* pointer to version number */
/* 内存管理 */
lua_Alloc frealloc;/* Lua的全局内存分配器,用户可以替换成自己的 - function to reallocate memory */
void *ud; /* 分配器的userdata - auxiliary data to 'frealloc' */
/* 线程管理 */
struct lua_State *mainthread; /* 主线程 */
struct lua_State *twups;/* 闭包了当前线程变量的其他线程列表 - list of threads with open upvalues */
/* 字符串管理 */
stringtable strt;/* 字符串table Lua的字符串分短字符串和长字符串 - hash table for strings */
TString *strcache;/* 字符串缓存 - cache for strings in API */
/* 虚函数表 */
TString *tmname;/* 预定义方法名字数组 -array with tag-method names */
struct Table *mt;/* 每个基本类型一个metatable(整个Lua最重要的Hook机制) - metatables for basic types */
/* 错误处理 */
lua_CFunction panic;/* to be called in unprotected errors */
TString *memerrmsg;/* memory-error message */
/* GC管理 */
unsigned int gcfinnum;/* number of finalizers to call in each GC step */
int gcpause;/* size of pause between successive GCs */
int gcstepmul;/* GC 'granularity' */
l_mem totalbytes;/* number of bytes currently allocated - GCdebt */
l_mem GCdebt;/* bytes allocated not yet compensated by the collector */
lu_mem GCmemtrav;/* memory traversed by the GC */
lu_mem GCestimate;/* an estimate of the non-garbage memory in use */
TValue l_registry;
unsigned int seed;/* randomized seed for hashes */
lu_byte currentwhite;
lu_byte gcstate;/* state of garbage collector */
lu_byte gckind;/* kind of GC running */
lu_byte gcrunning;/* true if GC is running */
GCObject *allgc;/* list of all collectable objects */
GCObject **sweepgc;/* current position of sweep in list */
GCObject *finobj;/* list of collectable objects with finalizers */
GCObject *gray;/* list of gray objects */
GCObject *grayagain;/* list of objects to be traversed atomically */
GCObject *weak;/* list of tables with weak values */
GCObject *ephemeron;/* list of ephemeron tables (weak keys) */
GCObject *allweak;/* list of all-weak tables */
GCObject *tobefnz;/* list of userdata to be GC */
GCObject *fixedgc;/* list of objects not to be collected */
} global_State; (5) 写一个可变参数得函数
StkId top;/* first free slot in the stack */
StkId stack_last;/* last free slot in the stack */
StkId stack;/* stack base */ 十八、lua的命名规范以及注释方法
1.lua命名规范
lua和C/C++一样,允许我们使用数字,下划线,字母任意组合成为变量的名称。
[*] 但是要注意以下几点:
[*] 1. 数字不能作为开头
[*] 2.下划线加英文大写单词一般是lua系统自己使用的,我们应该尽量避免使用
[*] 3.lua的一些保留字,如if,for,return,break等不能使用
[*] 4.lua命名是大小写敏感的,所以And和AND是不一样的变量
2.lua注释方式
lua使用 – 来进行单行的注释
使用–[[
长注释
]] 来进行长注释
十九、lua条件与循环
1.条件选择语句if
lua里面是这样实现条件选择语句的:
if + 条件 + then + 执行语句 + end
typedef union Value {
GCObject *gc; /* collectable objects */
void *p; /* light userdata */
int b; /* booleans */
lua_CFunction f; /* light C functions */
lua_Integer i; /* integer numbers */
lua_Number n; /* float numbers */
} Value;
#define TValuefields Value value_; int tt_
typedef struct lua_TValue {
TValuefields;
} TValue; 上述例子我们可以使用elseif来实现:
CallInfo base_ci;/* CallInfo for first level (C calling Lua) */
CallInfo *ci;/* call info for current function */ 此时我们比之前的例子就少写了一个end,如果要多次elseif就省略了多个end,比较方便。
2. while循环语句
语法:while + 循环条件 + do + 循环体 + end
typedef struct CallInfo {
StkId func;/* ci->func:指向正在调用操作的栈底位置。 function index in the stack */
StkId top;/* 指向调用栈的栈顶部分 top for this function */
struct CallInfo *previous, *next; /* previous和next是双向链表指针,用于连接各个调用栈。当执行完一个函数,通过previous回滚到上一个调用栈
CI dynamic call link */
union {
struct {/* only for Lua functions */
StkId base;/* base for this function */
const Instruction *savedpc;
} l;
struct {/* only for C functions */
lua_KFunction k;/* continuation in case of yields */
ptrdiff_t old_errfunc;
lua_KContext ctx;/* context info. in case of yields */
} c;
} u;
ptrdiff_t extra;
short nresults;/* expected number of results from this function */
unsigned short callstatus;
} CallInfo; 3. repeat…until循环语句
语法:repeat + 循环体 + until + 终止条件
int basehookcount;
int hookcount;
volatile lua_Hook hook;
l_signalT hookmask;
lu_byte allowhook; 4. for循环语句
语法:for var = exp1,exp2,exp3 do 循环体 end
exp1是初始值,exp2是终止值,exp3是步长,步长也就是我们每一次循环后var要增加或减少多少,若不写则默认为1。
问题来了:加入我初始值小于终止值,且步长为负程序会如何?
答: 此时它们会执行一次循环体结束循环。
回顾一下我们之前的pairs和ipairs:
struct lua_Debug {
int event;
const char *name; /* (n) */
const char *namewhat; /* (n) 'global', 'local', 'field', 'method' */
const char *what; /* (S) 'Lua', 'C', 'main', 'tail' */
const char *source; /* (S) */
int currentline; /* (l) */
int linedefined; /* (S) */
int lastlinedefined; /* (S) */
unsigned char nups; /* (u) number of upvalues */
unsigned char nparams;/* (u) number of parameters */
char isvararg; /* (u) */
char istailcall; /* (t) */
char short_src; /* (S) */
/* private part */
struct CallInfo *i_ci;/* active function */
}; 5.提前结束循环
1.break: 直接结束这一重循环。
2.return: 返回值后直接结束所有循环。
3.continue: 结束当重当次的循环。
4.goto: 转移去标签的位置,标签的语法::标签名 : : – 记得慎用。
二十、lua代码优化,别再依赖if…else了
今天讲解一下我在工作中遇到的代码优化问题。需求是这样的:我需要在项目的通用资源弹窗中加入一个新的资源道具,但是以前的通用弹窗道具可以换算成元宝,有自己的容量。但是新的道具是“英雄经验”是没有上述的属性的,所以有部分的逻辑是不适用于新增的道具,因此我一开始的处理方式是直接在各个判断的逻辑中加入if,else。只要展示的道具是英雄经验的话,则不执行那部分不适用的逻辑判断。写的代码又臭又长,但是没办法,只能硬着头皮递交上去。下面是我提交的部分代码:
https://img-blog.csdnimg.cn/ac9afae317aa4eada94fcdd76788bfa7.png
反正就是见招拆招,用ifelse来跳过所有的不合适的逻辑判断,整个项目下来,功能没改多少,ifelse多了十几个,然后原本大佬前辈们写出的优雅简洁的代码就被我破坏了。果不其然上交之后,主管找我谈话了,他问我,如果后面再多加几个资源,你是不是一直在后面加ifelse?这样的代码可读性和修改起来是不是很麻烦?
如果对于一些通用的资源,适合所有逻辑的,则按照以前的程序走,对于新增不合适的,我们则执行简略的逻辑,跳过某些判断。
听到这里,难道是要对资源分成两类,然后进行两套的逻辑判断吗?我于是问主管是不是传参,区分两类的资源,然后执行不同的逻辑。主管高深莫测的说,非也,你还是太小看lua了。然后啪啦啪啦的写下了下面这段代码:
https://img-blog.csdnimg.cn/c1ae0571c8214045bade8b9d386e1e22.png
元表,居然是元表!!!主管把逻辑抽了出来,然后把抽出表中的__index字段定义成了通用的逻辑判断函数,然后特殊的就直接写进去,我们知道在lua的面向对象的流程执行的过程中,如果查找一个表的元素,找得到就返回该值,找不到则通过__index字段往他父类上找,以前我一直以为__index只能是一个表,结果原来是一个函数的话,程序会执行__index字段的函数,获取其返回值。所以说我们只需要把通用的逻辑执行函数写在__index字段中,而特殊的写出来,则巧妙的完成了需求,且可拓展性增强了许多。
说实话,这次的经历让我触动挺大的,从实习以来,自己就仿佛成为了一个ifelse,for循环的无脑机器。确实有很多优雅的写法需要我们去学习,不要无脑的为了完成需求而去完成需求。
二十一、lua数值型for和泛型for
可能有老哥这个时候就要吐槽了,纳尼!我写了那么久lua,你今天叫我怎么使用for?这不是愚蠢的妈妈给愚蠢开门–愚蠢到家了吗?? 诶确实我知道各位大牛都已经熟练使用for语句了, 但是知其然,然后知其所以然。今天就给大家分析分析for语句的底层逻辑。
function traceback ()
local level = 1
while true do
local info = debug.getinfo(level, "Sl")
if not info then break end
if info.what == "C" then -- is a C function?
print(level, "C function")
else -- a Lua function
print(string.format("[%s]:%d",info.short_src, info.currentline))
end
level = level + 1
end
end 1.数值型for
数值型的for相对简单,如上述栗子,首先它会在循环体中生成这个i的局部变量,然后根据 起始数值,终止数值,步长来进行一个循环(1,10,2)。当我们想要执行最多次的循环(不是死循环)时,可以用 math.huge()来获取最大的循环次数。
值得一提的是:控制变量i实际上是一个局部变量,不可以在循环体外访问。以及步长可以不填,其默认值为1。
debug.sethook(print, "l") 2.泛型for
泛型for在循环过程内保存了迭代器函数。它实际上保存着3个值:一个迭代器函数、一个恒定状态和一个控制变量。
https://img-blog.csdnimg.cn/177509b7a89a41728e1c41156efa159e.png
line 136
line 113
line 76
line 77
line 113
line 118
[*] 迭代器函数:就是在in后面的表达式,如上面的代码中,就是遍历ipairs的方法(iter后文有写)。
[*] 恒定状态:恒定状态就是不会在循环体中被影响的数据结构,实际上就是遍历的这个表。
[*] 控制变量:上述代码中的控制变量又key,value。它存放着迭代器函数返回的多返回值,若多于两个则舍弃多余的,少于两个则用nil补齐。当控制变量中的第一个,也就是key为nil时,那么就会退出循环体。
所以for的模板就等价于下面的代码:
/usr/local/share/xmake/core/base/path.lua:46
/usr/local/share/xmake/core/base/path.lua:47
/usr/local/share/xmake/core/base/path.lua:56
/usr/local/share/xmake/core/base/string.lua:32
/usr/local/share/xmake/core/base/string.lua:33
/usr/local/share/xmake/core/base/string.lua:34
/usr/local/share/xmake/core/base/string.lua:35
/usr/local/share/xmake/core/base/string.lua:36
/usr/local/share/xmake/core/base/string.lua:38
/usr/local/share/xmake/core/base/string.lua:33 所以使用ipairs进入循环的例子就等价于下面的代码:
debug.sethook() pairs相类似,但是其迭代器函数直接使用了lua的基本函数next,所以其迭代其函数时next。
for i = 1, #a do
print(a)
end 二十二、lua模式匹配
lua中模式匹配的问题,由于lua中没有正则表达式,那么它是如何实现模式匹配的呢。
模式匹配相关的函数:
local parent = {}
parent["a"] = 111
parent.__index = parent // 把parent表的__index字段仍然设置为parent
local child = {}
setmetatable(child, parent) // 把parent表设置为child表的原表
print(child.a) 1.string.find()
string.find(原字符串,目标字符串)里面填入两个参数, 作用是从目源字符串中找到目标字符串的起始和终止索引,我们可以看看下面这个例子:
https://img-blog.csdnimg.cn/c2b83c55559f41b682671653d20c89c9.png
string.find()这个函数,能够找到目标字符串的起始位置和初始位置,但是只能返回第一个出现的目标字符串,若找不到目标,字符串则返回nil值。
2.string.match()
string.match()也同样在一个字符串中搜索模式,但是与string.find()不同的是,match返回的是字串本身。
Account = { balance = 0 }
--余额为0的账户
function Account.withdraw( v )
balance = balance - v
end
-- 取款
function Account.deposit( v )
balance = balance + v
end
-- 存款 是不是有人已经跟我一样准备吐槽了:这特码有啥用???
别急,match的强大之处是在于可以通过字符分类找到想找的答案,比如这样:
https://img-blog.csdnimg.cn/51d4024f003744a38519bbae7fc00419.png
至于%d是个什么东西,我们往下继续说。
3.string.gmatch()
string.gmatch()语法和string.match()一样,但是返回的是一个函数。
我们在使用string.match()和string.find()的时候,找到的都是第一个出现的字符串。
但是加入我想把所有目的字符串都找出来呢?string.gmatch()就可以帮我们解决这个问题了。
https://img-blog.csdnimg.cn/8c87178559ad4462acc9f9dd4c72650a.png
其中%a是找到英文字符,%a+是找到所有连着的字符,%d我们上文的意义是找数字,%d+是找连着的数字。那么究竟还有哪一些规格呢?本书给了如下的表格:
Account = { balance = 0 }
--余额为0的账户
function Account.withdraw( self, v )
self.balance = self.balance - v
end
-- 取款
function Account.deposit(self, v )
self.balance = self.balance + v
end
-- 存款 4.string.gsub()
string.gsub()这个函数我们之前接触过,它的作用是从原字符串中寻找目标字符串,然后将之改成想要的字符串。
语法:string.gsub(原字符串,目的字符串,更改后的字符串,更改的次数)。
其中参数四的作用是目的字符串要被更改的次数,比较少用,不使用时默认全部都要更改。
https://img-blog.csdnimg.cn/df9f972ca1bd464db2da1dff698c51dd.png
二十三、lua模式匹配练习
【【key1,value1】,【key2,value2】】这种存储结构的键值对取出来。
当时我冒出的想法是,先把它拆开小部分:先解决从【key1,value1】中提取键值对 。因为对库函数的不熟悉,所以我的代码是这样写的:
B.__index = B // 把B表的__index字段仍然设置为B
setmetatable(A,B) // 把B表设置为A表的原表 很复杂对吧:我想的是先消除了两个括号,然后再把键值对从拆开括号中提出 ,于是就出了版本二:
Account = { balance = 0 }
--余额为0的账户
function Account:withdraw( v )
if self.balance > v then
self.balance = self.balance - v
else print("balance is not enough!")
end
end
-- 取款
functionAccount:new(o)
o = o or {}
setmetatable(o,self)
self.__index = self
return o
end 这次我直接把键和值分别存到不同的表中,然后用的时候 再分别取值 。但是也不足够 , 还可以简化,后来我一想为何要分两次循环呢?于是版本三来了:
local parent = {}
parent["a"] = 111
parent.__index = parent // 把parent表的__index字段仍然设置为parent
local child = {}
setmetatable(child, parent) // 把parent表设置为child表的原表
print(child.a) 二十四、lua之数据结构(多维数组,链表,队列)
1.数组
1.一维数组
常见的一维数组起始就是我们的lua中的table序列,我们可以直接通过表构造器在一句表达式中创建并初始化数组:
print(myTable(1, 2)) 那么如果我们需要固定数组的大小,我们可以这样做:
https://img-blog.csdnimg.cn/882e1ca3e7f242f78ece62eaeaab9fb7.png
获取数组的长度可以使用#array,插入删除可以使用table自带的insert和delete函数,所以一维数组十分简单。
2.二维数组
实现二维数组也不难,比如说我们要实现一个NM二维矩阵,我们只需要把一个表中从1到N的索引都指向含有M个元素的表,那么就能实现NM的二维数组了。
https://img-blog.csdnimg.cn/576d315176b94d7daa4416392a4fed84.png
2.链表
实现链表起始也很简单,我们只需要定义这样的一个表:
print(myTable(1, 2)) 那么我们就可以通过以下的方法来便利整个表,从而实现插入,删除,搜索等各种功能。
https://img-blog.csdnimg.cn/987cac1dcc304006ad3eee91fdb21cd1.png
3.栈和队列
栈和队列的插入都是可以通过#表取得整个结构的大小然后对【#表+1】来进行赋值。
他们的差别是如果要取出元素的额话,栈是从最末尾段取出,也就是直接把【#表】置为nil就可以了。
队列的话是需要把首个元素个取出并删除,此时我们可以调用table里面的remove函数,table.remove(表,1)来进行操作。
二十五、rawset & rawget方法
1.什么是rawset & rawget
早在之前我们就讲述过,如果对一个表进行查找的时候,若表中不存在该值,则会查找该表的原表访问其原表__index字段来解决。而若对表输入一个不存在的值,则会查找该表的原表访问其原表__newindex字段来解决。
而rawset & rawget则是绕过原表这一过程,直接把操作这个表相应的结论直接输出。
https://img-blog.csdnimg.cn/6cee5cd01bc24287b07d867edb089951.png
举个例子:
https://img-blog.csdnimg.cn/21260510529f4c44ba621d06be355726.png
二十六、lua环境ENV
1.全局环境_G
lua使用一个表来保存全局变量,一方面简化了Lua语言内部的实现,另一方面可以像操作一个普通表一样操作这个表。 lua把全局环境本身保存到全局变量_G中(因此_G._G 和 _G是等价的),我们可以通过_G来访问/设置全局变量。
https://img-blog.csdnimg.cn/d744352369ce4e41886d293340e6244a.png
Lua中的全局变量不需要声明就可以使用,虽然这种行为对于小程序来说可能会比较方便。但是对于大型项目来说可能会引起一些BUG,因为_G就是一个表结构,因此我们可以利用元表的机制来避免这种情况。
对不存在的key赋值:
https://img-blog.csdnimg.cn/8ac672b5246e434e8797ef5d335bbf3c.png
访问不存在的key:
https://img-blog.csdnimg.cn/f26f1f8bd4e04fa69b8e05490f43f51c.png
那么如果我们需要声明一个新的全局变量的时候,使用rawset函数就可以了。rawset可以绕过元方法直接对表复制。
https://img-blog.csdnimg.cn/5637a0f88b0b4647850e33204d130325.png
Lua不允许值为nil的全局变量,因为值为nil的全局变量都会被自动地认为自己是未声明的。但是,要允许值为nil的全局变量也不难,只需要引入一个辅助表来保存已声明的名称即可。
https://img-blog.csdnimg.cn/8b0dd41c0fc54e8d8e1efe26e18a72cc.png
2.非全局环境_ENV
lua 5.2 正式发布了,对于 lua 语言本身的修改,重中之重就是对 environment 这个概念的修改,本质上,lua 取消了原有意义上的 environment,而是通过使用非全局变量_ENV(上值upvalue)来保存这个全局环境 。全局变量实际上只是一个语法糖,编译时再前面加上了 _ENV. 的前缀。这样,从 load 开始,第一个 chunk 就被加上了 _ENV 这个 upvalue ,然后依次传递下去。
https://img-blog.csdnimg.cn/470b778d0fe24bc0b8be913140439b35.png#pic_center
[*] 简单来说其实就是经历了这三个步骤:
[*]编译器在编译所有代码段之前,在外层创建局部变量_ENV。
[*]编译器将所有的自由名称var变换为_ENV.var。
[*]函数load使用全局环境(保存在_G)初始化代码段的第一个上值,即Lua语言内部维护的一个普通的表。
当我们声明一个"全局"变量时,其实是把这个变量声明到用全局环境初始化的上值_ENV中而已。当我们把_ENV置空之后就会丢失掉全局函数的环境。
https://img-blog.csdnimg.cn/386b7bf33072458b93bce8dc99819f05.png
这么做的好处是什么呢?
在我看来,这就有点像C++中的命名空间,一方面能够保护_G这个全局环境不被污染,另一方面则是避免了同名函数发生覆盖的情况。如果想调用某模块的全局函数需要先引入该模块。
https://img-blog.csdnimg.cn/b95a140b8b7d4a42b37890260aa6fbab.png
如果调用不同模块之间的同名函数,那么会调用最后require的模块函数。
https://img-blog.csdnimg.cn/95b2e3d4d18749c3b005d29540b58d7a.png
最后,其实_ENV也不一定非要使用全局环境初始化,我们可以通过loadfile中的可选参数,给这个模块创立一个新的环境。这样做的好处是哪怕此时有恶意代码进入该模块,也无法访问到其他全局数据。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页:
[1]