вас
познакомит один законченный пример. После него приводится
резюмирующий обзор выражений и с довольно подробно описываются
явное описание типа и работа со свободной памятью. Потом
представлена краткая сводка операций, а в конце обсуждаются стиль
выравнивания*1 и комментарии.
3.1 Настольный калькулятор
| 3.1.1 Программа синтаксического разбора | |
| 3.1.2 Функция ввода | |
| 3.1.3 Таблица имен | |
| 3.1.4 Обработка ошибок | |
| 3.1.5 Драйвер | |
| 3.1.6 Параметры командной строки |
С операторами и выражениями вас познакомит приведенная здесь программа настольного калькулятора, предоставляющего четыре стандартные арифметические операции над числами с плавающей точкой. Пользователь может также определять переменные. Например, если вводится
r=2.5 area=pi*r*r
2.5 19.635
Вот грамматика языка, допускаемого калькулятором:
program:
END // END - это конец ввода
expr_list END
expr_list:
expression PRINT // PRINT - это или '\n' или ';'
expression PRINT expr_list
expression:
expression + term
expression - term
term
term:
term / primary
term * primary
primary
primary:
NUMBER // число с плавающей точкой в C++
NAME // имя C++ за исключением '_'
NAME = expression
- primary
( expression )
enum token_value {
NAME NUMBER END
PLUS='+' MINUS='-' MUL='*' DIV='/'
PRINT=';' ASSIGN='=' LP='(' RP=')'
};
token_value curr_tok;
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;
}
}
| + | - | * | / | % | & | | | ^ | << | >> |
| += | -= | *= | /= | %= | &= | |= | ^= | <<= | >>= |
double expr(); // без этого нельзя
// ест '*'
left *= prim();
break;
case DIV:
get_token(); // ест '/'
double d = prim();
if (d == 0) return error("деление на 0");
left /= d;
break;
default:
return left;
}
}
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:
return error("должно быть primary");
}
}
srtuct name {
char* string;
char* next;
double value;
}
name* look(char*); name* insert(char*);
Чтение ввода - часто самая запутанная часть программы. Причина в
том, что если программа должна общаться с человеком, то она должна
справляться с его причудами, условностями и внешне случайными
ошибками. Попытки заставить человека вести себя более удобным для
машины образом часто (и справедливо) рассматриваются как
оскорбительные. Задача низкоуровневой программы ввода состоит в
том, чтобы читать символы по одному и составлять из них лексические
символы более высокого уровня. Далее эти лексемы служат вводом для
программ более высокого уровня. У нас ввод низкого уровня
осуществляется get_token(). Обнадеживает то, что написание программ
ввода низкого уровня не является ежедневной работой; в хорошей
системе для этого будут стандартные функции.
Для калькулятора правила ввода сознательно были выбраны такими,
чтобы функциям по работе с потоками было неудобно эти правила
обрабатывать; незначительные изменения в определении лексем сделали
бы get_token() обманчиво простой.
Первая сложность состоит в том, что символ новой строки '\n'
является для калькулятора существенным, а функции работы с потоками
считают его символом пропуска. То есть, для этих функций '\n'
значим только как ограничитель лексемы. Чтобы преодолеть это, надо
проверять пропуски (пробел, символы табуляции и т.п.):
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 '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;
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;
}
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;
}
}
К таблице имен доступ осуществляется с помощью одной функции
name* look(char* p, int ins =0);
inline name* insert(char* s) { return look(s,1);}
srtuct name {
char* string;
char* next;
double value;
}
const TBLSZ = 23; name* table[tblsz];
int ii = 0; // хэширование char* pp = p; while (*pp) ii = ii<<1 ^ *pp++; if (ii < 0) ii = -ii; ii %= tblsz;
ii <<= 1; ii ^= *pp++;
if (ii < 0) ii = -ii; ii %= tblsz;
хэширование
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;
}
Поскольку программа так проста, обработка ошибок не составляет большого труда. Функция обработки ошибок просто считает ошибки, пишет сообщение об ошибке и возвращает управление обратно:
int no_of_errors;
double error(char* s) {
cerr << "error: " << s << "\n";
no_of_errors++;
return 1;
}
Когда все части программы на месте, нам нужен только драйвер для инициализации и всего того, что связано с запуском. В этом простом примере 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;
}
cout << expr() << "\n";
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
}
в вычислении одного выражения. Если бы можно было представлять это выражение как параметр командной строки, не приходилось бы так много нажимать на клавиши.
Как уже говорилось, программа запускается вызовом main(). Когда это происходит, main() получает два параметра: указывающий число параметров, обычно называемый argc, и вектор параметров, обычно называемый argv. Параметры - это символьные строки, поэтому argv имеет тип char*[argc]. Имя программы (так, как оно стоит в командной строке) передается в качестве argv[0], поэтому argc всегда не меньше единицы. Например, в случае команды
dc 150/1.1934
argc 2 argv[0] "dc" argv[1] "150/1.1934"
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;
}
// как раньше
}
dc "rate=1.1934;150/rate;19.75/rate;217/rate"
| 3.2.1 Круглые скобки | |
| 3.2.2 Порядок вычисления | |
| 3.2.3 Увеличение и уменьшение *5 | |
| 3.2.4 Побитовые логические операции | |
| 3.2.5 Преобразование типа | |
| 3.2.6 Свободная память |
Операции C++ подробно и систематически описываются в #с.7;
прочитайте, пожалуйста, этот раздел. Здесь же приводится краткая
сводка и некоторые примеры. После каждой операции приведено одно
или более ее общеупотребительных названий и пример ее
использования. В этих примерах имя_класса - это имя класса, член -
имя члена, объект - выражение, дающее в результате объект класса,
указатель - выражение, дающее в результате указатель, выр -
выражение, а lvalue - выражение, денотирующее неконстантный объект.
Тип может быть совершенно произвольным именем типа (со *, () и
т.п.) только когда он стоит в скобках, во всех остальных случаях
существуют ограничения.
Унарные операции и операции присваивания правоассоциативны, все
остальные левоассоциативны. Это значит, что a=b=c означает a=(b=c),
a+b+c означает (a+b)+c, и *p++ означает *(p++), а не (*p)++.
:: разрешение области видимости имя_класса :: член :: глобальное :: имя
| -> | выбор члена | указатель->член |
| [] | индексация | указатель [ выр ] |
| () | вызов функции | выр (список_выр) |
| () | построение значения | тип (список_выр) |
| sizeof | размер объекта | sizeof выр |
| sizeof | размер типа | sizeof ( тип ) |
| ++ | приращение после | lvalue++ |
| ++ | приращение до | ++lvalue |
| -- | уменьшение после | lvalue-- |
| -- | уменьшение до | --lvalue |
| ~ | дополнение | ~ выр |
| ! | не | ! выр |
| - | унарный минус | - выр |
| + | унарный плюс | + выр |
| & | адрес объекта | & lvalue |
| * | разыменование | * выр |
| new | создание (размещение) | new тип |
| delete | уничтожение (освобождение) | delete указатель |
| delete[] | уничтожение вектора | delete[ выр ] указатель |
| () | приведение (преобразование типа) | ( тип ) выр |
| * | умножение | выр * выр |
| / | деление | выр / выр |
| % | взятие по модулю (остаток) | выр % выр |
| + | сложение (плюс) | выр + выр |
| - | вычитание (минус) | выр - выр |
| << | сдвиг влево | lvalue << выр |
| >> | сдвиг вправо | lvalue >> выр |
| < | меньше | выр < выр |
| <= | меньше или равно | выр <= выр |
| > | больше | выр > выр |
| >= | больше или равно | выр >= выр |
| == | равно | выр == выр |
| != | не равно | выр != выр |
| & | побитовое И | выр & выр |
| ^ | побитовое исключающее ИЛИ | выр ^ выр |
| | | побитовое включающее ИЛИ | выр | выр |
| && | логическое И | выр && выр |
| || | логическое включающее ИЛИ | выр || выр |
| ? : | арифметический if | выр ? выр : выр |
| = | простое присваивание | lvalue = выр |
| *= | умножить и присвоить | lvalue = выр |
| /= | разделить и присвоить | lvalue /= выр |
| %= | взять по модулю и присвоить | lvalue %= выр |
| += | сложить и присвоить | lvalue += выр |
| -= | вычесть и присвоить | lvalue -= выр |
| <<= | сдвинуть влево и присвоить | lvalue <<= выр |
| >>= | сдвинуть вправо и присвоить | lvalue >>= выр |
| &= | И и присвоить | lvalue &= выр |
| |= | включающее ИЛИ и присвоить | lvalue |= выр |
| ^= | исключающее ИЛИ и присвоить | lvalue ^= выр |
| , | запятая (последование) | выр , выр |
тип в преобразовании типа (приведении к типу), в именах типов для обозначения функций, а также для разрешения конфликтов приоритетов. К счастью, последнее требуется не слишком часто, потому что уровни приоритета и правила ассоциативности определены таким образом, чтобы выражения "работали ожидаемым образом" (то есть, отражали наиболее привычный способ употребления). Например, значение
if (i<=0 || max
Порядок вычисления подвыражений в выражении не определен. Например
int i = 1; v[i] = i++;
, && ||
f1(v[i],i++); // два параметра f2( (v[i],i++) ) // один параметр
в
свою очередь означает lvalue=lvalue+1 при условии, что lvalue не
вызывает никаких побочных эффектов. Выражение, обозначающее
(денотирующее) объект, который должен быть увеличен, вычисляется
один раз (только). Аналогично, уменьшение выражается операцией --.
Операции ++ и -- могут применяться и как префиксные, и как
постфиксные. Значением ++x является новое (то есть увеличенное)
значение x. Например, y=++x эквивалентно y=(x+=1). Значение x++,
напротив, есть старое значение x. Например, y=x++ эквивалентно
y=(t=x,x+=1,t), где t - переменная того же типа, что и x.
Операции приращения особенно полезны для увеличения и уменьшения
переменных в циклах. Например, оканчивающуюся нулем строку можно
копировать так:
inline void cpy(char* p, const char* q)
{
while (*p++ = *q++) ;
}
long(p+1) == long(p)+sizeof(t);
Побитовые логические операции
& | ^ ~ >> <<
enum state_value { _good=0, _eof=1, _fail=2, _bad=4};
// хорошо, конец файла, ошибка, плохо
cout.state = _good;
if (cout.state&(_bad|_fail)) // не good
cin.state |= _eof;
cin.state = _eof;
state_value diff = cin.state^cout.state;
unsigned short middle(int a) { return (a>>8)&0xffff; }
&& || !
Бывает необходимо явно преобразовать значение одного типа в значение другого. Явное преобразование типа дает значение одного типа для данного значения другого типа. Например:
float r = float(1);
char* p = (char*)0777;
typedef char* Pchar; char* p = pchar(0777);
Pname n2 = pbase(n1->tp)->b_name; // функциональная запись Pname n3 = ((pbase)n2->tp)->b_name; // запись приведения к типу
((Pbase)(n2->tp))->b_name
any_type* p = (any_type*)&some_object;
&i;
i = (int)pc;
pc = (char*)i; // остерегайтесь: значение pc может измениться
// на некоторых машинах
// sizeof(int)
Именованный объект является либо статическим, либо автоматическим см. #2.1.3 только до тех пор, пока из этого блока не вышли. Однако часто бывает полезно создать новый объект, существующий до тех пор, пока он не станет больше не нужен. В частности, часто полезно создать объект, который можно использовать после возврата из функции, где он создается. Такие объекты создает операция new, а в последствие уничтожать их можно операцией delete. Про объекты, выделенные с помощью операции new, говорят, что они в свободной памяти. Такими объектами обычно являются вершины деревьев или элементы связанных списков, являющиеся частью большей структуры данных, размер которой не может быть известен на стадии компиляции. Рассмотрим, как можно было бы написать компилятор в духе написанного настольного калькулятора. Функции синтаксического анализа могут строить древовидное представление выражений, которое будет использоваться при генерации кода. Например:
struct enode {
token_value oper;
enode* left;
enode* right;
};
enode* expr()
{
enode* left = term();
for(;;)
switch(curr_tok) {
case PLUS:
case MINUS:
get_token();
enode* n = new enode;
n->oper = curr_tok;
n->left = left;
n->right = term();
left = n;
break;
default:
return left;
}
}
void generate(enode* n)
{
switch (n->oper) {
case PLUS:
// делает нечто соответствующее
delete n;
}
}
char* save_string(char* p)
{
char* s = new char[strlen(p)+1];
strcpy(s,p);
return s;
}
int main(int argc, char* argv[])
{
if (argc < 2) exit(1);
char* p = save_string(argv[1]);
delete p;
}
int main(int argc, char* argv[])
{
if (argc < 2) exit(1);
int size = strlen(argv[1])+1;
char* p = save_string(argv[1]);
delete[size] p;
}
void operator new(long); void operator delete(void*);
char* p = new char[100000000];
#include
void out_of_store()
{
cerr << "операция new не прошла: за пределами памяти\n";
exit(1);
}
typedef void (*PF)(); // тип указатель на функцию
extern PF set_new_handler(PF);
main()
{
set_new_handler(out_of_store);
char* p = new char[100000000];
cout << "сделано, p = " << long(p) << "\n";
}
операция new не прошла: за пределами памяти
include
main()
{
char* p = new char[100000000];
cout << "сделано, p = " << long(p) << "\n";
}
сделано, p = 0
| 3.3.1 Проверки | |
| 3.3.2 Goto |
Операторы C++ систематически и полностью изложены в #с.9, прочитайте, пожалуйста, этот раздел. А здесь приводится краткая сводка и некоторые примеры.
оператор:
описание
{список_операторов opt}
выражение opt
if ( выражение ) опреатор
if ( выражение ) оператор else оператор
switch ( выражение ) оператор
while ( выражение ) оператор
do оператор while (выражение)
for ( оператор выражение opt ; выражение opt ) оператор
case константное_выражение : оператор
default : оператор
break ;
continue ;
return выражение opt ;
goto идентификатор ;
идентификатор : оператор
список_операторов:
оператор
оператор список_операторов
Проверка значения может осуществляться или оператором if, или оператором switch:
if ( выражение ) оператор if ( выражение ) оператор else оператор switch ( выражение ) оператор
== != < <= > >=
if (a) // ...
if (a != 0) // ...
&& || !
if (p && 1count) // ...
if (a <= d)
max = b;
else
max = a;
max = (a<=b) ? b : a;
switch (val) {
case 1:
f();
break;
case 2;
g();
break;
default:
h();
break;
}
if (val == 1)
f();
else if (val == 2)
g();
else
h();
switch (val) { // осторожно
case 1:
cout << "case 1\n";
case 2;
cout << "case 2\n";
default:
cout << "default: case не найден\n";
}
case 1 case 2 default: case не найден
switch (val) { // осторожно
case 0:
cout << "case 0\n";
case1:
case 1:
cout << "case 1\n";
return;
case 2;
cout << "case 2\n";
goto case1;
default:
cout << "default: case не найден\n";
return;
}
case 2 case 1
goto case 1; // синтаксическая ошибка
C++ снабжен имеющим дурную репутацию оператором goto.
goto идентификатор; идентификатор : оператор
for (int i = 0; i
Продуманное использование комментариев и согласованное
использование отступов может сделать чтение и понимание программы
намного более приятным. Существует несколько различных стилей
согласованного использования отступов. Автор не видит никаких
серьезных оснований предпочесть один другому (хотя как и у
большинства, у меня есть свои предпочтения). Сказанное относится
также и к стилю комментариев.
Неправильное использование комментариев может серьезно повлиять
на удобочитаемость программы, Компилятор не понимает содержание
комментария, поэтому он никаким способом не может убедиться в том,
что комментарий
[1] осмыслен;
[2] описывает программу; и
[3] не устарел.
Непонятные, двусмысленные и просто неправильные комментарии
содержатся в большинстве программ. Плохой комментарий может быть
хуже, чем никакой.
Если что-то можно сформулировать средствами самого языка, следует
это сделать, а не просто отметить в комментарии. Данное замечание
относится к комментариям вроде:
// переменная "v" должна быть инициализирована. // переменная "v" должна использоваться только функцией "f()". // вызвать функцию init() перед вызовом // любой другой функции в этом файле. // вызовите функцию очистки "cleanup()" в конце вашей программы. // не используйте функцию "wierd()". // функция "f()" получает два параметра.
a = b+c; // a становится b+c count++; // увеличить счетчик
// tbl.c: Реализация таблицы имен
/*
Гауссовское исключение с частичным
См. Ralston: "A first course ..." стр. 411.
*/
// swap() предполагает размещение стека AT&T sB20.
/**************************************
Copyright (c) 1984 AT&T, Inc.
All rights reserved
****************************************/
for (i=0; im
*p.m
*a[i]
a := b+1;
if (a = 3) // ...
if (a&077 == 0) // ...
void send(register* to, register* from, register count)
// Полезные комментарии несомненно уничтожены.
{
register n=(count+7)/8;
switch (count%8) {
case 0: do { *to++ = *from++;
case 7: do { *to++ = *from++;
case 6: do { *to++ = *from++;
case 5: do { *to++ = *from++;
case 4: do { *to++ = *from++;
case 3: do { *to++ = *from++;
case 2: do { *to++ = *from++;
case 1: do { *to++ = *from++;
while (--n>0);
}
}
struct symbol {
token_value tok;
union {
double number_value;
char* name_string;
};
};
*1 Нам неизвестен русскоязычный термин, эквивалентный английскому
indentation. Иногда это называется отступами. (прим. перев.)
*2 игра слов: "for" - "forever" (навсегда). (прим. перев.)
* 3В языке немного лучше этого с этими исключениями тоже надо бы
справляться. (прим. автора)
* 4знака этой операции. (прим. перев.)
* 5Следовало бы переводить как "инкремент" и "декремент", однако
мы следовали терминологии, принятой в переводной литературе по C,
поскольку эти операции унаследованы от C. (прим. перев.)
| Главная |