Я:
Результат
Архив

МЕТА - Украина. Рейтинг сайтов Webalta Уровень доверия



Союз образовательных сайтов
Главная / Учебники / Учебники на русском языке / Компьютерные науки / C++


Компьютерные науки - Учебники на русском языке - Скачать бесплатно


switch p->type (*
case 's':
cout << p->string_value;
break;
case 'i':
cout << p->int_value;
break;
default:
cerr << "испорчен type\n";
break;
*)
*)

Поскольку string_value и int_value никогда не могут ис-
пользоваться одновременно, ясно, что пространство пропадает
впустую. Это можно легко исправить, указав, что оба они долж-
ны быть членами union. Например, так:

struct entry (*
char* name;
char type;
union (*
char* string_value; //используется если type == 's'
int int_value; //используется если type == 'i'
*);
*);

Это оставляет всю часть программы, использующую entry,
без изменений, но обеспечивает, что при размещении entry
string_value и int_value имеют один и тот же адрес. Отсюда
следует, что все члены объединения вместе занимают лишь
столько памяти, сколько занимает наибольший член.

Использование объединений таким образом, чтобы при чте-
нии значения всегда применялся тот член, с применением кото-
рого оно записывалось, совершенно оптимально. Но в больших
программах непросто гарантировать, что объединения использу-
ются только таким образом, и из-за неправильного использова-
ния могут появляться трудно уловимые ошибки. Можно @капсули-
зировать объединение таким образом, чтобы соответствие между
полем типа и типами членов было гарантированно правильным
(#5.4.6).

Объединения иногда используют для "объединения и преоб-
разование типа" (это делают главным образом программисты,
воспитанные на языках, не обладающих средствами преобразова-
ния типов, где жульничество является необходимым). Например,
это "преобразует" на VAX'е int в int*, просто предполагая по-

- 60 -
битовую эквивалентность:

struct fudge (*
union (*
int i;
int* p;
*);
*);

fudge a;
a.i = 4096;
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.

2.6 Упражнения

1. (*1) Заставьте работать программу с "Hello, world"
(1.1.1).

2. (*1) Для каждого описания в #2.1 сделайте следующее:
Если описание не является определением, напишите для него оп-
ределение. Если описание является определением, напишите для
него описание, которое при этом не является определением.

3. (*1) Напишите описания для: указателя на символ; век-
тора из 10 целых; ссылки на вектор из 10 целых; указателя на
вектор из символьных строк; указателя на указатель на символ;
константного целого; указателя на константное целое; и конс-
тантного указателя на целое. Каждый из них инициализируйте.

4. (*1.5) Напишите программу, которая печатает размеры
основных и указательных типов. Используйте операцию sizeof.

5. (*1.5) Напишите программу, которая печатает буквы
'a'...'z' и цифры '0'...'9' и их числовые значения. Сделайте
то же для остальных печатаемых символов. Сделайте то же, но
используя шестнадцатиричную запись.

6. (*1) Напечатайте набор битов, которым представляется
указатель 0 на вашей системе. Подсказка: #2.5.2.

7. (*1.5) Напишите функцию, печатающую порядок и мантис-

- 61 -
су параметра типа double.

8. (*2) Каковы наибольшие и наименьшие значения, на ва-
шей системе, следующих типов: char, short, int, long, float,
double, unsigned, char*, int* и void*? Имеются ли дополни-
тельные ограничения на принимаемые ими значения? Может ли,
например, int* принимать нечетное значение? Как выравниваются
в памяти объекты этих типов? Может ли, например, int иметь
нечетный адрес?

9. (*1) Какое самое длинное локальное имя можно исполь-
зовать в С++ программе в вашей системе? Какое самое длинное
внешнее имя можно использовать в С++ программе в вашей систе-
ме? Есть ли какие-нибудь ограничения на символы, которые мож-
но употреблять в имени?

10. (*2) Определите one следующим образом:

const one = 1;

Попытайтесь поменять значение one на 2. Определите num
следующим образом:

const num[] = (* 1, 2 *);

Попытайтесь поменять значение num[1] на 2.

11. (*1) Напишите функцию, переставляющую два целых (ме-
няющую значения). Используйте в качестве типа параметра int*.
Напишите другую переставляющую функцию, использующую в ка-
честве типа параметра int&.

12. (*1) Каков размер вектора str в следующем примере:

char str[] = "a short string";

Какова длина строки "a short string"?

