Константы с плавающей точкой




Константы с плавающей точкой имеют тип double. Транслятор должен предупреждать о таких константах, значение которых не укладывается в формат, выбранный для представления чисел с плавающей точкой. Приведем примеры констант с плавающей точкой: 1.23.23 0.23 1. 1.0 1.2e10 1.23e-15 Отметим, что внутри константы с плавающей точкой не должно быть пробелов.Например, 65.43 e-21 не является константой с плавающей точкой, трансляторраспознает это как четыре отдельные лексемы: 65.43 e - 21 что вызовет синтаксическую ошибку.Если нужна константа с плавающей точкой типа float, то ее можно получить,используя окончание f: 3.14159265f 2.0f 2.997925f

Символьные константы

Символьной константой является символ, заключенный в одиночные кавычки,например, 'a' или '0'. Символьные константы можно считать константами,которые дают имена целым значениям символов из набора, принятого намашине, на которой выполняется программа.Это необязательно тот же набор символов, который есть на машине,где программа транслировалась. Таким образом, если вы запускаетепрограмму на машине, использующей набор символовASCII, то значение '0' равно 48, а если машина использует код EBCDIC,то оно будет равно 240. Использование символьных констант вместо ихдесятичного целого эквивалента повышает переносимость программ.Некоторые специальные комбинации символов, начинающиеся с обратнойдробной черты, имеют стандартные названия: Конец строки NL(LF) \n Горизонтальная табуляция HT \t Вертикальная табуляция VT \v Возврат BS \b Возврат каретки CR \r Перевод формата FF \f Сигнал BEL \a Обратная дробная черта \ \\ Знак вопроса? \? Одиночная кавычка ' \' Двойная кавычка " \" Нулевой символ NUL \0 Восьмеричное число ooo \ooo Шестнадцатеричное число hhh \xhhh Несмотря на их вид, все эти комбинации задают один символ. Типсимвольной константы - char. Можно также задавать символ с помощьювосьмеричного числа, представленного одной, двумя или тремявосьмеричными цифрами (перед цифрами идет \) или с помощьюшестнадцатеричного числа(перед шестнадцатеричными цифрами идет \x). Число шестнадцатеричныхцифр в такой последовательности неограничено. Последовательностьвосьмеричных или шестнадцатеричных цифр завершается первым символом,не являющимся такой цифрой. Приведем примеры: '\6' '\x6' 6 ASCII ack '\60' '\x30' 48 ASCII '0' '\137' '\x05f' 95 ASCII '_' Этим способом можно представить любой символ из набора символовмашины. В частности, задаваемые таким образом символы можновключать в символьные строки (см. следующий раздел). Заметим, чтоесли для символовиспользуется числовая форма задания, то нарушается переносимостьпрограммы между машинами с различными наборами символов.

Строки

Строка - это последовательность символов, заключенная в двойные кавычки: "это строка" Каждая строка содержит на один символ больше, чем явно задано:все строки оканчиваются нулевым символом ('\0'), имеющимзначение 0. Поэтому sizeof("asdf")==5; Типом строки считается "массив из соответствующего числа символов",поэтому тип "asdf" есть char[5]. Пустая строка записывается как"" и имеет тип char[1]. Отметим, что для любой строки s выполняетсяstrlen(s)==sizeof(s)-1, поскольку функция strlen() не учитываетзавершающий символ '\0'. Внутри строки можно использовать для представления невидимыхсимволов специальные комбинации с \. В частности, в строке можнозадать сам символ двойной кавычки " или символ \. Чаще всего изтаких символов оказывается нужным символ конца строки '\n', например: cout << "звуковой сигнал в конце сообщения\007\n" Здесь 7 - это значение в ASCII символа BEL (сигнал), который впереносимом виде обозначается как \a.Нет возможности задать в строке "настоящий" символ конца строки: "это не строка, а синтаксическая ошибка" Для большей наглядности программы длинные строки можно разбиватьпробелами, например: char alpha[] = "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; Подобные, подряд идущие, строки будут объединяться в одну, поэтомумассив alpha можно эквивалентным образом инициализировать с помощьюодной строки: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; В строке можно задавать символ '\0', но большинство программне ожидает после него встречи с какими-либо еще символами. Например,строку "asdf\000hjkl" стандартные функции strcpy() и strlen()будут рассматривать как строку "asdf". Если вы задаете в строке последовательностью восьмеричных цифрчисловую константу, то разумно указать все три цифры. Записьэтой строки и так не слишком проста, чтобы еще и раздумывать,относится ли цифра к числу или является отдельным символом.Для шестнадцатеричных констант используйте два разряда. Рассмотримследующие примеры: char v1[] = "a\x0fah\0129"; // 'a' '\xfa' 'h' '\12' '9' char v2[] = "a\xfah\129"; // 'a' '\xfa' 'h' '\12' '9' char v3[] = "a\xfad\127"; // 'a' '\xfad' '\127'

