很多编程语言都提供了数组,所谓数组是指包含多个元素的集合,有了数组就可以对多个有关联的数据进行统一管理,同时避免了变量名的开销,因为如果没有数 组,那么就需要为每个元素定义一个变量...

    很多编程语言都提供了数组,所谓数组是指包含多个元素的集合,有了数组就可以对多个有关联的数据进行统一管理,同时避免了变量名的开销,因为如果没有数 组,那么就需要为每个元素定义一个变量来进行访问,有了数组,就可以把所有元素放在数组里,通过数组名和索引进行访问。本节的v0.0.16的版本就实现 了数组,并利用数组,函数等实现了一个简单的命令行界面的21点扑克牌小游戏。

    本节v0.0.16版本的源代码下载地址为:http://pan.baidu.com/share/link?shareid=193204&uk=940392313 (此为百度云盘的共享链接地址),访问该地址可以看到三个文件:zengl_lang_v0.0.16_forXP.rar (XP系统下的vs2008解决方案和源代码), zengl_language_v0.0.16_forLinux.tar.gz  (Linux系统下的源代码和makefile) ,v0.0.16-v0.0.15-diffs.txt  (v0.0.16和v0.0.15的代码变化情况)。

    SourceForge.net上的仓库地址为:https://sourceforge.net/projects/zengl/files/   从里面可以看到各个版本的代码压缩包,比如本节的zengl_lang_v0.0.16_forXP.rar zengl_language_v0.0.16_forLinux.tar.gzv0.0.16-v0.0.15-diffs.txt

    先来看下本版本的描述 (在linux代码包里的usage.txt里有这段描述,在目前几个带有git版本的....-diffs.txt和git log中也有这段描述):

    v0.0.16版本,该版本添加了数组,修复了大量BUG,并最终利用条件,循环,数组,引用等所有当前可用的语法结构完成了《21点扑克小游戏》。
   
    在main.c中添加了'[',']'的token作为数组的左右界定符,以及'%'取余运算符,'break','continue'循环控制时的跳转 关键字。在makefile文件中为builtin.o模块添加了run.h和func.h的依赖,这样当run.h或func.h修改时就会重新编译 builtin.o模块了,就不会出现以前那种修改run.h后,builtin里的函数调用莫名其妙的参数错误的问题了。
    在parser.c中添加了数组,取余运算,break,continue的语法树生成。
    对于数组采用和函数一样的语法树生成方式,所以数组的写法有点像函数调用,例如:test[0,1]就表示test数组里索引为0的数组里的第一个元素。 也可以把这种写法看成是坐标形式,如value[x,y,z]即x,y,z三个坐标值确定一个点,用数组的解释就是value这个三维数组中的x这个2维 数组里的y一维数组中的z元素,当然还可以表示更多的维数像value[x,y,z,a,b,c...]在程序里会为每一维的数组都分配自己的内存空间。 之所以采用函数调用的写法,是因为有现成的函数调用的语法树的生成方式,方便构建数组的语法树。
    取余运算符采用和乘除一样的优先级,以及和乘除一样的语法树生成方式。
    break,continue使用nochild_stmt无子节点的函数来生成语法树。
    在symbol.c中将ARRAY_ITEM数组名也加入到变量符号表中。
    在assemble.c中生成了数组,取余运算符,break,continue的汇编代码。
    对于数组的汇编代码也采用了类似函数调用的方式,将数组的所有索引值压入栈中,并用ARRAY_ITEM寄存器指向栈中的第一个索引值。例如 value[x,y,z],就将value的x,y,z三个值压入栈中,对于取值运算,就生成GET_ARRAY加value的内存地址值,对于赋值语 句,就生成SET_ARRAY加value内存值的指令。这样在run.c中执行时,通过value内存里的memblock内存块指针找到数组的内存 块,再通过栈中的索引依次找到对应要操作的元素。
    另外,引用除了可以引用普通的变量外,还可以引用数组里的元素。例如:test = &value[x,y,z]那么test就是value[x,y,z]元素的引用,对test的操作就是对value[x,y,z]元素的操作。
    将一个数组赋值给某个变量,其实就是将数组的指针赋值给该变量,使得该变量也指向该数组,例如:value[x,y,z]中的value是一个数组,如果 test = value,那么test也和value指向同一个数组,和引用的区别是直接赋值会增加数组内存块的refcount值,就有点像面向对象中的对象里的引 用计数值一样,当释放test时,只会将数组内存块的refcount计数值减一,只有当refcount值为0时才释放对应的数组空间。
    最后利用当前的语法结构写了一个21点的小游戏,一个用户(user),一个机器人(android),当游戏开始时,用户选择要牌,机器人则根据自己的 点数通过概率计算判断是否需要要牌,当用户不要牌时,或者某一方点数超过21点时,一轮结束,最后在双方都没超过21点时,根据谁大谁赢该局,并且赢者得 到100虚拟币,输的扣除100虚拟币。最开始双方都有2000币。
    还修复了'||'运算及'&&'运算的BUG,以前当'||'的左边为TRUE时,还要判断右边,现在在左边表达式执行完后,会根据'JNE'判断是否为TRUE,如果为TRUE时,就不用判断右边了,加快了判断速度。
    其他的改动请用git log -p 或 gitk等图形化工具来查看。
   
    作者:zenglong
    时间:2012年4月9日
    官网网站:www.zengl.com

    因为从上一个版本开始,git版本控制信息都是在linux系统下生成的,所以在windows下就无法对该版本的git信息做修改,原因上一节提到过, 不过git信息一般都是在linux下生成和使用的,在windows下只要使用git bash工具查看版本信息就可以了(如果要在windows下修改,就只有git init重新初始化,而且会丢失之前的版本控制信息,并且windows下修改过的git信息在linux下也无法正常扩展修改,只能查看,目前我还不知 道解决方法)。
    另外,如果在linux下查看git信息时如果没有彩色输出的话(只有普通的黑白色),可以在linux终端下使用下面几个命令:


