Visual C++ 7.1Visual C++ 7.0Windows 2003Windows 2000Visual C++ 6.0Windows XP中级开发Visual StudioWindowsC++
最大咀嚼原理






4.31/5 (13投票s)
C++ 编译器如何将其输入分解为标记。
引言
大多数 STL 用户已经知道,当将模板作为任何类的模板参数时,需要在两个>
运算符之间添加一个额外的空格。例如,当你想要创建一个复数向量,而复数本身也是一个模板类时,你必须在两个 >
运算符之间添加一个额外的空格。std::vector<std::complex<int> > vecComp;
如果你忘记添加额外的空格,编译器会将其视为逻辑右移运算符 >>
。(尽管有人建议扩展该语言,使其能够根据上下文理解 >>
的含义 [VAN03])。为了更好地理解幕后发生的情况以及为什么需要这样做,我们需要了解编译器的运作方式及其构建过程。编译器的最简单定义是:它是一种将一种语言翻译成另一种语言的程序。C++ 编译器将 C++ 源程序翻译成特定平台所需的机器语言。
任何编译器的第一阶段是读取源程序,在去除所有空格和注释后将其转换为标记。标记由解析器使用。将输入流转换为标记的过程称为词法分析或扫描 [AHO86]。可以创建一个没有注释的语言,并且词法分析器不去除空格,但在这种情况下,由解析器和其他编译器阶段负责处理空格。而且你必须重写其语法来处理空格,这既不合理也不容易。
词法分析器通常还会存储行号以便提供更好的错误信息,并且还可能存储带错误信息的源程序副本。但对于源程序中的多行注释或仅包含注释的行,在删除注释行后,行号的存储将不再相同。
标记是具有集合意义的逻辑上连贯的字符序列。更技术地说,标记通常是语法中的终结符 [AHO86]。通常,标记类型包括关键字、运算符、标识符、常量、字符串、数字标点符号等。用于从输入流创建标记的字符序列称为词元。
一个典型的 C++ 扫描器可能会从给定的 C++ 语句生成以下标记。
int a = b + 10;
该语句生成七个标记。首先,它为 int
生成关键字标记。然后是一个标识符 a
。标记生成器首先在符号表中搜索它,符号表是一种用于存储有关标识符信息的数据结构。如果标记不在符号表中,则将其名称 a
存入符号表并生成一个类型为标识符的标记。然后生成标点符号标记,接着是标识符 b
,其符号表的规则与标识符 a
相同,然后是加号标记、值为 10
的数字标记,最后是分号的标点符号标记。标记通常带有其属性生成。对于标识符,指向其符号表条目的指针作为该标记的属性存储。如果我们写出上述语句的标记及其属性对,则会是这样的:{Keyword integer, NULL}
{Identifier, Pointer to symbol table entry of "a"}
{Assignment Operator, NULL }
{Identifier, Pointer to symbol table entry of "b"}
{Plus Operator, NULL}
{Digit, 10}
{Terminator symbol, NULL}
编译器通常从最长的词元创建标记,例如在 C++ 编译器中,对于 >>,它会创建一个右移运算符标记,而不是两个大于运算符 [HOL90]。这也称为最大吞噬标记化原则,或简称最大吞噬原则,即 C++ 实现必须考虑尽可能多的字符来创建标记。最大吞噬原则的其他示例可以在二联字符(digraphs)中看到。请看下面的代码。
std::list<::x> lst;
这里 <:
是一个二联字符,被视为 [
。所以上面的代码被编译为:std::list[:x> lst;
因此,你必须在 <
和冒号之间留一个额外的空格。正确的代码如下:std::list< ::x> lst;
在下面的例子中也是如此。int x = y%::z;
这里 %:
再次是一个二联字符,被视为 #
,该语句变成:int x = y#:z;
这里你也需要留一个额外的空格才能正确编译。int x = y % ::z;
有趣的是,当不应用最大吞噬原则时,情况正好相反,但代码读者却认为它被应用了。例如,有人曾问 Bjarne Stroustrup 是否可以重载或创建一个 **
运算符,该运算符在 COBOL 和一些其他语言中存在。C++ 中没有 **
运算符,也没有任何方法可以创建新的运算符。但在 C++ 中,*
运算符有两种含义:乘法运算符和解引用运算符。你可以重载这两个运算符,唯一的区别是参数的数量。乘法运算符需要两个参数,而解引用运算符只需要一个。Stroustrup 使用了一个巧妙的技术来重载这两个运算符,从而产生了 **
运算符的错觉。// overloading ** operator
#include <cmath>
#include <ciostream>
struct Index {
double d;
Index(double dd) :d(dd) { }
};
struct II {
double d;
II(double dd) :d(dd) { }
};
struct III {
double d;
III(double ddd) : d(ddd) { }
};
II operator*(Index i) {
return II(i.d);
}
double operator*(double d , II i) {
return pow(d,i.d);
}
int main () {
Index i = 3;
std::cout << 2**i << "\n";
}
这里 2**i
语句产生了 **
运算符的错觉,但实际上它是两个 *
运算符;一个乘法运算符,另一个是解引用运算符。参考文献
- [AHO98] Compilers Principles, Techniques and Tools. Alfred V. Aho, Ravi Sehi, Jeffrey D Ullman 1986
- [HOL90] Compiler Design in C. Allen I. Holub 1990
- [VAN03] C++ Templates, The Complete Guide. David Vandevoorde, Nicolai M. Josuttis 2003