13. (*1.5) Определите таблицу названий месяцев года и
числа дней в них. Выведите ее. Сделайте это два раза: один
раз используя вектор для названий и вектор для числа дней, и
один раз используя вектор структур, в каждой из которых хра-
нится название месяца и число дней в нем.

14. (*1) С помощью typedef определите типы: беззнаковый
char, константный беззнаковый char, указатель на целое, ука-
затель на указатель на char, указатель на вектора символов,
вектор из 7 целых указателей, указатель на вектор из 7 целых
указателей, и вектор из 8 векторов из 7 целых указателей.

- 62 -


Глава 3
Выражения и Операторы

С другой стороны,
мы не можем игнорировать эффективность
- Джон Бентли

С++ имеет небольшой, но гибкий набор различных видов
операторов для контроля потока управления в программе и бога-
тый набор операций для манипуляции данными. С наиболее общеп-
ринятыми средствами вас познакомит один законченный пример.
После него приводится резюмирующий обзор выражений и с до-
вольно подробно описываются явное описание типа и работа со
свободной памятью. Потом представлена краткая сводка опера-
ций, а в конце обсуждаются стиль выравнивания* и комментарии.

--------------------
* Нам неизвестен русскоязычный термин, эквивалентный ан-
глийскому indentation. Иногда это называется отступами.
(прим. перев.)

3.1 Настольный калькулятор

С операторами и выражениями вас познакомит приведенная
здесь программа настольного калькулятора, предоставляющего
четыре стандартные арифметические операции над числами с пла-
вающей точкой. Пользователь может также определять перемен-
ные. Например, если вводится

r=2.5
area=pi*r*r

(pi определено заранее), то программа калькулятора напишет:

2.5
19.635

где 2.5 - результат первой введенной строки, а 19.635 -
результат второй.

Калькулятор состоит из четырех основных частей: програм-
мы синтаксического разбора (parser'а), функции ввода, таблицы
имен и управляющей программы (драйвера). Фактически, это ми-
ниатюрный компилятор, в котором программа синтаксического
разбора производит синтаксический анализ, функция ввода осу-
ществляет ввод и лексический анализ, в таблице имен хранится
долговременная информация, а драйвер распоряжается инициали-
зацией, выводом и обработкой ошибок. Можно было бы многое до-
бавить в этот калькулятор, чтобы сделать его более полезным,
но в существующем виде эта программа и так достаточно длинна
(200 строк), и большая часть дополнительных возможностей
просто увеличит текст программы не давая дополнительного по-
нимания применения С++.

3.1.1 Программа синтаксического разбора

Вот грамматика языка, допускаемого калькулятором:

program:
END // END - это конец ввода
expr_list END

expr_list:
expression PRINT // PRINT - это или '\n' или ';'
expression PRINT expr_list


- 63 -
expression:
expression + term
expression - term
term

term:
term / primary
term * primary
primary

primary:
NUMBER // число с плавающей точкой в С++
NAME // имя С++ за исключением '_'
NAME = expression
- primary
( expression )

Другими словами, программа есть последовательность
строк. Каждая строка состоит из одного или более выражений,
разделенных запятой. Основными элементами выражения являются
числа, имена и операции *, /, +, - (унарный и бинарный) и =.
Имена не обязательно должны описываться до использования.

Используемый метод обычно называется рекурсивным спуском
это популярный и простой нисходящий метод. В таком языке, как
С++, в котором вызовы функций относительно дешевы, этот метод
к тому же и эффективен. Для каждого правила вывода грамматики
имеется функция, вызывающая другие функции. Терминальные сим-
волы (например, 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();

- 64 -
break;
case MINUS:
get_token(); // ест '-'
left -= term();
break;
default:
return left;
*)
*)

Фактически сама функция делает не очень много. В манере,
достаточно типичной для функций более высокого уровня в боль-
ших программах, она вызывает для выполнения работы другие
функции. Заметьте, что выражение 2-3+4 вычисляется как (2-3)+
4, как указано грамматикой.

Странная запись for(;;) - это стандартный способ задать
бесконечный цикл. Можно произносить это как "навсегда"*. Это
вырожденная форма оператора for, альтернатива - while(1). Вы-
полнение оператора switch повторяется до тех пор, пока не бу-
дет найдено ни + ни -, и тогда выполняется оператор return в
случае default.

--------------------
* игра слов: "for" - "forever" (навсегда). (прим. перев.)

