如何机算?

3 如何机算?

机算和手算的算法是一样的,不过要使用机算的话,要将抽象的概念转换为具象代码,还是有一定的难度,但大家不用灰心,我们一步一步地来。

3.1 Language定义

Language与上面的文法是对应的,知道上面文法的定义为一个四元组(VN, VT, P, S),如何将文法定义具象至代码呢?C/C++没有元组的概念,但是不用着急,我们可以先将元组的特性都抛开,定义一个数据结构,然后针对这种数据结构设计一套操作,让这种结构看起来像元组。读者很快可以想到,定义一个结构体或类就可以了,实际上,WACC确实是这样做的。

如下面所示,定义Language类。

class CLanguage

{

public:

    //string 是待计算的序列

    bool FirstFromVect(vector<CSymbol*>& str,CFirstSet& vSet, int &changes);    

    void InsertSymbol(CSymbol& vSymbol);

    void InsertRules(CRules& vRules);

    bool ComputerFirstSet();

    CLanguage(){ gensymbol = CSymbol(0,"$"); symbol.push_back(gensymbol);};

    CLanguage(vector<CSymbol>& symbol,

        vector<CRules>& rules);

    void SetStartSymbol(CSymbol& start);

    CSymbol gensymbol;                  //genarate symbol is $

    vector<CSymbol> symbol;

    vector<CRules> rules;

    CSymbol *startsym;

    void CopyVect(vector<CSymbol*> &stDes, 

        vector<CSymbol*> &stSource);

private:

    void FirstAllTerm();// 由computerFirstSet调用

};

我们可以看到灰色底框框住的三个域sysmbol,rules 和startsym就是对应文法中元组的符号,产生式和开始符号。在这里,我们没有弄出四元组出来,符号是终结符还是非终结符符,采用一个符号中设置一个标志来区分(在后面Csysmbol有定义)。而且,是否为终结符还是非终结符,是通过计算得出来的,不用规定。此外,大家可能很奇怪gensymbol,这个符号一定会被指定为$,计算LALR(1)同心集时候才使用的。rules就是产生式集合。Rule英文含义是规则,在编译原理中与产生式(produce)同义,我们代码中使用rules术语来代替产生式术语。主要考虑是rules单词简单,不容易产生歧义。

在这里我们可以看到,元组的概念我们使用类来表示,集合我们使用vector容器来表示。我们只要保证插入时没有相同元素就可以保证集合的寓意。规定一组操作InsertSymbol和InsertRules,还有其它的一些函数现在不用理会。Language源于文法的抽象定义,但又与其有所不同,在坚持原则的基础上,又灵活多变,是咱中国人的强项哦,呵呵。

3.2 Symbol定义

class CSymbol

public:

    int value;

    std::string name ;

    CSymbol(){};

    CSymbol( int value, 

        std::string name, bool isNT = false ,

        bool isEposilon = false );

    void setEpsilon(bool isEposilon);

    void setNT(bool isNT);

    CFirstSet FirstSet;

    bool isNT;//如果是非终结符,其值为true,否则为false

    bool isEposilon;

};

在该类定义中,value是symbol的核心,一个symbol对应一个value,而且是唯一对应,以至于可以直接由value确定哪个symbol。其中gensymbol的value值为0,这是规定的。name是symbol对应的字符串,主要用于测试,打印name我们就知道给symbol的含义了,不然测试时我们还要记住冷冰冰的数字所代表的哪个symbol,那只会让我们痛苦。FirstSet用来该符号的First集。可从名字知道它的含义哦。上面的一节说过我们没有特意定义终结符和非终结符,它们是使用一个标志位来区分的,该标志位就是isNT,其中NT就是not termintor的意思(不是终结者机器人哦)。此外,也没有定义epslion,我们使用isEposilon表示first集是否包含epsilon。采用isEposilon可能含义不准确。呵呵。其操作的也很简单,看名字就知道含义了。

注意:symbol的实例均加入到Language的symbol集合中了,Rules等其它类只能操作Language的symbol集合中对外隐射出来的句柄,在WACC中使用的是引用或指针来表示symbol集合的元素的句柄。呵呵。(实际上也可以使用整数,可能会更安全些,但俺就是这样实现的)

3.3 Rules定义

class CRules

{

public:

    CSymbol *LeftPart;

    vector<CSymbol*> RightPart;

    int RuleInd;

    CRules(){};

    CRules(CSymbol *LeftPart,

        vector<CSymbol*>& RP);

private:

    void insert(CSymbol *RightSymbol);

};

