C++和Lua交互总结
- [Chapter1. C++和Lua交互总结](#Chapter1. C++和Lua交互总结)
-
- 一、Lua与C++的交互机制------Lua堆栈
- 二、堆栈的操作
- [三、C++ 调用 Lua](#三、C++ 调用 Lua)
- [四、Lua 调用 C++](#四、Lua 调用 C++)
- 最后总结一下
Chapter1. C++和Lua交互总结
原文链接:https://blog.csdn.net/qq826364410/article/details/88624824/
一、Lua与C++的交互机制------Lua堆栈
Lua和C++ 的交互机制的基础在于Lua提供了一个虚拟栈,C++ 和Lua之间的所有类型的数据交换都通过这个栈完成。无论何时C++想从Lua中调用一个值,被请求的值将会被压入栈,无论何时C++想要传递一个值给Lua,首先将整个值压栈,然后就可以在Lua中调用。
Lua中,对虚拟栈提供正向索引和反向索引两种索引方式,区别是:正数索引1永远表示栈底,负数索引-1永远表示栈顶。重要!后面所有的交互,都是基于Lua的虚拟栈来通信。假设当前Lua的栈中有5个元素,如下图所示:
二、堆栈的操作
因为lua与c/c++是通过栈来通信,lua提供了C API对栈进行操作。
我们先来看一个最简单的例子:
cpp
#include <iostream>
#include <string.h>
using namespace std;
extern "C"
{
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"
}
int main()
{
//1.创建一个state
// luaL_newstate返回一个指向堆栈的指针
lua_State *L = luaL_newstate();
//2.入栈操作
lua_pushstring(L, "I am so cool~");
lua_pushnumber(L, 20);
//3.取值操作
if (lua_isstring(L, 1)) { //判断是否可以转为string
cout << lua_tostring(L, 1) << endl; //转为string并返回
}
if (lua_isnumber(L, 2)) {
cout << lua_tonumber(L, 2) << endl;
}
//4.关闭state
lua_close(L);
getchar();
return 0;
}
在执行这个例子之前,我们需要引入Lua.lib静态库,也就是上文中extern "C"中执行的include。
extern "C"
主要作用就是为了能够正确实现C++ 代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按c语言的进行编译,而不是C++ 的。由于C++ 支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名,比如_Log_int_int;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名,比如_Log。
cpp
void Log(int a, int b){}
Lua源代码的下载地址: http://www.lua.org/ftp/,但没有提供相应的静态库,只有.c和.h文件,需要我们自己生成静态库。
生成Lua静态库方法: https://blog.csdn.net/qq826364410/article/details/88563408
也可以直接下载我生成好的静态库:https://download.csdn.net/download/qq826364410/11029611
好了,我们有了Lua静态库,就可以开心的玩耍了。
引入Lua静态库
- 首先,新建一个Visual C++的空项目,右键点击工程属性,选择VC++目录,
- 把lua工程中的.h头文件所在的目录加到包含目录中
- 把Lua静态库文件所在的目录加到库目录中,
- 最后,点击链接器->输入->附加依赖项->加上生成的Lua静态库,比如Lua.lib,记得用分号";"与其他库隔开。
OK,大功告成!
Lua虚拟栈在源码中是如何实现的:
Lua栈是在创建lua_State时建立的,TValue stack[max_stack_len] ,欲知内情可以查 Lua源码lstate.c的stack_init函数
Lua栈可以存储数字,字符串,表,闭包等,它们最终都用TValue这种数据结构来保存 。
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存在栈上元素里的只是指针, 他们都会在生命周期结束后被垃圾回收。
三、C++ 调用 Lua
C++ 可以获取Lua中的值,可以调用Lua函数,还可以修改Lua文件
1)C++获取Lua值
使用lua_getglocal来获取值,然后将其压栈
使用lua_toXXX将栈中元素取出转成相应的C++类型的值
如果Lua值为table类型的话,通过lua_getfield和lua_setfield获取和修改表中元素的值
2)C++调用Lua函数
使用lua_getglobal来获取函数,然后将其压入栈;
如果这个函数有参数的话,就需要依次将函数的参数也压入栈;
这些准备工作都准备就绪以后,就调用lua_pcall开始调用函数了,调用完成以后,会将返回值压入栈中;
示例:
新建一个简单的lua放到工程的同级目录下
hello.lua
lua
str = "I am x-man."
tbl = {name = "DC", id = 20114442}
function add(a,b)
return a + b
end
然后,我们写一个Lua1.cpp来访问lua中的数据
cpp
#include <iostream>
#include <string.h>
using namespace std;
extern "C"
{
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"
}
int main()
{
//1.创建Lua状态,返回一个指向堆栈的指针
lua_State *L = luaL_newstate();
if (L == NULL)
{
return;
}
//2.加载lua文件
int bRet = luaL_loadfile(L, "hello.lua");
if (bRet)
{
cout << "load file error" << endl;
return;
}
//3.运行lua文件
bRet = lua_pcall(L, 0, 0, 0);
if (bRet)
{
cout << "pcall error" << endl;
return;
}
//4.读取全局变量,
// 1.把 str 压栈 2.由lua去寻找全局变量str的值,并将str的值返回栈顶(替换str)
// 如果存在相同命名的其他变量、table或函数,就会报错(读取位置发生访问冲突)
lua_getglobal(L, "str");
// -1取出栈顶元素,转化为string
string str = lua_tostring(L, -1);
cout << "str = " << str.c_str() << endl;
//5.读取table,把table压栈
lua_getglobal(L, "tbl");
//-------------------------------
// 1.把name压入栈中,2.由lua去寻找table中name键的值,并将键值返回栈顶(替换name)
// 相当于lua_pushstring(L, "name") + lua_gettable(L, -2)执行结果是一样的
lua_getfield(L, -1, "name");
// 把name压入栈中
//lua_pushstring(L, "name");
// 弹出栈上的name,并从表中找到name的键值,把结果放在栈上相同的位置
//lua_gettable(L, -2);
//---------------------------------
str = lua_tostring(L, -1);
// 因为table在栈顶的下面,所以取-2,把id压栈,由lua找到table中id键的值,并返回栈顶(替换id)
lua_getfield(L, -2, "id");
// id的值已经在栈顶,取-1
int id = lua_tonumber(L, -1);
cout << "tbl:name = " << str.c_str() << endl;
cout << "tbl:id = " << id << endl;
// 读取函数,
// 1.将函数add放入栈中,2.由lua去寻找函数add,并将函数add返回栈顶(替换add)。
lua_getglobal(L, "add"); // 获取函数,压入栈中
lua_pushnumber(L, 10); // 压入第一个参数
lua_pushnumber(L, 20); // 压入第二个参数
// 栈过程:参数出栈->保存参数->参数出栈->保存参数->函数出栈->调用函数->返回结果入栈
// 调用函数,调用完成以后,会将返回值压入栈中,2表示参数个数,1表示返回结果个数。
int iRet = lua_pcall(L, 2, 1, 0);
if (iRet)
{
// 调用出错
const char *pErrorMsg = lua_tostring(L, -1);
cout << pErrorMsg << endl;
lua_close(L);
return;
}
if (lua_isnumber(L, -1)) //取值输出
{
int fValue = lua_tonumber(L, -1);
cout << "Result is " << fValue << endl;
}
// 栈的索引方式可以是正数也可以是负数,区别是:1永远表示栈底元素,-1永远表示栈顶元素。
//至此,栈中的情况是:
//=================== 栈顶 ===================
// 索引 类型 值
// 5或-1 int 30
// 4或-2 int 20114442
// 3或-3 string shun
// 2或-4 table tbl
// 1或-5 string I am so cool~
//=================== 栈底 ===================
lua_pushstring(L, "Master");
// 会将"Master"值出栈,保存值,找到到table的name键,如果键存在,存储到name键中
lua_setfield(L, 2, "name");
// 读取
lua_getfield(L, 2, "name");
str = lua_tostring(L, -1);
cout << "tbl:name = " << str.c_str() << endl;
// 创建新的table
lua_newtable(L);
lua_pushstring(L, "A New Girlfriend");
lua_setfield(L, -2, "name");
// 读取
lua_getfield(L, -1, "name");
str = lua_tostring(L, -1);
cout << "newtbl:name = " << str.c_str() << endl;
//7.关闭state
// 销毁指定 Lua 状态机中的所有对象, 并且释放状态机中使用的所有动态内存。
// (如果有垃圾收集相关的元方法的话,会调用它们)
lua_close(L);
getchar();
return 0;
}
代码中,已经有很详细的注释了,这里总结一下:
1. 读取lua的全局变量:
cpp
lua_getglobal(L, "str");
内部实现:1.把全局变量 str 里的值压栈 2.由lua去寻找全局变量str的值,并将str的值返回栈顶(替换str)
注意:如果存在相同命名的其他变量、table或函数,就会报错(读取位置发生访问冲突)
2. 读取table中的键值:
cpp
lua_getglobal(L, "tbl");
lua_getfield(L, -1, "name");
lua_getglobal方法跟上面的实现是一样的。
lua_getfield方法:
内部实现: 1.把name压入栈中,2.由lua去寻找table中name键的值,如果键存在,将键值返回栈顶(替换name)
注意:这里的参数-1,就是表示把table中的键值返回到栈顶。
- 调用lua中的函数:
cpp
lua_getglobal(L, "add"); // 获取函数,压入栈中
lua_pushnumber(L, 10); // 压入第一个参数
lua_pushnumber(L, 20); // 压入第二个参数
// 栈过程:参数出栈->保存参数->参数出栈->保存参数->函数出栈->调用函数->返回结果入栈
// 调用函数,调用完成以后,会将返回值压入栈中,2表示参数个数,1表示返回结果个数,
// iRet为0表示调用成功
int iRet = lua_pcall(L, 2, 1, 0);
四、Lua 调用 C++
Lua可以调用由C++定义、实现具体的函数
步骤:
- 将C++的函数包装成Lua环境认可的Lua_CFunction格式
- 将包装好的函数注册到Lua环境中
- 像使用普通Lua函数那样使用注册函数
包装C++函数
为了从Lua脚本中调用C++函数,需要将被调用的C++函数从普通的C++函数包装成Lua_CFunction格式,并需要在函数中将返回值压入栈中,并返回返回值个数。
cpp
int (*lua_CFunction) (lua_State *L);
例如有一个C++函数:
cpp
int add(int a,int b)
{
return a+b;
}
包装为:
cpp
int add(lua_state *L)
{
int a = lua_tonumber(-1);
int b = lua_tonumber(-2);
int sum = a+b;
// 将返回值压入栈中
lua_pushnumber(L,sum);
// 返回返回值个数
return 1;
}
示例:
新建一个简单的lua放到工程的同级目录下
avg.lua
lua
avg, sum = average(10, 20, 30, 40, 50)
print("The average is ", avg)
print("The sum is ", sum)
print("age", age)
for k,v in pairs(newTable) do
print("k = ",k," v = ",v)
end
print("name", newTable.name)
然后,创建一个Lua2.cpp:
cpp
#include <stdio.h>
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}
static int average(lua_State *L)
{
/* 得到参数个数 */
int n = lua_gettop(L);
double sum = 0;
int i;
/* 循环求参数之和 */
for (i = 1; i <= n; i++)
{
/* 求和 */
sum += lua_tonumber(L, i);
}
/* 压入平均值 */
lua_pushnumber(L, sum / n);
/* 压入和 */
lua_pushnumber(L, sum);
/* 返回返回值的个数 */
return 2;
}
int main(int argc, char *argv[])
{
/* 初始化Lua */
/* 指向Lua解释器的指针 */
lua_State* L = luaL_newstate();
/* 载入Lua基本库 */
luaL_openlibs(L);
/* 注册函数 */
lua_register(L, "average", average);
// 设置lua中的全局变量
lua_pushinteger(L, 18); //入栈
lua_setglobal(L, "age"); //1.先将18值出栈,保存值,2.在lua中,把值存储到全局变量age中
// 设置lua中table
lua_newtable(L); //创建一张空表,并将其压栈
lua_pushstring(L, "lili");// 入栈
// 1.先将"lili"值出栈,保存值,2.找table的name键,如果键存在,存储到name键中
lua_setfield(L, -2, "name");//栈顶是lili,新创建的table在lili下,所以是-2
// 将table赋值到lua中,并弹出table
lua_setglobal(L, "newTable");
/* 运行脚本 */
luaL_dofile(L, "avg.lua");
/* 清除Lua */
lua_close(L);
/* 暂停 */
printf("Press enter to exit...");
getchar();
return 0;
}
1. 读取C++的变量:
cpp
// 设置lua中的全局变量
lua_pushinteger(L, 18); //入栈
lua_setglobal(L, "age");
lua_setglobal(L, "age") 内部实现: 1.先将值出栈,保存值,2.在lua中,把值存储到全局变量age中
2. 调用在C++中创建的新表的元素:
cpp
// 设置lua中table
lua_newtable(L); //创建一张空表,并将其压栈
lua_pushstring(L, "lili");// 入栈
// 1.先将"lili"值出栈,保存值,2.找table的name键,如果键存在,存储到name键中
lua_setfield(L, -2, "name");//栈顶是lili,新创建的table在lili下,所以是-2
// 将table赋值到lua中,并弹出table
lua_setglobal(L, "newTable");
lua_setglobal(L, "newTable") 内部实现: 1.先将table出栈,保存table,2.在lua中,存储到newTable表中
在lua中,print("name", newTable.name),使用newTable.name调用在C++中创建的新表的元素。
3. 调用C++中的函数:
将C++的函数包装成Lua环境认可的Lua_CFunction格式
将包装好的函数注册到Lua环境中
像使用普通Lua函数那样使用注册函数
包装C++函数:
cpp
static int average(lua_State *L)
{
/* 得到参数个数 */
int n = lua_gettop(L);
double sum = 0;
int i;
/* 循环求参数之和 */
for (i = 1; i <= n; i++)
{
/* 求和 */
sum += lua_tonumber(L, i);
}
/* 压入平均值 */
lua_pushnumber(L, sum / n);
/* 压入和 */
lua_pushnumber(L, sum);
/* 返回返回值的个数 */
return 2;
}
将包装好的函数注册到Lua环境中
cpp
/* 注册函数 */
lua_register(L, "average", average);
在lua中正常调用
lua
avg, sum = average(10, 20, 30, 40, 50)
4.把C++的函数封装成模块
把C++的函数封装成模块:
①将C++的函数包装成Lua环境认可的Lua_CFunction格式,调用luaL_newlib,放入到一个lua表中压入栈里。
②将自定义模块,注册到Lua环境中。
③在lua中,加上自定义模块名调用C++函数。
avg.lua,这里的lua文件,在调用C++的函数时,需要加上自定义的模块名。 比如,我们定义模块名为mylib。
lua
avg, sum = mylib.average(10, 20, 30, 40, 50)
print("The average is ", avg)
print("The sum is ", sum)
Lua1.cpp
cpp
#include <stdio.h>
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}
static int average(lua_State *L)
{
/* 得到参数个数 */
int n = lua_gettop(L);
double sum = 0;
int i;
/* 循环求参数之和 */
for (i = 1; i <= n; i++)
{
/* 求和 */
sum += lua_tonumber(L, i);
}
/* 压入平均值 */
lua_pushnumber(L, sum / n);
/* 压入和 */
lua_pushnumber(L, sum);
/* 返回返回值的个数 */
return 2;
}
// 1. 列出需要封装的C++函数
// luaL_Reg为注册函数的数组类型
static const luaL_Reg mylibs_funcs[] = {
{ "average", average },
{ NULL, NULL }
};
// 2. 将所有函数放到一个table中,并压入栈中
int lua_openmylib(lua_State* L) {
//创建一个新的表,将所有函数放到一个table中
//将这个table压到stack里
luaL_newlib(L, mylibs_funcs);
return 1;
}
// 3. 将自定义模块加到注册列表里
static const luaL_Reg lua_reg_libs[] = {
{ "base", luaopen_base },
{ "mylib", lua_openmylib }, //这里为自定义的模块名字mylib
{ NULL, NULL }
};
int main(int argc, char *argv[])
{
/* 初始化Lua */
/* 指向Lua解释器的指针 */
lua_State* L = luaL_newstate();
/* 载入Lua基本库 */
luaL_openlibs(L);
//4. 注册让lua使用的模块
const luaL_Reg* lua_reg = lua_reg_libs;
for (; lua_reg->func; ++lua_reg) {
// 加载模块
// 首先查找 package.loaded 表, 检测 modname 是否被加载过。
// 如果被加载过,require 返回 package.loaded[modname] 中保存的值。
// 如果 modname 不在 package.loaded 中, 则调用函数 openf ,并传入字符串 modname。
// 将其返回值置入 package.loaded[modname]。
// 如果最后一个参数为真, 同时也将模块设到全局变量 modname 里。在栈上留下该模块的副本。
luaL_requiref(L, lua_reg->name, lua_reg->func, 1);
// 从栈中弹出 1 个元素
lua_pop(L, 1);
}
/* 运行脚本 */
luaL_dofile(L, "avg.lua");
/* 清除Lua */
lua_close(L);
/* 暂停 */
printf("Press enter to exit...");
getchar();
return 0;
}
5. Lua调用C++类注册生成的Lua模块
由于篇幅的限制,请移步:https://blog.csdn.net/qq826364410/article/details/88652441
6. 在Lua中以面向对象的方式使用C++注册的类
由于篇幅的限制,请移步:https://blog.csdn.net/qq826364410/article/details/88639408
Lua和C++交互:全局数组交互
https://blog.csdn.net/qq826364410/article/details/88713839
补充
这里补充其他一些栈操作:
cpp
int lua_gettop (lua_State *L); //返回栈顶索引(即栈长度)
// lua_settop将栈顶设置为一个指定的位置,即修改栈中元素的数量。
// 如果值比原栈顶高,则高的部分nil补足,如果值比原栈低,则原栈高出的部分舍弃。
// 所以可以用lua_settop(0)来清空栈。
void lua_settop (lua_State *L, int idx);
void lua_pushvalue (lua_State *L, int idx); //将idx索引上的值的副本压入栈顶
void lua_remove (lua_State *L, int idx); //移除idx索引上的值
void lua_insert (lua_State *L, int idx); //弹出栈顶元素,并插入索引idx位置
void lua_replace (lua_State *L, int idx); //弹出栈顶元素,并替换索引idx位置的值
// 确保堆栈上至少有 n 个额外空位。 如果不能把堆栈扩展到相应的尺寸,
// 函数返回假。 失败的原因包括将把栈扩展到比固定最大尺寸还大 (至少是几
// 千个元素)或分配内存失败。 这个函数永远不会缩小堆栈; 如果堆栈已经
// 比需要的大了,那么就保持原样
int lua_checkstack (lua_State *L, int n);
下面就分两个主要部分进行介绍(C++和栈操作;以及Lua和栈操作)
2.C++和栈之间操作相关函数
①c -> stack 将C++数据压到栈里函数:lua_pushxxx
cpp
LUA_API void (lua_pushnil) (lua_State *L);
LUA_API void (lua_pushnumber) (lua_State *L, lua_Number n);
LUA_API void (lua_pushinteger) (lua_State *L, lua_Integer n);
LUA_API const char *(lua_pushlstring) (lua_State *L, const char *s, size_t len);
LUA_API const char *(lua_pushstring) (lua_State *L, const char *s);
LUA_API const char *(lua_pushvfstring) (lua_State *L, const char *fmt,
va_list argp);
LUA_API const char *(lua_pushfstring) (lua_State *L, const char *fmt, ...);
LUA_API void (lua_pushcclosure) (lua_State *L, lua_CFunction fn, int n);
LUA_API void (lua_pushboolean) (lua_State *L, int b);
LUA_API void (lua_pushlightuserdata) (lua_State *L, void *p);
LUA_API int (lua_pushthread) (lua_State *L);
②stack -> c 判断栈里类型相关函数: lua_isxxx(lua_State *L, int idx)
c
LUA_API int (lua_isnumber) (lua_State *L, int idx);
LUA_API int (lua_isstring) (lua_State *L, int idx);
LUA_API int (lua_iscfunction) (lua_State *L, int idx);
LUA_API int (lua_isinteger) (lua_State *L, int idx);
LUA_API int (lua_isuserdata) (lua_State *L, int idx);
LUA_API int (lua_type) (lua_State *L, int idx);
③stack -> c 获取栈里数据相关函数:lua_toxxx (lua_State *L, int idx)
c
LUA_API lua_Number (lua_tonumberx) (lua_State *L, int idx, int *isnum);
LUA_API lua_Integer (lua_tointegerx) (lua_State *L, int idx, int *isnum);
LUA_API int (lua_toboolean) (lua_State *L, int idx);
LUA_API const char *(lua_tolstring) (lua_State *L, int idx, size_t *len);
LUA_API size_t (lua_rawlen) (lua_State *L, int idx);
LUA_API lua_CFunction (lua_tocfunction) (lua_State *L, int idx);
LUA_API void *(lua_touserdata) (lua_State *L, int idx);
LUA_API lua_State *(lua_tothread) (lua_State *L, int idx);
LUA_API const void *(lua_topointer) (lua_State *L, int idx);
3.Lua和栈之间的操作相关函数
①从Lua中得到数据放到栈里进行操作:lua_getxxx
c
LUA_API int (lua_getglobal) (lua_State *L, const char *name);
LUA_API int (lua_gettable) (lua_State *L, int idx);
LUA_API int (lua_getfield) (lua_State *L, int idx, const char *k);
LUA_API int (lua_geti) (lua_State *L, int idx, lua_Integer n);
LUA_API int (lua_rawget) (lua_State *L, int idx);
LUA_API int (lua_rawgeti) (lua_State *L, int idx, lua_Integer n);
LUA_API int (lua_rawgetp) (lua_State *L, int idx, const void *p);
LUA_API void (lua_createtable) (lua_State *L, int narr, int nrec);
LUA_API void *(lua_newuserdata) (lua_State *L, size_t sz);
LUA_API int (lua_getmetatable) (lua_State *L, int objindex);
LUA_API int (lua_getuservalue) (lua_State *L, int idx);
②从栈里将数据写入到Lua中:lua_setxxx
c
LUA_API void (lua_setglobal) (lua_State *L, const char *name);
LUA_API void (lua_settable) (lua_State *L, int idx);
LUA_API void (lua_setfield) (lua_State *L, int idx, const char *k);
LUA_API void (lua_seti) (lua_State *L, int idx, lua_Integer n);
LUA_API void (lua_rawset) (lua_State *L, int idx);
LUA_API void (lua_rawseti) (lua_State *L, int idx, lua_Integer n);
LUA_API void (lua_rawsetp) (lua_State *L, int idx, const void *p);
LUA_API int (lua_setmetatable) (lua_State *L, int objindex);
LUA_API void (lua_setuservalue) (lua_State *L, int idx);
最后总结一下
-
Lua和C++是通过一个虚拟栈来交互通信的。
-
C++调用Lua: 由C++先把函数名、变量名、table中键放入栈中,然后把函数名、变量名、table中键出栈,并返回对应的值到栈顶,再由栈顶返回C++。
-
Lua调C++:
**Lua调用C++的函数:**先把普通的C++函数包装成Lua_CFunction格式,然后注册函数到Lua解释器中,然后由Lua去调用这个模块的函数。
**Lua以面向对象的方式调用C++的类:**新建一个元表metatable,并设置元表里key为"__index"的值的为metatable本身,然后将成员操作方法添加到元表metatable里,在创建对象函数中,把元表赋值给对象指针,这样通过":"操作符就可以找到对应的方法了。