奥门新浦京官方网站Lex Yacc学习

在这之前,我曾经尝试过一个项目,就是将我们的PHP代码自动生成so扩展,

前言

C语言预处理命令详解

一  前言

   
 预处理(或称预编译)是指在进行编译的第一遍扫描(词法扫描和语法分析)之前所作的工作。预处理指令指示在程序正式编译前就由编译器进行的操作,可放在程序中任何位置。

 

   
 预处理是C语言的一个重要功能,它由预处理程序负责完成。当对一个源文件进行编译时,系统将自动引用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。

 

   
 C语言提供多种预处理功能,主要处理#开始的预编译指令,如宏定义(#define)、文件包含(#include)、条件编译(#ifdef)等。合理使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。

 

   
 本文参考诸多资料,详细介绍常用的几种预处理功能。因成文较早,资料来源大多已不可考,敬请谅解。

 

 

 

 

 

二  宏定义

   
 C语言源程序中允许用一个标识符来表示一个字符串,称为“宏”。被定义为宏的标识符称为“宏名”。在编译预处理时,对程序中所有出现的宏名,都用宏定义中的字符串去代换,这称为宏替换或宏展开。

 

   
 宏定义是由源程序中的宏定义命令完成的。宏替换是由预处理程序自动完成的。

 

   
 在C语言中,宏定义分为有参数和无参数两种。下面分别讨论这两种宏的定义和调用。

 

2.1 无参宏定义

     无参宏的宏名后不带参数。其定义的一般形式为:

 

        #define  标识符  字符串

 

   
 其中,“#”表示这是一条预处理命令(以#开头的均为预处理命令)。“define”为宏定义命令。“标识符”为符号常量,即宏名。“字符串”可以是常数、表达式、格式串等。

 

   
 宏定义用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名。这只是一种简单的文本替换,预处理程序对它不作任何检查。如有错误,只能在编译已被宏展开后的源程序时发现。

 

   
 注意理解宏替换中“换”的概念,即在对相关命令或语句的含义和功能作具体分析之前就要进行文本替换。

 

   【例1】定义常量:

 

1 #define MAX_TIME 1000

     若在程序里面写if(time <
MAX_TIME){………},则编译器在处理该代码前会将MAX_TIME替换为1000。

 

     注意,这种情况下使用const定义常量可能更好,如const int MAX_TIME =
1000;。因为const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查,而对后者只进行简单的字符文本替换,没有类型安全检查,并且在字符替换时可能会产生意料不到的错误。

 

    【例2】反例:

 

1 #define pint (int*)

2 pint pa, pb;

     本意是定义pa和pb均为int型指针,但实际上变成int*
pa,pb;。pa是int型指针,而pb是int型变量。本例中可用typedef来代替define,这样pa和pb就都是int型指针了。因为宏定义只是简单的字符串代换,在预处理阶段完成,而typedef是在编译时处理的,它不是作简单的代换,而是对类型说明符重新命名,被命名的标识符具有类型定义说明的功能。typedef的具体说明见附录6.4。

 

     无参宏注意事项:

 

宏名一般用大写字母表示,以便于与变量区别。

宏定义末尾不必加分号,否则连分号一并替换。

宏定义可以嵌套。

可用#undef命令终止宏定义的作用域。

使用宏可提高程序通用性和易读性,减少不一致性,减少输入错误和便于修改。如数组大小常用宏定义。

预处理是在编译之前的处理,而编译工作的任务之一就是语法检查,预处理不做语法检查。

宏定义写在函数的花括号外边,作用域为其后的程序,通常在文件的最开头。

字符串” “中永远不包含宏,否则该宏名当字符串处理。

宏定义不分配内存,变量定义分配内存。

2.2 带参宏定义

   
 C语言允许宏带有参数。在宏定义中的参数称为形式参数,在宏调用中的参数称为实际参数。

 

     对带参数的宏,在调用中,不仅要宏展开,而且要用实参去代换形参。

 

     带参宏定义的一般形式为:

 

       #define  宏名(形参表)  字符串

 

     在字符串中含有各个形参。

 

     带参宏调用的一般形式为:

 

宏名(实参表);

 

     在宏定义中的形参是标识符,而宏调用中的实参可以是表达式。

 

   
 在带参宏定义中,形参不分配内存单元,因此不必作类型定义。而宏调用中的实参有具体的值,要用它们去代换形参,因此必须作类型说明,这点与函数不同。函数中形参和实参是两个不同的量,各有自己的作用域,调用时要把实参值赋予形参,进行“值传递”。而在带参宏中只是符号代换,不存在值传递问题。

 

    【例3】

 

1 #define INC(x) x+1  //宏定义

2 y = INC(5);         //宏调用

     在宏调用时,用实参5去代替形参x,经预处理宏展开后的语句为y=5+1。

 

    【例4】反例:

 

1 #define SQ(r) r*r

   
 上述这种实参为表达式的宏定义,在一般使用时没有问题;但遇到如area=SQ(a+b);时就会出现问题,宏展开后变为area=a+b*a+b;,显然违背本意。

 

   
 相比之下,函数调用时会先把实参表达式的值(a+b)求出来再赋予形参r;而宏替换对实参表达式不作计算直接地照原样代换。因此在宏定义中,字符串内的形参通常要用括号括起来以避免出错。

 

   
 进一步地,考虑到运算符优先级和结合性,遇到area=10/SQ(a+b);时即使形参加括号仍会出错。因此,还应在宏定义中的整个字符串外加括号,

 

     综上,正确的宏定义是#define SQ(r)
((r)*(r)),即宏定义时建议所有的层次都要加括号。

 

    【例5】带参函数和带参宏的区别:

 

复制代码

 1 #define SQUARE(x) ((x)*(x))

 2 int Square(int x){

 3     return (x * x); //未考虑溢出保护

 4 }

 5 

 6 int main(void){

 7     int i = 1;

 8     while(i <= 5)

 9         printf(“i = %d, Square = %dn”, i, Square(i++));

10 

11     int j = 1;

12     while(j <= 5)

13         printf(“j = %d, SQUARE = %dn”, j, SQUARE(j++));

14     

15     return 0;

16 }

复制代码

     执行后输出如下:

 

复制代码

1 i = 2, Square = 1

2 i = 3, Square = 4

3 i = 4, Square = 9

4 i = 5, Square = 16

5 i = 6, Square = 25

6 j = 3, SQUARE = 1

7 j = 5, SQUARE = 9

8 j = 7, SQUARE = 25