Нуль

Нуль (0) имеет тип int. Благодаря стандартным преобразованиям ($$R.4)0 можно использовать как константу целого типа, или типа с плавающейточкой, или типа указателя. Нельзя разместить никакой объект, есливместо адреса указан 0. Какой из типов нуля использовать, определяетсяконтекстом. Обычно (но необязательно) нуль представляетсяпоследовательностью разрядов "все нули" подходящей длины.

Поименованные константы

Добавив к описанию объекта служебное слово const, можно превратитьэтот объект из переменной в константу, например: const int model = 90; const int v[] = { 1, 2, 3, 4 }; Поскольку константе нельзя ничего присвоить, она должна бытьинициализирована. Описывая какой-либо объект как const, мы гарантируем,что его значение не изменяется в области видимости: model = 200; // ошибка model++; // ошибка Отметим, что спецификация const скорее ограничивает возможностииспользования объекта, чем указывает, где следует размещать объект.Может быть вполне разумным и даже полезным описание функции с типомвозвращаемого значения const: const char* peek(int i) // вернуть указатель на строку-константу { return hidden[i]; } Приведенную функцию можно было бы использовать для передачи строки,защищенной от записи, в другую программу, где она будет читаться.Вообще говоря, транслятор может воспользоваться тем фактом, что объектявляется const, для различных целей (конечно, это зависит от"разумности" транслятора). Самое очевидное - это то, что дляконстанты не нужно отводить память, поскольку ее значение известнотранслятору. Далее, инициализатор для константы, как правило (но невсегда) является постоянным выражением, которое можно вычислить наэтапе трансляции. Однако, для массива констант обычно приходитсяотводить память, поскольку в общем случае транслятор не знает,какой элемент массива используется в выражении. Но и в этом случаена многих машинах возможна оптимизация, если поместить такой массивв защищенную от записи память. Задавая указатель, мы имеем дело с двумя объектами: с самим указателеми с указуемым объектом. Если в описании указателя есть "префикс"const, то константой объявляется сам объект, но не указатель на него,например: const char* pc = "asdf"; // указатель на константу pc[3] = 'a'; // ошибка pc = "ghjk"; // нормально Чтобы описать как константу сам указатель, а не указуемый объект,нужно использовать операцию * перед const. Например: char *const cp = "asdf"; // указатель-константа cp[3] = 'a'; // нормально cp = "ghjk"; // ошибка Чтобы сделать константами и указатель, и объект, надо оба объявитьconst, например: const char *const cpc = "asdf"; // указатель-константа на const cpc[3] = 'a'; // ошибка cpc = "ghjk"; // ошибка Объект может быть объявлен константой при обращении к нему с помощьюуказателя, и в то же время быть изменяемым, если обращаться кнему другим способом. Особенно это удобно использовать для параметровфункции. Описав параметр-указатель функции как const, мы запрещаемизменять в ней указуемый объект, например: char* strcpy(char* p, const char* q); // не может изменять *q Указателю на константу можно присвоить адрес переменной, т.к. этоне принесет вреда. Однако, адрес константы нельзя присваивать указателюбез спецификации const, иначе станет возможным менять ее значение,например: int a = 1; const int c = 2; const int* p1 = &c; // нормально const int* p2 = &a; // нормально int* p3 = &c; // ошибка *p3 = 7; // меняет значение c

Перечисления

