当我们写好一段代码之后,我们通常的操作是:编译,链接, 执行。
而在编译阶段,我们的编译器就会进行词法分析这个阶段,来分析有没有词法错误。
而单词符号一般分为五大类:
1.关键字,也称保留字,比如:if,else,sizeof等(c语言中由32个关键字,在这里也就不一一列举);
2.标识符,用来表示各种名字,如:常量名,变量名,函数名等;
3.常数,各种类型的常数,如:23,3.1415,TURE,"ABC"等;
4.运算符,如:+,*,<=等;
5.界符,如:逗号,分号等。
因此,词法分析程序采用二元式的形式来输出。
比如:(1,“if”)这就是说当前遇到了一个关键字"if"。
要达到上面的这种输出模式,那么首先要进行的就是对一段代码我们应该怎样去识别它到底是属于上述5种类型的哪一类。
接下来,我们进行一个简单的分析:
我们可以利用文件指针,再使用getchar()来逐个读取字符来进行判断。
但是,我们这5种类型的单词符号不一定都是一个字符呀!而getchar()只能获取当前指针所指的下一个字符。
所以,我们应该首先定义一个来存放这一串字符的中间数组。并且可以将它作为一个全局变量。
但是,getchar()每次都会自动将文件指针下移,所以当我们对一个单词符号打印完之后,我们应该利用sweek(,,)对指针回退一个字符,后边代码会介绍这个函数。
在这里可能有的人就想不通了,为什么要回退一个字符呢?
我给大家举个例子:有这样的代码:
int x=5;
首先,获取字符'i',判断其为字母后,相继获取'n','t'直到获取到空格,然后与之前定义好的关键字数组相比较,发现他是关键字,并且打印.
此时,文件指针指向了空格,再一次获取到'x',同理当判断到'='的时候,两者不属于同一符号.将x打印.
此时,文件指针指向了'=',这是问题就出现了,当我们要接着判断时,我们获取到的字符是'5',而不是'='(getchar()会自动将文件指针下移动,先移动,再获取),所以程序就会出现纰漏.所以,就需要引入回退.
(1).关键字和表示符判断。
(2).常数判断
(3)运算符和界符判断
//1.关键字,保留字 main int double等
//2.标识符 自己定义的变量名,常量名等
//3.常数
//4.运算符+ - < > * /等
//5.界符(,;)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#define max 37
char *Position[max]={"auto","break","case","char","const","continue","default","do","double",
"else","enum","extern","float","for","if","int","long","register","restrict","return",
"short","signed","sizeof","static","struct","switch","typedef","union","unsigned","void",
"volatile","while"}; //自定义关键字
char Mid[20]; //中间缓冲区
void initfunctionMid() //缓冲区清除 每打印一次就对中间数组进行清空
{
int i;
for(i=0;i<20;i++)
Mid[i]='\0';
}
int Judgeletter(char ch)//判断字母
{
if((ch>='a'&&ch<='z')||(ch>='A'&&ch<='Z'))
return 1;
else
return 0;
}
int Judgenumber(char ch) //判断数字
{
if(ch>='0'&&ch<='9')
return 1;
else
return 0;
}
int Judgedelimiter(char ch) //判断界符
{
switch(ch)
{
case '.':
case '(':
case ')':
case ',':
case ';':
case ':': //这里自己还可以增加一些界符
case '{':
case '}':
case '[':
case ']':
case '"': return 1;
default:return 0;
}
}
int Judgeoperator(char ch) //判断运算符
{
switch(ch)
{
case '+':
case '-':
case '*':
case '/': //同上,可以自己添加
case '=':
case '<':
case '>':
case '&':
case '|':
case '%': return 1;
default:return 0;
}
}
int Judgekeyword(char *Mid)//判断关键字
{
int i;
for(i=0;i<max;i++)
{
if(strcmp(Mid,Position[i])==0) //strcmp()判断两个参数的大小,相等返回0,小于返回-1,大于返回1
{
return 1;
break;
}
}
if(i=max-1)
return 0;
}
void print(int a) //输出函数 变量a就是符号类型
{
printf("(%d,%s)\n",a,Mid); //a为种类(对应1---5行)
}
void WholeJudge(FILE *fp) //整体判断
{
char ch;
int i=0; //Mid下标归0;保证每次输出完之后存入数据从第一个开始
int j=0; //监视判错
while(1)
{
//判断是变量名还是保留字
ch=fgetc(fp);
if(Judgeletter(ch)==1)
{
Mid[i++]=ch;
ch=fgetc(fp);
while(Judgeletter(ch)==1||Judgenumber(ch)==1)
{
Mid[i++]=ch;
ch=fgetc(fp);
}
}
if(strlen(Mid)!=0)
{
if(Judgekeyword(Mid)==1)
print(1);
else
print(2);
}
if(!(ch==' '||ch==10||ch==13||ch==9)) //防止一个变量或保留字后边为空格等导致陷入死循环
fseek(fp,-1,SEEK_CUR); //fp指针回退一个字符 fseek(,,)有三个参数,第一个为文件指针,第二个为位移量,第三个是位移基准.用来移动指针
initfunctionMid(); //缓冲区清除
i=0; //Mid下标归0;保证每次输出完之后存入数据从第一个开始
//判断常数
ch=fgetc(fp);
if(feof(fp)) //判断文件结束
break;
while(Judgenumber(ch)==1)
{
Mid[i++]=ch;
ch=fgetc(fp);
if(Judgeletter(ch)==1)//判断数字开头是否后接字母
{
j=1;
break;
}
}
if(j==1) //判错
{
printf("下边运行出错!标识符不能以数字开头!请修改!");
break;
}
if(strlen(Mid)!=0)//如果中间数组不是空,就打印
print(3);
fseek(fp,-1,SEEK_CUR);
initfunctionMid();
i=0;
//判断运算符
ch=fgetc(fp);
while(Judgeoperator(ch)==1)
{
Mid[i++]=ch;
ch=fgetc(fp);
}
if(i>2) //运算符数量判错
{
printf("下方运算符出错!请修改!");
break;
}
if(strlen(Mid)!=0)
print(4);
fseek(fp,-1,SEEK_CUR);
initfunctionMid();
i=0;
//判断界符
ch=fgetc(fp);
if(Judgedelimiter(ch)==1)
{
Mid[i]=ch;
print(5);
}
if(strlen(Mid)==0)
fseek(fp,-1,SEEK_CUR);
initfunctionMid();
i=0;
}
}
int main()
{
char Filename[20];
FILE *fp;
char ch;
printf("请输入文件地址:");
scanf("%s",Filename);
fp=fopen(Filename,"r");
ch=fgetc(fp);
while(ch==' '||ch==10||ch==13||ch==9)//忽略空格,换行,回车和Tab
{
ch=fgetc(fp);
}
fseek(fp,-1,SEEK_CUR);//SEEK_END文件末尾 SEEK_CUR文件当前指针位置
WholeJudge(fp);
fclose(fp);
return 0;
}
当然,如果你有兴趣,还可以将我们的词法分析器做的更加完美,在这里我就只给大家提供一下思路。