自然,在CRules定义中,LeftPart和RightPart与文法中产生式的左部和右部概念是对应的,含义也很清楚,所以没有注释。命名规范的变量和函数,可以省去注释,即使注释,也是无效的注释。这种不需要注释的代码往往也称自注释代码。在很多编程规范中都有这样的要求。WACC并不算太规范,很多地方是没达到规范要求的。呵呵。这里有RuleInd的含义是规则索引号,可能让大家看的奇怪,这个变量旁边应该要有注释的。RuleInd是用来指示Rule在Rule集合中的编号,唯一一个RuleInd对应唯一的Rules。注意,insert仅供CRules构造函数使用,不应暴露给类外,否则没办法保证Rule的左部右部完整语义。在一条规则中LeftPart一定不能为空,而RightPart可空。设置类成员的访问权限,保证语义是其一条妙处。

在CRule的构造函数中,LeftPart设置为isNT属性的。

3.4 计算First集

注意,为了便于测试,以上一些成员函数和成员变量绝大多数地定义成public,实际上有些函数只是内部使用不应该暴露给类外的成员。

3.4.1 从测试函数开始

下面是一个计算First集的测试函数,名字为test1( )

void test1()

{

    CLanguage lang;

//定义一个符号集合V

    CSymbol stmt_sequence(1,"stmt_sequence");

    CSymbol stmt(2,"stmt");

    CSymbol stmt_seq(3,"stmt_seq");

    CSymbol semicolon(4,";");

    CSymbol s(5,"s");

    //一.获得CLanguage之vector<CSymbol>

    vector<CSymbol> vSym;

    lang.InsertSymbol(stmt_sequence);//1

    lang.InsertSymbol(stmt);         //2 

    lang.InsertSymbol(stmt_seq);     //3

    lang.InsertSymbol(semicolon);    //4

    lang.InsertSymbol(s);            //5

    showsym(lang.symbol);                //显示之

    //二.获得CLanguage之vector<Rules>

    //1.获得rightpart之Vector<CSymbol*>

#define vlang(x) (&lang.symbol[x])

    //rule1

    vector<CSymbol*> rpart1;

    rpart1.push_back(vlang( 2 ));

       cout<<vlang(2)<<" part"<<endl;

       cout<<rpart1[0]<<endl;

    rpart1.push_back(vlang( 3 ));

    //2。获得leftpart--取得rules

    CRules r1(vlang(1),rpart1);

    //3.插入vector<Rules>

    showrule( r1 );

    //r2

    vector<CSymbol*> rpart2;

    rpart2.push_back(vlang(4));

    rpart2.push_back(vlang(1));

    CRules r2(vlang(3),rpart2);

    showrule( r2 );

    //r3

    vector<CSymbol*> rpart3;

    CRules r3(vlang(3),rpart3);

    showrule( r3 );

    //r4

    vector<CSymbol*> rpart4;

    rpart4.push_back(vlang(5));

    CRules r4(vlang(2),rpart4);

    showrule(r4);

    cout<<" \nThe language's symbol is "<<endl;

   

    showsym( lang.symbol );

    lang.InsertRules(r1);

    lang.InsertRules(r2);

    lang.InsertRules(r3);

    lang.InsertRules(r4);

    cout << "\nThe languge's rules is"<<endl; 

    for ( int i = 0; i<lang.rules.size(); i++)

       showrule(lang.rules[i]);

#undef vlang  

//三.获得开始符号

lang.SetStartSymbol(lang.symbol[1]);

cout<<"\nlanguage's startsymbol is "<<lang.startsym->name

<<endl;

//language初始化结束

//计算first集合

//1.计算所有 终 结 符的first集合

//lang.FirstAllTerm();

    lang.ComputerFirstSet();

    for (  i = 0; i<lang.symbol.size(); i++)

       showFirst(lang.symbol[i],lang);

    //2.从rules中计算first集合

}

在main函数中直接调用test1()就可以了。如下所示:

void main()

    cout<<"\n----Test1-------- "<<endl;

    test1();

#if 0

    cout<<"\n------Test2------ "<<endl;

    test2();

    cout<<"\n------Test21----- "<<endl;

    test21();

    cout<<"\n---------+Test3+----"<<endl;

    test3();

    cout<<"\n*******===Test4====***"<<endl;

    test4();

    cout<<"\n*******===Test5====***"<<endl;

    test7();

    test5();

    test6();

    test8();

    test9();

    test10();

    test11();

#endif

}

注意,#if 0 ……. #endif 是一种预编译命令(框起来的部分),在它们之间的代码不会被编译进去,而不需直接删除代码或注释之,同时恢复起来也十分方便。是俺十分喜欢使用的一种删除命令。

执行结果如下:

----Test1-------- 

$: eposilon is  0 nt is 0 value is 0

