Функция, достигающая конца ввода, может сообщать об этом так:
cin.state |= _eof;
Операция |= используется потому, что поток уже может быть испорчен
(то есть, state==_bad), поэтому
cin.state = _eof;
очистло бы этот признак. Различие двух потоков можно находить так:
state_value diff = cin.state^cout.state;
- стр 95 -
В случае типа 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;
или определить новое имя типа:
typedef char* Pchar;
char* p = Pchar(0777);
По моему мнению, функциональная запись в нетривиальных случаях
предпочтительна. Рассмотрим два эквивалентных примера
- стр 96 -
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)
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;
}
}
Получающееся дерево генератор кода может использовать например так:
- стр 98 -
void generate(enode* n)
{
switch (n->oper) {
case PLUS:
// делает нечто соответствуюшее
delete n;
}
}
Объект, созданный с помощью new, существует, пока он не будет
явно уничтожен delete, после чего пространство, которое он
занимал, опять может использоваться new. Никакого "сборщика
мусора", который ищет объекты, на которые нет ссылок, и
предоставляет их в распоряжение new, нет. Операция delete может
применяться только к указателю, который был возвращен операцей 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*);
- стр 99 -
Стандартная реализация new не инициализирует возвращаемый объект.
Что происходит, когда new не находит памяти для выделения?
Поскольку даже виртуальная память конечна, это иногда должно
происходить. Запрос вроде
char* p = new char[100000000];
как правило, приводит к каким-то неприятностям. Когда у new ничего
не получается, она вызывает функцию, указываемую указаетелем
_new_handler (указатели на функции обсуждаются в #4.6.9). Вы можете
задать указатель явно или использовать функцию set_new_handler().
Например:
#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 не прошла: за пределами памяти
_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";
}
- стр 100 -
выдаст
сделано, p = 0
Вам сделали предупреждение! Заметьте, что тот, кто задает
_new_handler, берет на себя заботу по проверке истощения памяти при
каждом использовании new в програме (за исключением случая, когда
пользователь задал отдельные подпрограммы для размещения объектов
заданных типов, определяемых пользователем; см. #5.5.6).
3.3 Сводка операторов
Операторы C++ систематически и полностью изложены в #с.9,
прочитайте, пожалуйста, этот раздел. А здесь приводится краткая
сводка и некоторые примеры.
Синтаксис оператора
---------------------------------------------------------------
-----
оператор:
описание
{список_операторов opt}
выражение opt
if ( выражение ) опреатор
if ( выражение ) оператор else оператор
switch ( выражение ) оператор
while ( выражение ) оператор
do оператор while (выражение)
for ( оператор выражение opt ; выражение opt ) оператор
case константное_выражение : оператор
default : оператор
break ;
continue ;
return выражение opt ;
goto идентификатор ;
идентификатор : оператор
список_операторов:
оператор
оператор список_операторов
Заметьте, что описание является оператором, и что нет операторов
присваивания и вызова процедуры. Присваивание и вызов функции
обрабатываются как выражения.
- стр 101 -
3.3.1 Проверки
Проверка значения может осуществляться или оператором if, или
оператором switch:
if ( выражение ) оператор
if ( выражение ) оператор else оператор
switch ( выражение ) оператор
В C++ нет отдельного булевского типа. Операции сравнения
== != < <= > >=
возвращают целое 1, если сравнение истинно, иначе возвращают 0. Не
так уж непривычно видеть, что ИСТИНА определена как 1, а ЛОЖЬ
определена как 0.
В операторе if первый (или единственный) оператор выполняется в
том случае, если выражение ненулевое, иначе выполняется второй
оператор (если он задан). Отсюда следует, что в качестве условия
может использоваться любое целое выражение. В частности, если a