解析C/C++包含文件
本文版权归熊春雷 所有,我的邮箱:<pandaxcl@163.com>,欢迎大 家和我讨论计算机方面的问题,在我的博客 http://blog.csdn.net/pandaxcl上面还写了很多其他 的文档,有空来看看哦。如果转载,请保留此版权信息,并注明出处。 谢谢:)
摘要
在这一章里面将要涉及到处理C/C++的包含宏的解析。也就是说要从一大串C/C++ 包含文件的声明中提取出文件名,以及相互依赖关系等等。实际上在这一章里面 使用的Lex和Yacc技术也是非常重要的,这些都会在本章中进行详细讲解。
目录
1. 分析2. Lex文件3. Yacc文件4. main.hpp文件5. main.cpp文件6. Makefile文件7. sample.cpp文件8. 运行结果9. 总结
我们知道对于C/C++包含文件声明是为程序提供了一些库存的功能,因此存在一种依赖 关系,如果把这种依赖关系表达成为Makefile的形式,那么就可以自动生成Makefile 。在这一章里面并不会实现自动生成Makefile的功能,而是仅仅解析出所有的包含文 件名,并记录下来。
1. 分析
我们知道C/C++中存在两种形式的包含文件,一种是用“<>”包含的头文件, 一种是“""”包含的头文件,这两种不同的形式表达了头文件的不同的搜索方式。 另外还需要注意的是:这两种方式包含的都是磁盘上存在的文件名。也就是说,只 要是磁盘上存在的文件名都可以包含的,都是合法的,因而C/C++里面存在的有扩展 名的头文件和没有扩展名的头文件都是合法的。并且还需要注意的是C/C++包含的头 文件是可以续行的。
因而总结起来需要做到如下的几件事情:
处理“<>”和“""”两种包含方式
处理文件名
处理续行
2. Lex文件
%{
#include "main.hpp"// 在其中保存了记录头文件所需要的所有数据结构
#include "frame.tab.h"// 由Yacc自动生成的所有标记声明,实际上都是C宏
extern "C"{
int yywrap(void);
int yylex(void);
}
%}
%x _INCLUDE_
%x _INCLUDE_FILE_
%%
"#"[ \t]*"include" {
BEGIN _INCLUDE_;// 进入_INCLUDE_状态
yylval.clear();// 需要将所有的Include值初始化
return INCLUDE;// 返回INCLUDE标记
}
<_INCLUDE_>[\"|<] {
BEGIN _INCLUDE_FILE_;// 进入_INCLUDE_FILE_状态
return *yytext; // 返回引号或者尖括号
}
<_INCLUDE_FILE_>[^\">]* {
yylval.headerfile+=yytext;// 记录头文件字符串
return HEADERFILE;// 返回头文件标记
}
<_INCLUDE_FILE_>[\"|>] {
BEGIN INITIAL;// 恢复到初始状态,默认状态
return *yytext;// 返回引号或者尖括号
}
[ \t\n] ;// 对于额外的空白都不处理直接扔掉
%%
int yywrap(void)
{
return 1;// 只处理一个输入文件
}
3. Yacc文件
%{
#include <iostream>
#include "main.hpp"
#define YYDEBUG 0 // 将这个变量设置为1则表示启动Yacc的调试功能
extern "C"{
void yyerror(const char *s);
extern int yylex(void);
}
std::vector<Include> g_Includes;// 用来记录所有的包含声明
Include *g_pInclude;// 用来保存新增的包含声明信息的指针
%}
%token INCLUDE
%token HEADERFILE
%%
program:/* empty */
| program include_preprocess // 用这种递归的方式从有限的标记表达出无限的内容
;
include_preprocess:
INCLUDE '<' HEADERFILE '>'
{
// 注意这里的$3,实际上就是上面的标记的第三个的意思
// 因为yylval被声明为Include结构,参见main.hpp文件
// 因而每个标记都是Include结构类型。
g_Includes.push_back(Include());
g_pInclude = &g_Includes.back();
g_pInclude->clear();// 初始化
g_pInclude->headerfile = $3.headerfile;// 可以证明$3的类型就是Include类型
g_pInclude->is_angle = true;// 是尖括号
g_pInclude->is_quotation = false;// 不是双引号
}
| INCLUDE '\"' HEADERFILE '\"'
{
// 值得说明的是:上面的include_preprocess用$表示,
// 而不是用$0表示。从左向右依次为:
// include_preprocess $
// INCLUDE $1
// '\"' $2
// HEADERFILE $3
// '\"' $4
g_Includes.push_back(Include());
g_pInclude = &g_Includes.back();
g_pInclude->clear();// 初始化
g_pInclude->headerfile = $3.headerfile;
g_pInclude->is_angle = false;// 不是尖括号
g_pInclude->is_quotation = true;// 是双引号
}
;
%%
void yyerror(const char *s)
{
std::cerr<< s << std::endl;
}
int main()
{
#if YYDEBUG
yydebug = 1;
#endif//YYDEBUG
yyparse();// 进行语法分析,这个函数是Yacc自动生成的
// 下面的这行代码仅仅使用了STL的输出算法到标准输出
std::copy(g_Includes.begin(),g_Includes.end(),std::ostream_iterator<Include>(std::cout,"\n"));
return 0;
}
4. main.hpp文件
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <iterator>
// 对于每一个项目最好都用一个独立的数据结构来保存相应的信息
struct Include
{
void clear();// 设置Include的初始值
std::string headerfile;// 记录头文件全名(包括路径)
bool is_quotation;// 是否是双引号""括起来的头文件
bool is_angle;// 是否是尖括号<>括起来的头文件
// 下面的这个函数仅仅是用来输出到C++流而准备的
friend std::ostream&operator<<(std::ostream&s,const Include&I);
};
std::ostream&operator<<(std::ostream&s,const Include&I);
// 下面的这个宏定义用来取消Lex和Yacc默认的YYSTYPE定义,因为默认的YYSTYPE定义
// 仅仅只能够记录整数信息,因此要保存额外的信息必须这样定义宏,可以参见Yacc
// 自动生成的标记头文件frame.tab.h。
#define YYSTYPE Include
5. main.cpp文件
#include "main.hpp"
// 初始化所有的Include信息,避免前后关联
void Include::clear()
{
headerfile.clear();
is_quotation = false;
is_angle = false;
}
// 为了能够方便输出,在这里直接准备好了一个流输出函数
std::ostream&operator<<(std::ostream&s,const Include&I)
{
if(I.is_angle)
s << "采用尖括号的" ;
if(I.is_quotation)
s << "采用双引号的" ;
s << "头文件:[" << I.headerfile << "]" ;
return s;
}
6. Makefile文件
LEX=flex
YACC=bison
CC=g++
a.exe:lex.yy.o frame.tab.o main.o
$(CC) lex.yy.o frame.tab.o main.o -o a.exe
lex.yy.o:lex.yy.c frame.tab.h main.hpp
$(CC) -c lex.yy.c
frame.tab.o:frame.tab.c main.hpp
$(CC) -c frame.tab.c
main.o:main.hpp main.cpp
$(CC) -c main.cpp
frame.tab.c frame.tab.h:frame.y
$(YACC) -d frame.y
lex.yy.c:frame.l
$(LEX) frame.l
clean:
rm -f *.o *.c *.h
7. sample.cpp文件
#include <iostream>
#include <string>
#include <ffmpeg/avformat.h>
#include <ffmpeg/avcodec.h>
#include <ffmpeg/avutils.h>
#include <stdio.h>
#include <stdlib.h>
#include "hello.h"
#include "../hello.h"
# include "space.h"
8. 运行结果
pandaxcl@PANDAXCL-F78E7D /d/work/lex_yacc/chapter06
$ make
flex frame.l
bison -d frame.y
g++ -c lex.yy.c
g++ -c frame.tab.c
g++ -c main.cpp
g++ lex.yy.o frame.tab.o main.o -o a.exe
pandaxcl@PANDAXCL-F78E7D /d/work/lex_yacc/chapter06
$ ./a.exe < sample.cpp
采用尖括号的头文件:[iostream]
采用尖括号的头文件:[string]
采用尖括号的头文件:[ffmpeg/avformat.h]
采用尖括号的头文件:[ffmpeg/avcodec.h]
采用尖括号的头文件:[ffmpeg/avutils.h]
采用尖括号的头文件:[stdio.h]
采用尖括号的头文件:[stdlib.h]
采用双引号的头文件:[hello.h]
采用双引号的头文件:[../hello.h]
采用双引号的头文件:[space.h]
pandaxcl@PANDAXCL-F78E7D /d/work/lex_yacc/chapter06
$
9. 总结
总的来说,上面的解析C/C++包含预处理信息的时候需要了解如下的概念:
词法状态
所谓的词法状态就是指对文本进行词法分析的时候,词法分析器当前所处 的状态,默认情况下,词法分析器都处于INITIAL状态,这个INITIAL状态 是Lex内置的状态。用户可以通过%x来重新定义各种各样的状态。
至于为什么要使用状态,我们来看一个实际的例子:上面分析头文件的时候 采用了两个自定义的状态:_INCLUDE_状态和_INCLUDE_FILE_状态, _INCLUDE_状态是当遇到了#include开始的,因为这个状态之后是尖括号或者 是双引号括起来的头文件名,在后面分析模板(使用尖括号)和分析字符串 (使用双引号)的时候也会遇到尖括号和双引号,因而需要区分这两种情况 ,所以才需要使用_INCLUDE_状态,以此来区分是包含文件还是模板或者是字 符串了。这一点非常重要!
同样,状态_INCLUDE_FILE_存在也是为了区分双引号包含的头文件名称的, 因为双引号不同于尖括号,双引号在头文件名的开始和结束都是相同的,因 此为了区分头部和尾部的双引号,必须再增加一个状态。实际上这可以用来 简化词法分析器的编写,当您遇到这种类似的问题的时候可以考虑再增加一 种新的状态,通常来说就可以解决问题啦:)
不过还有一点特别需要强调的是当您感觉所添加的状态太多了,出现了混乱 现象,就说明用Lex状态已经不大适合处理这种问题了,就应该考虑采用Yacc 的一条独立的语法规则来进行处理了:)这也是Yacc语法文件存在的原因,要 不然全部都可以采用词法分析文件来解决啦,还要语法分析文件干什么!
递归表达
这里需要特别注意的是:frame.y文件中program的构成采用了左递归的形 式。从代码中可以看出:program可以是空(什么也没有)也可以是由现有 的program内容再追加一条include_preprocess类构成。当program内容为 空的时候增加一条include_preprocess类就表示program只有一条 include_preprocess内容,当program已经有了一条include_preprocess内 容之后再增加一条include_preprocess内容就可以表示两条 include_preprocess内容了,依次类推,可以表达无数的包含信息,从而 表达了无限的内容了。特别需要注意的是,这里的program表示的仅仅是现 有的内容,包括但不限于include_preprocess内容,还可以有其他的内容 ,这一点可以在增加其他内容的时候体现出来,因为C/C++源代码不仅仅是 由包含信息构成的嘛:)
特别需要注意的是,这里要特表强调一下使用左递归,不是说右递归不行,而 是出于程序运行效率考虑最好使用左递归。具体原因在后续的文档中会有详细 的说明的:)
YYSTYPE, yylval, $$,$1,$2,...$n
因为编写词法分析程序和语法分析程序的目的就是为了操作分析出来的数据 ,所以就需要有一种比较方便的形式来表达这些分析出来的数据。一种是词 法分析程序使用的方式,叫做yylval;一种是语法分析程序使用的,叫做$n ,从上面的词法分析程序和语法分析程序中已经可以看到它们在相应的文件 中的使用了。
至于YYSTYPE那就更简单了,因为要表达词法分析程序和语法分析程序中的数据 ,既然是数据,在C/C++中就有数据类型的概念,这里的YYSTYPE就是yylval和 $n的数据类型。
特别需要注意的是,语法分析程序中每一个语法规则冒号左边的类的值用$$表 示,而冒号右边的第一项用$1表示,第二项用$2表示,依次类推。
标记和值
标记指的是由%token定义的INCLUDE和HEADERFILE,他们都对应着一个具体 值,而且具体值类型还有可能完全不一样。这里需要特别强调的一点是: 每一个标记都对应着一个值,你可以不使用,但是他就是确确实实存在着 ,而且从始至终都保持着这种对应。例如上面的INCLUDE标记的值就没有使 用,但是HEADERFILE标记的值就被使用了。在Lex和Yacc中标记都是用一个 C宏定义的一个整数,而标记的值都是由YYSTYPE定义着的一个变量,这个 变量的名字就是yylval,其中保存着相关的信息,这个信息就是在词法分 析文件中进行设置的,而在语法分析文件中就直接采用了。
实际上%token还可以更进一步的简化Yacc语法程序的编写,从而避免一些不 必要的错误。从上面的语法分析来看,对于不同的$n,还需要记住$n的精确 类型和变量名,这一点其实是不必要的,可以通过%token <headerfile> HEADERFILE来声明标记,那么在Yacc程序的语法规则 中就可以直接使用$3来表示yylval.headerfile了,从而也就不需要记住那 些具体变量名啦:)
值得注意的是,尽管标记是可以用%token来定义,但是并不仅仅限于这种方 式,Yacc中还可以用%type来定义,采用%type来定义的目的就是为那些不是 标记的类也准备一个对应的值的,例如:完全可以为include_preprocess定 义一个值,用来保存一些额外的信息,不过本文中并不需要,后续的文档中 就会需要这个功能了,在此先简单说明一下:)
词法动作
对于词法分析程序中的每一个正则表达式对应的规则,都有相应的C/C++ 语句来做一些额外的处理,这个额外的处理就是词法动作。
语法动作
对于语法分析程序中的每一个语法规则,都有相应的C/C++语句来做一些额 外的处理,这个额外的处理就是语法动作。不过语法动作和词法动作的不同 之处在于,语法动作允许嵌入式的语法动作,而词法动作不行。至于什么是 嵌入式的语法动作,在后续的文档中会有详细的说明的!
好了,本章中还残留有一些问题故意没有解决,例如:包含文件的续行问题!留个 读者自己思考,可以在本文所讨论的基础上稍微改动一下就可以了。后续的文档正 在努力写出,敬请关注;)