stmt_sequence: eposilon is  0 nt is 0 value is 1

stmt: eposilon is  0 nt is 0 value is 2

stmt_seq: eposilon is  0 nt is 0 value is 3

;: eposilon is  0 nt is 0 value is 4

s: eposilon is  0 nt is 0 value is 5

00033A78 part

00033A78

Rule: stmt_sequence->stmt stmt_seq 

Rule: stmt_seq->; stmt_sequence 

Rule: stmt_seq->

Rule: stmt->s 

 

The language's symbol is 

$: eposilon is  0 nt is 0 value is 0

stmt_sequence: eposilon is  0 nt is 1 value is 1

stmt: eposilon is  0 nt is 1 value is 2

stmt_seq: eposilon is  1 nt is 1 value is 3

;: eposilon is  0 nt is 0 value is 4

s: eposilon is  0 nt is 0 value is 5

The languge's rules is

Rule: stmt_sequence->stmt stmt_seq 

Rule: stmt_seq->; stmt_sequence 

Rule: stmt_seq->

Rule: stmt->s 

language's startsymbol is stmt_sequence

$:firstset value is $ 

stmt_sequence:firstset value is s 

stmt:firstset value is s 

stmt_seq:firstset value is ;  epsilon 

;:firstset value is ; 

s:firstset value is s

由阴影部分可见,结果和我们手算的得出的结果是相同的。大家可以看到,在这里构造符号和规则十分麻烦。但是这仅仅是在初期测试算法是否正确的一种手段,最后大家可以看到,WACC最终是读取文件来构造符号和规则的。

后面的章节我们依次介绍该测试函数,游历计算First集的整个过程。注意测试代码中的阴影部分以及戴圈数字1,2,3,4.

3.4.2 CSymbol构造函数

在Test1()中第三行,我们看到,它首先定义了一个lang,类型为CLanguage,在默认的CLanguage构造函数中,会隐式构造一个symbol,称为gensymbol,即产生符号。序号为0,名为$并将它压入自己的symbol集合中,参见Language定义。

因此,在1中,不能以0开始,只能以1开始。0已经被gensymbol占用。此外,value值即符号出现的序号。CSymbol stmt_sequence(1,"stmt_sequence");表示创建stmt_sequence的符号,序号为1,名字为"stmt_sequence" ,其它的依次类推。

CSymbol 构造函数如下所示:

定义 :

   CSymbol( int value, 

        std::string name, bool isNT = false ,

        bool isEposilon = false );

实现:

CSymbol::CSymbol( int value, 

        std::string name, bool isNT  ,

        bool isEposilon  )

{

    this->value      = value;

    this->name       = name; 

    this->isNT       = isNT;

    this->isEposilon = isEposilon;

}

可知,在Symbol构建过程中,isNT,和isEposilon是默认为false的,也就是说,是否包含eposilon,是否是非终结符,在创建时并没有确定下来。那么这两个属性是在什么时候确定的呢?不急,慢慢来。

3.4.3 CLanguage的 InsertSymbol 函数

上面说过,CRules以及下一章会讲到CItem等类,操作的symbol均是句柄,是Language中symbol集合对外映像出来的引用或指针。实例均在Language中symbol成员所包含的集合中。上面创建的symbol都是临时的对象,只有通过InsertSymbol函数插入Language中symbol集合中才能进行后续计算。实例与映像的关系可能很难理解,读者可以复习一下C/C++教材,慢慢体会。

Test1函数第14~18行,阴影部分戴圈2,注意执行操作的顺序与符号的序号是一致的,不要乱序,否则会有问题。我们现在测试的部分是WACC的底层初级部分,要注意这注意那,并不健壮。但是如果和其它部分结合起来,还是相对健壮的,是容错的。呵呵,大家现在还是要耐点心。

InsertSymbol实现很简单,仅一个语句

void CLanguage::InsertSymbol(CSymbol& vSymbol)

{

    symbol.push_back(vSymbol);

}

但这里应该注意一点,可能是在编程时容易出错的地方,symbol定义为vector,而stl中vector每次push_back可能会导致vector空间重新分配,所以不要随意对vector中的元素取指针或引用,如果要使用,必须保证vector的中的元素已经稳定,不再进行插入或删除操作了。

3.4.4 CRules构造函数

CRules构造函数如下:

CRules::CRules(CSymbol *LeftPart,vector<CSymbol*>&

               RP)

{

    vector<CSymbol*>::iterator i;

    this->LeftPart = LeftPart;

    for ( i = RP.begin(); i!=RP.end(); i++)

    {

        insert(*i);

    }

    this->LeftPart->setNT(true);

    if ( (this->RightPart).empty() )

    {

        this->LeftPart->setEpsilon( true );

    }

}

