Страница:
строк из файла <string.h>:
int strcpy(char*, const char*);
Поразрядные логические операции
& | ^ ~ >> <<
применяются к целым, то есть к объектам типа char, short, int, long и
к их беззнаковым аналогам. Результат операции также будет целым.
Чаще всего поразрядные логические операции используются для
работы с небольшим по величине множеством данных (массивом разрядов).
В этом случае каждый разряд беззнакового целого представляет один
элемент множества, и число элементов определяется количеством разрядов.
Бинарная операция & интерпретируется как пересечение множеств,
операция | как объединение, а операция ^ как разность множеств.
С помощью перечисления можно задать имена элементам множества.
Ниже приведен пример, заимствованный из <iostream.h>:
class ios {
public:
enum io_state {
goodbit=0, eofbit=1, failbit=2, badbit=4
};
// ...
};
Состояние потока можно установить следующим присваиванием:
cout.state = ios::goodbit;
Уточнение именем ios необходимо, потому что определение io_state находится
в классе ios, а также чтобы не возникло коллизий, если пользователь заведет свои
имена наподобие goodbit.
Проверку на корректность потока и успешное окончание операции можно
задать так:
if (cout.state&(ios::badbit|ios::failbit)) // ошибка в потоке
Еще одни скобки необходимы потому, что операция & имеет более высокий
приоритет, чем операция "|".
Функция, обнаружившая конец входного потока, может сообщать об этом так:
cin.state |= ios::eofbit;
Операция |= используется потому, что в потоке уже могла быть ошибка
(т.е. state==ios::badbit), и присваивание
cin.state =ios::eofbit;
могло бы затереть ее признак. Установить отличия в состоянии двух
потоков можно следующим способом:
ios::io_state diff = cin.state^cout.state;
Для таких типов, как io_state, нахождение различий не слишком полезная
операция, но для других сходных типов она может оказаться весьма
полезной. Например, полезно сравнение двух разрядных массива, один из
которых представляет набор всех возможных обрабатываемых прерываний,
а другой - набор прерываний, ожидающих обработки.
Отметим, что использование полей ($$R.9.6) может служить удобным
и более лаконичным способом работы с частями слова, чем сдвиги и
маскирование. С частями слова можно работать и с помощью поразрядных
логических операций. Например, можно выделить средние 16 разрядов
из средины 32-разрядного целого:
unsigned short middle(int a) { return (a>>8)&0xffff; }
Только не путайте поразрядные логические операции с просто логическими
операциями:
&& || !
Результатом последних может быть 0 или 1, и они в основном
используются в условных выражениях операторов if, while или for
($$3.3.1). Например, !0 (не нуль) имеет значение 1, тогда как ~0
(дополнение нуля) представляет собой набор разрядов "все единицы",
который обычно является значением -1 в дополнительном коде.
Иногда бывает необходимо явно преобразовать значение одного типа в
значение другого. Результатом явного преобразования будет
значение указанного типа, полученное из значения другого типа.
Например:
float r = float(1);
Здесь перед присваиванием целое значение 1 преобразуется в значение
с плавающей точкой 1.0f. Результат преобразования типа не является
адресом, поэтому ему присваивать нельзя (если только тип не является
ссылкой).
Существуют два вида записи явного преобразования типа:
традиционная запись, как операция приведения в С, например, (double)a
и функциональная запись, например, double(a). Функциональную запись
нельзя использовать для типов, которые не имеют простого имени.
Например, чтобы преобразовать значение в тип указателя, надо или
использовать приведение
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;
позволит обращаться к некоторому объекту (some_object) через указатель
p как к объекту произвольного типа (any_type). Тем не менее, если
some_object в действительности имеет тип не any_type, могут получиться
странные и нежелательные результаты.
Если преобразование типа не является необходимым, его вообще следует
избегать. Программы, в которых есть такие преобразования, обычно
труднее понимать, чем программы, их не имеющие. В то же время
программы с явно заданными преобразованиями типа понятнее,
чем программы, которые обходятся без таких преобразований, потому что
не вводят типов для представления понятий более высокого уровня.
Так, например, поступают программы, управляющие регистром устройства с
помощью сдвига и маскирования целых, вместо того, чтобы определить
подходящую структуру (struct) и работать непосредственно с ней
(см. $$2.6.1). Корректность явного преобразования типа часто
существенно зависит от того, насколько программист понимает, как язык
работает с объектами различных типов, и какова специфика данной реализации
языка. Приведем пример:
int i = 1;
char* pc = "asdf";
int* pi = &i;
i = (int)pc;
pc = (char*)i; // осторожно: значение pc может измениться.
// На некоторых машинах sizeof(int)
// меньше, чем sizeof(char*)
pi = (int*)pc;
pc = (char*)pi; // осторожно: pc может измениться
// На некоторых машинах char* имеет не такое
// представление, как int*
Для многих машин эти присваивания ничем не грозят, но для некоторых
результат может быть плачевным. В лучшем случае подобная программа
будет переносимой. Обычно без особого риска можно предположить,
что указатели на различные структуры имеют одинаковое представление.
Далее, произвольный указатель можно присвоить (без явного преобразования
типа) указателю типа void*, а void* может быть явно преобразован
обратно в указатель произвольного типа.
В языке С++ явные преобразования типа оказывается излишними во многих
случаях, когда в С (и других языках) они требуются. Во многих
программах можно вообще обойтись без явных преобразований типа, а во
многих других они могут быть локализованы в нескольких подпрограммах.
Именованный объект является либо статическим, либо автоматическим
(см.$$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 может быть
только указатель, который возвращает операция new, или нуль.
Применение delete к нулю не приводит ни к каким действиям.
Операция new может также создавать массивы объектов, например:
char* save_string(const 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(arg[1]);
delete[] p;
}
Чтобы добиться этого, приходится под объект, размещаемый стандартной
операцией new, отводить немного больше памяти, чем под статический
(обычно, больше на одно слово). Простой оператор delete уничтожает
отдельные объекты, а операция delete[] используется для уничтожения
массивов.
Операции со свободной памятью реализуются функциями ($$R.5.3.3-4):
void* operator new(size_t);
void operator delete(void*);
Здесь size_t - беззнаковый целочисленный тип, определенный в <stddef.h>.
Стандартная реализация функции operator new() не инициализирует
предоставляемую память.
Что случится, когда операция new не сможет больше найти свободной
памяти для размещения? Поскольку даже виртуальная память небесконечна,
такое время от времени происходит. Так, запрос вида:
char* p = new char [100000000];
обычно не проходит нормально. Когда операция new не может выполнить
запрос, она вызывает функцию, которая была задана как параметр
при обращении к функции set_new_handler() из <new.h>. Например,
в следующей программе:
#include <iostream.h>
#include <new.h>
#include <stdlib.h>
void out_of_store()
{
cerr << "operator new failed: out of store\n";
exit(1);
}
int main()
{
set_new_handler(&out_of_store);
char* p = new char[100000000];
cout << "done, p = " << long(p) << '\n';
}
скорее всего, будет напечатано не "done", а сообщение:
operator new failed: out of store
// операция new не прошла: нет памяти
С помощью функции new_handler можно сделать нечто более сложное,
чем просто завершить программу. Если известен алгоритм операций new и
delete (например, потому, что пользователь определил свои функции
operator new и operator delete), то обработчик new_handler может
попытаться найти свободную память для new. Другими словами,
пользователь может написать свой "сборщик мусора", тем самым сделав
вызов операции delete необязательным. Однако такая задача,
безусловно, не под силу новичку.
По традиции операция new просто возвращает указатель 0, если не
удалось найти достаточно свободной памяти. Реакция же на это
new_handler не была установлена. Например, следующая программа:
#include <stream.h>
main()
{
char* p = new char[100000000];
cout << "done, p = " << long(p) << '\n';
}
выдаст
done, p = 0
Память не выделена, и вам сделано предупреждение! Отметим, что, задав
реакцию на такую ситуацию в функции new_handler, пользователь берет
на себя проверку: исчерпана ли свободная память. Она должна выполняться
при каждом обращении в программе к new (если только пользователь
не определил собственные функции для размещения объектов
пользовательских типов; см.$$R.5.5.6).
Полное и последовательное описание операторов С++ содержится в
$$R.6. Советуем ознакомиться с этим разделом. Здесь же дается
сводка операторов и несколько примеров.
------------------------------------------------------------------
Синтаксис операторов
------------------------------------------------------------------
оператор:
описание
{ список-операторов opt }
выражение opt ;
if ( выражение ) оператор
if ( выражение ) оператор else оператор
switch ( выражение ) оператор
while ( выражение ) оператор
do оператор while ( выражение )
for (начальный-оператор-for выражение opt; выражение opt) оператор
case выражение-константа : оператор
default : оператор
break ;
continue ;
return выражение opt ;
goto идентификатор ;
идентификатор : оператор
список-операторов:
оператор
список-операторов оператор
начальный-оператор-for:
описание
выражение opt ;
----------------------------------------------------------------------
Обратите внимание, что описание является оператором, но нет операторов
присваивания или вызова функции (они относятся к выражениям).
Значение можно проверить с помощью операторов if или switch:
if ( выражение ) оператор
if ( выражение ) оператор else оператор
switch ( выражение ) оператор
В языке С++ среди основных типов нет отдельного булевского (тип
со значениями истина, ложь). Все операции отношений:
== != < > <= >=
дают в результате целое 1, если отношение выполняется, и 0 в противном
случае. Обычно определяют константы TRUE как 1 и FALSE как 0.
В операторе if, если выражение имеет ненулевое значение,
выполняется первый оператор, а иначе выполняется второй (если
он указан). Таким образом, в качестве условия допускается любое выражение
типа целое или указатель. Пусть a целое, тогда
if (a) // ...
эквивалентно
if (a != 0) ...
Логические операции
&& || !
обычно используются в условиях. В операциях && и || второй операнд
не вычисляется, если результат определяется значением первого
операнда. Например, в выражении
if (p && l<p->count) // ...
сначала проверяется значение p, и только если оно не равно нулю, то
проверяется отношение l<p->count.
Некоторые простые операторы if удобно заменять выражениями
условия. Например, вместо оператора
if (a <= b)
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();
Смысл обеих конструкций совпадает, но все же первая предпочтительнее,
поскольку в ней нагляднее показана суть операции: проверка на
совпадение значения val со значением из множества констант. Поэтому в
нетривиальных случаях запись, использующая переключатель, понятнее.
Нужно позаботиться о каком-то завершении оператора, указанного
в варианте переключателя, если только вы не хотите, чтобы стали
выполняться операторы из следующего варианта. Например,
переключатель
switch (val) { // возможна ошибка
case 1:
cout << "case 1\n";
case 2:
cout << "case 2\n";
default:
cout << "default: case not found\n";
}
при val==1 напечатает к большому удивлению непосвященных:
case 1
case 2
default: case not found
Имеет смысл отметить в комментариях те редкие случаи, когда стандартный
переход на следующий вариант оставлен намеренно. Тогда этот переход
во всех остальных случаях можно смело считать ошибкой. Для
завершения оператора в варианте чаще всего используется break, но
иногда используются return и даже goto. Приведем пример:
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 not found\n";
return;
}
Здесь при значении val равном 2 мы получим:
case 2
case 1
Отметим, что метку варианта нельзя использовать в операторе goto:
goto case 2; // синтаксическая ошибка
Презираемый оператор goto все-таки есть в С++:
goto идентификатор;
идентификатор: оператор
Вообще говоря, он мало используется в языках высокого уровня, но
может быть очень полезен, если текст на С++ создается не человеком,
а автоматически, т.е. с помощью программы. Например,
операторы goto используются при создании анализатора по заданной
грамматике языка с помощью программных средств.
Кроме того, операторы goto могут пригодиться в тех случаях,
когда на первый план выходит скорость работы программы. Один из
них - когда в реальном времени происходят какие-то вычисления во
внутреннем цикле программы.
Есть немногие ситуации и в обычных программах, когда применение
goto оправдано. Одна из них - выход из вложенного цикла или
переключателя. Дело в том, что оператор break во вложенных циклах
или переключателях позволяет перейти только на один уровень выше.
Приведем пример:
void f()
{
int i;
int j;
for ( i = 0; i < n; i++)
for (j = 0; j<m; j++)
if (nm[i][j] == a) goto found;
// здесь a не найдено
// ...
found:
// nm[i][j] == a
}
Есть еще оператор continue, который позволяет перейти на конец
цикла. Что это значит, объяснено в $$3.1.5.
Программу гораздо легче читать, и она становится намного понятнее, если
разумно использовать комментарии и систематически выделять текст
программы пробелами. Есть несколько способов расположения текста
программы, но нет причин считать, что один из них - наилучший. Хотя
у каждого свой вкус. То же можно сказать и о комментариях.
Однако можно заполнить программу такими комментариями, что читать
и понимать ее будет только труднее. Транслятор не в силах понять
комментарий, поэтому он не может убедиться в том, что комментарий:
[1] осмысленный,
[2] действительно описывает программу,
[3] не устарел.
Во многих программах попадаются непостижимые, двусмысленные и просто
неверные комментарии. Лучше вообще обходиться без них, чем давать
такие комментарии.
Если некий факт можно прямо выразить в языке, то так и следует
делать, и не надо считать, что достаточно упомянуть его в комментарии.
Последнее замечание относится к комментариям, подобным приведенным
ниже:
// переменную "v" необходимо инициализировать.
// переменная "v" может использоваться только в функции "f()".
// до вызова любой функции из этого файла
// необходимо вызвать функцию "init()".
// в конце своей программы вызовите функцию "cleanup()".
// не используйте функцию "weird()".
// функция "f()" имеет два параметра.
При правильном программировании на С++ такие комментарии обычно
оказываются излишними. Чтобы именно эти комментарии стали ненужными,
можно воспользоваться правилами связывания ($$4.2) и областей
видимости, а также правилами инициализации и уничтожения объектов
класса ($$5.5).
Если некоторое утверждение выражается самой программой, не нужно
повторять его в комментарии. Например:
a = b + c; // a принимает значение b+c
count++; // увеличим счетчик count
Такие комментарии хуже, чем избыточные. Они раздувают объем текста,
затуманивают программу и могут быть даже ложными. В то же время
комментарии именно такого рода используют для примеров в учебниках
по языкам программирования, подобных этой книге. Это одна из
многих причин, по которой учебная программа отличается от настоящей.
Можно рекомендовать такой стиль введения комментариев в
программу:
[1] начинать с комментария каждый файл программы: указать в
общих чертах, что в ней определяется, дать ссылки на
справочные руководства, общие идеи по сопровождению
программы и т.д.;
[2] снабжать комментарием каждое определение класса или шаблона
типа;
[3] комментировать каждую нетривиальную функцию, указав: ее
назначение, используемый алгоритм (если только он неочевиден)
и, возможно, предположения об окружении, в котором работает
функция;
[4] комментировать определение каждой глобальной переменной;
[5] давать некоторое число комментариев в тех местах, где
алгоритм неочевиден или непереносим;
[6] больше практически ничего.
Приведем пример:
// tbl.c: Реализация таблицы имен.
/*
Использован метод Гаусса
см. Ральстон "Начальный курс по ..." стр. 411.
*/
// в swap() предполагается, что стек AT&T начинается с 3B20.
/************************************
Авторские права (c) 1991 AT&T, Inc
Все права сохранены
**************************************/
Правильно подобранные и хорошо составленные комментарии играют в
программе важную роль. Написать хорошие комментарии не менее
трудно, чем саму программу, и это - искусство, в котором стоит
совершенствоваться.
Заметим, что если в функции используются только комментарии
вида //, то любую ее часть можно сделать комментарием с помощью
/* */, и наоборот.
1. (*1) Следующий цикл for перепишите с помощью оператора while:
for (i=0; i<max_length; i++)
if (input_line[i] == '?') quest_count++;
Запишите цикл, используя в качестве его управляющей переменной
указатель так, чтобы условие имело вид *p=='?'.
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) Укажите 5 различных конструкций на С++, значение которых
неопределено.
4. (*2) Приведите 10 разных примеров непереносимых конструкций
на С++.
5. (*1) Что произойдет при делении на нуль в вашей программе на С++?
Что будет в случае переполнения или потери значимости?
6. (*1) Укажите порядок вычисления следующих выражений, задав их
полную скобочную структуру:
*p++
*--p
++a--
(int*)p->m
*p.m
*a[i]
7. (*2) Напишите такие функции: strlen() - подсчет длины строки,
strcpy() - копирование строк и strcmp() - сравнение строк. Какими
должны быть типы параметров и результатов функций? Сравните их
со стандартными версиями, имеющимися в <string.h> и в вашем
руководстве.
8. (*1) Выясните, как ваш транслятор отреагирует на такие ошибки:
void f(int a, int b)
{
if (a = 3) // ...
if (a&077 == 0) // ...
a := b+1;
}
Посмотрите, какова будет реакция на более простые ошибки.
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: *to++ = *from++;
case 6: *to++ = *from++;
case 5: *to++ = *from++;
case 4: *to++ = *from++;
case 3: *to++ = *from++;
case 2: *to++ = *from++;
case 1: *to++ = *from++;
} while (--n>0);
}
}
Каков может быть смысл этой функции?
11. (*2) Напишите функцию atoi(), которая имеет параметр - строку цифр
и возвращает соответствующее ей целое. Например, atoi("123")
равно 123. Измените функцию atoi() так, чтобы она могла
переводить в число последовательность цифр не только в десятичной,
но и в восьмеричной и шестнадцатеричной записи, принятой в С++.
Добавьте возможность перевода символьных констант С++. Напишите
функцию itoa() для перевода целого значения в строковое
представление.
12. (*2) Перепишите функцию get_token() ($$3.12) так, чтобы она читала
целую строку в буфер, а затем выдавала лексемы, читая по
символу из буфера.
13. (*2) Введите в программу калькулятора из $$3.1 такие функции, как
sqrt(), log() и sin(). Подсказка: задайте предопределенные
имена и вызывайте функции с помощью массива указателей на них.
Не забывайте проверять параметры, передаваемые этим
функциям.
14. (*3) Введите в калькулятор возможность определять пользовательские
функции. Подсказка: определите функцию как последовательность
операторов, будто бы заданную самим пользователем. Эту
последовательность можно хранить или как строку символов, или
как список лексем. Когда вызывается функция, надо выбирать и
выполнять операции. Если пользовательские функции могут
иметь параметры, то придется придумать форму записи и для них.
15. (*1.5) Переделайте программу калькулятора, используя структуру
symbol вместо статических переменных name_string и number_value:
struct symbol {
token_value tok;
union {
double number_value;
char* name_string;
};
};
16.(*2.5) Напишите программу, которая удаляет все комментарии из
программы на С++. Это значит, надо читать символы из cin и
удалять комментарии двух видов: // и /* */. Получившийся текст
запишите в cout. Не заботьтесь о красивом виде получившегося
текста (это уже другая, более сложная задача). Корректность
программ неважна. Нужно учитывать возможность появления символов
//, /* и */ в комментариях, строках и символьных константах.
17. (*2) Исследуйте различные программы и выясните, какие способы
выделения текста пробелами и какие комментарии используются.
Итерация присуща человеку,
а рекурсия - богу.
- Л. Дойч
Все нетривиальные программы состоят из нескольких раздельно
транслируемых единиц, по традиции называемых файлами. В этой главе
описано, как раздельно транслируемые функции могут вызывать друг друга,
каким образом они могут иметь общие данные, и как добиться
непротиворечивости типов, используемых в разных файлах программы.
Подробно обсуждаются функции, в том числе:
передача параметров, перегрузка имени функции,
int strcpy(char*, const char*);
Поразрядные логические операции
& | ^ ~ >> <<
применяются к целым, то есть к объектам типа char, short, int, long и
к их беззнаковым аналогам. Результат операции также будет целым.
Чаще всего поразрядные логические операции используются для
работы с небольшим по величине множеством данных (массивом разрядов).
В этом случае каждый разряд беззнакового целого представляет один
элемент множества, и число элементов определяется количеством разрядов.
Бинарная операция & интерпретируется как пересечение множеств,
операция | как объединение, а операция ^ как разность множеств.
С помощью перечисления можно задать имена элементам множества.
Ниже приведен пример, заимствованный из <iostream.h>:
class ios {
public:
enum io_state {
goodbit=0, eofbit=1, failbit=2, badbit=4
};
// ...
};
Состояние потока можно установить следующим присваиванием:
cout.state = ios::goodbit;
Уточнение именем ios необходимо, потому что определение io_state находится
в классе ios, а также чтобы не возникло коллизий, если пользователь заведет свои
имена наподобие goodbit.
Проверку на корректность потока и успешное окончание операции можно
задать так:
if (cout.state&(ios::badbit|ios::failbit)) // ошибка в потоке
Еще одни скобки необходимы потому, что операция & имеет более высокий
приоритет, чем операция "|".
Функция, обнаружившая конец входного потока, может сообщать об этом так:
cin.state |= ios::eofbit;
Операция |= используется потому, что в потоке уже могла быть ошибка
(т.е. state==ios::badbit), и присваивание
cin.state =ios::eofbit;
могло бы затереть ее признак. Установить отличия в состоянии двух
потоков можно следующим способом:
ios::io_state diff = cin.state^cout.state;
Для таких типов, как io_state, нахождение различий не слишком полезная
операция, но для других сходных типов она может оказаться весьма
полезной. Например, полезно сравнение двух разрядных массива, один из
которых представляет набор всех возможных обрабатываемых прерываний,
а другой - набор прерываний, ожидающих обработки.
Отметим, что использование полей ($$R.9.6) может служить удобным
и более лаконичным способом работы с частями слова, чем сдвиги и
маскирование. С частями слова можно работать и с помощью поразрядных
логических операций. Например, можно выделить средние 16 разрядов
из средины 32-разрядного целого:
unsigned short middle(int a) { return (a>>8)&0xffff; }
Только не путайте поразрядные логические операции с просто логическими
операциями:
&& || !
Результатом последних может быть 0 или 1, и они в основном
используются в условных выражениях операторов if, while или for
($$3.3.1). Например, !0 (не нуль) имеет значение 1, тогда как ~0
(дополнение нуля) представляет собой набор разрядов "все единицы",
который обычно является значением -1 в дополнительном коде.
Иногда бывает необходимо явно преобразовать значение одного типа в
значение другого. Результатом явного преобразования будет
значение указанного типа, полученное из значения другого типа.
Например:
float r = float(1);
Здесь перед присваиванием целое значение 1 преобразуется в значение
с плавающей точкой 1.0f. Результат преобразования типа не является
адресом, поэтому ему присваивать нельзя (если только тип не является
ссылкой).
Существуют два вида записи явного преобразования типа:
традиционная запись, как операция приведения в С, например, (double)a
и функциональная запись, например, double(a). Функциональную запись
нельзя использовать для типов, которые не имеют простого имени.
Например, чтобы преобразовать значение в тип указателя, надо или
использовать приведение
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;
позволит обращаться к некоторому объекту (some_object) через указатель
p как к объекту произвольного типа (any_type). Тем не менее, если
some_object в действительности имеет тип не any_type, могут получиться
странные и нежелательные результаты.
Если преобразование типа не является необходимым, его вообще следует
избегать. Программы, в которых есть такие преобразования, обычно
труднее понимать, чем программы, их не имеющие. В то же время
программы с явно заданными преобразованиями типа понятнее,
чем программы, которые обходятся без таких преобразований, потому что
не вводят типов для представления понятий более высокого уровня.
Так, например, поступают программы, управляющие регистром устройства с
помощью сдвига и маскирования целых, вместо того, чтобы определить
подходящую структуру (struct) и работать непосредственно с ней
(см. $$2.6.1). Корректность явного преобразования типа часто
существенно зависит от того, насколько программист понимает, как язык
работает с объектами различных типов, и какова специфика данной реализации
языка. Приведем пример:
int i = 1;
char* pc = "asdf";
int* pi = &i;
i = (int)pc;
pc = (char*)i; // осторожно: значение pc может измениться.
// На некоторых машинах sizeof(int)
// меньше, чем sizeof(char*)
pi = (int*)pc;
pc = (char*)pi; // осторожно: pc может измениться
// На некоторых машинах char* имеет не такое
// представление, как int*
Для многих машин эти присваивания ничем не грозят, но для некоторых
результат может быть плачевным. В лучшем случае подобная программа
будет переносимой. Обычно без особого риска можно предположить,
что указатели на различные структуры имеют одинаковое представление.
Далее, произвольный указатель можно присвоить (без явного преобразования
типа) указателю типа void*, а void* может быть явно преобразован
обратно в указатель произвольного типа.
В языке С++ явные преобразования типа оказывается излишними во многих
случаях, когда в С (и других языках) они требуются. Во многих
программах можно вообще обойтись без явных преобразований типа, а во
многих других они могут быть локализованы в нескольких подпрограммах.
Именованный объект является либо статическим, либо автоматическим
(см.$$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 может быть
только указатель, который возвращает операция new, или нуль.
Применение delete к нулю не приводит ни к каким действиям.
Операция new может также создавать массивы объектов, например:
char* save_string(const 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(arg[1]);
delete[] p;
}
Чтобы добиться этого, приходится под объект, размещаемый стандартной
операцией new, отводить немного больше памяти, чем под статический
(обычно, больше на одно слово). Простой оператор delete уничтожает
отдельные объекты, а операция delete[] используется для уничтожения
массивов.
Операции со свободной памятью реализуются функциями ($$R.5.3.3-4):
void* operator new(size_t);
void operator delete(void*);
Здесь size_t - беззнаковый целочисленный тип, определенный в <stddef.h>.
Стандартная реализация функции operator new() не инициализирует
предоставляемую память.
Что случится, когда операция new не сможет больше найти свободной
памяти для размещения? Поскольку даже виртуальная память небесконечна,
такое время от времени происходит. Так, запрос вида:
char* p = new char [100000000];
обычно не проходит нормально. Когда операция new не может выполнить
запрос, она вызывает функцию, которая была задана как параметр
при обращении к функции set_new_handler() из <new.h>. Например,
в следующей программе:
#include <iostream.h>
#include <new.h>
#include <stdlib.h>
void out_of_store()
{
cerr << "operator new failed: out of store\n";
exit(1);
}
int main()
{
set_new_handler(&out_of_store);
char* p = new char[100000000];
cout << "done, p = " << long(p) << '\n';
}
скорее всего, будет напечатано не "done", а сообщение:
operator new failed: out of store
// операция new не прошла: нет памяти
С помощью функции new_handler можно сделать нечто более сложное,
чем просто завершить программу. Если известен алгоритм операций new и
delete (например, потому, что пользователь определил свои функции
operator new и operator delete), то обработчик new_handler может
попытаться найти свободную память для new. Другими словами,
пользователь может написать свой "сборщик мусора", тем самым сделав
вызов операции delete необязательным. Однако такая задача,
безусловно, не под силу новичку.
По традиции операция new просто возвращает указатель 0, если не
удалось найти достаточно свободной памяти. Реакция же на это
new_handler не была установлена. Например, следующая программа:
#include <stream.h>
main()
{
char* p = new char[100000000];
cout << "done, p = " << long(p) << '\n';
}
выдаст
done, p = 0
Память не выделена, и вам сделано предупреждение! Отметим, что, задав
реакцию на такую ситуацию в функции new_handler, пользователь берет
на себя проверку: исчерпана ли свободная память. Она должна выполняться
при каждом обращении в программе к new (если только пользователь
не определил собственные функции для размещения объектов
пользовательских типов; см.$$R.5.5.6).
Полное и последовательное описание операторов С++ содержится в
$$R.6. Советуем ознакомиться с этим разделом. Здесь же дается
сводка операторов и несколько примеров.
------------------------------------------------------------------
Синтаксис операторов
------------------------------------------------------------------
оператор:
описание
{ список-операторов opt }
выражение opt ;
if ( выражение ) оператор
if ( выражение ) оператор else оператор
switch ( выражение ) оператор
while ( выражение ) оператор
do оператор while ( выражение )
for (начальный-оператор-for выражение opt; выражение opt) оператор
case выражение-константа : оператор
default : оператор
break ;
continue ;
return выражение opt ;
goto идентификатор ;
идентификатор : оператор
список-операторов:
оператор
список-операторов оператор
начальный-оператор-for:
описание
выражение opt ;
----------------------------------------------------------------------
Обратите внимание, что описание является оператором, но нет операторов
присваивания или вызова функции (они относятся к выражениям).
Значение можно проверить с помощью операторов if или switch:
if ( выражение ) оператор
if ( выражение ) оператор else оператор
switch ( выражение ) оператор
В языке С++ среди основных типов нет отдельного булевского (тип
со значениями истина, ложь). Все операции отношений:
== != < > <= >=
дают в результате целое 1, если отношение выполняется, и 0 в противном
случае. Обычно определяют константы TRUE как 1 и FALSE как 0.
В операторе if, если выражение имеет ненулевое значение,
выполняется первый оператор, а иначе выполняется второй (если
он указан). Таким образом, в качестве условия допускается любое выражение
типа целое или указатель. Пусть a целое, тогда
if (a) // ...
эквивалентно
if (a != 0) ...
Логические операции
&& || !
обычно используются в условиях. В операциях && и || второй операнд
не вычисляется, если результат определяется значением первого
операнда. Например, в выражении
if (p && l<p->count) // ...
сначала проверяется значение p, и только если оно не равно нулю, то
проверяется отношение l<p->count.
Некоторые простые операторы if удобно заменять выражениями
условия. Например, вместо оператора
if (a <= b)
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();
Смысл обеих конструкций совпадает, но все же первая предпочтительнее,
поскольку в ней нагляднее показана суть операции: проверка на
совпадение значения val со значением из множества констант. Поэтому в
нетривиальных случаях запись, использующая переключатель, понятнее.
Нужно позаботиться о каком-то завершении оператора, указанного
в варианте переключателя, если только вы не хотите, чтобы стали
выполняться операторы из следующего варианта. Например,
переключатель
switch (val) { // возможна ошибка
case 1:
cout << "case 1\n";
case 2:
cout << "case 2\n";
default:
cout << "default: case not found\n";
}
при val==1 напечатает к большому удивлению непосвященных:
case 1
case 2
default: case not found
Имеет смысл отметить в комментариях те редкие случаи, когда стандартный
переход на следующий вариант оставлен намеренно. Тогда этот переход
во всех остальных случаях можно смело считать ошибкой. Для
завершения оператора в варианте чаще всего используется break, но
иногда используются return и даже goto. Приведем пример:
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 not found\n";
return;
}
Здесь при значении val равном 2 мы получим:
case 2
case 1
Отметим, что метку варианта нельзя использовать в операторе goto:
goto case 2; // синтаксическая ошибка
Презираемый оператор goto все-таки есть в С++:
goto идентификатор;
идентификатор: оператор
Вообще говоря, он мало используется в языках высокого уровня, но
может быть очень полезен, если текст на С++ создается не человеком,
а автоматически, т.е. с помощью программы. Например,
операторы goto используются при создании анализатора по заданной
грамматике языка с помощью программных средств.
Кроме того, операторы goto могут пригодиться в тех случаях,
когда на первый план выходит скорость работы программы. Один из
них - когда в реальном времени происходят какие-то вычисления во
внутреннем цикле программы.
Есть немногие ситуации и в обычных программах, когда применение
goto оправдано. Одна из них - выход из вложенного цикла или
переключателя. Дело в том, что оператор break во вложенных циклах
или переключателях позволяет перейти только на один уровень выше.
Приведем пример:
void f()
{
int i;
int j;
for ( i = 0; i < n; i++)
for (j = 0; j<m; j++)
if (nm[i][j] == a) goto found;
// здесь a не найдено
// ...
found:
// nm[i][j] == a
}
Есть еще оператор continue, который позволяет перейти на конец
цикла. Что это значит, объяснено в $$3.1.5.
Программу гораздо легче читать, и она становится намного понятнее, если
разумно использовать комментарии и систематически выделять текст
программы пробелами. Есть несколько способов расположения текста
программы, но нет причин считать, что один из них - наилучший. Хотя
у каждого свой вкус. То же можно сказать и о комментариях.
Однако можно заполнить программу такими комментариями, что читать
и понимать ее будет только труднее. Транслятор не в силах понять
комментарий, поэтому он не может убедиться в том, что комментарий:
[1] осмысленный,
[2] действительно описывает программу,
[3] не устарел.
Во многих программах попадаются непостижимые, двусмысленные и просто
неверные комментарии. Лучше вообще обходиться без них, чем давать
такие комментарии.
Если некий факт можно прямо выразить в языке, то так и следует
делать, и не надо считать, что достаточно упомянуть его в комментарии.
Последнее замечание относится к комментариям, подобным приведенным
ниже:
// переменную "v" необходимо инициализировать.
// переменная "v" может использоваться только в функции "f()".
// до вызова любой функции из этого файла
// необходимо вызвать функцию "init()".
// в конце своей программы вызовите функцию "cleanup()".
// не используйте функцию "weird()".
// функция "f()" имеет два параметра.
При правильном программировании на С++ такие комментарии обычно
оказываются излишними. Чтобы именно эти комментарии стали ненужными,
можно воспользоваться правилами связывания ($$4.2) и областей
видимости, а также правилами инициализации и уничтожения объектов
класса ($$5.5).
Если некоторое утверждение выражается самой программой, не нужно
повторять его в комментарии. Например:
a = b + c; // a принимает значение b+c
count++; // увеличим счетчик count
Такие комментарии хуже, чем избыточные. Они раздувают объем текста,
затуманивают программу и могут быть даже ложными. В то же время
комментарии именно такого рода используют для примеров в учебниках
по языкам программирования, подобных этой книге. Это одна из
многих причин, по которой учебная программа отличается от настоящей.
Можно рекомендовать такой стиль введения комментариев в
программу:
[1] начинать с комментария каждый файл программы: указать в
общих чертах, что в ней определяется, дать ссылки на
справочные руководства, общие идеи по сопровождению
программы и т.д.;
[2] снабжать комментарием каждое определение класса или шаблона
типа;
[3] комментировать каждую нетривиальную функцию, указав: ее
назначение, используемый алгоритм (если только он неочевиден)
и, возможно, предположения об окружении, в котором работает
функция;
[4] комментировать определение каждой глобальной переменной;
[5] давать некоторое число комментариев в тех местах, где
алгоритм неочевиден или непереносим;
[6] больше практически ничего.
Приведем пример:
// tbl.c: Реализация таблицы имен.
/*
Использован метод Гаусса
см. Ральстон "Начальный курс по ..." стр. 411.
*/
// в swap() предполагается, что стек AT&T начинается с 3B20.
/************************************
Авторские права (c) 1991 AT&T, Inc
Все права сохранены
**************************************/
Правильно подобранные и хорошо составленные комментарии играют в
программе важную роль. Написать хорошие комментарии не менее
трудно, чем саму программу, и это - искусство, в котором стоит
совершенствоваться.
Заметим, что если в функции используются только комментарии
вида //, то любую ее часть можно сделать комментарием с помощью
/* */, и наоборот.
1. (*1) Следующий цикл for перепишите с помощью оператора while:
for (i=0; i<max_length; i++)
if (input_line[i] == '?') quest_count++;
Запишите цикл, используя в качестве его управляющей переменной
указатель так, чтобы условие имело вид *p=='?'.
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) Укажите 5 различных конструкций на С++, значение которых
неопределено.
4. (*2) Приведите 10 разных примеров непереносимых конструкций
на С++.
5. (*1) Что произойдет при делении на нуль в вашей программе на С++?
Что будет в случае переполнения или потери значимости?
6. (*1) Укажите порядок вычисления следующих выражений, задав их
полную скобочную структуру:
*p++
*--p
++a--
(int*)p->m
*p.m
*a[i]
7. (*2) Напишите такие функции: strlen() - подсчет длины строки,
strcpy() - копирование строк и strcmp() - сравнение строк. Какими
должны быть типы параметров и результатов функций? Сравните их
со стандартными версиями, имеющимися в <string.h> и в вашем
руководстве.
8. (*1) Выясните, как ваш транслятор отреагирует на такие ошибки:
void f(int a, int b)
{
if (a = 3) // ...
if (a&077 == 0) // ...
a := b+1;
}
Посмотрите, какова будет реакция на более простые ошибки.
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: *to++ = *from++;
case 6: *to++ = *from++;
case 5: *to++ = *from++;
case 4: *to++ = *from++;
case 3: *to++ = *from++;
case 2: *to++ = *from++;
case 1: *to++ = *from++;
} while (--n>0);
}
}
Каков может быть смысл этой функции?
11. (*2) Напишите функцию atoi(), которая имеет параметр - строку цифр
и возвращает соответствующее ей целое. Например, atoi("123")
равно 123. Измените функцию atoi() так, чтобы она могла
переводить в число последовательность цифр не только в десятичной,
но и в восьмеричной и шестнадцатеричной записи, принятой в С++.
Добавьте возможность перевода символьных констант С++. Напишите
функцию itoa() для перевода целого значения в строковое
представление.
12. (*2) Перепишите функцию get_token() ($$3.12) так, чтобы она читала
целую строку в буфер, а затем выдавала лексемы, читая по
символу из буфера.
13. (*2) Введите в программу калькулятора из $$3.1 такие функции, как
sqrt(), log() и sin(). Подсказка: задайте предопределенные
имена и вызывайте функции с помощью массива указателей на них.
Не забывайте проверять параметры, передаваемые этим
функциям.
14. (*3) Введите в калькулятор возможность определять пользовательские
функции. Подсказка: определите функцию как последовательность
операторов, будто бы заданную самим пользователем. Эту
последовательность можно хранить или как строку символов, или
как список лексем. Когда вызывается функция, надо выбирать и
выполнять операции. Если пользовательские функции могут
иметь параметры, то придется придумать форму записи и для них.
15. (*1.5) Переделайте программу калькулятора, используя структуру
symbol вместо статических переменных name_string и number_value:
struct symbol {
token_value tok;
union {
double number_value;
char* name_string;
};
};
16.(*2.5) Напишите программу, которая удаляет все комментарии из
программы на С++. Это значит, надо читать символы из cin и
удалять комментарии двух видов: // и /* */. Получившийся текст
запишите в cout. Не заботьтесь о красивом виде получившегося
текста (это уже другая, более сложная задача). Корректность
программ неважна. Нужно учитывать возможность появления символов
//, /* и */ в комментариях, строках и символьных константах.
17. (*2) Исследуйте различные программы и выясните, какие способы
выделения текста пробелами и какие комментарии используются.
Итерация присуща человеку,
а рекурсия - богу.
- Л. Дойч
Все нетривиальные программы состоят из нескольких раздельно
транслируемых единиц, по традиции называемых файлами. В этой главе
описано, как раздельно транслируемые функции могут вызывать друг друга,
каким образом они могут иметь общие данные, и как добиться
непротиворечивости типов, используемых в разных файлах программы.
Подробно обсуждаются функции, в том числе:
передача параметров, перегрузка имени функции,