<< Пред. стр. 15 (из 121) След. >>
Упражнение 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-й бит:
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 каждый четный бит:
for ( int index=0; index<32; ++index )
if ( index % 2 == 0 )
bitvec[ index ] = 1;
Аналогично существует два способа проверки значений каждого бита – с помощью функции test() и с помощью индекса. Функция () возвращает true, если соответствующий бит равен 1, и false в противном случае. Например:
if ( bitvec.test( 0 ))
// присваивание bitvec[0]=1 сработало!;
Значения битов с помощью индекса проверяются таким образом:
cout << "bitvec: включенные биты:\n\t";
for ( int index = 0; index < 32; ++-index )
if ( bitvec[ index ] )
cout << index << " ";
cout << endl;
Следующая пара операторов демонстрирует сброс первого бита двумя способами:
bitvec.reset(0);
bitvec[0] = 0;
Функции set() и reset() могут применяться ко всему битовому вектору в целом. В этом случае они должны быть вызваны без параметра. Например:
// сброс всех битов
bitvec.reset();
if (bitvec.none() != true)
// что-то не сработало
// установить в 1 все биты вектора bitvec
if ( bitvec.any() != true )
// что-то опять не сработало
Функция flip() меняет значение отдельного бита или всего битового вектора:
bitvec.f1ip( 0 ); // меняет значение первого бита
bitvec[0].flip(); // тоже меняет значение первого бита
bitvec.flip(); // меняет значения всех битов
Существуют еще два способа определить объект типа bitset. Оба они дают возможность проинициализировать объект определенным набором нулей и единиц. Первый способ – явно задать целое беззнаковое число как аргумент конструктору. Начальные N позиций битового вектора получат значения соответствующих двоичных разрядов аргумента. Например:
bitset< 32 > bitvec2( Oxffff );
инициализирует bitvec2 следующим набором значений:
00000000000000001111111111111111
В результате определения
bitset< 32 > bitvec3( 012 );
у bitvec3 окажутся ненулевыми биты на местах 1 и 3:
00000000000000000000000000001010
В качестве аргумента конструктору может быть передано и строковое значение, состоящее из нулей и единиц. Например, следующее определение инициализирует bitvec4 тем же набором значений, что и bitvec3:
// эквивалентно bitvec3
string bitva1( "1010" );
bitset< 32 > bitvec4( bitval );
Можно также указать диапазон символов строки, выступающих как начальные значения для битового вектора. Например:
// подстрока с шестой позиции длиной 4: 1010
string bitval ( "1111110101100011010101" );
bitset< 32 > bitvec5( bitval, 6, 4 );
Мы получаем то же значение, что и для bitvec3 и bitvec4. Если опустить третий параметр, подстрока берется до конца исходной строки:
// подстрока с шестой позиции до конца строки: 1010101
string bitva1( "1111110101100011010101" );
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.
bitset<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
Допущены ли ошибки в следующих операциях с битовыми векторами?
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 )) ...
(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
Следующая конструкция ведет себя не так, как можно было бы ожидать. Приоритет операции присваивания меньше, чем операции сравнения:
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.
(a) ! ptr == ptr->next
(b) ~ uc ^ 0377 & ui << 4
(c) ch = buf[ bp++ ] != '\n'
Упражнение 4.19
Все три выражения из предыдущего упражнения вычисляются не в той последовательности, какую, по-видимому, хотел задать программист. Расставьте скобки так, чтобы реализовать его первоначальный замысел.
Упражнение 4.20
Следующие выражения вызывают ошибку компиляции из-за неправильно понятого приоритета операций. Объясните, как их исправить, используя таблицу 4.4.
(a) int i = doSomething(), 0;
(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
dynamic_cast преобразование типа dynamic_cast
reinterpret_cast приведение типа reinterpret_cast
static_cast приведение типа static_cast
sizeof размер объекта sizeof expr
sizeof размер типа sizeof( type)
++ префиксный инкремент ++lvalue
-- префиксный декремент --lvalue
~ побитовое НЕ ~expr
! логическое НЕ !expr
- унарный минус -expr
+ унарный плюс +expr
* разыменование *expr
& адрес &expr
() приведение типа (type)expr
new выделение памяти new type
new выделение памяти и инициализация new type(exprlist)
new выделение памяти new (exprlist) type(exprlist)
new выделение памяти под массив все формы
delete освобождение памяти все формы
delete освобождение памяти из-под массива все формы
->* доступ к члену классу по указателю pointer-> *pointer_to_member
.* доступ к члену класса по указателю object.*pointer_to_member
* умножение expr * expr
/ деление expr / expr
% деление по модулю expr % expr
+ сложение expr + expr
- вычитание expr - expr
<< сдвиг влево expr << expr
>> сдвиг вправо expr >> expr
< меньше expr < expr
<= меньше или равно expr <= expr
> больше expr > expr
>= больше или равно expr >= expr
== равно expr == expr
!= не равно expr != expr
& побитовое И expr & expr
^ побитовое ИСКЛЮЧАЮЩЕЕ ИЛИ expr ^ expr
| побитовое ИЛИ expr | expr
&& логическое И expr && expr
|| логическое ИЛИ expr || expr
?: условный оператор expr ? expr * expr
= присваивание l-значение = expr
=, *=, /=, %=, +=, -=, <<=, >>=, &=, |=, ^= составное присваивание l-значение += expr и т.д.
throw возбуждение исключения throw expr
, запятая expr, expr
4.14. Преобразования типов
Представим себе следующий оператор присваивания:
int ival = 0;
// обычно компилируется с предупреждением
ival = 3.541 + 3;
В результате ival получит значение 6. Вот что происходит: мы складываем литералы разных типов – 3.541 типа double и 3 типа int. C++ не может непосредственно сложить подобные операнды, сначала ему нужно привести их к одному типу. Для этого существуют правила преобразования арифметических типов. Общий принцип таков: перейти от операнда меньшего типа к большему, чтобы не потерять точность вычислений.
В нашем случае целое значение 3 трансформируется в тип double, и только после этого производится сложение. Такое преобразование выполняется независимо от желания программиста, поэтому оно получило название неявного преобразования типов.
Результат сложения двух чисел типа double тоже имеет тип double. Значение равно 6.541. Теперь его нужно присвоить переменной ival. Типы переменной и результата 6.541 не совпадают, следовательно, тип этого значения приводится к типу переменной слева от знака равенства. В нашем случае это int. Преобразование double в int производится автоматически, отбрасыванием дробной части (а не округлением). Таким образом, 6.541 превращается в 6, и этот результат присваивается переменной ival. Поскольку при таком преобразовании может быть потеряна точность, большинство компиляторов выдают предупреждение.
Так как компилятор не округляет числа при преобразовании double в int, при необходимости мы должны позаботиться об этом сами. Например:
double dva1 = 8.6;
int iva1 = 5;
ival += dva1 + 0.5; // преобразование с округлением
При желании мы можем произвести явное преобразование типов:
// инструкция компилятору привести double к int
ival = static_cast< int >( 3.541 ) + 3;
В этом примере мы явно даем указание компилятору привести величину 3.541 к типу int, а не следовать правилам по умолчанию.
В этом разделе мы детально обсудим вопросы и неявного (как в первом примере), и явного преобразования типов (как во втором).
4.14.1. Неявное преобразование типов
Язык определяет набор стандартных преобразований между объектами встроенного типа, неявно выполняющихся компилятором в следующих случаях:
арифметическое выражение с операндами разных типов: все операнды приводятся к наибольшему типу из встретившихся. Это называется арифметическим преобразованием. Например:
int ival = 3;
double dva1 = 3.14159;
// ival преобразуется в double: 3.0
ival + dva1;
присваивание значения выражения одного типа объекту другого типа. В этом случае результирующим является тип объекта, которому значение присваивается. Так, в первом примере литерал 0 типа int присваивается указателю типа int*, значением которого будет 0. Во втором примере double преобразуется в int.
// 0 преобразуется в нулевой указатель типа int*
int *pi = 0;
// dva1 преобразуется в int: 3
ivat = dva1;
передача функции аргумента, тип которого отличается от типа соответствующего формального параметра. Тип фактического аргумента приводится к типу параметра:
extern double sqrt( double );
// 2 преобразуется в double: 2.0
cout << "Квадратный корень из 2: " << sqrt( 2 ) << endt;
возврат из функции значения, тип которого не совпадает с типом возвращаемого результата, заданным в объявлении функции. Тип фактически возвращаемого значения приводится к объявленному. Например:
double difference( int ivati, int iva12 )
{
// результат преобразуется в double
return ivati - iva12;
}
4.14.2. Арифметические преобразования типов
Арифметические преобразования приводят оба операнда бинарного арифметического выражения к одному типу, который и будет типом результата выражения. Два общих правила таковы:
типы всегда приводятся к тому из типов, который способен обеспечить наибольший диапазон значений при наибольшей точности. Это помогает уменьшить потери точности при преобразовании;
любое арифметическое выражение, включающее в себя целые операнды типов, меньших чем int, перед вычислением всегда преобразует их в int.
Мы рассмотрим иерархию правил преобразований, начиная с наибольшего типа long double.
Если один из операндов имеет тип long double, второй приводится к этому же типу в любом случае. Например, в следующем выражении символьная константа 'a' трансформируется в long double (значение 97 для представления ASCII) и затем прибавляется к литералу того же типа: 3.14159L + 'a'.
Если в выражении нет операндов long double, но есть операнд double, все преобразуется к этому типу. Например:
int iva1;
float fval;
double dval;
// fva1 и iva1 преобразуются к double перед сложением
dval + fva1 + ival;
В том случае, если нет операндов типа double и long double, но есть операнд float, тип остальных операндов меняется на float:
char cvat;
int iva1;
float fva1;
// iva1 и cval преобразуются к float перед сложением
cvat + fva1 + iva1;
Если у нас нет вещественных операндов , значит, все они представляют собой целые типы. Прежде чем определить тип результата, производится преобразование, называемое приведением к целому: все операнды с типом меньше, чем int, заменяются на int.
При приведении к целому типы char, signed char, unsigned char и short int преобразуются в int. Тип unsigned short int трансформируется в int, если этот тип достаточен для представления всего диапазона значений unsigned short int (обычно это происходит в системах, отводящих полслова под short и целое слово под int), в противном случае unsigned short int заменяется на unsigned int.
Тип wchar_t и перечисления приводятся к наименьшему целому типу, способному представить все их значения. Например, в перечислении
enum status { bad, ok };
значения элементов равны 0 и 1. Оба эти значения могут быть представлены типом char, значит char и станет типом внутреннего представления данного перечисления. Приведение к целому преобразует char в int.
В следующем выражении
char cval;
bool found;
enum mumble { ml, m2, m3 } mval;
unsigned long ulong;
cval + ulong; ulong + found; mval + ulong;
перед определением типа результата cval, found и mval преобразуются в int.
После приведения к целому сравниваются получившиеся типы операндов. Если один из них имеет тип unsigned long, то остальные будут того же типа. В нашем примере все три объекта, прибавляемые к ulong, приводятся к типу unsigned long.
Если в выражении нет объектов unsigned long, но есть объекты типа long, тип остальных операндов меняется на long. Например:
char cval;
long lval;
// cval и 1024 преобразуются в long перед сложением
cval + 1024 + lval;
Из этого правила есть одно исключение: преобразование unsigned int в long происходит только в том случае, если тип long способен вместить весь диапазон значений unsigned int. (Обычно это не так в 32-битных системах, где и long, и int представляются одним машинным словом.) Если же тип long не способен представить весь диапазон unsigned int, оба операнда приводятся к unsigned long.
В случае отсутствия операндов типов unsigned long и long, используется тип unsigned int. Если же нет операндов и этого типа, то к int.