160
}
4.11.
Побитовые операторы
Таблица 4.3. Побитовые операторы
Символ операции
Значение
Использование
Побитовое НЕ
expr
<<
Сдвиг влево expr1 << expr2
>>
Сдвиг вправо expr1 >> expr2
&
Побитовое И expr1 & expr2
^
Побитовое
ИСКЛЮЧАЮЩЕЕ ИЛИ expr1 ^ expr2
|
Побитовое ИЛИ expr1 | expr2
&=
Побитовое
И с присваиванием expr1 &= expr2
^=
Побитовое
ИСКЛЮЧАЮЩЕЕ ИЛИ с присваиванием expr1 ^= expr2
|=
Побитовое
ИЛИ с присваиванием expr1 |= expr2
<<=
Сдвиг влево с присваиванием expr1 <<= expr2
>>=
Сдвиг вправо с присваиванием expr1 >>= expr2
Побитовые операции рассматривают операнды как упорядоченные наборы битов, каждый бит может иметь одно из двух значений – 0 или 1. Такие операции позволяют программисту манипулировать значениями отдельных битов. Объект, содержащий набор битов, иногда называют битовым вектором. Он позволяет компактно хранить набор
флагов – переменных, принимающих значение “да” “нет”. Например, компиляторы зачастую помещают в битовые векторы спецификаторы типов, такие, как const и volatile
. Библиотека iostream использует эти векторы для хранения состояния формата вывода.
Как мы видели, в С++ существуют два способа работы со строками: использование C- строк и объектов типа string стандартной библиотеки – и два подхода к массивам: массивы встроенного типа и объект vector. При работе с битовыми векторами также int main()
{
// примеры оператора "запятая"
// переменные ia, sz и index определены в другом месте ... int ival = (ia != 0)
? ix=get_va1ue(), ia[index]=ix
: ia=new int[sz], ia[index]=0;
// ...
С++ для начинающих
161можно применять подход, заимствованный из С, – использовать для представления такого вектора объект встроенного целого типа, обычно unsigned int, или класс bitset стандартной библиотеки С++. Этот класс инкапсулирует семантику вектора, предоставляя операции для манипулирования отдельными битами. Кроме того, он позволяет ответить на вопросы типа: есть ли “взведенные” биты (со значением 1) в векторе? Сколько битов “взведено”?
В общем случае предпочтительнее пользоваться классом bitset, однако, понимание работы с битовыми векторами на уровне встроенных типов данных очень полезно. В этом разделе мы рассмотрим применение встроенных типов для представления битовых векторов, а в следующем – класс bitset.
При использовании встроенных типов для представления битовых
векторов можно пользоваться как знаковыми, так и беззнаковыми целыми типами, но мы настоятельно советуем пользоваться беззнаковыми: поведение побитовых операторов со знаковыми типами может различаться в разных реализациях компиляторов.
Побитовое НЕ () меняет значение каждого бита операнда. Бит, установленный в 1, меняет значение на 0 и наоборот.
Операторы сдвига (<<, >>) сдвигают биты в левом операнде на указанное правым операндом количество позиций. “Выталкиваемые наружу” биты пропадают, освобождающиеся биты (справа для сдвига влево, слева для сдвига вправо) заполняются нулями. Однако нужно иметь в виду, что для сдвига вправо заполнение левых битов нулями гарантируется только для беззнакового операнда, для знакового в некоторых реализациях возможно заполнение значением знакового (самого левого) бита.
Побитовое И (&) применяет операцию И ко всем битам своих операндов. Каждый бит левого операнда сравнивается с битом правого, находящимся в той же позиции. Если оба бита равны 1, то бит в данной позиции получает значение 1, в любом другом случае – 0.
(Побитовое И (&) не надо путать с логическим И (&&),но, к сожалению, каждый программист хоть раз в жизни совершал подобную ошибку.)
Побитовое ИСКЛЮЧАЮЩЕЕ ИЛИ (^) сравнивает биты операндов. Соответствующий бит результата равен 1, если операнды различны (один равен 0, а другой 1). Если же оба операнда равны, результата равен 0.
Побитовое ИЛИ (|) применяет операцию логического сложения к каждому биту операндов. Бит в позиции результата получает значение 1, если хотя бы один из соответствующих битов операндов равен 1, и 0, если биты обоих операндов равны 0.
(Побитовое ИЛИ не нужно смешивать с логическим ИЛИ.)
Рассмотрим простой пример. Пусть у нас есть класс из 30 студентов. Каждую неделю преподаватель проводит зачет, результат которого – сдал/не сдал. Итоги можно представить в виде битового вектора. (Заметим, что
нумерация битов начинается с нуля, первый бит на самом деле является вторым по счету. Однако для удобства мы не будем использовать нулевой бит; таким образом, студенту номер 1 соответствует бит номер 1. В конце концов, наш преподаватель – не специалист в области программирования.) unsigned int quiz1 = 0;
Нам нужно иметь возможность менять значение каждого бита и проверять это значение.
Предположим, студент 27 сдал зачет. Бит 27 необходимо выставить в 1, не меняя значения других битов. Это можно сделать за два шага. Сначала нужно начать с числа, содержащего 1 в 27-м бите и 0 в остальных. Для этого используем операцию сдвига:
С++ для начинающих
162
1 << 27;
Применив побитовую операцию ИЛИ к переменной quiz1 и нашей константе, получим нужный результат: значение 27-й бита станет равным значение 1, а другие биты останутся неизменными. quiz1 |= 1<<27;
Теперь представим себе, что преподаватель перепроверил результаты теста и выяснил, что студент 27 зачет не сдал. Теперь нужно присвоить нуль 27-му биту, не трогая остальных. Сначала применим побитовое НЕ к предыдущей константе и получим число, в котором все биты, кроме 27-го, равны 1:
(1<<27 );
Теперь побитово умножим (И) эту константу на quiz1 и получим нужный результат: 0 в
27-м бите и неизменные значения остальных. quiz1 &= (1<<27);
Как проверить значение того же 27-го бита? Побитовое И дает true, если 27-й бит равен
1
, и false, если 0: bool hasPassed = quiz1 & (1<<27);
При использовании побитовых операций подобным образом очень легко допустить ошибку. Поэтому чаще всего такие операции инкапсулируются в макросы препроцессора или встроенные функции:
}
Вот пример использования: has_passed_quiz[ index ] = bit_on( quiz1, index );
Раз уж мы начали инкапсулировать действия с битовым вектором в функции, следующим шагом нужно создать класс. Стандартная библиотека С++ включает такой класс bitset, его использование описано ниже. inline boo1 bit_on (unsigned int ui, int pos)
{ return u1 & ( 1 << pos ); enum students { Danny = 1, Jeffrey, Ethan, Zev, Ebie, // ...
AnnaP = 26, AnnaL = 27 }; const int student_size = 27;
// наш битовый вектор начинается с 1 boo1 has_passed_quiz[ student_size+l ]; for ( int index = 1; index <= student_size; ++-index )
С++ для начинающих
163Упражнение 4.12
Даны два целых числа: unsigned int ui1 = 3, ui2 = 7;
Каков результат следующих выражений?
(a) ui1 & ui2 (c) uil | ui2
(b) ui1 && ui2 (d) uil || ui2
Упражнение 4.13
Используя пример функции bit_on(), создайте функции bit_turn_on() (выставляет бит в 1), bit_turn_off() (сбрасывает бит в 0), flip_bit() (меняет значение на противоположное) и bit_off() (возвращает true, если бит равен 0). Напишите программу, использующую ваши функции.
Упражнение 4.14
В чем
недостаток функций из предыдущего упражнения, использующих тип unsigned int
? Их реализацию можно улучшить, используя определение типа с помощью typedef или механизм функций-шаблонов. Перепишите функцию bit_on(),применив сначала typedef
, а затем механизм шаблонов.
4.12.
Класс bitset
Таблица 4.4. Операции с классом bitset
Операция
Значение
Использование test(pos)
Бит pos равен 1? a.test(4) any()
Хотя бы один бит равен 1? a.any() none()
Ни один бит не равен 1? a.none() count()
Количество битов, равных 1 a.count() size()
Общее количество битов a.size()
[pos]
Доступ к биту pos a[4] flip()
Изменить значения всех a.flip() flip(pos)
Изменить значение бита pos a.flip(4) set()
Выставить все биты в 1 a.set() set(pos)
Выставить бит pos в 1 a.set(4) reset()
Выставить все биты в 0 a.reset() reset(pos)
Выставить бит pos в 0 a.reset(4)
Как мы уже говорили, необходимость создавать сложные выражения для манипуляции битовыми векторами затрудняет использование встроенных типов данных. Класс bitset упрощает работу с битовым вектором. Вот какое выражение нам приходилось писать в предыдущем разделе для того, чтобы “взвести” 27-й бит:
С++ для начинающих
164
quiz1 |= 1<<27;
При использовании bitset то же самое мы можем сделать двумя способами: quiz1[27] = 1; или quiz1.set(27);
(В нашем примере мы не используем нулевой бит, чтобы сохранить “естественную” нумерацию. На самом деле, нумерация битов начинается с 0.)
Для использования класса bitset необходимо включить заголовочный файл:
#include
Объект типа bitset может быть объявлен тремя способами. В определении по умолчанию мы просто указываем размер битового вектора: bitset<32> bitvec;
Это определение задает объект bitset, содержащий 32 бита с номерами от 0 до 31. Все биты инициализируются нулем. С помощью функции any() можно проверить, есть ли в векторе единичные биты. Эта функция возвращает true, если хотя бы один бит отличен от нуля. Например: bool is_set = bitvec.any();
Переменная is_set получит значение false, так как объект bitset по умолчанию инициализируется нулями. Парная функция none() возвращает true, если все биты равны нулю: bool is_not_set = bitvec.none();
Изменить значение отдельного бита можно двумя способами: воспользовавшись функциями set() и reset() или индексом. Так, следующий цикл выставляет в 1 каждый четный бит: bitvec[ index ] = 1;
Аналогично существует два способа проверки значений каждого бита – с помощью функции test() и с помощью индекса. Функция () возвращает true, если соответствующий бит равен 1, и false в противном случае. Например:
// присваивание bitvec[0]=1 сработало!; for ( int index=0; index<32; ++index ) if ( index % 2 == 0 ) if ( bitvec.test( 0 ))
С++ для начинающих
165
Значения битов с помощью индекса проверяются таким образом: cout << endl;
Следующая пара операторов демонстрирует сброс первого бита двумя способами: bitvec[0] = 0;
Функции set() и reset() могут применяться ко всему битовому вектору в целом. В этом случае они должны быть вызваны без параметра. Например:
// что-то опять не сработало
Функция flip() меняет значение отдельного бита или всего битового вектора: bitvec.flip(); // меняет значения всех битов
Существуют еще два способа определить объект типа bitset. Оба они дают возможность проинициализировать объект определенным набором нулей и единиц. Первый способ – явно задать целое беззнаковое число как аргумент конструктору. Начальные N позиций битового вектора получат значения соответствующих двоичных разрядов аргумента.
Например: bitset< 32 > bitvec2( Oxffff ); инициализирует bitvec2 следующим набором значений:
00000000000000001111111111111111
В результате определения cout << "bitvec: включенные биты:\n\t"; for ( int index = 0; index < 32; ++-index ) if ( bitvec[ index ] ) cout << index << " "; bitvec.reset(0);
// сброс всех битов bitvec.reset(); if (bitvec.none() != true)
// что-то не сработало
// установить в 1 все биты вектора bitvec if ( bitvec.any() != true ) bitvec.f1ip( 0 ); // меняет значение первого бита bitvec[0].flip(); // тоже меняет значение первого бита
С++ для начинающих
166bitset< 32 > bitvec3( 012 ); у bitvec3 окажутся ненулевыми биты на местах 1 и 3:
00000000000000000000000000001010
В качестве аргумента конструктору
может быть передано и строковое значение, состоящее из нулей и единиц. Например, следующее определение инициализирует bitvec4
тем же набором значений, что и bitvec3: bitset< 32 > bitvec4( bitval );
Можно также указать диапазон символов строки, выступающих как начальные значения для битового вектора. Например: bitset< 32 > bitvec5( bitval, 6, 4 );
Мы получаем то же значение, что и для bitvec3 и bitvec4. Если опустить третий параметр, подстрока берется до конца исходной строки: bitset< 32 > bitvec6( bitval, 6 );
Класс bitset предоставляет две функции-члена для преобразования объекта bitset в другой тип. Для трансформации в строку, состоящую из символов нулей и единиц, служит функция to_string(): string bitva1( bitvec3.to_string() );
Вторая функция, to_long(), преобразует битовый вектор в его целочисленное представление в виде unsigned long, если, конечно, оно помещается в unsigned long.
Это видоизменение особенно полезно, если мы хотим передать битовый вектор функции на С или С++, не пользующейся стандартной библиотекой.
К объектам типа bitset можно применять побитовые операции. Например: bitset<32> bitvec7 = bitvec2 & bitvec3;
Объект bitvec7 инициализируется результатом побитового И двух битовых векторов bitvec2
и bitvec3.
// эквивалентно bitvec3 string bitva1( "1010" );
// подстрока с шестой позиции длиной 4: 1010 string bitval ( "1111110101100011010101" );
// подстрока с шестой позиции до конца строки: 1010101 string bitva1( "1111110101100011010101" );
С++ для начинающих
167bitset<32> bitvec8 = bitvec2 | bitvec3;
Здесь bitvec8 инициализируется результатом побитового ИЛИ векторов bitvec2 и bitvec3
. Точно так же поддерживаются и составные операции присваивания и сдвига.
Упражнение 4.15
Допущены ли ошибки в приведенных определениях битовых векторов?
(a) bitset<64> bitvec(32);
(b) bitset<32> bv( 1010101 );
(c) string bstr; cin >> bstr; bitset<8>bv( bstr );
(d) bitset<32> bv; bitset<16> bvl6( bv );
Упражнение 4.16
Допущены ли ошибки в следующих операциях с битовыми векторами?
(c) bitvec.f1ip( bitvec.count() );
Упражнение 4.17
Дана последовательность: 1,2,3,5,8,13,21. Каким образом можно инициализировать объект bitset<32> для ее представления? Как присвоить значения для представления этой последовательности пустому битовому вектору? Напишите вариант инициализации и вариант с присваиванием значения каждому биту.
4.13.
Приоритеты
Приоритеты операций задают последовательность вычислений в сложном выражении.
Например, какое значение получит ival? int ival = 6 + 3 * 4 / 2 + 2;
Если вычислять операции слева направо, получится 20. Среди других возможных результатов будут 9, 14 и 36. Правильный ответ: 14.
В С++ умножение и деление имеют более высокий приоритет, чем сложение, поэтому они будут вычислены раньше. Их
собственные приоритеты равны, поэтому умножение и деление будут вычисляться слева направо. Таким образом, порядок вычисления данного выражения таков:
1. 3 * 4 => 12 2. 12 / 2 => 6 3. 6 + 6 => 12 4. 12 + 2 => 14 extern void bitstring(const char*); bool bit_on (unsigned long, int); bitset<32> bitvec;
(a) bitsting( bitvec.to_string().c_str() );
(b) if ( bit_on( bitvec.to_1ong(), 64 )) ...
С++ для начинающих
168Следующая конструкция ведет себя не так, как можно было бы ожидать. Приоритет операции присваивания меньше, чем операции сравнения: while ( ch = nextChar() != '\n' )
Программист хотел присвоить переменной ch значение, а затем проверить, равно ли оно символу новой строки. Однако на самом деле выражение сначала сравнивает значение, полученное от nextChar(), с '\n', и результат – true или false – присваивает переменной ch.
Приоритеты операций можно изменить с помощью скобок. Выражения в скобках вычисляются в первую очередь. Например:
4 * 5 + 7 * 2 ==> 34 4 * ( 5 + 7 * 2 ) ==> 76 4 * ( (5 + 7) * 2 ) ==> 96
Вот как с помощью скобок исправить поведение предыдущего примера: while ( (ch = nextChar()) != '\n' )
Операторы обладают и приоритетом, и ассоциативностью. Оператор присваивания правоассоциативен, поэтому вычисляется справа налево: ival = jval = kva1 = lval
Сначала kval получает значение lval, затем jval – значение результата этого присваивания, и в конце концов ival получает значение jval.
Арифметические операции, наоборот, левоассоциативны. Следовательно, в выражении ival + jval + kva1 + 1va1 сначала складываются ival и jval, потом к результату прибавляется kval, а затем и lval
В таблице 4.4 приведен полный список операторов С++ в порядке уменьшения их приоритета. Операторы внутри одной секции таблицы имеют равные приоритеты. Все операторы
некоторой секции имеют более высокий приоритет, чем операторы из секций, следующих за ней. Так, операции умножения и деления имеют одинаковый приоритет, и он выше приоритета любой из операций сравнения.
Упражнение 4.18
Каков порядок вычисления следующих выражений? При ответе используйте таблицу 4.4.
(c) ch = buf[ bp++ ] != '\n'
Упражнение 4.19
(a) ! ptr == ptr->next
(b) uc ^ 0377 & ui << 4
С++ для начинающих
169
Все три выражения из предыдущего упражнения вычисляются не в той последовательности, какую, по-видимому, хотел задать программист. Расставьте скобки так, чтобы реализовать его первоначальный замысел.
Упражнение 4.20
Следующие выражения вызывают ошибку компиляции из-за неправильно понятого приоритета операций. Объясните, как их исправить, используя таблицу 4.4.
(b) cout << ival % 2 ? "odd" : "even";
Таблица 4.4. Приоритеты операций
Оператор
Значение
Использование
::
Глобальная область видимости
::name
::
Область видимости класса class::name
::
Область видимости пространства имен namespace::name
Доступ к члену object.member
->
Доступ к члену по указателю pointer->member
[]
Взятие индекса variable[expr]
()
Вызов функции name(expr_list)
()
Построение значения type(expr_list)
++ постфиксный инкремент lvalue++
-- постфиксный декремент lvalue-- typeid идентификатор типа typeid(type) typeid идентификатор типа выражения typeid(expr) const_cast преобразование типа const_cast(expr) dynamic_cast преобразование типа dynamic_cast(expr) reinterpret_cast приведение типа reinterpret_cast
(expr) static_cast приведение типа static_cast(expr) sizeof размер объекта sizeof expr sizeof размер типа sizeof( type)
++ префиксный инкремент ++lvalue
-- префиксный декремент --lvalue
(a) int i = doSomething(), 0;
С++ для начинающих
32>32>16>32>8>32>64>32>32>32>27>