Основы языка Си: структура Си-программы, базовые типы и конструирование новых типов, операции и выражения
Логические операции
Логические операции и выражения были подробно рассмотрены в разделе 1.4.4. В Си используются следующие обозначения для логических операций:
|| логическое "или" (логическое сложение) && логическое "и" (логическое умножение) ! логическое "не" (логическое отрицание)
Логические константы "истина" и "ложь" обозначаются через true и false (это ключевые слова языка). Примеры логических выражений:
bool a, b, c, d; int x, y; a = b || c; // логическое "или" d = b && c; // логическое "и" a = !b; // логическое "не" a = (x == y); // сравнение в правой части a = false; // ложь b = true; // истина c = (x > 0 && y != 1); // c истинно, когда // оба сравнения истинны
Самый высокий приоритет у операции логического отрицания, затем следует логическое умножение, самый низкий приоритет у логического сложения.
Чрезвычайно важной особенностью операций логического сложения и умножения является так называемое "сокращенное вычисление" результата. А именно, при вычислении результата операции логического сложения или умножения всегда сначала вычисляется значение первого аргумента. Если оно истинно в случае логического сложения или ложно в случае логического умножения, то второй аргумент операции не вычисляется вовсе! Результат операции полагается истинным в случае логического сложения или ложным в случае логического умножения. Подробно это рассмотрено в разделе 1.4.4.
Операции сравнения
Операция сравнения сравнивает два выражения. В результате вырабатывается логическое значение - true или false (истина или ложь) в зависимости от значений выражений. Примеры:
bool res; int x, y; res = (x == y); // true, если x равно y, иначе false res = (x == x); // всегда true res = (2 < 1); // всегда false
Операции сравнения в Си обозначаются следующим образом:
== равно, != не равно, > больше, >= больше или равно, < меньше, <= меньше или равно.
Побитовые логические операции
Кроме обычных логических операций, в Си имеются побитовые логические операции, которые выполняются независимо для каждого отдельного бита операндов. Побитовые операции имеют следующие обозначения:
& побитовое логическое умножение ("и") | побитовое логическое сложение ("или") ~ побитовое логическое отрицание ("не") ^ побитовое сложение по модулю 2 (исключающее "или")
(Необходимо помнить, что логические операции умножения и сложения записываются с помощью двойных знаков && или ||, а побитовые - с помощью одинарных.)
Ни в коем случае не используйте побитовые операции в качестве логических условий, это может приводить к непредсказуемым ошибкам!
В основном побитовые операции применяются для манипуляций с битовыми масками. Например, пусть целое число x описывает набор признаков некоторого объекта, состоящий из четырех признаков. Назовем их условно A, B, C, D. Пусть за признак A отвечает нулевой бит слова x (биты в двоичном представлении числа нумеруются справа налево, начиная с нуля). Если бит равен единице (программисты говорят бит установлен), то считается, что объект обладает признаком A. За признаки B, C, D отвечают биты с номерами 1, 2, 3. Общепринятая практика состоит в том, чтобы определить константы, отвечающие за соответствующие признаки (их обычно называют масками ):
const int MASK_A = 1; const int MASK_B = 2; const int MASK_C = 4; const int MASK_D = 8;
Эти константы содержат единицу в соответствующем бите и нули в остальных битах. Для того чтобы проверить, установлен ли в слове x бит, соответствующий, к примеру, признаку D, используется операция побитового логического умножения. Число x умножается на константу MASK_D ; если результат отличен от нуля, то бит установлен, т.е. объект обладает признаком D, если нет, то не обладает. Такая проверка реализуется следующим фрагментом:
if ((x & MASK_D) != 0) { // Бит D установлен в слове x, т.е. // объект обладает признаком D . . . } else { // Объект не обладает признаком D . . . }
При побитовом логическом умножении константа MASK_D обнуляет все биты слова x, кроме бита D, т.е. как бы вырезает бит D из x. В двоичном представлении это выглядит примерно так:
x: 0101110110...10*101 MASK_D: 0000000000...001000 x & MASK_D: 0000000000...00*000
Звездочкой здесь обозначено произвольное значение бита D слова x.
Для установки бита D в слове x используется операция побитового логического сложения:
x = (x | MASK_D); // Установить бит D в слове x
Чаще это записывается с использованием операции |= типа "увеличить на" (см. раздел 3.4.4):
x |= MASK_D; // Установить бит D в слове x
В двоичном виде это выглядит так:
x: 0101110110...10*101 MASK_D: 0000000000...001000 x | MASK_D: 0101110110...101101
Операция побитового отрицания " ~ " инвертирует биты слова:
x: 0101110110...101101 ~x: 1010001001...010010
Для очистки (т.е. установки в ноль) бита D используется комбинация операций побитового отрицания и побитового логического умножения:
x = (x & ~MASK_D); // Очистить бит D в слове x
или, применяя операцию " &= " типа "домножить на":
x &= ~MASK_D; // Очистить бит D в слове x
Здесь сначала инвертируется маска, соответствующая биту D,
MASK_D: 0000000000...001000 ~MASK_D: 1111111111...110111
в результате получаются единицы во всех битах, кроме бита D. Затем слово x побитно домножается на инвертированную маску:
x: 0101110110...10*101 ~MASK_D: 1111111111...110111 x & ~MASK_D: 0101110110...100101
В результате в слове x бит D обнуляется, а остальные биты остаются неизменными.
Приоритеты побитовых операций в Си выбраны достаточно странно (они такие же, как у соответствующих логических операций), это иногда приводит к неожиданным ошибкам. Например, если не заключить в скобки операцию побитового умножения в приведенном выше примере, то получится ошибочный результат: строка
if (x & MASK_D != 0) {
эквивалентна строке
if ((x & 1) != 0) {
т.е. проверяется бит A, а вовсе не D! Дело в том, что приоритет операции сравнения != выше, чем операции побитового умножения &, т.е. в приведенной строке скобки неявно расставлены так:
if (x & (MASK_D != 0)) {
Выражение ( MASK_D != 0 ) истинно и, таким образом, равно единице, поэтому строка эквивалентна
if (x & 1) {
что, в свою очередь, эквивалентно более канонической записи:
if ((x & 1) != 0) {
Чтобы избежать подобных ошибок, всегда заключайте все побитовые операции в скобки.
Побитовую операцию ^ называют сложением по модулю 2, а также "исключающим или". Часто для нее используется аббревиатура XOR, от eXclusive OR. "Таблица сложения" для этой операции выглядит следующим образом:
0 ^ 0 = 0, 0 ^ 1 = 1, 1 ^ 0 = 1, 1 ^ 1 = 0.
Пусть x - произвольное целое число, m - маска, т.е. число, в котором интересующие программиста биты установлены в единицу, остальные в ноль. В результате выполнения операции XOR
x = (x ^ m);
или, в более удобной записи,
x ^= m;
биты в слове x, соответствующие установленным в единицу битам маски m, изменяются на противоположные (инвертируются). Биты слова x, соответствующие нулевым битам маски, не меняют своих значений. Пример:
x: 101101...1001011110 m: 000000...0011111100 x ^ m: 101101...1010100010
Операция XOR обладает замечательным свойством: если дважды прибавить к слову x произвольную маску m, то в результате получается исходное значение x:
((x ^ m) ^ m) == x
Прибавление к слову x маски m можно трактовать как шифрование x, ведь в результате биты x, соответсвующие единичным битам маски m, инвертируются. Если маска достаточно случайная, то в результате x тоже принимает случайное значение. Процедура расшифровки в данном случае совпадает с процедурой шифрования и состоит в повторном прибавлении маски m.
Операции сдвига
Операции сдвига применяются к целочисленным переменным: двоичный код числа сдвигается вправо или влево на указанное количество позиций. Сдвиг вправо обозначается двумя символами "больше" >>, сдвиг влево - двумя символами "меньше" <<. Примеры:
int x, y; . . . x = (y >> 3); // Сдвиг на 3 позиции вправо y = (y << 2); // Сдвиг на 2 позиции влево
При сдвиге влево на k позиций младшие k разрядов результата устанавливаются в ноль. Сдвиг влево на k позиций эквивалентен умножению на число 2k. Сдвиг вправо более сложен, он по-разному определяется для беззнаковых и знаковых чисел. При сдвиге вправо беззнакового числа на k позиций освободившиеся k старших разрядов устанавливаются в ноль. Например, в двоичной записи имеем:
unsigned x; x = 110111000...10110011 x >> 3 = 000110111000...10110
Сдвиг вправо на k позиций соответствует целочисленному делению на число 2k.
При сдвиге вправо чисел со знаком происходит так называемое "расширение знакового разряда". Именно, если число неотрицательно, т.е. старший, или знаковый, разряд числа равен нулю, то происходит обычный сдвиг, как и в случае беззнаковых чисел. Если же число отрицательное, т.е. его старший разряд равен единице, то освободившиеся в результате сдвига k старших разрядов устанавливаются в единицу. Число, таким образом, остается отрицательным. При k = 1 это соответствует делению на 2 только для отрицательных чисел, не равных -1. Для числа -1, все биты двоичного кода которого равны единице, сдвиг вправо не приводит к его изменению. Пример (используется двоичная запись):
int x; x = 110111000...10110011 x >> 3 = 111110111000...10110
В программах лучше не полагаться на эту особенность сдвига вправо для знаковых чисел и использовать конструкции, которые заведомо одинаково работают для знаковых и беззнаковых чисел. Например, следующий фрагмент кода выделяет из целого числа составляющие его байты и записывает их в целочисленные переменные x0, x1, x2, x3, младший байт в x0, старший в x3. При этом байты трактуются как неотрицательные числа. Фрагмент выполняется одинаково для знаковых и беззнаковых чисел:
int x; int x0, x1, x2, x3; . . . x0 = (x & 255); x1 = ((x >> 8) & 255); x2 = ((x >> 16) & 255); x3 = ((x >> 24) & 255);
Здесь число 255 играет роль маски, см. раздел 3.4.7. При побитовом умножении на эту маску из целого числа вырезается его младший байт, поскольку маска 255 содержит единицы в младших восьми разрядах. Чтобы получить байт числа x с номером n, n = 0,1,2,3, мы сначала сдвигаем двоичный код x вправо на 8n разрядов, таким образом, байт с номером n становится младшим. Затем с помощью побитового умножения вырезается младший байт.