Операции +=, -= используются для осуществления сложения
и вычитания. Можно было бы не изменяя смысла программы ис-
пользовать 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(). Этот круг надо как-то разорвать;

Описание

double expr(); // без этого нельзя

перед prim() прекрасно справляется с этим.

Функция term() аналогичным образом обрабатывает умноже-
ние и сложение:

double term() // умножает и складывает
(*
double left = prim();

- 65 -
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(char*) будет описана позже. Переменная d вво-
дится в программе там, где она нужна, и сразу же инициализи-
руется. Во многих языках описание может располагаться только
в голове блока. Это ограничение может приводить к довольно
скверному искажению стиля программирования и/или излишним
ошибкам. Чаще всего неинициализированные локальные переменные
являются просто признаком плохого стиля; исключением являются
переменные, подлежащие инициализации посредством ввода, и пе-
ременные векторного или структурного типа, которые нельзя
удобно инициализировать одними присваиваниями*. Заметьте, что
= является операцией присваивания, а == операцией сравнения.

--------------------
* В языке немного лучше этого с этими исключениями тоже надо
бы справляться. (прим. автора)

Функция prim, обрабатывающая primary, написана в основ-
ном в том же духе, не считая того, что немного реальной рабо-
ты в ней все-таки выполняется, и нет нужды в цикле, поскольку
мы попадаем на более низкий уровень иерархии вызовов:

double prim() // обрабатывает primary (первичные)
(*
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:

- 66 -
return error("должно быть primary");
*)
*)

При обнаружении NUMBER (то есть, константы с плавающей
точкой), возвращается его значение. Функция ввода get_token()
помещает значение в глобальную переменную number_value. Ис-
пользование в программе глобальных переменных часто указывает
на то, что структура не совсем прозрачна, что применялась не-
которого рода оптимизация. Здесь дело обстоит именно так. Те-
оретически лексический символ обычно состоит из двух частей:
значения, определяющего вид лексемы (в данной программе token
_value), и (если необходимо) значения лексемы. У нас имеется
только одна простая переменная curr_tok, поэтому для хранения
значения последнего считанного NUMBER понадобилась глобальная
переменная переменная number_value. Это работает только пото-
му, что калькулятор при вычислениях использует только одно
число перед чтением со входа другого.

Так же, как значение последнего встреченного NUMBER хра-
нится в number_value, в name_string в виде символьной строки
хранится представление последнего прочитанного NAME. Перед
тем, как что-либо сделать с именем, калькулятор должен загля-
нуть вперед, чтобы посмотреть, осуществляется ли присваивание
ему, или оно просто используется. В обоих случаях надо спра-
виться в таблице имен. Сама таблица описывается в #3.1.3;
здесь надо знать только, что она состоит из элементов вида:

srtuct name (*
char* string;
char* next;
double value;
*)

где next используется только функциями, которые поддерживают
работу с таблицей:

name* look(char*);
name* insert(char*);

Обе возвращают указатель на name, соответствующее пара-
метру - символьной строке; look() выражает недовольство, если
имя не было определено. Это значит, что в калькуляторе можно
использовать имя без предварительного описания, но первый раз
оно должно использоваться в левой части присваивания.

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

Чтение ввода - часто самая запутанная часть программы.
Причина в том, что если программа должна общаться с челове-
ком, то она должна справляться с его причудами, условностями
и внешне случайными ошибками. Попытки заставить человека вес-
ти себя более удобным для машины образом часто (и справедли-
во) рассматриваются как оскорбительные. Задача низкоуровневой
программы ввода состоит в том, чтобы читать символы по одному
и составлять из них лексические символы более высокого уров-
ня. Далее эти лексемы служат вводом для программ более высо-
кого уровня. У нас ввод низкого уровня осуществляется
get_token(). Обнадеживает то, что написание программ ввода
низкого уровня не является ежедневной работой; в хорошей сис-
теме для этого будут стандартные функции.

Для калькулятора правила сознательно были выбраны таки-
ми, чтобы функциям по работе с потоками было неудобно эти
правила обрабатывать; незначительные изменения в определении
лексем сделали бы get_token() обманчиво простой.
Первая сложность состоит в том, что символ новой строки

- 67 -
'\n' является для калькулятора существенным, а функции работы
с потоками считают его символом пропуска. То есть, для этих
функций '\n' значим только как ограничитель лексемы. Чтобы
преодолеть это, надо проверять пропуски (пробел, символы та-
буляции и т.п.):

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, чтобы завершить сеанс работы кальку-
лятора. Используется операция ! (НЕ), поскольку get() возвра-
щает в случае успеха ненулевое значение.

