65.9K
CodeProject 正在变化。 阅读更多。
Home

最大咀嚼原理

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.31/5 (13投票s)

2003年9月2日

CPOL

4分钟阅读

viewsIcon

53151

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 语句产生了 ** 运算符的错觉,但实际上它是两个 * 运算符;一个乘法运算符,另一个是解引用运算符。

参考文献

  1. [AHO98] Compilers Principles, Techniques and Tools. Alfred V. Aho, Ravi Sehi, Jeffrey D Ullman 1986
  2. [HOL90] Compiler Design in C. Allen I. Holub 1990
  3. [VAN03] C++ Templates, The Complete Guide. David Vandevoorde, Nicolai M. Josuttis 2003
© . All rights reserved.