Компьютерные науки - Учебники на русском языке - Скачать бесплатно
> больше выр > выр
>= больше или равно выр >= выр
===========================================================
== равно выр == выр
!= не равно выр != выр
===========================================================
& побитовое И выр & выр
===========================================================
^ побитовое исключающее ИЛИ выр ^ выр
===========================================================
! побитовое включающее ИЛИ выр ! выр
===========================================================
&& логическое И выр && выр
===========================================================
!! логическое включающее ИЛИ выр !! выр
===========================================================
? : арифметический if выр ? выр : выр
===========================================================
= простое присваивание lvalue = выр
*= умножить и присвоить lvalue = выр
/= разделить и присвоить lvalue /= выр
%= взять по модулю и присвоить lvalue %= выр
+= сложить и присвоить lvalue += выр
-= вычесть и присвоить lvalue -= выр
<<= сдвинуть влево и присвоить lvalue <<= выр
>>= сдвинуть вправо и присвоить lvalue >>= выр
&= И и присвоить lvalue &= выр
!= включающее ИЛИ и присвоить lvalue != выр
^= исключающее ИЛИ и присвоить lvalue ^= выр
===========================================================
, запятая (следование) выр , выр
===========================================================
В каждой очерченной части находятся операции с одинако-
вым приоритетом. Операция имеет приоритет больше, чем опера-
ции из частей, расположенных ниже. Например: a+b*c означает a
+(b*c), так как * имеет приоритет выше, чем +, а a+b-c озна-
чает (a+b)-c, поскольку + и - имеют одинаковый приоритет (и
поскольку + левоассоциативен).
3.2.1 Круглые скобки
Скобками синтаксис С++ злоупотребляет; количество спосо-
бов их использования приводит в замешательство: они применя-
ются для заключения в них параметров в вызовах функций, в них
заключается тип в преобразовании типа (приведении к типу), в
именах типов для обозначения функций, а также для разрешения
конфликтов приоритетов. К счастью, последнее требуется не
слишком часто, потому что уровни приоритета и правила ассоци-
ативности определены таким образом, чтобы выражения "работали
ожидаемым образом" (то есть, отражали наиболее привычный спо-
соб употребления). Например, значение
if (i<=0 !! max
очевидно. Тем не менее, всегда, когда программист сомне-
вается относительно этих правил, следует употреблять скобки,
и некоторые программисты предпочитают немного более длинное и
менее элегантное
if ( (i<=0) !! (max
При усложнении подвыражений употребление скобок стано-
вится более обычным явлением, но сложные подвыражения являют-
ся источником ошибок, поэтому если вы чувствуете потребность
в скобках, попробуйте оборвать выражение и использовать до-
- 75 -
полнительную переменную. Есть и такие случаи, когда приорите-
ты операций не приводят к "очевидному" результату. Например в
if (i&mask == 0) // ...
не происходит применения маски mask к i и последующей
проверки результата на ноль. Поскольку == имеет приоритет вы-
ше, чем &, выражение интерпретируется как i&(mask==0). В этом
случае скобки оказываются важны:
if ((i&mask) == 0) // ...
Но, с другой стороны, то, что следующее выражение не ра-
ботает так, как может ожидать наивный пользователь, ничего не
значит:
if (0 <= a <= 99) // ...
Оно допустимо, но интерпретируется оно как (0<=a)<=99,
где результат первого подвыражения или 0 или 1, но не a (если
только a не равно 1). Чтобы проверить, лежит ли a в диапазоне
0...99, можно написать
if (0<=a && a<=99) // ...
3.2.2 Порядок вычисления
Порядок вычисления подвыражений в выражении неопределен.
Например
int i = 1;
v[i] = i++;
может вычисляться или как v[1]=1, или как v[2]=1. При
отсутствии ограничений на порядок вычисления выражения может
генерироваться более хороший код. Было бы замечательно, если
бы компилятор предупреждал о подобных неоднозначностях, но
большинство компиляторов этого не делают.
Относительно операций && и !! гарантируется, что их левый
операнд вычисляется раньше, чем правый. Например, b=(a=2,a=1)
присвоит b 3.В #3.3.1приводятся примеры использования&& и !!.
Заметьте, что операция следования , (запятая) логически
отличается от запятой, которая используется для разделения
параметров в вызове функции. Рассмотрим
f1(v[i],i++); // два параметра
f2( (v[i],i++) ) // один параметр
В вызове f1 два параметра, v[i] и i++, и порядок вычис-
ления выражений-параметров неопределен. Зависимость выражения
-параметра от порядка вычисления - это очень плохой стиль, а
также непереносимо. В вызове f2 один параметр, выражение с
запятой, которое эквивалентно i++.
С помощью скобок нельзя задать порядок вычисления. Нап-
ример, a*(b/c) может вычисляться и как (a*b)/c, поскольку * и
/ имеют одинаковый приоритет. В тех случаях, когда важен по-
рядок вычисления, можно вводить дополнительную переменную,
например, (t=b/c,a*t).
3.2.3 Увеличение и уменьшение*
--------------------
* Следовало бы переводить как "инкремент" и "декремент",
однако мы следовали терминологии, принятой в переводной лите-
ратуре по C, поскольку эти операции унаследованы от C. (прим.
- 76 -
перев.)
Операция ++ используется для явного выражения приращения
вместо его неявного выражения с помощью комбинации сложения и
присваивания. По определению ++lvalue означает lvalue+=1, что
в свою очередь означает 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++) ;
*)
Напомню, что увеличение и уменьшение арифметических ука-
зателей, так же как сложение и вычитание указателей, осущест-
вляется в терминах элементов вектора, на которые указывает
указатель p++ приводит к тому, что p указывает на следующий
элемент. Для указателя p типа T* по определению выполняется
следующее:
long(p+1) == long(p)+sizeof(T);
3.2.4 Побитовые логические операции
Побитовые логические операции
& ! ^ ~ >> <<
применяются к целым, то есть к объектам типа char,
short, int, long и их unsigned аналогам, результаты тоже це-
лые.
Одно из стандартных применений побитовых логических опе-
раций - реализация маленького множества (вектор битов). В
этом случае каждый бит беззнакового целого представляет один
член множества, а число членов ограничено числом битов. Би-
нарная операция & интерпретируется как пересечение, ! как
объединение, а ^ как разность. Для наименования членов такого
множества можно использовать перечисление. Вот маленький при-
мер, заимствованный из реализации (не пользовательского ин-
терфейса) :
enum state_value (* _good=0, _eof=1, _fail=2, _bad=4 *);
// хорошо, конец файла, ошибка, плохо
Определение _good не является необходимым. Я просто хо-
тел, чтобы состояние, когда все в порядке, имело подходящее
имя. Состояние потока можно установить заново следующим обра-
зом:
cout.state = _good;
Например, так можно проверить, не был ли испорчен поток
или допущена операционная ошибка:
- 77 -
if (cout.state&(_bad!_fail)) // не good
Еще одни скобки необходимы, поскольку & имеет более вы-
сокий приоритет, чем !.
Функция, достигающая конца ввода, может сообщать об этом
так:
cin.state != _eof;
Операция != используется потому, что поток уже может
быть испорчен (то есть, state==_bad), поэтому
cin.state = _eof;
очистило бы этот признак. Различие двух потоков можно
находить так:
state_value diff = cin.state^cout.state;
В случае типа stream_state (состояние потока) такая раз-
ность не очень нужна, но для других похожих типов она оказы-
вается самой полезной. Например, при сравнении вектора бит,
представляющего множество прерываний, которые обрабатываются,
с другим, представляющим прерывания, ждущие обработки.
Следует заметить, что использование полей (#2.5.1) в
действительности является сокращенной записью сдвига и маски-
рования для извлечения полей бит из слова. Это, конечно, мож-
но сделать и с помощью побитовых логических операций, Напри-
мер, извлечь средние 16 бит из 32-битового int можно
следующим образом:
unsigned short middle(int a) (* return (a>>8)&0xffff; *)
Не путайте побитовые логические операции с логическими
операциями:
&& !! !
Последние возвращают 0 или 1, и они главным образом ис-
пользуются для записи проверки в операторах if, while или for
(#3.3.1). Например, !0 (не ноль) есть значение 1, тогда как ~
0 (дополнение нуля) есть набор битов все-единицы, который
обычно является значением -1.
3.2.5 Преобразование типа
Бывает необходимо явно преобразовать значение одного ти-
па в значение другого. Явное преобразование типа дает значе-
ние одного типа для данного значения другого типа. Например:
float r = float(1);
перед присваиванием преобразует целое значение 1 к зна-
чению с плавающей точкой 1.0. Результат преобразования типа
не является lvalue, поэтому ему нельзя присваивать (если
только тип не является ссылочным типом).
Есть два способа записи явного преобразования типа: тра-
диционная в C запись приведения к типу (double)a и функцио-
нальная запись double(a). Функциональная запись не может при-
меняться для типов, которые не имеют простого имени.
Например, чтобы преобразовать значение к указательному типу
надо или использовать запись преобразования типа
char* p = (char*)0777;
- 78 -
или определить новое имя типа:
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;
позволит работать посредством p с некоторым объектом
some_object как с любым типом any_type.
Когда преобразование типа не необходимо, его следует из-
бегать. Программы, в которых используется много явных преоб-
разований типов, труднее понимать, чем те, в которых это не
делается. Однако такие программы легче понимать, чем програм-
мы, просто не использующие типы для представления понятий бо-
лее высокого уровня (например, программу, которая оперирует
регистром устройства с помощью сдвига и маскирования, вместо
того, чтобы определить подходящую struct и оперировать ею,
см. #2.5.2). Кроме того, правильность явного преобразования
типа часто критическим образом зависит от понимания програм-
мистом того, каким образом объекты различных типов обрабаты-
ваются в языке, и очень часто от подробностей реализации.
Например:
int i = 1;
char* pc = "asdf";
int* pi = &i;
i = (int)pc;
pc = (char*)i; // остерегайтесь! значение pc может изме-
//ниться
// на некоторых машинах
// sizeof(int)
pi = (int*)pc;
pc = (char*)pi; // остерегайтесь! значение pc может изме-
// ниться
// на некоторых машинах char*
// представляется иначе, чем int*
На многих машинах ничего плохого не произойдет, но на
других результаты будут катастрофическими. Этот код в лучшем
случае непереносим. Обычно можно без риска предполагать, что
указатели на различные структуры имеют одинаковое представле-
ние. Кроме того, любой указатель можно (без явного преобразо-
вания типа) присвоить void*, а void* можно явно преобразовать
к указателю любого типа.
В С++ явное преобразование типа оказывается ненужным во
многих случаях, когда C (и другие языки) требуют его. Во мно-
гих программах явного преобразования типа можно совсем избе-
жать, а во многих других его применение можно локализовать в
- 79 -
небольшом числе подпрограмм.
3.2.6 Свободная память
Именованный объект является либо статическим, либо авто-
матическим (см. #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;
*)
*)
Объект, созданный с помощью new, существует, пока он не
будет явно уничтожен delete, после чего пространство, которое
он занимал, опять может использоваться new. Никакого "сборщи-
ка мусора", который ищет объекты, на которые нет ссылок, и
предоставляет их в распоряжение new, нет. Операция delete мо-
- 80 -
жет применяться только к указателю, который был возвращен
операцией new, или к нулю. Применение delete к нулю не вызы-
вает никаких действий.
С помощью new можно также создавать вектора объектов.
Например:
char* save_string(char* p)
(*
char* s = new char[strlen(p)+1];
strcpy(s,p);
return s;
*)
Следует заметить, что чтобы освободить пространство, вы-
деленное new, delete должна иметь возможность определить раз-
мер выделенного объекта. Например:
int main(int argc, char* argv[])
(*
if (argc < 2) exit(1);
char* p = save_string(argv[1]);
delete p;
*)
Это приводит к тому, что объект, выделенный стандартной
реализацией new, будет занимать больше места, чем статический
объект (обычно, больше на одно слово).
Можно также явно указывать размер вектора в операции
уничтожения delete. Например:
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;
*)
Заданный пользователем размер вектора игнорируется за
исключением некоторых типов, определяемых пользователем
(#5.5.5).
Операции свободной памяти реализуются функциями (#с.7.2.3):
void operator new(long);
void operator delete(void*);
Стандартная реализация new не инициализирует возвращае-
мый объект.
Что происходит, когда new не находит памяти для выделе-
ния? Поскольку даже виртуальная память конечна, это иногда
должно происходить. Запрос вроде
char* p = new char[100000000];
как правило, приводит к каким-то неприятностям. Когда у
new ничего не получается, она вызывает функцию, указываемую
указателем _new_handler (указатели на функции обсуждаются в #
4.6.9). Вы можете задать указатель явно или использовать
функцию set_new_handler(). Например:
#include
void out_of_store()
- 81 -
(*
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 не прошла: за пределами памяти
Функция _new_handler может делать и кое-что поумней, чем
просто завершать выполнение программы. Если вы знаете, как
работают new и delete, например, потому, что вы задали свои
собственные operator new() и operator delete(), программа об-
работки может попытаться найти некоторое количество памяти,
которое возвратит new. Другими словами, пользователь может
сделать сборщик мусора, сделав, таким образом, использование
delete необязательным. Но это, конечно, все-таки задача не
для начинающего.
По историческим причинам new просто возвращает указатель
0, если она не может найти достаточное количество памяти и не
был задан никакой _new_handler. Например
include
main()
(*
char* p = new char[100000000];
cout << "сделано, p = " << long(p) << "\n";
*)
выдаст
сделано, p = 0
Вам сделали предупреждение! Заметьте, что тот, кто зада-
ет _new_handler, берет на себя заботу по проверке истощения
памяти при каждом использовании new в программе (за исключе-
нием случая, когда пользователь задал отдельные подпрограммы
для размещения объектов заданных типов, определяемых пользо-
вателем, см. #5.5.6).
3.3 Сводка операторов
Операторы С++ систематически и полностью изложены в
#с.9, прочитайте, пожалуйста, этот раздел. А здесь приводится
краткая сводка и некоторые примеры.
Синтаксис оператора
----------------------------------------------------------
оператор:
описание
(*список_операторов opt*)
выражение opt
- 82 -
if оператор
if ( выражение ) оператор
if ( выражение ) оператор else оператор
switch оператор
switch ( выражение ) оператор
while ( выражение ) оператор
do оператор while (выражение)
for ( оператор выражение opt; выражение opt ) оператор
case константное_выражение : оператор
default : оператор
break ;
continue ;
return выражение opt ;
goto идентификатор ;
идентификатор : оператор
список_операторов:
оператор
оператор список_операторов
Заметьте, что описание является оператором, и что нет
операторов присваивания и вызова процедуры. Присваивание и
вызов функции обрабатываются как выражения.
3.3.1 Проверки
Проверка значения может осуществляться или оператором
if, или оператором switch:
if ( выражение ) оператор
if ( выражение ) оператор else оператор
switch ( выражение ) оператор
В С++ нет отдельного булевского типа. Операции сравнения
== != < <= > >=
возвращают целое 1, если сравнение истинно, иначе возв-
ращают 0. Не так уж непривычно видеть, что ИСТИНА определена
как 1, а ЛОЖЬ определена как 0.
В операторе if первый (или единственный) оператор выпол-
няется в том случае, если выражение ненулевое, иначе выполня-
ется второй оператор (если он задан). Отсюда следует, что в
качестве условия может использоваться любое целое выражение.
В частности, если a целое, то
if (a) // ...
эквивалентно
if (a != 0) // ...
Логические операции && !! ! наиболее часто используются
в условиях. Операции && и !! не будут вычислять второй аргу-
мент, если это ненужно. Например:
if (p && 1count) // ...
вначале проверяет, является ли p не нулем, и только если
это так, то проверяет 1count.
Некоторые простые операторы if могут быть с удобством
- 83 -
заменены выражениями арифметического if. Например:
if (a <= d)
max = b;
else
max = a;
лучше выражается так:
max = (a<=b) ? b : a;
Скобки вокруг условия необязательны, но я считаю, что
когда они используются, программу легче читать.
Некоторые простые операторы switch можно по-другому за-
писать в виде набора операторов if. Например:
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) предпочти-
тельнее, поскольку в этом случае явно выражается сущность
действия (сопоставление значения с рядом констант). Поэтому в
нетривиальных случаях оператор switch читается легче.
Заботьтесь о том, что switch должен как-то завершаться,
если только вы не хотите, чтобы выполнялся следующий case.
Например:
switch (val) (* // осторожно
case 1:
cout << "case 1\n";
case 2;
cout << "case 2\n";
default:
cout << "default: case не найден\n";
*)
при val==1 напечатает
case 1
case 2
default: case не найден
к великому изумлению непосвященного. Самый обычный спо-
соб завершить случай - это break, иногда можно даже использо-
вать goto. Например:
switch (val) (* // осторожно
- 84 -
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;
*)
При обращении к нему с val==2 выдаст
case 2
case 1
Заметьте, что метка case не подходит как метка для упот-
ребления в операторе goto:
goto case 1; // синтаксическая ошибка
3.3.2 Goto
С++ снабжен имеющим дурную репутацию оператором goto.
goto идентификатор;
идентификатор : оператор
В общем, в программировании высокого уровня он имеет
очень мало применений, но он может быть очень полезен, когда
С++ программа генерируется программой, а не пишется непос-
редственно человеком. Например, операторы goto можно исполь-
зовать в синтаксическом анализаторе, порождаемом генератором
синтаксических анализаторов. Оператор goto может быть также
важен в тех редких случаях, когда важна наилучшая эффектив-
ность, например, во внутреннем цикле какой-нибудь программы,
работающей в реальном времени.
Одно из немногих разумных применений состоит в выходе из
вложенного цикла или переключателя (break лишь прекращает вы-
полнение самого внутреннего охватывающего его цикла или пе-
реключателя). Например:
for (int i = 0; i
for (int j = 0; j
if (nm[i][j] == a) goto found // найдено
// не найдено
// ...
found: // найдено
// nm[i][j] == a
Имеется также оператор continue, который по сути делает
переход на конец оператора цикла, как объясняется в #3.1.5.
3.4 Комментарии и Выравнивание
Продуманное использование комментариев и согласованное
использование отступов может сделать чтение и понимание прог-
раммы намного более приятным. Существует несколько различных
стилей согласованного использования отступов. Автор не видит
никаких серьезных оснований предпочесть один другому (хотя
как и у большинства, у меня есть свои предпочтения). Сказан-
ное относится также и к стилю комментариев.
- 85 -
Неправильное использование комментариев может серьезно
повлиять на удобочитаемость программы, Компилятор не понимает
содержание комментария, поэтому он никаким способом не может
убедиться в том, что комментарий
[1] осмыслен,
[2] описывает программу и
[3] не устарел.
Непонятные, двусмысленные и просто неправильные коммен-
тарии содержатся в большинстве программ. Плохой комментарий
может быть хуже, чем никакой.
Если что-то можно сформулировать средствами самого язы-
ка, следует это сделать, а не просто отметить в комментарии.
Данное замечание относится к комментариям вроде:
// переменная "v" должна быть инициализирована.
//переменная"v"должна использоваться только функцией "f()".
// вызвать функцию init() перед вызовом
// любой другой функции в этом файле.
// вызовите функцию очистки "cleanup()" в конце вашей
// программы.
// не используйте функцию "wierd()".
// функция "f()" получает два параметра.
При правильном использовании С++ подобные комментарии
как правило становятся ненужными. Чтобы предыдущие коммента-
рии стали излишними, можно, например, использовать правила
компоновки (#4.2) и видимость, инициализацию и правила очист-
ки для классов (см. #5.5.2).
Если что-то было ясно сформулировано на языке, второй
раз упоминать это в комментарии не следует. Например:
a = b+c; // a становится b+c
count++; // увеличить счетчик
Такие комментарии хуже чем просто излишни, они увеличи-
вают объем текста, который надо прочитать, они часто затума-
нивают структуру программы, и они могут быть неправильными.
Автор предпочитает:
[1] Комментарий для каждого исходного файла, сообщающий,
для чего в целом предназначены находящиеся в нем комментарии,
дающий ссылки на справочники и руководства, общие рекоменда-
ции по использованию и т.д.,
[2] Комментарий для каждой нетривиальной функции, в ко-
тором сформулировано ее назначение, используемый алгоритм
(если он неочевиден) и, быть может, что-то о принимаемых в
ней предположениях относительно среды выполнения,
[3] Небольшое число комментариев в тех местах, где прог-
рамма неочевидна и/или непереносима и
[4] Очень мало что еще.
- 86 -
Например:
// tbl.c: Реализация таблицы имен
/*
Гауссовское исключение с частичным
См. Ralston: "A first course ..." стр. 411.
*/
// swap() предполагает размещение стека AT&T sB20.
/**************************************
Copyright (c) 1984 AT&T, Inc.
All rights reserved
****************************************/
Удачно подобранные и хорошо написанные комментарии - су-
щественная часть программы. Написание хороших комментариев
может быть столь же сложным, сколь и написание самой програм-
мы.
Заметьте также, что если в функции используются исключи-
тельно комментарии //, то любую часть этой функции можно за-
комментировать с помощью комментариев /* */, и наоборот.
3.5 Упражнения
1. (*1) Перепишите следующий оператор for в виде
эквивалентного оператора while:
for (i=0; i
if (input_line[i] == '?') quest_count++;
2. (*1) Полностью расставьте скобки в следующих выраже-
ниях:
a = b + c * d << 2 & 8
a & 077 != 3
a == b !! a == c && c < 5
c = x != 0
0 <= i < 7
f(1,2)+3
a = -1 + + b -- - 5
a = b == c ++
a = b = c = 0
a[4][2] *= * b ? c : * d * 2
a-b,c=d
3. (*2) Найдите пять различных конструкций С++, значение
которых неопределено.
4. (*2) Найдите десять различных примеров непереносимой
С++ программы.
5. (*1) Что происходит в вашей системе, если вы делите
на ноль? Что происходит при переполнении и потере значимости?
6. (*1) Полностью расставьте скобки в следующих выраже-
ниях:
*p++
*--p
++a--
(int*)p->m
*p.m
*a[i]
- 87 -
7. (*2) Напишите функции: strlen(), которая возвращает
длину строки, strcpy(), которая копирует одну строку в дру-
гую, и strcmp(), которая сравнивает две строки. Разберитесь,
какие должны быть типы параметров и типы возвращаемых значе-
ний, а потом сравните их со стандартными версиями, которые
описаны в и в вашем руководстве.
8. (*1) Посмотрите, как ваш компилятор реагирует на
ошибки:
a := b+1;
if (a = 3) // ...
if (a&077 == 0) // ...
Придумайте ошибки попроще, и посмотрите, как компилятор
на них реагирует.
9. (*2) Напишите функцию cat(), получающую два строковых
параметра и возвращающую строку, которая является конкатена-
цией параметров. Используйте new, чтобы найти память для ре-
зультата. Напишите функцию rev(), которая получает строку и
переставляет в ней символы в обратном порядке. То есть, после
вызова rev(p) последний символ p становится первым.
10. (*2) Что делает следующая программа?
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);
*)
*)
Зачем кто-то мог написать нечто похожее?
11. (*2) Напишите функцию atoi(), которая получает стро-
ку, содержащую цифры, и возвращает соответствующее int. Нап-
ример, atoi("123") - это 123. Модифицируйте atoi() так, чтобы
помимо обычной десятичной она обрабатывала еще восьмеричную и
шестнадцатиричную записи С++. Модифицируйте atoi() так, чтобы
обрабатывать запись символьной константы. Напишите функцию
itoa(), которая строит представление целого параметра в виде
строки.
12. (*2) Перепишите get_token() (#3.1.2), чтобы она за
один раз читала строку в буфер, а затем составляла лексемы,
читая символы из буфера.
13. (*2) Добавьте в настольный калькулятор из #3.1 такие
функции, как sqrt(), log() и sin(). Подсказка: предопределите
имена и вызывайте функции с помощью вектора указателей на
функции. Не забывайте проверять параметры в вызове функции.
14. (*3) Дайте пользователю возможность определять функ-
ции в настольном калькуляторе. Подсказка: определяйте функции
как последовательность действий, прямо так, как их набрал
- 88 -
пользователь. Такую последовательность можно хранить или как
символьную строку, или как список лексем. После этого, когда
функция вызывается, читайте и выполняйте эти действия. Если
вы хотите, чтобы пользовательская функция получала параметры,
вы должны придумать форму записи этого.
15. (*1.5) Преобразуйте настольный калькулятор так, что-
бы вместо статических переменных name_string и number_value
использовалась структура символа symbol:
struct symbol (*
token_value tok;
union (*
double number_value;
char* name_string;
*);
*);
16. (*2.5) Напишите программу, которая выбрасывает ком-
ментарии из С++ программы. То есть, читает из cin, удаляет //
и /* */ комментарии и пишет результат в cout. Не заботьтесь о
приятном виде выходного текста (это могло бы быть другим, бо-
лее сложным упражнением). Не беспокойтесь о правильности
программ. Остерегайтесь // и /* и */ внутри комментариев,
строк и символьных констант.
17. (*2) Посмотрите какие-нибудь программы, чтобы понять
принцип различных стилей комментирования и выравнивания, ко-
торые используются на практике.
- 89 -
Глава 4
Функции и Файлы
|