Функция (inline) isspace() из обеспечивает
стандартную проверку на то, является ли символ пропуском
(#8.4.1); isspace(c) возвращает ненулевое значение, если c
является символом пропуска, и ноль в противном случае. Про-
верка реализуется в виде поиска в таблице, поэтому использо-
вание isspace() намного быстрее, чем проверка на отдельные
символы пропуска; это же относится и к функциям isalpha(),
isdigit() и isalnum(), которые используются в get_token().

После того, как пустое место пропущено, следующий символ
используется для определения того, какого вида какого вида
лексема приходит. Давайте сначала рассмотрим некоторые случаи
отдельно, прежде чем приводить всю функцию. Ограничители лек-
сем '\n' и ';' обрабатываются так:

switch (ch) (*
case ';':
case '\n':
cin >> WS; // пропустить пропуск
return curr_tok=PRINT;

Пропуск пустого места делать необязательно, но он позво-
ляет избежать повторных обращений к get_token(). WS - это
стандартный пропусковый объект, описанный в ; он
используется только для сброса пропуска. Ошибка во вводе или
конец ввода не будут обнаружены до следующего обращения к get
_token(). Обратите внимание на то, как можно использовать
несколько меток case (случаев) для одной и той же последова-
тельности операторов, обрабатывающих эти случаи. В обоих слу-
чаях возвращается лексема 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;

Располагать метки случаев case горизонтально, а не вер-
тикально, не очень хорошая мысль, поскольку читать это
гораздо труднее, но отводить по одной строке на каждую цифру
нудно.

Поскольку операция >> определена также и для чтения
констант с плавающей точкой в double, программирование этого
не составляет труда: сперва начальный символ (цифра или точ-

- 68 -
ка) помещается обратно в 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() заданы в ;
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=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 (значение лексемы) операции было
определено как целое значение этой операции*, обработка всех
операций тривиальна.


- 69 -
--------------------
* знака этой операции. (прим. перев.)

3.1.3 Таблица имен

К таблице имен доступ осуществляется с помощью одной
функции

name* look(char* p, int ins =0);

Ее второй параметр указывает, нужно ли сначала поместить
строку символов в таблицу. Инициализатор =0 задает параметр,
который надлежит использовать по умолчанию, когда look() вы-
зывается с одним параметром. Это дает удобство записи, когда
look("sqrt2") означает look("sqrt2",0), то есть просмотр, без
помещения в таблицу. Чтобы получить такое же удобство записи
для помещения в таблицу, определяется вторая функция:

inline name* insert(char* s) (* return look(s,1);*)

Как уже отмечалось раньше, элементы этой таблицы имеют тип:

srtuct name (*
char* string;
char* next;
double value;
*)

Член next используется только для сцепления вместе имен в
таблице.

Сама таблица - это просто вектор указателей на объекты типа
name:

const TBLSZ = 23;
name* table[TBLSZ];

Поскольку все статические объекты инициализируются ну-
лем, это тривиальное описание таблицы table гарантирует также
надлежащую инициализацию.

Для нахождения элемента в таблице в look() принимается
простой алгоритм хэширования (имена с одним и тем же хэш-ко-
дом зацепляются вместе):

int ii = 0; // хэширование
char* pp = p;
while (*pp) ii = ii<<1 ^ *pp++;
if (ii < 0) ii = -ii;
ii %= TBLSZ;

То есть, с помощью исключающего ИЛИ каждый символ во
входной строке "добавляется" к ii ("сумме" предыдущих симво-
лов). Бит в x^y устанавливается единичным тогда и только тог-
да, когда соответствующие биты в x и y различны. Перед приме-
нением в символе исключающего ИЛИ, ii сдвигается на один бит
влево, чтобы не использовать в слове только один байт. Это
можно было написать и так:

ii <<= 1;
ii ^= *pp++;

Кстати, применение ^ лучше и быстрее, чем +. Сдвиг важен
для получения приемлемого хэш-кода в обоих случаях. Операторы

if (ii < 0) ii = -ii;
ii %= TBLSZ;

- 70 -

обеспечивают, что ii будет лежать в диапазоне 0...TBLSZ-
1; % - это операция взятия по модулю (еще называемая получе-
нием остатка).

Вот функция полностью:

extern int strlen(const char*);
extern int strcmp(const char*, const char*);
extern int strcpy(const char*, const char*);

name* look(char* p, int ins =0)
(*
int ii = 0; // хэширование
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. Проверка каждого name осуществляется
с помощью стандартной функции strcmp(). Если строка найдена,
возвращается ее name, иначе добавляется новое name.

Добавление нового name включает в себя создание нового
объекта в свободной памяти с помощью операции new (см.
#3.2.6), его инициализацию, и добавление его к списку имен.
Последнее осуществляется просто путем помещения нового имени
в голову списка, поскольку это можно делать даже не проверяя,
имеется список, или нет. Символьную строку для имени тоже
нужно сохранить в свободной памяти. Функция strlen() исполь-
зуется для определения того, сколько памяти нужно, new - для
выделения этой памяти, и strcpy() - для копирования строки в
память.

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

Поскольку программа так проста, обработка ошибок не сос-
тавляет большого труда. Функция обработки ошибок просто счи-
тает ошибки, пишет сообщение об ошибке и возвращает управле-
ние обратно:

int no_of_errors;

double error(char* s) (*
cerr << "error: " << s << "\n";
no_of_errors++;
return 1;
*)

Возвращается значение потому, что ошибки обычно
встречаются в середине вычисления выражения, и поэтому надо
либо полностью прекращать вычисление, либо возвращать значе-

- 71 -
ние, которое по всей видимости не должно вызвать последующих
ошибок. Для простого калькулятора больше подходит последнее.
Если бы get_token() отслеживала номера строк, то error() мог-
ла бы сообщать пользователю, где приблизительно обнаружена
ошибка. Это наверняка было бы полезно, если бы калькулятор
использовался неитерактивно.

Часто бывает так, что после появления ошибки программа
должна завершиться, поскольку нет никакого разумного пути
продолжить работу. Это можно сделать с помощью вызова exit(),
которая очищает все вроде потоков вывода (#8.3.2), а затем
завершает программу используя свой параметр в качестве ее
возвращаемого значения. Более радикальный способ завершения
программы - это вызов abort(), которая обрывает выполнение
сразу же или сразу после сохранения где-то информации для от-
ладчика (дамп памяти); о подробностях справьтесь, пожалуйста,
в вашем руководстве.

3.1.5 Драйвер

Когда все части программы на месте, нам нужен только
драйвер для инициализации и всего того, что связано с запус-
ком. В этом простом примере main() может работать так:

int main()
(*
// вставить предопределенные имена:
insert("pi")->value = 3.1415926535897932385;
insert("e")->value = 2.7182818284590452354;

while (cin) (*
get_token();
if (curr_tok == END) break;
if (curr_tok == PRINT) continue;
cout << expr() << "\n";
*)
return no_of_errors;
*)

Принято обычно, что main() возвращает ноль при нормаль-
ном завершении программы и не ноль в противном случае, поэто-
му это прекрасно может сделать возвращение числа ошибок. В
данном случае оказывается, что инициализация нужна только для
введения предопределенных имен в таблицу имен.

Основная работа цикла - читать выражения и писать ответ.
Это делает строка:

cout << expr() << "\n";

Проверка cin на каждом проходе цикла обеспечивает завер-
шение программы в случае, если с потоком ввода что-то не так,
а проверка на END обеспечивает корректный выход из цикла,
когда get_token() встречает конец файла. Оператор break осу-
ществляет выход из ближайшего содержащего его оператора
switch или оператора цикла (то есть, оператора for, оператора
while или оператора do). Проверка на PRINT (то есть, на '\n'
или ';') освобождает expr() от обязанности обрабатывать пус-
тые выражения. Оператор continue равносилен переходу к самому
концу цикла, поэтому в данном случае








- 72 -
while (cin) (*
// ...
if (curr_tok == PRINT) continue;
cout << expr() << "\n";
*)

эквивалентно

while (cin) (*
// ...
if (curr_tok == PRINT) goto end_of_loop;
cout << expr() << "\n";
end_of_loop
*)

Более подробно циклы описываются в #с.9.

3.1.6 Параметры командной строки

После того, как программа была написана и оттестирована,
я заметил, что часто набирать выражения на клавиатуре в стан-
дартный ввод надоедает, поскольку обычно использование прог-
раммы состоит в вычислении одного выражения. Если бы можно
было представлять это выражение как параметр командной стро-
ки, не приходилось бы так много нажимать на клавиши.

Как уже говорилось, программа запускается вызовом
main(). Когда это происходит, main() получает два параметра
указывающий число параметров, обычно называемый argc и вектор
параметров, обычно называемый argv. Параметры - это символь-
ные строки, поэтому argv имеет тип char*[argc]. Имя программы
(так, как оно стоит в командной строке) передается в качестве
argv[0], поэтому argc всегда не меньше единицы. Например, в
случае команды

dc 150/1.1934

параметры имеют значения:

argc 2
argv[0] "dc"
argv[1] "150/1.1934"

Научиться пользоваться параметрами командной строки нес-
ложно. Сложность состоит в том, как использовать их без пе-
репрограммирования. В данном случае это оказывается совсем
просто, поскольку поток ввода можно связать с символьной
строкой, а не с файлом (#8.5). Например, можно заставить cin
читать символы из стандартного ввода:

int main(int argc, char* argv[])
(*
switch(argc) (*
case 1: // читать из стандартного ввода
break;
case 2: // читать параметр строку
cin = *new istream(strlen(argv[1]),argv[1]);
break;
default:
error("слишком много параметров");
return 1;
*)
// как раньше
*)

Программа осталась без изменений, за исключением добав-
ления в main() параметров и использования этих параметров в

- 73 -
операторе switch. Можно было бы легко модифицировать main()
так, чтобы она получала несколько параметров командной стро-
ки, но это оказывается ненужным, особенно потому, что нес-
колько выражений можно передавать как один параметр:
dc "rate=1.1934;150/rate;19.75/rate;217/rate"

Здесь кавычки необходимы, поскольку ; является раздели-
телем команд в системе UNIX.

3.2 Краткая сводка операций

Операции С++ подробно и систематически описываются в #с.
7; прочитайте, пожалуйста, этот раздел. Здесь же приводится
операция краткая сводка и некоторые примеры. После каждой
операции приведено одно или более ее общеупотребительных наз-
ваний и пример ее использования. В этих примерах имя_класса -
это имя класса, член - имя члена, объект - выражение, дающее
в результате объект класса, указатель - выражение, дающее в
результате указатель, выр - выражение, а lvalue - выражение,
денотирующее неконстантный объект. Тип может быть совершенно
произвольным именем типа (со * () и т.п.) только когда он
стоит в скобках, во всех остальных случаях существуют ограни-
чения.

Унарные операции и операции присваивания правоассоциа-
тивны, все остальные левоассоциативны. Это значит, что a=b=c
означает a=(b=c), a+b+c означает (a+b)+c, и *p++ означает *(p
++), а не (*p)++.

Сводка Операций (часть 1)
===========================================================
:: разрешение области видимости имя_класса :: член
:: глобальное :: имя
===========================================================
-> выбор члена указатель->член
[] индексация указатель [ выр ]
() вызов функции выр (список_выр)
() построение значения тип (список_выр)
sizeof размер объекта sizeof выр
sizeof размер типа sizeof ( тип )
===========================================================
++ приращение после lvalue++
++ приращение до ++lvalue
-- уменьшение после lvalue--
-- уменьшение до --lvalue
~ дополнение ~ выр
! не ! выр
- унарный минус - выр
+ унарный плюс + выр
& адрес объекта & lvalue
* разыменование * выр
new создание (размещение) new тип
delete уничтожение (освобождение) delete указатель
delete[] уничтожение вектора delete[ выр ] указатель
() приведение (преобразование типа) ( тип ) выр
===========================================================
* умножение выр * выр
/ деление выр / выр
% взятие по модулю (остаток) выр % выр
===========================================================
+ сложение (плюс) выр + выр
- вычитание (минус) выр - выр
===========================================================
<< сдвиг влево lvalue << выр
>> сдвиг вправо lvalue >> выр
===========================================================
< меньше выр < выр

- 74 -
<= меньше или равно выр <= выр




Назад


Новые поступления

Украинский Зеленый Портал Рефератик создан с целью поуляризации украинской культуры и облегчения поиска учебных материалов для украинских школьников, а также студентов и аспирантов украинских ВУЗов. Все материалы, опубликованные на сайте взяты из открытых источников. Однако, следует помнить, что тексты, опубликованных работ в первую очередь принадлежат их авторам. Используя материалы, размещенные на сайте, пожалуйста, давайте ссылку на название публикации и ее автора.

281311062 © insoft.com.ua,2007г. © il.lusion,2007г.
Карта сайта