复制代码

   
 本例意在说明,把同一表达式用函数处理与用宏处理两者的结果有可能是不同的。

 

   
 调用Square函数时,把实参i值传给形参x后自增1,再输出函数值。因此循环5次,输出1~5的平方值。

 

   
 调用SQUARE宏时,SQUARE(j++)被代换为((j++)*(j++))。在第一次循环时,表达式中j初值为1,两者相乘的结果为1。相乘后j自增两次变为3,因此表达式中第二次相乘时结果为3*3=9。同理,第三次相乘时结果为5*5=25,并在此次循环后j值变为7,不再满足循环条件,停止循环。

 

   
 从以上分析可以看出函数调用和宏调用二者在形式上相似,在本质上是完全不同的。

 

     带参宏注意事项:

 

宏名和形参表的括号间不能有空格。

宏替换只作替换,不做计算,不做表达式求解。

函数调用在编译后程序运行时进行,并且分配内存。宏替换在编译前进行,不分配内存。

宏的哑实结合不存在类型,也没有类型转换。

函数只有一个返回值,利用宏则可以设法得到多个值。

宏展开使源程序变长,函数调用不会。

宏展开不占用运行时间,只占编译时间,函数调用占运行时间(分配内存、保留现场、值传递、返回值)。

为防止无限制递归展开,当宏调用自身时,不再继续展开。如:#define TEST(x)
 (x + TEST(x))被展开为1 + TEST(1)。

2.3 实践用例

     包括基本用法(及技巧)和特殊用法(#和##等)。

 

   
 #define可以定义多条语句,以替代多行的代码,但应注意替换后的形式,避免出错。宏定义在换行时要加上一个反斜杠””,而且反斜杠后面直接回车,不能有空格。

 

2.3.1 基本用法

     1. 定义常量:

 

1 #define PI   3.1415926

     将程序中出现的PI全部换成3.1415926。

 

     2. 定义表达式:

 

1 #define M   (y*y+3*y)

   
 编码时所有的表达式(y*y+3*y)都可由M代替,而编译时先由预处理程序进行宏替换,即用(y*y+3*y)表达式去置换所有的宏名M,然后再进行编译。

 

   
 注意,在宏定义中表达式(y*y+3*y)两边的括号不能少,否则可能会发生错误。如s=3*M+4*M在预处理时经宏展开变为s=3*(y*y+3*y)+4*(y*y+3*y),如果宏定义时不加括号就展开为s=3*y*y+3*y+4*y*y+3*y,显然不符合原意。因此在作宏定义时必须十分注意。应保证在宏替换之后不发生错误。

 

     3. 得到指定地址上的一个字节或字:

 

1 #define MEM_B(x)     (*((char *)(x)))

2 #define MEM_W(x)     (*((short *)(x)))

     4. 求最大值和最小值:

 

1 #define MAX(x, y)     (((x) > (y)) ? (x) : (y))

2 #define MIN(x, y)     (((x) < (y)) ? (x) : (y))

     以后使用MAX (x,y)或MIN (x,y),就可分别得到x和y中较大或较小的数。

 

     但这种方法存在弊病,例如执行MAX(a++,
b)时,a++被执行多少次取决于a和b的大小!所以建议用内联函数而不是这种方法提高速度。不过,虽然存在这样的弊病,但宏定义非常灵活,因为a和b可以是各种数据类型。

 

     5. 得到一个成员在结构体中的偏移量(lint
545告警表示”&用法值得怀疑”,此处抑制该警告):

 

1 #define FPOS( type, field )

2 /*lint -e545 */ ((int)&((type *)0)-> field) /*lint +e545 */

     6. 得到一个结构体中某成员所占用的字节数:

 

1 #define FSIZ(type, field)    sizeof(((type *)0)->field)

     7. 按照LSB格式把两个字节转化为一个字(word):

 

1 #define FLIPW(arr)          ((((short)(arr)[0]) * 256) +
(arr)[1])

     8. 按照LSB格式把一个字(word)转化为两个字节:

 

1 #define FLOPW(arr, val )

2     (arr)[0] = ((val) / 256);

3     (arr)[1] = ((val) & 0xFF)

     9. 得到一个变量的地址:

 

1 #define B_PTR(var)       ((char *)(void *)&(var))

2 #define W_PTR(var)       ((short *)(void *)&(var))

     10. 得到一个字(word)的高位和低位字节:

 

1 #define WORD_LO(x)       ((char)((short)(x)&0xFF))

2 #define WORD_HI(x)       ((char)((short)(x)>>0x8))

     11. 返回一个比X大的最接近的8的倍数:

 

1 #define RND8(x)           ((((x) + 7) / 8) * 8)

     12. 将一个字母转换为大写:

 

1 #define UPCASE(c)         (((c) >= ‘a’ && (c) <= ‘z’) ? ((c) –
0x20) : (c))

     13. 判断字符是不是10进值的数字:

 

1 #define ISDEC(c)          ((c) >= ‘0’ && (c) <= ‘9’)

     14. 判断字符是不是16进值的数字:

 

1 #define ISHEX(c)          (((c) >= ‘0’ && (c) <= ‘9’) ||

2     ((c) >= ‘A’ && (c) <= ‘F’) ||

3     ((c) >= ‘a’ && (c) <= ‘f’))

     15. 防止溢出的一个方法:

 

1 #define INC_SAT(val)      (val = ((val)+1 > (val)) ? (val)+1 :
(val))

     16. 返回数组元素的个数:

 

1 #define ARR_SIZE(arr)     (sizeof((arr)) / sizeof((arr[0])))

     17. 对于IO空间映射在存储空间的结构,输入输出处理:

 

1 #define INP(port)           (*((volatile char *)(port)))

2 #define INPW(port)          (*((volatile short *)(port)))

3 #define INPDW(port)         (*((volatile int *)(port)))

4 #define OUTP(port, val)     (*((volatile char *)(port)) =
((char)(val)))

5 #define OUTPW(port, val)    (*((volatile short *)(port)) =
((short)(val)))

6 #define OUTPDW(port, val)   (*((volatile int *)(port)) =
((int)(val)))

     18. 使用一些宏跟踪调试:

 

   
 ANSI标准说明了五个预定义的宏名(注意双下划线),即:__LINE__、__FILE
__、__DATE__、__TIME__、__STDC __。

 

   
 若编译器未遵循ANSI标准,则可能仅支持以上宏名中的几个,或根本不支持。此外,编译程序可能还提供其它预定义的宏名(如__FUCTION__)。

 

   
 __DATE__宏指令含有形式为月/日/年的串,表示源文件被翻译到代码时的日期;源代码翻译到目标代码的时间作为串包含在__TIME__中。串形式为时:分:秒。

 

   
 如果实现是标准的,则宏__STDC__含有十进制常量1。如果它含有任何其它数,则实现是非标准的。

 

   
 可以借助上面的宏来定义调试宏,输出数据信息和所在文件所在行。如下所示:

 