git config --global color.status auto
git config --global color.diff auto
git config --global color.ui auto
git config --global color.
branch auto

    如果这样设置以后,在git diff或git log时看到很多ESC[1m...之类的,说明你的LESS没有开启颜色解析,可以使用下面命令添加一个环境变量即可:

export LESS="-eirMX" 
    如果不想每次登录都输入这个指令可以把这条指令写入/etc/profile文件(我使用的是slackware的linux),别的linux可能在~/.bash_profile里,请对照自己的系统进行修改。

    下面通过本节的game_21_point.zl(21点扑克小游戏)的脚本代码来说明数组等相关知识(下面的注释仅在此起说明作用,原文件中并没有,因为当前还没实现注释功能):

use builtin;  //加载内建模块

Name=0; //目前先用变量名表示索引,以后添加了宏和类之后,就可以用宏或类的字段来替代。Name表示玩家或机器人名字的数组元素索引
PokerCnt=1;
//PokerCnt对应数组索引1,即数组的第二个元素,该元素里存放的是发给玩家或机器人的扑克牌数量。
Pokers=2;  // Pokers数组第三个元素,又是一个数组,里面存放了玩家或机器人庄家的所有扑克牌。
TotalPoker = 3;
//TotalPoker第4个元素,存放的是所有扑克的总点数
Money = 4; //Money代表玩家或机器人所拥有的虚拟币。
android[Name]='Android';  //设置机器人庄家名字为"Android"
user[Name]='User';   //设置玩家名字为"User"
user[Money] = android[Money] = 2000;  //设置玩家和机器人的开始虚拟币。
print 'user:'+user[Name];    //打印显示出玩家名字
fun myAddPoker(array,poker) 
//myAddPoker函数定义,给玩家和机器人发牌的函数。
    global Pokers,PokerCnt,TotalPoker; //引入全局变量,这样下面才能用这些全局变量作为数组的索引。
    count = &array[PokerCnt];  //获取用户或机器人数组里的扑克数目的元素的引用。下面对count的操作就是对该数组元素的操作。
    return array[TotalPoker] += array[Pokers,count++] = poker; //将所发的扑克点数添加到数组末尾,同时增加扑克的总点数值。
endfun

fun myIsNeedPoker(array)  //机器人庄家判断是否需要继续要牌
    global TotalPoker;  //全局变量数组索引
    num = 21 - array[TotalPoker];   //先得到当前机器人总点数和21点的差值。
    if(bltRandom()%13 +1 <= num)  //bltRandom是内建模块里的随机数生成函数。这里之所以和13取余,是因为该游戏中扑克范围为A到K,即一张牌的点数是1点到13点,单张扑克 最大是13点,所以和13取余,如果当前的总点数小于等于8,那么不管得到什么牌都不会超过21点,当前总点数大于8时,越接近21点,继续要牌的概率就 越小。

        return 1;  //返回1表示继续要牌
    else
        return 0;  //返回0表示此轮不继续要牌
    endif
endfun

turn_num = 0;  //表示当前进行了几轮。

fun myGameStart()  //每一轮游戏的实际执行函数。
    global user,android,Name,Pokers,PokerCnt,TotalPoker,Money,
        turn_num;   //函数中的数组需要使用这些全局变量作为索引。
    print 'please input [y] to get poker,except to end!';  //输入y表示要牌,否则表示不要牌。
    input = 'y';
    for(;input=='y';)  //循环获取用户输入,并设置玩家和机器人庄家的扑克信息。
        input = read(); //获取用户输入
        if(input == 'y') 
//输入y则通过myAddPoker来给玩家发牌。
            myAddPoker(user,bltRandom()%13+1);  //生成1到13的随机点数,并设置玩家的扑克数组。
            print 'you get ' + user[Pokers,user[PokerCnt]-1];  //打印显示出玩家获取的点数。
            if(user[TotalPoker] > 21)  //如果超过21点,则break跳出循环
                break;
            endif
        endif
       
        if(android[TotalPoker] < user[TotalPoker] ||
            myIsNeedPoker(android))   //如果是机器人的总点数小于用户的总点数或者myIsNeedPoker函数判断需要进行要牌时,则执行myAddPoker操作。
            myAddPoker(android,bltRandom()%13+1);  //生成1到13的随机点数,并设置机器人的扑克数组。
            print 'android get '+ android[Pokers,android[PokerCnt]-1]; //打印出机器人的点数。
            if(android[TotalPoker] > 21)
//如果超过21点,则break跳出循环               
                 break;
            endif
        endif
       
        print user[Name] + ' total:'+ user[TotalPoker] + ' '+
              android[Name] + ' total:'+ android[TotalPoker];   //双方每次要牌结束时显示各自的总点数。
    endfor
   
    printf(user[Name] +" have gets :");
    for(i=0;i  //从user[Pokers]数组中循环打印出玩家所获取到的所有牌的点数。

        printf('Poker:'+user[Pokers,i]+' ');
    endfor
    print ' total: ' + user[TotalPoker];  //打印出玩家此轮的总点数
   
    printf(android[Name] +" have gets :");
    for(i=0;i//从android[Pokers]数组中循环打印出机器人庄家所获取到的所有牌的点数       
        printf('Poker:'+android[Pokers,i]+' ');
    endfor
    print ' total: ' + android[TotalPoker];  //打印机器人的总点数。
   
    bool_userwin = 1;  //判断玩家是否赢的局部变量。
    if(android[TotalPoker] > 21)  //如果机器人超过21点,则用户赢
        print user[Name] +' win';
    elif(user[TotalPoker] > 21)  //如果玩家超过21点,则机器人庄家赢
        print android[Name] +' win';
        bool_userwin = 0;  //将变量设为0,表示玩家输了
    else  //如果都没超过21点,则进行下面的判断
        if(android[TotalPoker] >= user[TotalPoker])  //如果庄家的点数大于等于玩家总点数,则玩家输,所以玩家只有总点数大于庄家才能赢。
            print android[Name] +' win';
            bool_userwin = 0;
        else
            print user[Name] +' win';
        endif
    endif
   
    if(bool_userwin)  //通过bool_userwin判断玩家的输赢,并根据结果对赢的加100虚拟币,输的扣100虚拟币。
        user[Money] +=100;
        android[Money] -= 100;
    else
        user[Money] -= 100;
        android[Money] +=100;
    endif
    print 'the '+ ++turn_num +' turn '+user[Name] + ' money:'+user[Money] +' '+ android[Name]+' money:'+
        android[Money];   //打印出目前为止游戏经历的总局数,以及玩家和机器人庄家的总虚拟币。
    user[PokerCnt] = user[TotalPoker] = android[PokerCnt] =
    android[TotalPoker] = 0;  //一轮结束后,将玩家和机器人的扑克牌数量及总点数重置为0,为下一轮做准备。
endfun

input = 's';
blfirst = 1;
for(;(!blfirst && input=='r') || (blfirst && input=='s');) //如果第一次时且输入变量为s时或其他时候输入变量为r时,就继续循环
    if(blfirst)
        print 'if you want start ,type [s]';  //如果是第一轮,则输入s来开局
    else
        print 'if you want play again ,type [r]'; //如果是其他轮,则输入r来重开局。
    endif
    input = read();
    if((!blfirst && input=='r') || (blfirst && input=='s'))
//如果第一次时且输入变量为s时或其他时候输入变量为r时,就调用myGameStart开始一轮游戏
        myGameStart();  //每局游戏的具体执行函数。
        if(blfirst)
            input = 'r';
            blfirst = 0; 
//第一轮结束后,将blfirst设为0
        endif
    endif
endfor


    game_21_point.zl生成的中间代码文件game_21_point.zlc有740多行代码,限于篇幅这里就罗列些和本节数组相关的汇编指令:
    例如game_21_point.zl中有这样一句设置数组元素的语句:

    android[Name]='Android';

    该语句对应的汇编代码如下(下面的注释在此仅起说明作用):
........................ //省略其他的代码
11 MOV AX "Android";
12 PUSH AX;   //将Android字符串压入栈作为要设置的值
13 PUSH ARRAY_ITEM;  //将原环境下的ARRAY_ITEM寄存器的值压入栈
14 RESET ARRAY_ITEM; //将ARRAY_ITEM设为当前栈顶位置,指向数组的第一维度的索引值。
15 MOV AX (1);  //将Name变量的值作为索引先赋值给AX
16 PUSH AX;  //将AX里的索引值压入栈。
17 SET_ARRAY (6);   //根据栈中的索引值和android数组变量所在的内存块,将之前压入栈的Android字符串设置到android[Name]的数组元素中。
........................ //省略其他的代码

    这段代码对应的栈结构如下:

    函数的栈结构也发生了变化,原来的栈结构如下:

    新的函数栈结构如下:

    具体的C文件代码部分,请结合源代码中的注释,加上git工具以及vs2008或者eclipse+CDT或者gcc,gdb等工具进行分析。

   
最后还是老生常谈的话题:
    windowsXP压缩包中的代码包括
game_21_point.zl测试脚本都是采用GBK的编码,Linux压缩包中的代码包括测试文件以及git里的信息都是UTF8的编码,所以如果哪些地方出现了乱码,请自行调整。

    对于windows用户,请确保在项目属性的配置里,命令行参数配置的是game_21_point.zl(对于zengl_lang_v0.0.16的项目)或game_21_point.zlc(对于zenglrun的项目),好像每一节都提到过。
    另外对于vs2008的用户,我在项目属性里:[配置属性>>>>C/C++ >>>> 高级] 部分设置了禁用特定警告:4013,4715,4996 ,这几个警告会显示一些某某函数是非安全的函数,或者函数没有返回值等,这里禁用掉,防止出现过多的警告。另外还有个警告是显示某某变量没被使用过的, 这个警告我没禁用,可以不用管它。我最开始是使用Linux系统开发的zengl ,在我的GCC下面并没有显示过这些讨厌的警告,所以就没处理,不过还好这些警告都无关痛痒,无需理会。
    还有一个地方:VS2008项目中,在[配置属性>>>> C/C++ >>>> 预处理器] 部分都设置了预处理器定义的宏:OS_IN_WINDOWS ,因为源代码既要在WINDOWS下编译,又要在LINUX下编译,所以需要通过这个宏来告诉程序当前的环境是windows还是linux,在 windows下面,在程序结束时会执行system ("pause");这条语句(vs2008下为了能看到结果,需要暂停,否则就一闪而过,什么都看不到咯。) 而linux系统主要在bash终端下执行,不需要这条语句。

    linux系统下的用户请结合usage.txt的说明,先运行make clean 将原来生成的zengl zenglrun 和 main.o parser.o assemble.o ld.o func.o run.o  symbol.o builtin.o文件
删除。

    再运行make all (单纯的make只能生成zengl,所以需要make all来生成所有的目标)

    生成zengl zenglrun 和 main.o parser.o assemble.o ld.o func.o run.o  symbol.o 
builtin.o。(在生成过程中如果出现一些警告,暂不管他)

    最后运行 ./zengl test.zl(本节是
game_21_point.zl) 查看printASTnodes函数打印抽象语法树节点的结果,以及符号表输出的变量信息以及函数信息等。(例如变量的内存地址,以及在源文件的行列号,函数的唯一标识ID等)。
    接着运行./zenglrun test.zlc(本节是
game_21_point.zlc) (注意是.zlc结尾的文件名,因为zenglrun虚拟机只能运行.zlc里的汇编代码)。

    zengl语言涉及到的很多高级的编译原理都可以在《龙书》中找到。

    最后的最后,如果转载请注明来源 http://www.zengl.com   , OK , 先到这里,休息,休息一下 O(∩_∩)O~

上下篇

下一篇: zengl编程语言v0.0.17单行多行注释

上一篇: zengl编程语言v0.0.15变量引用

相关文章

zengl编程语言v1.0.0图形版21点扑克游戏,do,while循环结构

zengl编程语言v0.0.17单行多行注释

zengl编程语言v0.0.22实现switch...case

zengl v1.4.1 及zengl_SDL新增的Android工程

zengl v1.9.1 函数参数可以使用负数作为默认值

zengl编程语言v0.0.8第二代语法解析函数