在Test1函数中,第三十行戴圈3中 ,以及之后的代码中均可以看到CRule的构造函数的使用。在这里,你可以看到设置isNT的函数setNT(第10行)和setEpsilon(第13行)。现在您应该明白了凡是在左部的符号均设置为非终结符(not terminator)而如果右边为空,则左部first集一定包含epsilon的,而包含epsilon的不一定右部为空。所以设置epsilon的地方除此之外,还有其它地方要使用setEpsilon函数。这在后面的讲到的函数中会涉及到的。

3.4.5 CLanguage 的ComputerFirstSet 函数

参见Test1函数戴圈字4,约74行,我们就可以看到ComputerFirstSet函数。这个函数就是计算First集的。与2.1.3节的算法对应起来,我们来看看这个函数的代码

bool CLanguage::ComputerFirstSet()

{

    FirstAllTerm();//找出所有terminate symbol 的first集合

int changes = 1; //初始化为1

bool isEpsi ;

    int rIndex  ; //rules index 

//if there is any changes do it cycle

while ( changes )

changes = 0;

//for each rule choice A->X1X2X3...Xn

for ( rIndex = 0; rIndex < this->rules.size(); rIndex++ )

{

            isEpsi = FirstFromVect((this->rules[rIndex]).RightPart,

                        (this->rules[rIndex]).LeftPart->FirstSet, changes);

                   

                   (this->rules[rIndex]).LeftPart->setEpsilon(isEpsi);

}

    }

    return false;

}

这段代码基本与2.1.3节的算法描述是一致的。注意框住的函数名FirstFromVect,该函数作用就是计算一条规则产生的First集。第一个参数是规则的右部,第二个参数就是LeftPart的First集。还有一个参数是change。用来表示First集是否可增大。它还有一个返回值表示右部元素是否均包含epsilon,如果包含,则将LeftPart也要设置epsilon。

FirstFromVect函数如下实现:

bool  CLanguage::FirstFromVect(vector<CSymbol*>& str,CFirstSet& vFirst,

   int& changes)

{

    int k = 1-1;

int n = str.size();

bool bContinue = true;

    while ( bContinue && k<= n-1 )

{

//add first(Xk)-epsilon to vFirst

changes += vFirst.SetUnion( vFirst.set,  str[ k ]->FirstSet.set );

//if epsilon NOT in First(Xk) then Continue = false

if ( !(str[ k ]->isEposilon) )

{

bContinue = false;

}

k = k + 1;

}

    return bContinue;//from the vector we know wether it has epsilon

}

在上面的框中,您是否看到2.1.3节的算法的影子了呢?

至此,First集合计算已经接近尾声。该函数还包含了FirstAllTerm ,它的作用正如注释所说,用来找出terminate symbol的first集。这里不再赘述。

3.5 小结

本章的带着读者从文法等基础概念,游历到实际的代码的全过程。采用的C++面向对象的思想,而不是纯C的过程编程方法。思维如何从抽象到具象的平滑过渡,或从具象平滑到抽象。是研究编译原理的难点,也是一种乐趣。国外的大师能在具象与抽象自由穿梭,能在哲学与具体问题之间架设通畅的桥梁。软件中的对象思想以及模块化的思想,均可能来自工程管理的经验,但引入计算机领域便掀起了一场革命。而目前计算机领域大多数的新技术均是美国发起的,而非我国。很可能与东方人(包括我国和日本)喜欢沉迷于具体问题有关。我国的“道”(哲学)与“艺”(技术)是脱节的。学哲学的人只能在抽象的世界里徘徊,学成后只能到学校去教书。而学技术的人恰恰相反,往往在具象中不能自拔,只能算一匠,而非大师。

在使用C++编程应该注意这样的顺序,先确定数据结构,数据结构与实际问题的对象是密切相关的,数据结构定义得如何直接影响后期的框架。再是定义接口和框架,这个过程较为抽象。最后才实现各个函数的算法。框架搭得好,后面的实现就完全是体力活了。很多程序员有最大的一个毛病就是一开始就写代码,从不去设计,不去做模块化工作。编码中途不能被打断,往往要熬夜加班才能勉强将一个小功能的程序做好,如果中途睡了一觉,睡醒了坐在计算机旁就不知道后面的代码该怎么写了。这就是不会抽象,只能在具象中徘徊的表现。这样的程序员不具备做大型程序开发的能力。

当然,WACC并不算是个大程序,算是抛砖引玉,引领大家进门而已。此外,大家要注意类的封装性,慎用全局变量。养成测试的习惯,写的代码尽早测试尽早解决Bug。避免在后期这些Bug引起“蝴蝶效应”。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值