1 #define MSG(msg, date)    
 printf(msg);printf(“[%d][%d][%s]”,date,__LINE__,__FILE__)

     19. 用do{…}while(0)语句包含多语句防止错误:

 

1 #define DO(a, b) do{

2     a+b;

3     a++;

4 }while(0)

     20. 实现类似“重载”功能

 

   
 C语言中没有swap函数,而且不支持重载,也没有模板概念,所以对于每种数据类型都要写出相应的swap函数,如:

 

1 IntSwap(int *,  int *);  

2 LongSwap(long *,  long *);  

3 StringSwap(char *,  char *); 

     可采用宏定义SWAP(t,x,y)以交换t类型的两个参数(要使用程序块结构):

 

复制代码

 1 #define SWAP(t, x, y) do{

 2     t temp = *y;

 3     *y = *x;

 4     *x = temp;

 5 }while(0)

 6 

 7 int main(void){

 8     int a = 10, b = 5;

 9     SWAP(int, &a, &b);

10     printf(“a=%d, b=%dn”, a, b);

11     return 0;

12 }

复制代码

     21. 1年中有多少秒(忽略闰年问题) :

 

1 #define SECONDS_PER_YEAR    (60UL * 60 * 24 * 365)

   
 该表达式将使一个16位机的整型数溢出,因此用长整型符号L告诉编译器该常数为长整型数。

 

     注意,不可定义为#define SECONDS_PER_YEAR (60 * 60 * 24 *
365)UL,否则将产生(31536000)UL而非31536000UL,这会导致编译报错。

 

     以下几种写法也正确:

 

1 #define SECONDS_PER_YEAR    60 * 60 * 24 * 365UL

2 #define SECONDS_PER_YEAR    (60UL * 60UL * 24UL * 365UL)

3 #define SECONDS_PER_YEAR    ((unsigned long)(60 * 60 * 24 *
365))

     22. 取消宏定义:

 

          #define [MacroName] [MacroValue]       //定义宏

 

          #undef [MacroName]                               //取消宏

 

   
 宏定义必须写在函数外,其作用域为宏定义起到源程序结束。如要终止其作用域可使用#undef命令:

 

复制代码

1 #define PI   3.14159

2 int main(void){

3     //……

4 }

5 #undef PI

6 int func(void){

7     //……

8 }

复制代码

     表示PI只在main函数中有效,在func1中无效。

 

2.3.2 特殊用法

     主要涉及C语言宏里#和##的用法,以及可变参数宏。

 

2.3.2.1 字符串化操作符#

   
 在C语言的宏中,#的功能是将其后面的宏参数进行字符串化操作(Stringfication),简单说就是将宏定义中的传入参数名转换成用一对双引号括起来参数名字符串。#只能用于有传入参数的宏定义中,且必须置于宏定义体中的参数名前。例如:

 

1 #define EXAMPLE(instr)      printf(“The input string is:t%sn”,
#instr)

2 #define EXAMPLE1(instr)     #instr

     当使用该宏定义时,example(abc)在编译时将会展开成printf(“the input
string is:t%sn”,”abc”);string str=example1(abc)将会展成string
str=”abc”。

 

     又如下面代码中的宏:

 

1 define WARN_IF(exp) do{

2     if(exp)

3         fprintf(stderr, “Warning: ” #exp”n”);

4 }while(0)

     则代码WARN_IF (divider == 0)会被替换为:

 

1 do{

2     if(divider == 0)

3         fprintf(stderr, “Warning” “divider == 0” “n”);

4 }while(0)

     这样,每次divider(除数)为0时便会在标准错误流上输出一个提示信息。

 

     注意#宏对空格的处理:

 

忽略传入参数名前面和后面的空格。如str= example1(   abc )会被扩展成
str=”abc”。

当传入参数名间存在空格时,编译器会自动连接各个子字符串,每个子字符串间只以一个空格连接。如str=
example1( abc    def)会被扩展成 str=”abc def”。

2.3.2.2 符号连接操作符##

   
 ##称为连接符(concatenator或token-pasting),用来将两个Token连接为一个Token。注意这里连接的对象是Token就行,而不一定是宏的变量。例如:

 

1 #define PASTER(n)     printf( “token” #n ” = %d”, token##n)

2 int token9 = 9;

     则运行PASTER(9)后输出结果为token9 = 9。

 

   
 又如要做一个菜单项命令名和函数指针组成的结构体数组,并希望在函数名和菜单项命令名之间有直观的、名字上的关系。那么下面的代码就非常实用:

 

1 struct command{

2     char * name;

3     void (*function)(void);

4 };

5 #define COMMAND(NAME)   {NAME, NAME##_command}

   
 然后,就可用一些预先定义好的命令来方便地初始化一个command结构的数组:

 

1 struct command commands[] = {

2     COMMAND(quit),

3     COMMAND(help),

4     //…

5 }

   
 COMMAND宏在此充当一个代码生成器的作用,这样可在一定程度上减少代码密度,间接地也可减少不留心所造成的错误。

 

     还可以用n个##符号连接n+1个Token,这个特性是#符号所不具备的。如:

 

1 #define  LINK_MULTIPLE(a, b, c, d)    
 a##_##b##_##c##_##d

2 typedef struct record_type LINK_MULTIPLE(name, company, position,
salary);

     这里这个语句将展开为typedef struct record_type
name_company_position_salary。

 

     注意:

 

当用##连接形参时,##前后的空格可有可无。

连接后的实际参数名,必须为实际存在的参数名或是编译器已知的宏定义。

凡是宏定义里有用’#’或’##’的地方,宏参数是不会再展开。如:

1 #define STR(s)       #s

2 #define CONS(a,b)    int(a##e##b)

     则printf(“int max: %sn”, STR(INT_MAX))会被展开为printf(“int max:
%sn”,
“INT_MAX”)。其中,变量INT_MAX为int型的最大值,其值定义在<climits.h>中。printf(“%sn”,
CONS(A, A))会被展开为printf(“%sn”, int(AeA)),从而编译报错。

 

   
 INT_MAX和A都不会再被展开,多加一层中间转换宏即可解决这个问题。加这层宏是为了把所有宏的参数在这层里全部展开,那么在转换宏里的那一个宏(如_STR)就能得到正确的宏参数。

 

1 #define _STR(s)         #s 

2 #define STR(s)          _STR(s)       // 转换宏

3 #define _CONS(a,b)      int(a##e##b)

4 #define CONS(a,b)       _CONS(a,b)    // 转换宏

     则printf(“int max: %sn”, STR(INT_MAX))输出为int max:
0x7fffffff;而printf(“%dn”, CONS(A, A))输出为200。

 

     这种分层展开的技术称为宏的Argument Prescan,参见附录6.1。

 

    【’#’和’##’的一些应用特例】

 

     1. 合并匿名变量名

 

1 #define ___ANONYMOUS1(type, var, line)   type  var##line

2 #define __ANONYMOUS0(type, line)         ___ANONYMOUS1(type,
_anonymous, line)