Есть способ связывания имен с целыми константами, который часто болееудобен, чем описание с const. Например: enum { ASM, AUTO, BREAK }; Здесь определены три целых константы, которые называются элементамиперечисления, и им присвоены значения. Поскольку по умолчанию значенияэлементов перечисления начинаются с 0 и идут в возрастающем порядке,то приведенное перечисление эквивалентно определениям: const ASM = 0; const AUTO = 1; const BREAK = 2; Перечисление может иметь имя, например: enum keyword { ASM, AUTO, BREAK }; Имя перечисления становится новым типом. С помощью стандартныхпреобразований тип перечисления может неявно приводиться к типу int.Обратное преобразование (из типа int в перечисление) должно быть заданоявно. Например: void f() { keyword k = ASM; int i = ASM; k = i // ошибка k = keyword(i); i = k; k = 4; // ошибка } Последнее преобразование поясняет, почему нет неявного преобразованияиз int в перечисление: большинство значений типа int не имеетпредставления в данном перечислении.Описав переменную с типом keyword вместо очевидного int, мы даликак пользователю, так и транслятору определенную информацию о том,как будет использоваться эта переменная. Например, для следующегооператора keyword key; switch (key) { case ASM: // выполнить что-либо break; case BREAK: // выполнить что-либо break; } транслятор может выдать предупреждение, поскольку из трех возможныхзначений типа keyword используются только два. Значения элементов перечисления можно задавать и явно. Например: enum int16 { sign=0100000, most_significant=040000, least_significant=1 }; Задаваемые значения необязательно должны быть различными, положительнымии идти в возрастающем порядке.

Экономия памяти

В процессе создания нетривиальной программы рано или поздно наступаетмомент, когда требуется больше памяти, чем можно выделить илизапросить. Есть два способа выжать еще некоторое количество памяти:[1] паковать в байты переменные с малыми значениями;[2] использовать одну и ту же память для хранения разных объектов в разное время.Первый способ реализуется с помощью полей, а второй - с помощьюобъединений. И те, и другие описываются ниже. Поскольку назначениеэтих конструкций связано в основном с оптимизацией программы, ипоскольку, как правило, они непереносимы, программисту следуетхорошенько подумать, прежде чем использовать их. Часто лучше изменитьалгоритм работы с данными, например, больше использовать динамическивыделяемую память, чем заранее отведенную статическую память.

Поля

Кажется расточительным использовать для признака, принимающеготолько два значения (например: да, нет) тип char, но объект типаchar является в С++ наименьшим объектом, который может независиморазмещаться в памяти. Однако, есть возможность собрать переменныес малым диапазоном значений воедино, определив их как поля структуры.Член структуры является полем, если в его определении после имениуказано число разрядов, которое он должен занимать. Допустимыбезымянные поля. Они не влияют на работу с поименованными полями,но могут улучшить размещение полей в памяти для конкретной машины: struct sreg { unsigned enable: 1; unsigned page: 3; unsigned: 1; // не используется unsigned mode: 2; unsigned: 4; // не используется unsigned access: 1; unsigned length: 1; unsigned non_resident: 1; }; Приведенная структура описывает разряды нулевогорегистра состояния DEC PDP11/45 (предполагается, что поля в словеразмещаются слева направо). Этот пример показывает также другоевозможное применение полей: давать имена тем частямобъекта, размещение которых определено извне. Поле должно иметьцелый тип ($$R.3.6.1 и $$R.9.6), и оно используется аналогично другимобъектам целого типа. Но есть исключение: нельзя брать адрес поля.В ядре операционной системы или в отладчике тип sreg мог быиспользоваться следующим образом: sreg* sr0 = (sreg*)0777572; //... if (sr0->access) { // нарушение прав доступа // разобраться в ситуации sr0->access = 0; } Тем не менее, применяя поля для упаковки нескольких переменных в один байт, мы необязательно сэкономим память. Экономится память для данных, но на большинстве машин одновременно возрастает объем команд, нужных для работы с упакованными данными. Известны даже такие программы, которые значительно сокращались в объеме, если двоичные переменные, задаваемые полями, преобразовывались в переменные типа char! Кроме того, доступ к char или int обычно происходит намного быстрее, чем доступ к полю. Поля - это просто удобная краткая форма задания логических операций для извлечения или занесения информации в части слова.

Объединения

Рассмотрим таблицу имен, в которой каждый элемент содержит имя и его значение. Значение может задаваться либо строкой, либо целым числом: struct entry { char* name; char type; char* string_value; // используется если type == 's' int int_value; // используется если type == 'i' }; void print_entry(entry* p) { switch(p->type) { case 's': cout << p->string_value; break; case 'i': cout << p->int_value; break; default: cerr << "type corrupted\n"; break; } } Поскольку переменные string_value и int_value никогда не могут использоваться одновременно, очевидно, что часть памяти пропадает впустую. Это можно легко исправить, описав обе переменные как члены объединения, например, так: struct entry { char* name; char type; union { char* string_value; // используется если type == 's' int int_value; // используется если type == 'i' }; }; Теперь гарантируется, что при выделении памяти для entry членыstring_value и int_value будут размещаться с одного адреса, ипри этом не нужно менять все части программы, работающие с entry.Из этого следует, что все члены объединения вместе занимают такой жеобъем памяти, какой занимает наибольший член объединения. Надежный способ работы с объединением заключается в том, чтобывыбирать значение с помощью того же самого члена, который его записывал.Однако, в больших программах трудно гарантировать, что объединениеиспользуется только таким способом, а в результате использованияне того члена обЪединения могут возникать трудно обнаруживаемые ошибки.Но можно встроить объединение в такую структуру, которая обеспечитправильную связь между значением поля типа и текущим типом членаобъединения ($$5.4.6). Иногда объединения используют для "псевдопреобразований" типа(в основном на это идут программисты, привыкшие к языкам, в которыхнет средств преобразования типов, и в результате приходится обманыватьтранслятор). Приведем пример такого "преобразования" int в int*на машине VAX, которое достигается простым совпадением разрядов: struct fudge { union { int i; int* p; }; }; fudge a; a.i = 4095; int* p = a.p; // некорректное использование В действительности это вовсе не преобразование типа, т.к. на однихмашинах int и int* занимают разный объем памяти, а на других целоене может размещаться по адресу, задаваемому нечетным числом. Такоеиспользование объединений не является переносимым, тогда каксуществует переносимый способ задания явного преобразованиятипа ($$3.2.5). Иногда объединения используют специально, чтобы избежатьпреобразования типов. Например, можно использовать fudge, чтобыузнать, как представляется указатель 0: fudge.p = 0; int i = fudge.i; // i необязательно должно быть 0 Объединению можно дать имя, то есть можно сделать егополноправным типом. Например, fudge можно описать так: union fudge { int i; int* p; }; и использовать (некорректно) точно так же, как и раньше. Вместе с тем,поименованные объединения можно использовать и вполне корректными оправданным способом (см. $$5.4.6).

Упражнения

1. (*1) Запустить программу "Hello, world" (см. $$1.3.1).2. (*1) Для каждого описания из $$2.1 сделать следующее: если описание не является определением, то написать соответствующее определение; если же описание является определением, написать для него описание, которое не являлось бы одновременно и определением.3. (*1) Напишите описания следующих объектов: указателя на символ; массива из 10 целых; ссылки на массив из 10 целых; указателя на массив символьных строк; указателя на указатель на символ; целого-константы; указателя на целое-константу; константного указателя на целое. Описания снабдить инициализацией.4. (*1.5) Напишите программу, которая печатает размеры основных типов и типа указателя. Используйте операцию sizeof.5. (*1.5) Напишите программу, которая печатает буквы от 'a' до 'z' и цифры от '0' до '9' и их целые значения. Проделайте то же самое для других видимых символов. Проделайте это, используя шестнадцатеричную запись.6. (*1) Напечатайте последовательность разрядов представления указателя 0 на вашей машине. Подсказка: см.$$2.6.2.7. (*1.5) Напишите функцию, печатающую порядок и мантиссу параметра типа double.8. (*2) Каковы на используемой вами машине наибольшие и наименьшие значения следующих типов: char, short,int,long, float, double, long double, unsigned, char*, int* и void*? Есть ли какие-то особые ограничения на эти значения? Например, может ли int* быть нечетным целым? Как выравниваются в памяти объекты этих типов? Например, может ли целое иметь нечетный адрес?9. (*1) Какова максимальная длина локального имени, которое можно использовать в вашей реализации С++? Какова максимальная длина внешнего имени? Есть ли какие-нибудь ограничения на символы, которые можно использовать в имени? 10. (*1) Напишите функцию, которая меняет местами значения двух целых. В качестве типа параметров используйте int*. Напишите другую функцию с тем же назначением, используя в качестве типа параметров int&. 11. (*1) Каков размер массива str в следующем примере: char str[] = "a short string"; Какова длина строки "a short string"? 12. (*1.5) Составьте таблицу из названий месяцев года и числа дней в каждом из них. Напишите программу, печатающую ее. Проделайте это дважды: один раз - используя массивы для названий месяцев и количества дней, а другой раз - используя массив структур, каждая из которых содержит название месяца и количество дней в нем. 13. (*1) С помощью typedef определите типы: unsigned char, константный unsigned char, указатель на целое, указатель на указатель на символ, указатель на массив символов, массив из 7 указателей на целое, указатель на массив из 7 указателей на целое и массив из 8 массивов из 7 указателей на целое. 14. (*1) Определить функции f(char), g(char&) и h(const char&) и вызвать их, используя в качестве параметров 'a', 49, 3300, c, uc, и sc, где c - char, uc - unsigned char и sc - signed char. Какой вызов является законным? При каком вызове транслятору придется завести временную переменную?

* ГЛАВА 3. ВЫРАЖЕНИЯ И ОПЕРАТОРЫ

"Но с другой стороны не следует забывать про эффективность" (Джон Бентли) С++ имеет сравнительно небольшой набор операторов, который позволяетсоздавать гибкие структуры управления, и богатый набор операций дляработы с данными. Основные их возможности показаны в этой главе на одномзавершенном примере. Затем приводится сводка выражений, и подробнообсуждаются операции преобразования типа и размещение в свободной памяти.Далее дана сводка операторов, а в конце главы обсуждается выделениетекста пробелами и использование комментариев.

Калькулятор

Мы познакомимся с выражениями и операторами на примере программыкалькулятора. Калькулятор реализует четыре основных арифметическихдействия в виде инфиксных операций над числами с плавающей точкой.В качестве упражнения предлагается добавить к калькуляторупеременные. Допустим, входной поток имеет вид: r=2.5 area=pi*r*r (здесь pi имеет предопределенное значение). Тогда программа калькуляторавыдаст: 2.5 19.635 Результат вычислений для первой входной строки равен 2.5, а результат для второй строки - это 19.635. Программа калькулятора состоит из четырех основных частей:анализатора, функции ввода, таблицы имен и драйвера. По сути - этотранслятор в миниатюре, в котором анализатор проводит синтаксическийанализ, функция ввода обрабатывает входные данные и проводитлексический анализ, таблица имен хранит постоянную информацию, нужнуюдля работы, а драйвер выполняет инициализацию,вывод результатов и обработку ошибок. К такому калькулятору можнодобавить много других полезных возможностей, но программа его и такдостаточно велика (200 строк), а введение новых возможностейтолько увеличит ее объем, не давая дополнительнойинформации для изучения С++.

Анализатор

Грамматика языка калькулятора определяется следующими правилами: программа: END // END - это конец ввода список-выражений END список-выражений: выражение PRINT // PRINT - это '\n' или ';' выражение PRINT список-выражений выражение: выражение + терм выражение - терм терм терм: терм / первичное терм * первичное первичное первичное: NUMBER // число с плавающей запятой в С++ NAME // имя в языке С++ за исключением '_' NAME = выражение - первичное (выражение) Иными словами, программа есть последовательность строк, а каждая строка содержит одно или несколько выражений, разделенных точкой с запятой. Основные элементы выражения - это числа, имена и операции *, /, +, - (унарный и бинарный минус) и =. Имена необязательно описывать до использования. Для синтаксического анализа используется метод, обычно называемыйрекурсивным спуском. Это распространенный и достаточно очевидныйметод. В таких языках как С++, то есть в которых операция вызоване сопряжена с большими накладными расходами, это метод эффективен.Для каждого правила грамматики имеется своя функция, которая вызываетдругие функции. Терминальные символы (например, END, NUMBER, + и -)распознаются лексическим анализатором get_token(). Нетерминальныесимволы распознаются функциями синтаксического анализатора expr(),term() и prim(). Как только оба операнда выражения или подвыражениястали известны, оно вычисляется. В настоящем трансляторе в этотмомент создаются команды, вычисляющие выражение. Анализатор использует для ввода функцию get_token().Значение последнего вызова get_token() хранится в глобальной переменнойcurr_tok. Переменная curr_tok принимает значения элементов перечисленияtoken_value: enum token_value { NAME, NUMBER, END, PLUS='+', MINUS='-', MUL='*', DIV='/', PRINT=';', ASSIGN='=', LP='(', RP=')' }; token_value curr_tok; Для всех функций анализатора предполагается, что get_token() уже была вызвана, и поэтому в curr_tok хранится следующая лексема, подлежащая анализу. Это позволяет анализатору заглядывать на одну лексему вперед. Каждая функция анализатора всегда читает на одну лексему больше, чем нужно для распознавания того правила, для которого она вызывалась. Каждая функция анализатора вычисляет "свое" выражение и возвращает его результат. Функция expr() обрабатывает сложение и вычитание. Она состоит из одного цикла, в котором распознанные термы складываются или вычитаются: double expr() // складывает и вычитает { double left = term(); for(;;) // ``вечно'' switch(curr_tok) { case PLUS: get_token(); // случай '+' left += term(); break; case MINUS: get_token(); // случай '-' left -= term(); break; default: return left; } } Сама по себе эта функция делает немного. Как принято ввысокоуровневых функциях больших программ, она выполняет задание,вызывая другие функции. Отметим, что выражения вида 2-3+4вычисляются как (2-3)+4, что предопределяется правилами грамматики.Непривычная запись for(;;) - это стандартный способ задания бесконечногоцикла, и его можно обозначить словом "вечно". Это вырожденная формаоператора for, и альтернативой ей может служить оператор while(1).Оператор switch выполняется повторно до тех пор, пока неперестанут появляться операции + или -, а тогда по умолчанию выполняетсяоператор return (default). Операции += и -= используются для выполнения операций сложения ивычитания. Можно написать эквивалентные присваивания: left=left+term() иleft=left-term(). Однако вариант left+=term() и left-=term() нетолько короче, но и более четко определяет требуемое действие. Для бинарнойоперации @ выражение x@=y означает x=x@y, за исключением того, что xвычисляется только один раз. Это применимо к бинарным операциям: + - * / % & | ^ << >> поэтому возможны следующие операции присваивания: += -= *= /= %= &= |= ^= <<= >>= Каждая операция является отдельной лексемой, поэтому a + =1содержит синтаксическую ошибку (из-за пробела между + и =). Расшифровкаопераций следующая: % - взятие остатка, &, | и ^ - разрядные логическиеоперации И, ИЛИ и Исключающее ИЛИ; << и >> сдвиг влево и сдвиг вправо.Функции term() и get_token() должны быть описаны до определения expr().В главе 4 рассматривается построение программы в виде совокупностифайлов. За одним исключением, все программы калькулятора можно составитьтак, чтобы в них все объекты описывались только один раз и до ихиспользования. Исключением является функция expr(), которая вызываетфункцию term(), а она, в свою очередь, вызывает prim(), и уже та, наконец,вызывает expr(). Этот цикл необходимо как-то разорвать, для чего вполнеподходит заданное до определения prim() описание: double expr(); // это описание необходимо Функция term() справляется с умножением и делением аналогичнотому, как функция expr() со сложением и вычитанием: double term() // умножает и складывает { double left = prim(); for(;;) switch(curr_tok) { case MUL: get_token(); // случай '*' left *= prim(); break; case DIV: get_token(); // случай '/' double d = prim(); if (d == 0) return error("деление на 0"); left /= d; break; default: return left; } } Проверка отсутствия деления на нуль необходима, посколькурезультат деления на нуль неопределен и, как правило, приводит ккатастрофе.Функция error() будет рассмотрена позже. Переменная d появляется впрограмме там, где она действительно нужна, и сразу же инициализируется.Во многих языках описание может находиться только в начале блока.Но такое ограничение может искажать естественную структуру программы испособствовать появлению ошибок.Чаще всего не инициализированные локальные переменныесвидетельствуют о плохом стиле программирования. Исключение составляютте переменные, которые инициализируются операторами ввода, и переменныетипа массива или структуры, для которых нет традиционнойинициализации с помощью одиночных присваиваний. Следует напомнить, что =является операцией присваивания, тогда как == есть операция сравнения. Функция prim, обрабатывающая первичное, во многом похожа нафункции expr и term(). Но раз мы дошли до низа в иерархии вызовов,то в ней кое-что придется сделать. Цикл для нее не нужен: double number_value; char name_string[256]; double prim() // обрабатывает первичное { switch (curr_tok) { case NUMBER: // константа с плавающей точкой get_token(); return number_value; case NAME: if (get_token() == ASSIGN) { name* n = insert(name_string); get_token(); n->value = expr(); return n->value; } return look(name_string)->value; case MINUS: // унарный минус get_token(); return -prim(); case LP: get_token(); double e = expr(); if (curr_tok!= RP) return error("требуется)"); get_token(); return e; case END: return 1; default: return error("требуется первичное"); } } Когда появляется NUMBER (то есть константа с плавающей точкой),возвращается ее значение. Функция ввода get_token() помещает значениеконстанты в глобальную переменную number_value. Если в программеиспользуются глобальные переменные, то часто это указывает на то, чтоструктура не до конца проработана, и поэтому требуется некотораяоптимизация. Именно так обстоит дело в данном случае. В идеале лексемадолжна состоять из двух частей: значения, определяющего вид лексемы(в данной программе это token_value), и (если необходимо) собственнозначения лексемы. Здесь же имеется только одна простая переменнаяcurr_tok, поэтому для хранения последнего прочитанного значения NUMBERтребуется глобальная переменная number_value. Такое решение проходитпотому, что калькулятор во всех вычислениях вначале выбирает одно число,а затем считывает другое из входного потока. В качестве упражненияпредлагается избавиться от этой излишней глобальной переменной($$3.5 [15]). Если последнее значение NUMBER хранится в глобальной переменнойnumber_value, то строковое представление последнего значения NAMEхранится в name_string. Перед тем, как что-либо делать с именем,калькулятор должен заглянуть вперед, чтобы выяснить, будет ли емуприсваиваться значение, или же будет только использоваться существующееего значение. В обоих случаях надо обратиться к таблице имен. Эта таблицарассматривается в $$3.1.3; а здесь достаточно только знать, что онасостоит из записей, имеющих вид: struct name { char* string; name* next; double value; }; Член next используется только служебными функциями, работающимис таблицей: name* look(const char*); name* insert(const char*); Обе функции возвращают указатель на ту запись name, которая соответствуетих параметру-строке. Функция look() "ругается", если имя не былозанесено в таблицу. Это означает, что в калькуляторе можно использоватьимя без предварительного описания, но в первый раз оно можетпоявиться только в левой части присваивания.

Функция ввода

Получение входных данных - часто самая запутанная часть программы.Причина кроется в том, что программа должна взаимодействоватьс пользователем, то есть "мириться" с его прихотями, учитывать принятыесоглашения и предусматривать кажущиеся редкими ошибки.Попытки заставить человека вести себя более удобным для машины образом,как правило, рассматриваются как неприемлемые, что справедливо.Задача ввода для функции низкого уровня состоит в последовательномсчитывании символов и составлении из них лексемы, с которой работаютуже функции более высокого уровня. В этом примере низкоуровневый вводделает функция get_token(). К счастью, написание низкоуровневойфункции ввода достаточно редкая задача. В хороших системах естьстандартные функции для таких операций. Правила ввода для калькулятора были специально выбраны несколькогромоздкими для потоковых функций ввода. Незначительные измененияв определениях лексем превратили бы get_token() в обманчиво простуюфункцию. Первая сложность состоит в том, что символ конца строки '\n'важен для калькулятора, но потоковые функции ввода воспринимают егокак символ обобщенного пробела. Иначе говоря, для этих функций '\n'имеет значение только как символ, завершающий лексему.Поэтому приходится анализировать все обобщенные пробелы (пробел,табуляция и т.п.). Это делается в операторе do, который эквивалентеноператору while, за исключением того, что тело оператора doвсегда выполняется хотя бы один раз: char ch; do { // пропускает пробелы за исключением '\n' if(!cin.get(ch)) return curr_tok = END; } while (ch!='\n' && isspace(ch)); Функция cin.get(ch) читает один символ из стандартного входного потокав ch. Значение условия if(!cin.get(ch)) - ложь, если из потока cinнельзя получить ни одного символа. Тогда возвращается лексема END, чтобызакончить работу калькулятора. Операция! (NOT) нужна потому, чтов случае успешного считывания get() возвращает ненулевое значение. Функция-подстановка isspace() из <ctype.h> проверяет, не являетсяли ее параметр обобщенным пробелом ($$10.3.1). Она возвращает ненулевоезначение, если является, и нуль в противном случае. Проверка реализуетсякак обращение к таблице, поэтому для скорости лучше вызывать isspace(),чем проверять самому. То же можно сказать о функциях isalpha(), isdigit()и isalnum(), которые используются в get_token(). После пропуска обобщенных пробелов следующий считанный символопределяет, какой будет начинающаяся с него лексема. Прежде, чемпривести всю функцию, рассмотрим некоторые случаи отдельно. Лексемы'\n' и ';', завершающие выражение, обрабатываются следующим образом: switch (ch) { case ';': case '\n': cin >> ws; // пропуск обобщенного пробела return curr_tok=PRINT; Необязательно снова пропускать пробел, но, сделав это, мыизбежим повторных вызовов функции get_token(). Переменная ws, описаннаяв файле <stream.h>, используется только как приемник ненужных пробелов.Ошибка во входных данных, а также конец ввода не будут обнаружены доследующего вызова функции get_token(). Обратите внимание, как несколькометок выбора помечают одну последовательность операторов, заданнуюдля этих вариантов. Для обоих символов ('\n' и ';') возвращается лексемаPRINT, и она же помещается в curr_tok. Числа обрабатываются следующим образом: case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '.': cin.putback(ch); cin >> number_value; return curr_tok=NUMBER; Размещать метки вариантов горизонтально, а не вертикально,- не самыйлучший способ, поскольку такой текст труднее читать; но писать строкудля каждой цифры утомительно. Поскольку оператор >> может читатьконстанту с плавающей точкой типа double, программа тривиальна:прежде всего начальный символ (цифра или точка) возвращается назадв cin, а затем константу можно считать в number_value.Имя, т.е. лексема NAME, определяется как буква, за которой можетидти несколько букв или цифр: if (isalpha(ch)) { char* p = name_string; *p++ = ch; while (cin.get(ch) && isalnum(ch)) *p++ = ch; cin.putback(ch); *p = 0; return curr_tok=NAME; } Этот фрагмент программы заносит в name_string строку, оканчивающуюся нулевым символом. Функции isalpha() и isalnum() определены в <ctype.h>. Результат isalnum(c) ненулевой, если c - буква или цифра, и нулевой в противном случае. Приведем, наконец, функцию ввода полностью: token_value get_token() { char ch; do { // пропускает обобщенные пробелы за исключением '\n' if(!cin.get(ch)) return curr_tok = END; } while (ch!='\n' && isspace(ch)); switch (ch) { case ';': case '\n': cin >> ws; // пропуск обобщенного пробела return curr_tok=PRINT; case '*': case '/': case '+': case '-': case '(': case ')': case '=': return curr_tok=token_value(ch); case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '.': cin.putback(ch); cin >> number_value; return curr_tok=NUMBER; default: // NAME, NAME= или ошибка if (isalpha(ch)) { char* p = name_string; *p++ = ch; while (cin.get(ch) && isalnum(ch)) *p++ = ch; cin.putback(ch); *p = 0; return curr_tok=NAME; } error("недопустимая лексема"); return curr_tok=PRINT; } } Преобразование операции в значение лексемы для нее тривиально, поскольку в перечислении token_value лексема операции была определена как целое (код символа операции).

Таблица имен

Есть функция поиска в таблице имен: name* look(char* p, int ins =0); Второй ее параметр показывает, была ли символьная строка, обозначающаяимя, ранее занесена в таблицу. Инициализатор =0 задает стандартноезначение параметра, которое используется, если функция look()вызывается только с одним параметром. Это удобно, так какможно писать look("sqrt2"), что означает look("sqrt2",0),т.е. поиск, а не занесение в таблицу. Чтобы было так же удобно задаватьоперацию занесения в таблицу, определяется вторая функция: inline name* insert(const char* s) { return look(s,1); } Как ранее упоминалось, записи в этой таблице имеют такой тип: struct name { char* string; name* next; double value; }; Член next используется для связи записей в таблице.Собственно таблица - это просто массив указателей на объекты типа name: const TBLSZ = 23; name* table[TBLSZ]; Поскольку по умолчанию все статические объекты инициализируются нулем, такое тривиальное описание таблицы table обеспечивает также и нужную инициализацию. Для поиска имени в таблице функция look() использует простой хэш-код (записи, в которых имена имеют одинаковый хэш-код, связываются): вместе): int ii = 0; // хэш-код const char* pp = p; while (*pp) ii = ii<<1 ^ *pp++; if (ii < 0) ii = -ii; ii %= TBLSZ; Иными словами, с помощью операции ^ ("исключающее ИЛИ") все символывходной строки p поочередно добавляются к ii. Разряд в результате x^yравен 1 тогда и только тогда, когда эти разряды в операндах x и y различны.До выполнения операции ^ значение ii сдвигается на один разряд влево,чтобы использовался не только один байт ii. Эти действия можнозаписать таким образом: ii <<= 1; ii ^= *pp++; Для хорошего хэш-кода лучше использовать операцию ^, чем +. Операциясдвига важна для получения приемлемого хэш-кода в обоих случаях.Операторы if (ii < 0) ii = -ii; ii %= TBLSZ; гарантируют, что значение ii будет из диапазона 0...TBLSZ-1. Напомним,что % - это операция взятия остатка. Ниже полностью приведенафункция look: #include <string.h> name* look(const char* p, int ins =0) { int ii = 0; // хэш-код const char* pp = p; while (*pp) ii = ii<<1 ^ *pp++; if (ii < 0) ii = -ii; ii %= TBLSZ; for (name* n=table[ii]; n; n=n->next) // поиск if (strcmp(p,n->string) == 0) return n; if (ins == 0) error("имя не найдено"); name* nn = new name; // занесение nn->string = new char[strlen(p)+1]; strcpy(nn->string,p); nn->value = 1; nn->next = table[ii]; table[ii] = nn; return nn; } После вычисления хэш-кода ii идет простой поиск имени по членам next. Имена сравниваются с помощью стандартной функции сравнения строк strcmp(). Если имя найдено, то возвращается указатель на содержащую его запись, а в противном случае заводится новая запись с этим именем. Добавление нового имени означает создание нового объекта name в свободной памяти с помощью операции new (см. $$3.2.6), его инициализацию и включение в список имен. Последнее выполняется как занесение нового имени в начало списка, поскольку это можно сделать даже без проверки того, есть ли список вообще. Символьная строка имени также размещается в свободной памяти. Функция strlen() указывает, сколько памяти нужно для строки, операция new отводит нужную память, а функция strcpy() копирует в нее строку. Все строковые функции описаны в <string.h>: extern int strlen(const char*); extern int strcmp(const char*, const char*); extern char* strcpy(char*, const char*);

Обработка ошибок

Поскольку программа достаточно проста, не надо особо беспокоитьсяоб обработке ошибок. Функция error просто подсчитывает число ошибок,выдает сообщение о них и возвращает управление обратно: int no_of_errors; double error(const char* s) { cerr << "error: " << s << "\n"; no_of_errors++; return 1; } Небуферизованный выходной поток cerr обычно используется именно длявыдачи сообщений об ошибках.Управление возвращается из error() потому, что ошибки, как правило,встречаются посреди вычисления выражения. Значит надо либо полностьюпрекращать вычисления, либо возвращать значение, которое не должновызвать последующих ошибок. Для простого калькулятора больше подходитпоследнее. Если бы функция get_token() отслеживала номера строк, тофункция error() могла бы указывать пользователю приблизительное местоошибки. Это было бы полезно при неинтерактивной работе с калькулятором.Ча


Поделиться:




Поиск по сайту

©2015-2024 poisk-ru.ru
Все права принадлежать их авторам. Данный сайт не претендует на авторства, а предоставляет бесплатное использование.
Дата создания страницы: 2016-02-16 Нарушение авторских прав и Нарушение персональных данных


Поиск по сайту: