Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Язык Программирования C++. Вводный курс [rus]

.pdf
Скачиваний:
1102
Добавлен:
16.08.2013
Размер:
5.67 Mб
Скачать

С++ для начинающих

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);

При использовании побитовых операций подобным образом очень легко допустить ошибку. Поэтому чаще всего такие операции инкапсулируются в макросы препроцессора

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 )

Вот пример использования:

has_passed_quiz[ index ] = bit_on( quiz1, index );

Раз уж мы начали инкапсулировать действия с битовым вектором в функции, следующим шагом нужно создать класс. Стандартная библиотека С++ включает такой класс bitset, его использование описано ниже.

С++ для начинающих

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 может быть объявлен тремя способами. В определении по умолчанию мы просто указываем размер битового вектора:

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, если

if ( bitvec.test( 0 ))

соответствующий бит равен 1, и false в противном случае. Например:

// присваивание bitvec[0]=1 сработало!;

С++ для начинающих

165

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 )

этом случае они должны быть вызваны без параметра. Например:

// что-то опять не сработало

bitvec.f1ip( 0 ); // меняет значение первого бита bitvec[0].flip(); // тоже меняет значение первого бита

Функция flip() меняет значение отдельного бита или всего битового вектора:

bitvec.flip();

// меняет значения всех битов

Существуют еще два способа определить объект типа bitset. Оба они дают возможность проинициализировать объект определенным набором нулей и единиц. Первый способ явно задать целое беззнаковое число как аргумент конструктору. Начальные N позиций битового вектора получат значения соответствующих двоичных разрядов аргумента. Например:

bitset< 32 > bitvec2( Oxffff );

инициализирует bitvec2 следующим набором значений:

00000000000000001111111111111111

В результате определения

С++ для начинающих

166

bitset< 32 > bitvec3( 012 );

у bitvec3 окажутся ненулевыми биты на местах 1 и 3:

00000000000000000000000000001010

В качестве аргумента конструктору может быть передано и строковое значение, состоящее из нулей и единиц. Например, следующее определение инициализирует

// эквивалентно bitvec3 string bitva1( "1010" );

bitvec4 тем же набором значений, что и bitvec3: 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.

С++ для начинающих

167

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

С++ для начинающих

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

(a)! ptr == ptr->next

(b)~ uc ^ 0377 & ui << 4

Каков порядок вычисления следующих выражений? При ответе используйте таблицу 4.4.

(c) ch = buf[ bp++ ] != '\n'

Упражнение 4.19

С++ для начинающих

169

Все три выражения из предыдущего упражнения вычисляются не в той последовательности, какую, по-видимому, хотел задать программист. Расставьте скобки так, чтобы реализовать его первоначальный замысел.

Упражнение 4.20 Следующие выражения вызывают ошибку компиляции из-за неправильно понятого

(a) int i = doSomething(), 0;

приоритета операций. Объясните, как их исправить, используя таблицу 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<type>(expr)

 

dynamic_cast

преобразование типа

dynamic_cast<type>(expr)

 

reinterpret_cast

приведение типа

 

reinterpret_cast<type>

 

 

 

 

 

(expr)

 

static_cast

приведение типа

 

static_cast<type>(expr)

 

sizeof

размер объекта

 

sizeof expr

 

sizeof

размер типа

 

 

sizeof( type)

 

++

префиксный инкремент

++lvalue

 

--

префиксный декремент

--lvalue

 

С++ для начинающих

 

 

 

 

 

170

 

 

 

 

 

 

 

~

побитовое НЕ

 

 

~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

 

 

 

ИСКЛЮЧАЮЩЕЕ

 

 

 

 

С++ для начинающих

 

171

 

 

 

 

 

ИЛИ

 

 

 

 

 

 

|

побитовое ИЛИ

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;

необходимости мы должны позаботиться об этом сами. Например:

Соседние файлы в предмете Программирование на C++