3 #define ANONYMOUS(type)                  __ANONYMOUS0(type,
__LINE__)

     例:ANONYMOUS(static int)即static int
_anonymous70,70表示该行行号。

 

     第一层:ANONYMOUS(static int)  →  __ANONYMOUS0(static int,
__LINE__)

 

     第二层:                                   →
 ___ANONYMOUS1(static int, _anonymous, 70)

 

     第三层:                                   →  static int
_anonymous70

 

     即每次只能解开当前层的宏,所以__LINE__在第二层才能被解开。

 

     2. 填充结构

 

复制代码

 1 #define FILL(a)   {a, #a} 

 2 

 3 enum IDD{OPEN, CLOSE};

 4 typedef struct{

 5     IDD id;

 6     const char * msg; 

 7 }T_MSG;

复制代码

     则T_MSG tMsg[ ] = {FILL(OPEN), FILL(CLOSE)}相当于:

 

1 T_MSG tMsg[] = {{OPEN,  “OPEN”},

2                 {CLOSE, “CLOSE”}};

     3. 记录文件名

 

1 #define _GET_FILE_NAME(f)     #f

2 #define GET_FILE_NAME(f)      _GET_FILE_NAME(f)

3 static char  FILE_NAME[] = GET_FILE_NAME(__FILE__);

     4. 得到一个数值类型所对应的字符串缓冲大小

 

1 #define _TYPE_BUF_SIZE(type)   sizeof #type

2 #define TYPE_BUF_SIZE(type)    _TYPE_BUF_SIZE(type)

3 char  buf[TYPE_BUF_SIZE(INT_MAX)];

4      //–>  char  buf[_TYPE_BUF_SIZE(0x7fffffff)];

5      //–>  char  buf[sizeof “0x7fffffff”];

     这里相当于:char  buf[11]; 

 

2.3.2.3 字符化操作符@#

   
 @#称为字符化操作符(charizing),只能用于有传入参数的宏定义中,且必须置于宏定义体的参数名前。作用是将传入的单字符参数名转换成字符,以一对单引号括起来。

 

1 #define makechar(x)    #@x

2 a = makechar(b);

     展开后变成a= ‘b’。 

 

2.3.2.4 可变参数宏

     …在C语言宏中称为Variadic
Macro,即变参宏。C99编译器标准允许定义可变参数宏(Macros with a Variable
Number of Arguments),这样就可以使用拥有可变参数表的宏。

 

     可变参数宏的一般形式为:

 

                        #define  DBGMSG(format, …)  fprintf (stderr,
format, __VA_ARGS__)

 

   
 省略号代表一个可以变化的参数表,变参必须作为参数表的最右一项出现。使用保留名__VA_ARGS__
把参数传递给宏。在调用宏时,省略号被表示成零个或多个符号(包括里面的逗号),一直到到右括号结束为止。当被调用时,在宏体(macro
body)中,那些符号序列集合将代替里面的__VA_ARGS__标识符。当宏的调用展开时,实际的参数就传递给fprintf
()。

 

     注意:可变参数宏不被ANSI/ISO
C++所正式支持。因此,应当检查编译器是否支持这项技术。 

 

   
 在标准C里,不能省略可变参数,但却可以给它传递一个空的参数,这会导致编译出错。因为宏展开后,里面的字符串后面会有个多余的逗号。为解决这个问题,GNU
CPP中做了如下扩展定义:

 

                       #define  DBGMSG(format, …)  fprintf (stderr,
format, ##__VA_ARGS__)

 

   
 若可变参数被忽略或为空,##操作将使编译器删除它前面多余的逗号(否则会编译出错)。若宏调用时提供了可变参数,编译器会把这些可变参数放到逗号的后面。

 

   
 同时,GCC还支持显式地命名变参为args,如同其它参数一样。如下格式的宏扩展:

 

                              #define  DBGMSG(format, args…)  fprintf
