Побитовые логические операции
Побитовые логические операции
& | ^ ~ >> <<
применяются к целым, то есть к объектам типа char, short, int, long и их unsigned аналогам, результаты тоже целые.
Одно из стандартных применений побитовых логических операций - реализация маленького множества (вектора битов). В этом случае каждый бит беззнакового целого представляет один член множества, а число членов ограничено числом битов. Бинарная операция & интерпретируется как пересечение, | как объединение, а ^ как разность. Для именования членов такого множества можно использовать перечисление. Вот маленький пример, заимствованный из реализации (не пользовательского интерфейса) :
enum state_value { _good=0, _eof=1, _fail=2, _bad=4}; // хорошо, конец файла, ошибка, плохо
Определение _good не является необходимым. Я просто хотел, чтобы состояние, когда все в порядке, имело подходящее имя. Состояние потока можно установить заново следующим образом:
cout.state = _good;
Hапример, так можно проверить, не был ли испорчен поток или допущена операционная ошибка:
if (cout.state&(_bad|_fail)) // не good
Еще одни скобки необходимы, поскольку & имеет более высокий приоритет, чем |. Функция, достигающая конца ввода, может сообщать об этом так:
cin.state |= _eof;
Операция |= используется потому, что поток уже может быть испорчен (то есть, state==_bad), поэтому
cin.state = _eof;
очистило бы этот признак. Различие двух потоков можно находить так:
state_value diff = cin.state^cout.state;
В случае типа stream_state (состояние потока) такая разность не очень нужна, но для других похожих типов она оказывается самой полезной. Например, при сравнении вектора бит, представляющего множество прерываний, которые обрабатываются, с другим, представляющим прерывания, ждущие обработки.
Следует заметить, что использование полей (см. этот пункт) в действительности является сокращенной записью сдвига и маскирования для извлечения полей бит из слова. Это, конечно, можно сделать и с помощью побитовых логических операций, Например, извлечь средние 16 бит из 32-битового int можно следующим образом:
unsigned short middle(int a) { return (a>>8)&0xffff; }
Не путайте побитовые логические операции с логическими операциями:
&& || !
Последние возвращают 0 или 1, и они главным образом используются для записи проверки в операторах if, while или for (см. этот пункт). Например, !0 (не ноль) есть значение 1, тогда как ~0 (дополнение нуля) есть набор битов все-единицы, который обычно является значением -1.
Преобразование типа
Бывает необходимо явно преобразовать значение одного типа в значение другого. Явное преобразование типа дает значение одного типа для данного значения другого типа.
Например:
float r = float(1);
перед присваиванием преобразует целое значение 1 к значению с плавающей точкой 1.0. Результат преобразования типа не является lvalue, поэтому ему нельзя присваивать (если только тип не является ссылочным типом).
Есть два способа записи явного преобразования типа: традиционная в C запись приведения к типу (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;
позволит работать посредством p с некоторым объектом some_object как с любым типом any_type.
Когда преобразование типа не необходимо, его следует избегать. Программы, в которых используется много явных преобразований типов, труднее понимать, чем те, в которых это не делается. Однако такие программы легче понимать, чем программы, просто не использующие типы для представления понятий более высокого уровня (например, программу, которая оперирует регистром устройства с помощью сдвига и маскирования, вместо того, чтобы определить подходящую struct и оперировать ею; см. этот пункт). Кроме того, правильность явного преобразования типа часто критическим образом зависит от понимания программистом того, каким образом объекты различных типов обрабатываются в языке, и очень часто от подробностей реализации.
Например:
int i = 1; char* pc = "asdf"; int* pi = &i; i = (int)pc; pc = (char*)i; // остерегайтесь: значение pc может измениться // на некоторых машинах // sizeof(int)
1 2 3 4
8 8 8
| |