Вид идентифицирует лексему как число, оператор или скобку. Для чисел (в нашем примере — только для чисел) в качестве значения используется само число.
Итак, как же выразить идею о паре (вид, значение) в программе? Для этого определим тип Token, представляющий лексемы. Почему? Вспомните, почему мы вообще используем типы: они хранят данные, которые нам нужны, и предоставляют возможность выполнять полезные операции над этими данными. Например, тип
int
позволяет хранить целые числа и выполнять операции сложения, вычитания, умножения и вычисления остатка, в то время как тип
string
позволяет хранить последовательности символов и выполнять конкатенацию и доступ к символу по индексу. В языке С++ и его стандартной библиотеке определено много типов, например
char
,
int
,
double
,
string
,
vector
и
ostream
, но не тип
Token
. На самом деле существует огромное количество типов — тысячи и сотни тысяч, — которые мы хотели бы иметь, но которых нет в языке и в стандартной библиотеке.
Среди наших любимых типов, которых нет в библиотеке, — классы
Matrix
(см. главу 24),
Date
(см. главу 9) и целые числа с бесконечной точностью (поищите в веб класс
Bignum
). Если вы еще раз поразмыслите над этим, то поймете, что язык не может поддерживать десятки тысяч типов: кто их определит, кто их реализует, как их найти и какое толстое руководство по использованию языка при этом получится? Как и большинство современных языков программирования, язык С++ решает эту проблему, позволяя программисту при необходимости определять свои собственные типы (типы, определенные пользователем).
6.3.3. Реализация лексем
Как должна выглядеть лексема в нашей программе? Иначе говоря, как должен выглядеть тип
Token
? Класс
Token
должен предусматривать выполнение операторов, например
+
и
–
, а также представлять числа, такие как
42
и
3.14
. В самой простой реализации нужно придумать, как задать вид лексемы и как хранить числа.
Существует много способов реализации этой идеи в программе на языке С++. Вот ее простейший вариант:
class Token { // очень простой тип, определенный пользователем
public:
char kind;
double value;
};
Класс
Token
— это тип (такой же, как
int
или
char
), поэтому его можно использовать для определения переменных и хранения значений. Он состоит из двух частей (членов):
kind
и
value
. Ключевое слово
class
означает “тип, определенный пользователем”; это значит, что он содержит члены (хотя в принципе может их и не содержать). Первый член,
kind
, имеет тип
char
и представляет собой символ. С его помощью удобно хранить символы
'+'
и
'*'
, чтобы представить операции
*
и
+
. Рассмотрим пример использования этого типа.
Token t; // t — объект класса Token
t.kind = '+'; // t представляет операцию +
Token t2; // t2 — другой объект класса Token
t2.kind = '8'; // цифра 8 означает, что "вид" является числом
t2.value = 3.14;
Для доступа к члену класса используется обозначение имя_объекта.имя_члена. Выражение
t.kind
читается как “член
kind
объекта
t
”, а выражение
t2.value
— как “член
value
объекта
t2
”. Объекты класса
Token
можно копировать так же, как и переменные типа
int
.
Token tt = t; // копирование при инициализации
if (tt.kind != t.kind) error("невозможно!");
t = t2; // присваивание
cout << t.value; //
вывод числа 3.14
Имея класс
Token
, можно выразить выражение
(1.5+4)*11
с помощью семи лексем.
Обратите внимание на то, что для простых лексем значение не требуется, поэтому мы не используем член
value
. Нам нужен символ для обозначения чисел. Мы выбрали символ
'8'
просто потому, что он явно не оператор и не знак пунктуации. Использование символа
'8'
для обозначения чисел немного загадочно, но это лишь на первых порах.
Класс
Token
представляет пример типа, определенного пользователем. Тип, определенный пользователем, может иметь функции-члены (операции), а также данные члены. Существует много причин для определения функций-членов. В данном примере мы описали две функции-члена для того, чтобы инициализация объекта класса
Token
стала проще.
class Token {
public:
char kind; // вид лексемы
double value; // для чисел: значение
Token(char ch) // создает объект класса Token
// из переменной типа char
:kind(ch), value(0) { }
Token(char ch, double val) // создает объект класса Token
:kind(ch), value(val) { } // из переменных типа
// char и double
};
Эти две функции-члена называют конструкторами (constructors). Их имя совпадает с именем типа, и они используются для инициализации (конструирования) объектов класса
Token
. Рассмотрим пример.
Token t1('+'); // инициализируем t1, так что t1.kind = '+'
Token t2('8',11.5); // инициализируем t2,
// так что t2.kind = '8' и t2.value = 11.5
В первом конструкторе фрагмент
:kind(ch)
,
value(0)
означает “инициализировать член kind значением переменной
ch
и установить член
value
равным нулю”. Во втором конструкторе фрагмент
:kind(ch)
,
value(val)
означает “инициализировать член
kind
значением переменной
ch
и установить член
value
равным переменной val”. В обоих вариантах нам требуется лишь создать объект класса
Token
, поэтому тело функции ничего не содержит:
{ }
. Специальный синтаксис инициализации (список инициализации членов класса) начинается с двоеточия и используется только в конструкторах.
Обратите внимание на то, что конструктор не возвращает никаких значений, потому что в конструкторе это не предусмотрено. (Подробности изложены в разделах 9.4.2 и 9.7.)
6.3.4. Использование лексем
Итак, похоже, что мы можем завершить нашу программу, имитирующую калькулятор! Однако следует уделить немного времени для планирования. Как использовать класс
Token
в калькуляторе?
Можно считать входную информацию в вектор объектов
Token
.
Token get_token; // считывает объекты класса Token из потока cin
vector<Token> tok; // здесь храним объекты класса Token
int main
{
while (cin) {
Token t = get_token;
tok.push_back(t);
}
// ...
}
Теперь можно сначала считать выражение, а вычислить его позднее. Например, для выражения