(stderr, format, ##args)

 

     这样写可读性更强,并且更容易进行描述。

 

     用GCC和C99的可变参数宏, 可以更方便地打印调试信息,如:

 

1 #ifdef DEBUG

2     #define DBGPRINT(format, args…)

3         fprintf(stderr, format, ##args)

4 #else

5     #define DBGPRINT(format, args…)

6 #endif

     这样定义之后,代码中就可以用dbgprint了,例如dbgprint (“aaa [%s]”,
__FILE__)。

 

     结合第4节的“条件编译”功能,可以构造出如下调试打印宏:

 

复制代码

 1 #ifdef LOG_TEST_DEBUG

 2     /* OMCI调试日志宏 */

 3     //以10进制格式日志整型变量

 4     #define PRINT_DEC(x)          printf(#x” = %dn”, x)

 5     #define PRINT_DEC2(x,y)       printf(#x” = %dn”, y)

 6     //以16进制格式日志整型变量

 7     #define PRINT_HEX(x)          printf(#x” = 0x%-Xn”, x)

 8     #define PRINT_HEX2(x,y)       printf(#x” = 0x%-Xn”, y)

 9     //以字符串格式日志字符串变量

10     #define PRINT_STR(x)          printf(#x” = %sn”, x)

11     #define PRINT_STR2(x,y)       printf(#x” = %sn”, y)

12 

13     //日志提示信息

14     #define PROMPT(info)          printf(“%sn”, info)

15 

16     //调试定位信息打印宏

17     #define  TP                   printf(“%-4u –
[%s<%s>]n”, __LINE__, __FILE__, __FUNCTION__);

18 

19     //调试跟踪宏,在待日志信息前附加日志文件名、行数、函数名等信息

20     #define TRACE(fmt, args…)

21     do{

22         printf(“[%s(%d)<%s>]”, __FILE__, __LINE__,
__FUNCTION__);

23         printf((fmt), ##args);

24     }while(0)

25 #else

26     #define PRINT_DEC(x)

27     #define PRINT_DEC2(x,y)

28 

29     #define PRINT_HEX(x)

30     #define PRINT_HEX2(x,y)

31 

32     #define PRINT_STR(x)

33     #define PRINT_STR2(x,y)

34 

35     #define PROMPT(info)

36 

37     #define  TP

38 

39     #define TRACE(fmt, args…)

40 #endif

复制代码

 

 

 

 

三  文件包含

     文件包含命令行的一般形式为:

 

#include”文件名”

 

   
 通常,该文件是后缀名为”h”或”hpp”的头文件。文件包含命令把指定头文件插入该命令行位置取代该命令行,从而把指定的文件和当前的源程序文件连成一个源文件。

 

   
 在程序设计中,文件包含是很有用的。一个大程序可以分为多个模块,由多个程序员分别编程。有些公用的符号常量或宏定义等可单独组成一个文件,在其它文件的开头用包含命令包含该文件即可使用。这样,可避免在每个文件开头都去书写那些公用量,从而节省时间,并减少出错。

 

     对文件包含命令要说明以下几点:

 

包含命令中的文件名可用双引号括起来,也可用尖括号括起来,如#include
“common.h”和#include<math.h>。但这两种形式是有区别的:使用尖括号表示在包含文件目录中去查找(包含目录是由用户在设置环境时设置的include目录),而不在当前源文件目录去查找;使用双引号则表示首先在当前源文件目录中查找,若未找到才到包含目录中去查找。用户编程时可根据自己文件所在的目录来选择某一种命令形式。

一个include命令只能指定一个被包含文件,若有多个文件要包含,则需用多个include命令。

文件包含允许嵌套,即在一个被包含的文件中又可以包含另一个文件。

 

 

 

 

四  条件编译

   
 一般情况下,源程序中所有的行都参加编译。但有时希望对其中一部分内容只在满足一定条件才进行编译,也就是对一部分内容指定编译的条件,这就是“条件编译”。有时,希望当满足某条件时对一组语句进行编译,而当条件不满足时则编译另一组语句。

 

   
 条件编译功能可按不同的条件去编译不同的程序部分,从而产生不同的目标代码文件。这对于程序的移植和调试是很有用的。

 

     条件编译有三种形式,下面分别介绍。

 

4.1 #ifdef形式

#ifdef  标识符  (或#if defined标识符)

 

    程序段1

 

#else

 

    程序段2

 

#endif

 

   
 如果标识符已被#define命令定义过,则对程序段1进行编译;否则对程序段2进行编译。如果没有程序段2(它为空),#else可以没有,即可以写为:

 

#ifdef  标识符  (或#if defined标识符)

 

    程序段

 

#endif

 

   
 这里的“程序段”可以是语句组,也可以是命令行。这种条件编译可以提高C源程序的通用性。

 

    【例6】

 

复制代码

 1 #define NUM OK

 2 int main(void){

 3     struct stu{

 4         int num;

 5         char *name;

 6         char sex;

 7         float score;

 8     }*ps;

 9     ps=(struct stu*)malloc(sizeof(struct stu));

10     ps->num = 102;

11     ps->name = “Zhang ping”;

12     ps->sex = ‘M’;

13     ps->score = 62.5;

14 #ifdef NUM

15     printf(“Number=%dnScore=%fn”, ps->num, ps->score);
/*–Execute–*/

16 #else

17     printf(“Name=%snSex=%cn”, ps->name, ps->sex);

18 #endif

19     free(ps);

20     return 0;

21 }

复制代码

   
 由于在程序中插入了条件编译预处理命令,因此要根据NUM是否被定义过来决定编译哪个printf语句。而程序首行已对NUM作过宏定义,因此应对第一个printf语句作编译,故运行结果是输出了学号和成绩。

 

   
 程序首行定义NUM为字符串“OK”,其实可为任何字符串,甚至不给出任何字符串,即#define
NUM也具有同样的意义。只有取消程序首行宏定义才会去编译第二个printf语句。

 

4.2 #ifndef形式

#ifndef  标识符

 

    程序段1

 

#else

 

    程序段2

 

#endif

 

   
 如果标识符未被#define命令定义过,则对程序段1进行编译,否则对程序段2进行编译。这与#ifdef形式的功能正相反。

 

     “#ifndef  标识符”也可写为“#if  !(defined 标识符)”。

 

4.3 #if形式

#if 常量表达式

 

    程序段1

 

#else

 

    程序段2

 

#endif

 

     如果常量表达式的值为真(非0),则对程序段1
进行编译,否则对程序段2进行编译。因此可使程序在不同条件下,完成不同的功能。

 

   
【例7】输入一行字母字符,根据需要设置条件编译,使之能将字母全改为大写或小写字母输出。

 

复制代码

 1 #define LETTER 1

 2 int main(void){

 3     char str[20]=”C Language”,c;

 4     int i=”0″;

 5     while((c=str[i])!=’’){

 6         i++;

 7 #if LETTER

 8         if(c>=’a’&&c<=’z’) c=”c-32″;

 9 #else

10         if(c>=’A’&&c<=’Z’) c=”c”+32;

11 #endif

12         printf(“%c”,c);

13     }

14     return 0;

15 }

复制代码

   
 在程序第一行定义宏LETTER为1,因此在条件编译时常量表达式LETTER的值为真(非零),故运行后使小写字母变成大写(C
LANGUAGE)。

 

   
 本例的条件编译当然也可以用if条件语句来实现。但是用条件语句将会对整个源程序进行编译,生成的目标代码程序很长;而采用条件编译,则根据条件只编译其中的程序段1或程序段2,生成的目标程序较短。如果条件编译的程序段很长,采用条件编译的方法是十分必要的。

 

4.4 实践用例

     1. 屏蔽跨平台差异

 

   
 在大规模开发过程中,特别是跨平台和系统的软件里,可以在编译时通过条件编译设置编译环境。

 

   
 例如,有一个数据类型,在Windows平台中应使用long类型表示,而在其他平台应使用float表示。这样往往需要对源程序作必要的修改,这就降低了程序的通用性。可以用以下的条件编译:

 

#ifdef WINDOWS

    #define MYTYPE long

#else

    #define MYTYPE float

#endif

     如果在Windows上编译程序,则可以在程序的开始加上#define
WINDOWS,这样就编译命令行    #define MYTYPE
long;如果在这组条件编译命令前曾出现命令行#define WINDOWS
0,则预编译后程序中的MYTYPE都用float代替。这样,源程序可以不必作任何修改就可以用于不同类型的计算机系统。

 

     2. 包含程序功能模块

 

     例如,在程序首部定义#ifdef FLV:

 

1 #ifdef FLV

2     include”fastleave.c”

3 #endif

   
 如果不许向别的用户提供该功能,则在编译之前将首部的FLV加一下划线即可。

 

     3. 开关调试信息

 

   
 调试程序时,常常希望输出一些所需的信息以便追踪程序的运行。而在调试完成后不再输出这些信息。可以在源程序中插入以下的条件编译段:

 

1 #ifdef DEBUG

2     printf(“device_open(%p)n”, file);

3 #endif

     如果在它的前面有以下命令行#define
DEBUG,则在程序运行时输出file指针的值,以便调试分析。调试完成后只需将这个define命令行删除即可,这时所有使用DEBUG作标识符的条件编译段中的printf语句不起作用,即起到“开关”一样统一控制的作用。 

 

     4. 避开硬件的限制。

 

   
 有时一些具体应用环境的硬件不同,但限于条件本地缺乏这种设备,可绕过硬件直接写出预期结果:

 

1 #ifndef TEST

2     i = dial();  //程序调试运行时绕过此语句

3 #else

4     i = 0;

5 #endif

     调试通过后,再屏蔽TEST的定义并重新编译即可。   

 

     5. 防止头文件重复包含

 

   
 头文件(.h)可以被头文件或C文件包含。由于头文件包含可以嵌套,C文件就有可能多次包含同一个头文件;或者不同的C文件都包含同一个头文件,编译时就可能出现重复包含(重复定义)的问题。

 

   
 在头文件中为了避免重复调用(如两个头文件互相包含对方),常采用这样的结构:

 

1 #ifndef  <标识符>

2     #define  <标识符>

3     //真正的内容,如函数声明之类

4 #endif

   
 <标识符>可以自由命名,但一般形如__HEADER_H,且每个头文件标识都应该是唯一的。

 

   
 事实上,不管头文件会不会被多个文件引用,都要加上条件编译开关来避免重复包含。 

 

     6. 在#ifndef中定义变量出现的问题(一般不定义在#ifndef中)。

 

1 #ifndef PRECMPL

2     #define PRECMPL

3     int var;

4 #endif

   
 其中有个变量定义,在VC中链接时会出现变量var重复定义的错误,而在C中成功编译。

 

     (1)
当第一个使用这个头文件的.cpp文件生成.obj时,var在里面定义;当另一个使用该头文件的.cpp文件再次(单独)生成.obj时,var又被定义;然后两个obj被第三个包含该头文件.cpp连接在一起,会出现重复定义。

 

     (2)
把源程序文件扩展名改成.c后,VC按照C语言语法对源程序进行编译。在C语言中,遇到多个int
var则自动认为其中一个是定义,其他的是声明。

 

     (3)
C语言和C++语言连接结果不同,可能是在进行编译时,C++语言将全局变量默认为强符号,所以连接出错。C语言则依照是否初始化进行强弱的判断的(仅供参考)。

 

     解决方法:

 

     (1) 把源程序文件扩展名改成.c。

 

     (2) .h中只声明 extern int var;,在.cpp中定义(推荐)

 

复制代码

1 //<x.h>

2 #ifndef  __X_H

3     #define  __X_H

4     extern int var;

5 #endif

6 <x.c>

7 int var = 0;

复制代码

     综上,变量一般不要定义在.h文件中。

 

 

 

 

一 前言
预处理(或称预编译)是指在进行编译的第一遍扫描(词法扫描和语法分析)之前所作的工作。预处理指令指示在…

编译到PHP中,我叫它 phptoc。

  刚接触lex
yacc,学习一下!!

但是由于各种原因,暂停了此项目。

lex是什么?

写这篇文章一是因为这方面资料太少,二是把自己的收获总结下来,以便以后参考,如果能明白PHP语法分析

  lex:lexical
analysis,词法分析。符号提取。实际上这是提取编程语言占用的各种保留字、操作符等等语言的一种解决措施。

那对PHP源码的研究会更上一层楼地 ^.^…

  它的另外一个名字scanner,就有扫描的意思。

我尽可能写的通俗易懂些。

  lex把每个扫描出来的单词统统叫做token,token可以有很多类。对比自然语言的话,英语中的每个单词都是token,token有很多类,比如non(名词)就是一个类token,apple就是属于这个类型的一个具体token。对于某个编程语言来说,token的个数是很有限的,可以理解为在文件中的关键字。

这个项目思路源于facebook的开源项目 HipHop .

  lex工具会帮我们生成一个yylex函数,yacc通过调用这个函数来得知拿到的token是什么类型的,但是token的类型是在yacc中定义的。

其实我对这个项目的性能提高50%-60%持怀疑态度,从根本来讲,如果PHP用到APC缓存,它的性能是否低

  lex的输入文件一般会被命名为.l文件,通过lex
XX.l我们得到输出文件是lex.yy.c

于HipHop,我还没有做测试,不敢断言。

yacc是什么?

PHPtoc,我只是想把C程序员解放出来,希望能达到,让PHPer用PHP代码就可以写出接近于PHP扩展性能的一个扩展,

  yacc:syntactic
analysis,语法分析。有lex提取了单词再有yacc做语法表达。

它的流程如下,读取PHP文件,解析PHP代码,对其进行语法分析器,生成对应的ZendAPI,编译成扩展。

  yacc帮我们生成一个yyparse函数,这个函数不断调用上面的yylex函数来得到token的类型。

奥门新浦京官方网站 1

  yyac的输入文件一般会被命名成.y文件,通过yacc -d
XX.y我们得到的输出文件是y.tab.h
y.tab.c,前者包含了lex需要的token类型定义,需要被include进.l文件中。

进入正题

Lex和yacc的输入文件格式

这里最难的就是语法分析器了,大家应该都知道,PHP也有自己的语法分析器,现在版本用到的是re2c
和 Bison。

Definition section
%%
Rules secition
%%
C code seciton

所以,我自然也用到了这个组合。

.l和.y的文件格式都是分成三段,用%%来分割,三个section的含义是:

如果要用PHP的语法分析器就不太现实了,因为需要修改zend_language_parser.y和
zend_language_scanner.l 并重新编译,这难度大不说,还可能影响PHP自身。

  • Definition Section
    • 这块可以放C语言的各种各样include,define等声明语句,但是要用%{%}括起来。
    • 如果是.l文件,可以放预定义的正则表达式:minus”-“还要放token的定义,方法是:代号正则表达式。然后到了,Rules
      Seciton就可以通过{符号}来引用正则表达式
    • 如果是.y文件,可以放token的定义,如:%token INTEGER
      PLUS,这里定义的每个toekn都可以在y.tab.h中看到。
  • Rules section
    • .l文件在这里放置的rules就是每个正则表达式要对应的动作,一般是返回一个token
    • .y文件在这里放置的rules就是满足一个语法描述时要执行的动作
    • 无论是.l文件还是.y文件这里的动作都是用{}括起来的,用C语言来描述,这些代码可以做你任何想做的事情
  • C code Section
    • main函数,yyerror函数等的定义

所以决定重新写一套自己的语法分析规则,这个功能就等于是重写了PHP的语法分析器,当然会舍弃一些不常用的。

 lex和yacc能帮我们做什么??

re2c &&
yacc/bison,通过引用自己的对应文件,然后将他们统一编译成一个*.c文件,最后再gcc编译就会生

  解释执行自定义语言。有几点注意:

成我们自己的程序。所以说,他们从根本来讲不是语法分析程序,他们只是将我们的规则生成一个独立的c文

  1. 自定义语言要做的事情必须可以通过C语言来实现。lex和yacc存在的意义在于简化语言,让使用者能够以一种比较简单的语言来实现复杂的操作。比如:对于数据库的查询肯定有现成的库可以来完成,但是使用起来比较麻烦,要自己写函数调用API,编译才行。如果我们想连接自定义的简单语言(SQL)来实现操作,这个时候就可以用lex和yacc
  2. lex和yacc做的事情只是:用C语言来实现另外一个语言。所以,他没办法实现C语言自己,但是可以实现java、python等。当然你可以通过Antlr来实现C语言的解析和执行,如果你怎么做的话,C语言程序首先是通过java来执行,然后java又变成了本地语言(C语言)来执行。

件,这个c文件才是真正的我们需要的语法分析程序,我更愿意叫它
语法生成器。如下图:

使用lex和yacc我们要做那几件事情?

奥门新浦京官方网站 2

  1. 定义各种token类型。他们在.y中定义,这些token既会被lex使用到,也会被.y文件中的BNF使用到。
  2. 写词汇分析代码。这部分代码在.I文件(就是lex输入文件)中。这块的定义方式是:正则表达式->对应操作。如果和yacc一起来使用的话,对应的操作通常是返回一个token类型,这个token的类型要在yacc中提前定义好。
  3. 写BNF。这些东西定义了语言的规约方式。

注:图中a.c是 扫描器生成的最终代码。。

关于BNF

re2c扫描器,假如我们写的扫描规则文件叫scanner.l,它会将我们写的PHP文件内容,进行扫描,然后根据

是一种context-free grammars

我们写的规则,生成不同的token传递给parse。

在yacc中定义的方式其实是:

我们写的(f)lex语法规则,比如我们叫他Parse.y

<symbol>:_expression_{operation}

会通过
yacc/bison编译成一个parse.tab.h,parse.tab.c的文件,parse根据不同的token进行不同的操作

|_expression_{operation}

比如我们PHP代码是 “echo 1″;

operation是满足语法时要执行的C语言代码,这里的C语言代码可以使用一些变量,他们是:$$$1$2等等。$$代表规约的结果,就是表达式_expression_的值,$1代表的是前面_expression_中出现的各个word。举个例子:

扫描其中有一个规则:

 

"echo" {

return T_ECHO;
 }

扫描器函数scan会拿到”echo
1″字符串,它对这一段代码进行循环,如果发现有echo字符串,那么它就作为关键字返回token:T_ECHO,

parse.y和scanner.l会分别生成两个c文件,scanner.c和parse.tab.c,用gcc编译到一起,就成了。

下面会具体的说一说

re2c,关于它的英文文档在

还么有结束,稍后我会放上来。

re2c提供了一些宏接口,方面我们使用,我简单做了翻译,英语水平不好,可能有误,需要原文的可以去上面那个地址查看。

接口代码:

不像其他的扫描器程序,re2c
不会生成完整的扫描器:用户必须提供一些接口代码。用户必须定义下面的宏或者是其他相应的配置。

YYCONDTYPE
用-c
模式你可以使用-to参数用来生成一个文件:使用包含枚举类型的作为条件。每个值都会在规则集合里面作为条件来使用。
YYCTYPE
用来维持一个输入符号。通常是 char 或者unsigned char。
YYCTXMARKER
*YYCTYPE类型的表达式,生成的代码回溯信息的上下文会保存在
YYCTXMARKER。如果扫描器规则需要使用上下文中的一个或多个正则表达式则用户需要定义这个宏。
YYCURSOR
*YYCTYPE类型的表达式指针指向当前输入的符号,生成的代码作为符号相匹配,在开始的地方,YYCURSOR假定指向当前token的第一个字符。结束时,YYCURSOR将会指向下一个token的第一个字符。
YYDEBUG(state,current)
这个只有指定-d标示符的时候才会需要。调用用户定义的函数时可以非常容易的调试生成的代码。
这个函数应该有以下签名:void YYDEBUG(int state,char
current)。第一个参数接受 state
,默认值为-1第二个参数接受输入的当前位置。
YYFILL(n)
当缓冲器需要填充的时候,生成的代码将会调用YYFILL(n):至少提供n个字符。YYFILL(n)将会根据需要调整YYCURSOR,YYLIMIT,YYMARKER

YYCTXMARKER。注意在典型的程序语言当中,n等于最长的关键词的长度加一。用户可以在/*!max:re2c*/一次定义YYMAXFILL来指定最长长度。如果使用了-1,YYMAXFILL将会在/*!re2c*/之后调用一次阻塞。
YYGETCONDITION()
如果使用了-c模式,这个定义将会在扫描器代码之前获取条件集。这个值,必须初始化为枚举YYCONDTYPE的类型。
YYGETSTATE()
如果-f模式指定了,用户就需要定义这个宏。如果这样,扫描器在开始时为了获取保存的状态,生成的代码将会调用YYGETSTATE(),YYGETSTATE()必须返回一个带符号的整数,这个值如果是-1,告诉扫描器这是第一次执行,否则这个值等于以前YYSETSTATE(s)
保存的状态。否则,扫描器将会恢复操作之后立即调用YYFILL(n)。
YYLIMIT
表达式的类型 *YYCTYPE
标记缓冲器的结尾(YYLIMIT(-1)是缓冲区的最后一个字符)。生成的代码将会不断的比较YYCORSUR
和 YYLIMIT 以决定 什么时候填充缓冲区。
YYSETCONDITION(c)
这个宏用来在转换规则中设置条件,它只会在指定-c模式 和
使用转换规则时有用。
YYSETSTATE(s)
用户只需要在指定-f模式时定义这个宏,如果是这样,生成的代码将会在YYFILL(n)之前调用YYSETSTATE(s),YYSETSTATE的参数是一个有符号整型,被称为唯一的标示特定的YYFILL(n)实例。
YYMARKER
类型为*YYCTYPE的表达式,生成的代码保存回溯信息到YYMARKER。一些简单的扫描器可能用不到。

扫描器,顾名思义,就是对文件扫描,找出关键代码来。

扫描器文件结构:

/* #include 文件*/
/*宏定义*/
//扫描函数
int scan(char *p){
/*扫描器规则区*/
}
//执行scan扫描函数,返回token到yacc/bison中。
int yylex(){
        int token;
        char *p=YYCURSOR;//YYCURSOR是一个指针,指向我们的PHP文本内容
        while(token=scan(p)){//这里会移动指针p,一个一个判断是不是我们上面定义好的scanner...
                return token;
        }
}
int main(int argc,char**argv){
        BEGIN(INITIAL);//
        YYCURSOR=argv[1];//YYCURSOR是一个指针,指向我们的PHP文本内容,
        yyparse();
}

BEGIN 是定义的宏

#define YYCTYPE char   //输入符号的类型
#define STATE(name)     yyc##name
#define BEGIN(n)        YYSETCONDITION(STATE(n))
#define LANG_SCNG(v)    (sc_globals.v)
#define SCNG    LANG_SCNG
#define YYGETCONDITION()        SCNG(yy_state)
#define YYSETCONDITION(s)       SCNG(yy_state)=s

yyparse函数是在yacc 中定义的,

里面有一个关键宏: YYLEX

#define YYLEX yylex()

它会执行scaner扫描器的yylex

可能会有点绕,重新缕一缕:

在scanner.l中,通过调用parse.y解析器函数yyparse,该函数调用scanner.l的yylex生成关键代码token,yylex

将扫描器返回的

token返回给parse.y,parse根据不同的token执行不同的代码.

举例:

scanner.l
#include “scanner.h”
#include “parse.tab.h”
int scan(char *p){
/*!re2c
<INITIAL>”<?php”([ t]|{NEWLINE})? {
BEGIN(ST_IN_SCRIPTING);
return T_OPEN_TAG;
}
“echo” {

return T_ECHO;
}
[0-9]+ {
return T_LNUMBER;
}
*/
}
int yylex(){
int c;

//       return T_STRING;
int token;
char *p=YYCURSOR;
while(token=scan(p)){
return token;
}
}

int main (int argc,char ** argv){
BEGIN(INITIAL);//初始化
YYCURSOR=argv[1];//将用户输入的字符串放到YYCURSOR
yyparse();//yyparse() -》yylex()-》yyparse()
return 0;
}

这样一个简单的扫描器就做成了,

那解析器呢?

解析器我用的是flex和bison。。。

关于flex的文件结构:

%{
/*
C代码段将逐字拷贝到lex编译后产生的C源文件中
可以定义一些全局变量,数组,函数例程等…
*/
#include
#include “scanner.h”
extern int yylex();//它在scanner.l中定义的。。
void yyerror(char *);
# define YYPARSE_PARAM tsrm_ls
# define YYLEX_PARAM tsrm_ls
%}
{定义段,也就是token定义的地方}
//这就是关键  token程序是根据这是做switch的。
%token T_OPEN_TAG
%token T_ECHO
%token T_LNUMBER
%%
{规则段}
start:
T_OPEN_TAG{printf(“startn”); }
|start statement
;
statement:
T_ECHO expr {printf(“echo :%sn”,$3)}
;
expr:
T_LNUMBER {$$=$1;}
%%
{用户代码段}
void yyerror(char *msg){
printf(“error:%sn”,msg);
}

在规则段中,start是开始的地方,如果
scan识别到PHP开始标签就会返回T_OPEN_TAG,然后执行括号的代码,输出start.

在scanner.l中,调用scan的是个while循环,所以它会检查到php代码的末尾,

yyparse会根据scan返回的标记做switch,然后goto到相应的代码,比如
yyparse.y发现当前的token是T_OPEN_TAG,

它会通过宏 #line 映射到 parse.y所对应
21行,T_OPEN_TAG的位置,然后执行

画个图来说明一下,
奥门新浦京官方网站 3

那,TOKEN返回给yyparse之后做了什么呢?

为了能直观一些,我用gdb跟踪:

奥门新浦京官方网站 4

这个时候yychar是258,258是什么?

奥门新浦京官方网站 5

258是bison自动生成的枚举类型数据。

继续

YYTRANSLATE宏接受yychar,然后返回所对应的值

#define
YYTRANSLATE(YYX)                                               
((unsigned int) (YYX) <= YYMAXUTOK ? yytranslate[YYX] :
YYUNDEFTOK)

/* YYTRANSLATE[YYLEX] — Bison symbol number corresponding to
YYLEX.  */
static const yytype_uint8 yytranslate[] =
{
0,     2,     2,     2,     2,     2,     2,     2,     2,     2,
2,     2,     2,     2,     2,     2,     2,     2,     2,     2,
2,     2,     2,     2,     2,     2,     2,     2,     2,     2,
2,     2,     2,     2,     2,     2,     2,     2,    27,     2,
22,    23,     2,     2,    28,     2,     2,     2,     2,     2,
2,     2,     2,     2,     2,     2,     2,     2,     2,    21,
2,    26,     2,     2,     2,     2,     2,     2,     2,     2,
2,     2,     2,     2,     2,     2,     2,     2,     2,     2,
2,     2,     2,     2,     2,     2,     2,     2,     2,     2,
2,     2,     2,     2,     2,     2,     2,     2,     2,     2,
2,     2,     2,     2,     2,     2,     2,     2,     2,     2,
2,     2,     2,     2,     2,     2,     2,     2,     2,     2,
2,     2,     2,    24,     2,    25,     2,     2,     2,     2,
2,     2,     2,     2,     2,     2,     2,     2,     2,     2,
2,     2,     2,     2,     2,     2,     2,     2,     2,     2,
2,     2,     2,     2,     2,     2,     2,     2,     2,     2,
2,     2,     2,     2,     2,     2,     2,     2,     2,     2,
2,     2,     2,     2,     2,     2,     2,     2,     2,     2,
2,     2,     2,     2,     2,     2,     2,     2,     2,     2,
2,     2,     2,     2,     2,     2,     2,     2,     2,     2,
2,     2,     2,     2,     2,     2,     2,     2,     2,     2,
2,     2,     2,     2,     2,     2,     2,     2,     2,     2,
2,     2,     2,     2,     2,     2,     2,     2,     2,     2,
2,     2,     2,     2,     2,     2,     2,     2,     2,     2,
2,     2,     2,     2,     2,     2,     2,     2,     2,     2,
2,     2,     2,     2,     2,     2,     1,     2,     3,     4,
5,     6,     7,     8,     9,    10,    11,    12,    13,    14,
15,    16,    17,    18,    19,    20
};

yyparse拿到这个值,不断地translate,

奥门新浦京官方网站 6

bison会生成很多用来映射的数组,将最终的translate保存到yyn,

这样bison就能找到token所对应的代码

  switch (yyn)
{
case 2:

/* Line 1455 of yacc.c  */
#line 30 “parse.y”
{printf(“startn”); ;}
break;

不断循环,生成token逐条执行,然后解析成所对应的zend
函数等,生成对应的op保存在哈希表中,这些不是本文的重点,

就不细说了。

到这里,整体的流程就结束了。。剩下的就是细化的工作,如果有时间,我再继续phptoc
:)

发表评论

电子邮件地址不会被公开。 必填项已用*标注