编译原理课程的实验二:编制简单词法分析程序。
实验内容:通过了解词法分析程序的功能,设计词法分析程序,通过逐个字符的扫描和分解,能够识别出一个一个单词以及单词的分类;删除注释并进行词法检查,报告所发现的错误(比如标识符不能以数字开头,123aaa就不是合法的标识符),建立符号表。
实验说明:单词通常分为五种类型:
(1)基本字:(关键字,保留字)如if else int break等
(2)运算符:如+ - * / = < >等
(3)标识符:用户定义的变量名,常数名,函数名等
(4)常数
(5)界符:如 , ; ( ) . 等
目录
实验要求就如上所示。下面先说一下思路,在完成第一个符号表的实验的时候
详细见这里。由于当时的需求是找出标识符。fgets方法可以将源文件内容一行一行读取出。所以当时我想的是将读取到的每一行内容以空格等进行分割,然后将分割出的东西通过一个方法进行判断是否是一个合法的标识符,但是当时在写的时候遇上一个问题,比如读取到 int a=12; 这一行,以空格分割会分割成 'int' ,'a=12;' ,显然找不到标识符。但是c语言提供了一个强大的函数strtok函数,该函数的一个参数可以接收多个字符,比如'\n\t=',这样就会以'\n'和'\t' 和'='分割。由于合法标识符只有数字子母和下划线组成,所以可以将等号、分号、加减乘除等通通放进去,这样就能将与合法标识符相邻的东西通通去除,比如int a=1;这样分割后就会变成'int'、'a' 、'1',这时候再去判断就会简单很多。感觉这算一种曲线救国的方法。
思路
到了本次实验。由于需要分类,将界符,数字,标识符,运算符等都找出来,用上一条思路就不行了。第二个思路:通过fgets将源文件以行的形式读取后。一个一个字符去读取每一行的内容。首先判断,如果是字母数字下划线,那么就继续读,读到不是字母数字下划线为止。这里我使用的是while循环,大致如下。WaitWord变量用来临时存储读到的字符,用来以后是否插入等判断。
while (word[i] != '\0' && word[i] <= 'z' && word[i] >= 'a' ||
word[i] <= 'Z' && word[i] >= 'A' || word[i] == '_' ||
word[i] <= '9' && word[i] >= '0') {
WaitWord[j] = word[i];
j++;i++;
*flag = 2;
}
WaitWord[j] = '\0';
其余判断运算符和界符都是差不多的思路。不过需要注意一下,判断常数不需要重新写是否在0-9内了。这样会和上一段判断标识符的代码冲突。所以上面这一行代码可以判断三种类别,即标识符,关键字,常数。
下面看一下我用到的几种方法
//单链表初始化
LinkList Init()
//尾插
void InsertTail(LinkList L, char data[], int ECode)
//打印单链表
void print(LinkList LL)
//以空格分割
void splitByBS()
//是否是系统关键字,1表示是,-1表示不是
bool isExist(char string[])
//判断是否是常数
bool isNum(char string[])
//是否为合法标识符 数字开头不合法
bool isLegal(char string[])
//判断是否需要插入,成功插入返回true
bool isInsert(LinkList L, int* flag, char WaitWord[])
//传入的第二个参数是读取到的以空格分割的数据
void wordSort(LinkList L, char word[], bool *isNote)
//分割读取的字符串方法
void split(LinkList L, bool *isNote)
这个程序总流程大致如下:进行文件的读取(读取到的为一行一行的数据,集成在split方法中)--->将读取到的数据以空格 换行符 制表符分割 ---> 将分割后的字符串复制一份给临时变量word ---> 将word送入wordSort中进行加工(该方法中有一个isNote变量,是后期去除注释所用,目前可以忽略) ---> 判断接收的word中各个字符属于什么(比如接受的word为printf("hello,world");)那么会读取到printf,这时候将printf送入isInsert方法中判断是否插入 ---> isInsert方法内也有一系列的判断机制(首先通过isExist方法判断是否为系统关键字,是则以系统关键字的身份插入到符号表内,不是则判断是否为常数、是否合法,等等) ---> 工作基本完成。还有去除注释下文再说。
去除注释
注释在c里好像有两种,以//开始的单行注释以及/* 开始并以 */结束的多行注释。单行注释比较好解决,他们有个共同特征即以// 开始,而后面的内容都不需要进行判断,所以在wordSort中进行判断,注意,该判断应放在判断运算符的前面,因为/是一个运算符,如果在判断运算符后面,可能会出bug。当word[i] == '/' && word[i + 1] == '/' 时,直接return结束wordSort方法即可。这样单行注释就跳过了。而多行注释有一点点麻烦因为最开始有/*可以模仿单行注释去除/* 这一行,结束的那一行也可以模仿上面。但是中间的怎么办,比如下面第二三行,由于是一行一行读取的,所以二三行中的东西会被读取并判断。
/*我是一条多行注释
char a = 'a';
int aaa = 21312312
哈哈哈*/
这时候使用一个变量指针isNote来判断。最开始的时候isNote(有多行注释)是false。在wordSort中进行如下判断,如果读取到了/*,将isNote改为true,并结束方法。即进行下一行的读取判断操作。在wordSort中继续添加判断语句,即如果isNote为true,就一直往右读取,直到读取到了*/,将isNote改为false即可。
代码
下面直接上代码,将第五行变量PATH改为自己电脑上文件的路径即可。记得使用转义字符。
//关键字1 标识符2 常数3 运算符4 分解符5
#include<stdio.h>
#include <corecrt_malloc.h>
#include <string.h>
# define PATH "D:\\编译原理代码\\test_04.cpp"
//D:\\编译原理代码\\编译原理\\符号表\\符号表.cpp
//D:\\编译原理代码\\test_01.cpp
typedef struct LNode {
char data[100];
LNode* next;
int location;
int ECode;
}LNode, * LinkList;
//单链表初始化
LinkList Init() {
LinkList L;
L = (LNode*)malloc(sizeof(LNode));
L->next = NULL;
L->location = 0;
return L;
}
//尾插
void InsertTail(LinkList L, char data[], int ECode) {
LNode* first = L;
while (L->next != NULL) {
L = L->next;
}//找到最后一个
L->next = (LNode*)malloc(sizeof(LNode));
if (!L) {
printf("扩容失败!");
return;
}
L->next->location = L->location + 1;
L = L->next;
int i = 0;
while (data[i] != '\0') {
L->data[i] = data[i];
i++;
}
L->data[i] = '\0';
L->ECode = ECode;
L->next = NULL;
L = first;
}
//打印单链表
void print(LinkList LL) {
int i = 0;
LNode* first = LL;
while (LL->next != NULL) {
printf("位置%d ", LL->location);
LL = LL->next;
while (LL->data[i] != '\0') {
printf("%c", LL->data[i]);
i++;
}
switch (LL->ECode) {
case 1:
printf("关键字\n");
break;
case 2:
printf("标识符\n");
break;
case 3:
printf("常数\n");
break;
case 4:
printf("运算符\n");
break;
case 5:
printf("分界符\n");
break;
}
//printf("编码为%d", LL->ECode);
i = 0;
}
LL = first;
}
//读文件方法,测试
int readFile() {
FILE *fp;
char buff[100];
if((fopen_s(&fp, PATH, "r") != 0))//打开失败返回非0
return -1;
while (fgets(buff, 100, fp) != NULL) {
printf("%s", buff);
}
}
//以空格分割
void splitByBS() {
FILE* fp = NULL;
int i = 0;
char buff[100];
if ((fopen_s(&fp, PATH, "r") != 0))//打开失败返回非0
printf("打开文件失败!");
while (fgets(buff, 100, fp) != NULL) {//每次读取一行
char* ptr;
char word[100];
char* p = strtok_s(buff, " \t\n", &ptr);
while (p != NULL) {
while (*p != '\0') {
word[i++] = *p++;
}
word[i] = '\0';
printf("%s\n", word);
i = 0;
p = strtok_s(NULL, " \t\n", &ptr);
}
}
}
//是否是系统关键字,1表示是,-1表示不是
bool isExist(char string[]) {
char exist[37][10] = { {"char"},{"double"},{"enum"},{"float"},{"int"},{"long"} ,{"short"} ,{"signed"} ,
{"struct"} ,{"union"} ,{"unsigned"} ,{"void"} ,{"for"} ,{"do"} ,{"while"},{"break"},{"continue"},
{"if"},{"else"},{"goto"},{"switch"},{"case"},{"default"},{"return"},{"auto"},{"extern"},{"register"},{"static"},
{"const"},{"sizeof"},{"typedef"},{"volatile"},{"printf"},{"true"},{"bool"},{"false"} };
bool continue_ = false;
for (int i = 0; i < 35; i++) {
for (int j = 0; j < 35; j++) {
if (string[j] == exist[i][j]) {
continue_ = true;
}
else {
continue_ = false;
break;
}
if (string[j] == '\0' && exist[i][j] == '\0' && continue_ == true) {
return true;//与关键字重复
}
}
}
return false;
}
//判断是否是常数
bool isNum(char string[]){
bool continue_ = true;
int i = 0;
while (string[i] != '\0') {
if (string[i] <= '9' && string[i] >= '0' && continue_ == true) {
i++;
}
else {
continue_ = false;
return false;
}
}
if (string[i] == '\0' && continue_ == true) {
return true;
}
}
//是否为合法标识符 数字开头不合法
bool isLegal(char string[]) {
bool flag = false;
if (string[0] <= '9' && string[0] >= '0') {
printf("标识符%s不合法\n", string);
return flag;
}
return true;
}
//判断是否需要插入,成功插入返回true
bool isInsert(LinkList L, int* flag, char WaitWord[]) {
if (*flag != 0) {
if (*flag == 2 && isExist(WaitWord)) {//是否为系统关键字
InsertTail(L, WaitWord, 1);
*flag = 0;
return true;
}
if (*flag == 2 && isNum(WaitWord) ) {//是否为常数
InsertTail(L, WaitWord, 3);
*flag = 0;
return true;
}
if (*flag == 2) {//判断是否为合法标识符
isLegal(WaitWord);
InsertTail(L, WaitWord, *flag);//插入到表内
*flag = 0;
return true;
}
if (*flag == 5) {//是否为分界符
InsertTail(L, WaitWord, *flag);
*flag = 0;
return true;
}
if (*flag == 4) {//是否为运算符
InsertTail(L, WaitWord, *flag);
*flag = 0;
return true;
}
return false;
}
return false;
}
//传入的第二个参数是读取到的以空格分割的数据
void wordSort(LinkList L, char word[], bool *isNote) {
int i = 0;
int j = 0;
int k = 0;//给指针用
int* flag = &k;
bool continue_ = true;
char WaitWord[50];//等待判断的词
bool hasNormal = false;//是否是下面规定中出现的字符
while (word[i] != '\0') {
if (*isNote == true) {
while (word[i] != '*' && word[i + 1] != '/') {
++i;
}
if (word[i] == '*' && word[i + 1] == '/') {
*isNote = false;
break;
}
else {
break;
}
}
if(*isNote == false){
if (word[i] != '\0' && word[i] == '\"') {
++i;
while (word[i] != '\"' && word[i] != '\0') {
++i;
}
}
if (word[i] != '\0' && word[i] == '\'') {
++i;
while (word[i] != '\'' && word[i] != '\0') {
++i;
}
}
//遇上注释直接结束方法
if (word[i] != '\0' && word[i] == '/' && word[i + 1] == '/') {
return;
}
if (word[i] == '/' && word[i + 1] == '*') {
*isNote = true;
break;
}
//标识符或系统关键字
while (word[i] != '\0' && word[i] <= 'z' && word[i] >= 'a' ||
word[i] <= 'Z' && word[i] >= 'A' || word[i] == '_' ||
word[i] <= '9' && word[i] >= '0') {
hasNormal = true;
WaitWord[j] = word[i];
j++;i++;
*flag = 2;
}
WaitWord[j] = '\0';
j = 0;//将WaitWord下标重置
if (isInsert(L, flag, WaitWord)) {
hasNormal = false;
continue;
}
//isInsert(L, flag, WaitWord);
//hasNormal = false;
//运算符
while (word[i] != '\0' && word[i] == '+' || word[i] == '-' || word[i] == '*'
|| word[i] == '/' || word[i] == '>'
|| word[i] == '<' || word[i] == '=') {
WaitWord[j] = word[i];
j++;i++;
hasNormal = true;
*flag = 4;
}
WaitWord[j] = '\0';
j = 0;
if (isInsert(L, flag, WaitWord)) {
hasNormal = false;
continue;
}
//界符
//每读取到一个界符插入即可
if (word[i] != '\0' && (word[i] == ',' || word[i] == ';' || word[i] == '(' ||
word[i] == ')' || word[i] == '.' || word[i] == '#' || word[i] == '{' ||
word[i] == '}' || word[i] == '%' || word[i] == '&' || word[i] == '[' || word[i] == ']')) {
WaitWord[j] = word[i];
//j++;
hasNormal = true;
*flag = 5;
}
WaitWord[j + 1] = '\0';
isInsert(L, flag, WaitWord);
j = 0;
//遇上不认识的字符直接结束方法
if (word[i] != '\0' && hasNormal == false) {
return;
}
i++;
}
}
}
//分割读取的字符串方法
void split(LinkList L, bool *isNote) {
FILE* fp = NULL;
int i = 0;
char buff[100];
if ((fopen_s(&fp, PATH, "r") != 0))//打开失败返回非0
printf("打开文件失败!");
while (fgets(buff, 100, fp) != NULL) {//每次读取一行
char* ptr;
char word[100];
char* p = strtok_s(buff, " \t\n", &ptr);
while (p != NULL) {
while (*p != '\0') {
word[i++] = *p++;
}
word[i] = '\0';
wordSort(L, word, isNote);
i = 0;
p = strtok_s(NULL, " \t\n", &ptr);
}
}
}
//主方法
int main() {
bool F = false;
bool* isNote = &F;//该变量是用来判断是否有多行注释
LinkList L = Init();
splitByBS();
split(L, isNote);
print(L);
}
实验结果
包含多行注释。图一是程序读取到的源文件内容(分割后)。可以看到图二图三中,将单行注释和多行注释都去除了。