Сделай Сам Свою Работу на 5

Операции инкремента и декремента





Операции инкремента (++) и декремента (--) дают возможность компактной и удобной записи для изменения значения переменной на единицу. Чаще всего они используются при работе с массивами и коллекциями – для изменения величины индекса, указателя или итератора:

#include <vector> #include <cassert>   int main() { int ia[10] = {0,1,2,3,4,5,6,7,8,9}; vector<int> ivec( 10 );   int ix_vec = 0, ix_ia = 9; while ( ix_vec < 10 ) ivec[ ix_vec++ ] = ia[ ix_ia-- ];   int *pia = &ia[9]; vector<int>::iterator iter = ivec.begin();   while ( iter != ivec.end() ) assert( *iter++ == *pia-- );

}

Выражение

ix_vec++

является постфиксной формой оператора инкремента. Значение переменной ix_vec увеличивается после того, как ее текущее значение употреблено в качестве индекса. Например, на первой итерации цикла значение ix_vec равно 0. Именно это значение применяется как индекс массива ivec, после чего ix_vec увеличивается и становится равным 1, однако новое значение используется только на следующей итерации. Постфиксная форма операции декремента работает точно так же: текущее значение ix_ia берется в качестве индекса для ia, затем ix_ia уменьшается на 1.

Существует и префиксная форма этих операторов. При использовании такой формы текущее значение сначала уменьшается или увеличивается, а затем используется новое значение. Если мы пишем:



// неверно: ошибки с границами индексов в // обоих случаях int ix_vec = 0, ix_ia = 9; while ( ix_vec < 10 )

ivec[ ++ix_vec ] = ia[ --ix_ia ];

значение ix_vec увеличивается на единицу и становится равным 1 до первого использования в качестве индекса. Аналогично ix_ia получает значение 8 при первом использовании. Для того чтобы наша программа работала правильно, мы должны скорректировать начальные значения переменных ix_ivec и ix_ia:

// правильно int ix_vec = -1, ix_ia = 8; while ( ix_vec < 10 )

ivec[ ++ix_vec ] = ia[ --ix_ia ];

В качестве последнего примера рассмотрим понятие стека. Это фундаментальная абстракция компьютерного мира, позволяющая помещать и извлекать элементы в последовательности LIFO (last in, fist out – последним вошел, первым вышел). Стек реализует две основные операции – поместить (push) и извлечь (pop).

Текущий свободный элемент называют вершиной стека. Операция push присваивает этому элементу новое значение , после чего вершина смещается вверх (становится на 1 больше). Пусть наш стек использует для хранения элементов вектор. Какую из форм операции увеличения следует применить? Сначала мы используем текущее значение, потом увеличиваем его. Это постфиксная форма:



stack[ top++ ] = value;

Что делает операция pop? Уменьшает значение вершины (текущая вершина показывает на пустой элемент), затем извлекает значение. Это префиксная форма операции уменьшения:

int value = stack[ --top ];

(Реализация класса stack приведена в конце этой главы. Стандартный класс stack рассматривается в разделе 6.16.)

Упражнение 4.8

Как вы думаете, почему язык программирования получил название С++, а не ++С?

Операции с комплексными числами

Класс комплексных чисел стандартной библиотеки С++ представляет собой хороший пример использования объектной модели. Благодаря перегруженным арифметическим операциям объекты этого класса используются так, как будто они принадлежат одному из встроенных типов данных. Более того, в подобных операциях могут одновременно принимать участие и переменные встроенного арифметического типа, и комплексные числа. (Отметим, что здесь мы не рассматриваем общие вопросы математики комплексных чисел. См. [PERSON68] или любую книгу по математике.) Например, можно написать:

#inc1ude <complex>   comp1ex< double > a; comp1ex< double > b;   // ...  

complex< double > с = a * b + a / b;

Комплексные и арифметические типы разрешается смешивать в одном выражении:

complex< double > complex_obj = a + 3.14159;

Аналогично комплексные числа инициализируются арифметическим типом, и им может быть присвоено такое значение:

double dval = 3.14159;

complex_obj = dval;



Или

int ival = 3;

complex_obj = ival;

Однако обратное неверно. Например, следующее выражение вызовет ошибку компиляции:

// ошибка: нет неявного преобразования // в арифметический тип  

double dval = complex_obj;

Нужно явно указать, какую часть комплексного числа – вещественную или мнимую – мы хотим присвоить обычному числу. Класс комплексных чисел имеет две функции, возвращающих соответственно вещественную и мнимую части. Мы можем обращаться к ним, используя синтаксис доступа к членам класса:

double re = complex_obj.real();

double im = complex_obj.imag();

или эквивалентный синтаксис вызова функции:

double re = real(complex_obj);

double im = imag(complex_obj);

Класс комплексных чисел поддерживает четыре составных оператора присваивания: +=, -=, *= и /=. Таким образом,

complex_obj += second_complex_obj;

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

complex< double > complex0( 3.14159, -2.171 ); comp1ex< double > complex1( complexO.real() );

 

cout << complexO << " " << complex1 << endl;

выглядит так:

 

( 3.14159, -2.171 ) ( 3.14159, 0.0 )

 

Оператор ввода понимает любой из следующих форматов:

// допустимые форматы для ввода комплексного числа // 3.14159 ==> comp1ex( 3.14159 ); // ( 3.14159 ) ==> comp1ex( 3.14159 ); // ( 3.14, -1.0 ) ==> comp1ex( 3.14, -1.0 );   // может быть считано как // cin >> a >> b >> с // где a, b, с - комплексные числа  

3.14159 ( 3.14159 ) ( 3.14, -1.0 )

Кроме этих операций, класс комплексных чисел имеет следующие функции-члены: sqrt(), abs(), polar(), sin(), cos(), tan(), exp(), log(), log10() и pow().

Упражнение 4.9

Реализация стандартной библиотеки С++, доступная нам в момент написания книги, не поддерживает составных операций присваивания, если правый операнд не является комплексным числом. Например, подобная запись недопустима:

complex_obj += 1;

(Хотя согласно стандарту С++ такое выражение должно быть корректно, производители часто не успевают за стандартом.) Мы можем определить свой собственный оператор для реализации такой операции. Вот вариант функции, реализующий оператор сложения для complex<double>:

#include <complex> inline complex<double>& operator+=( complex<double> &cval, double dval ) { return cval += complex<double>( dval );

}

(Это пример перегрузки оператора для определенного типа данных, детально рассмотренной в главе 15.)

Используя этот пример, реализуйте три других составных оператора присваивания для типа complex<double>. Добавьте свою реализацию к программе, приведенной ниже, и запустите ее для проверки.

#include <iostream> #include <complex>   // определения операций...   int main() { complex< double > cval ( 4.0, 1.0 );   cout << cval << endl; cval += 1; cout << cval << endl; cval -= 1; cout << cval << endl; cval *= 2; cout << cval << endl; cout /= 2; cout << cval << endl;

}

Упражнение 4.10

Стандарт С++ не специфицирует реализацию операций инкремента и декремента для комплексного числа. Однако их семантика вполне понятна: если уж мы можем написать:

cval += 1;

что означает увеличение на 1 вещественной части cval, то и операция инкремента выглядела бы вполне законно. Реализуйте эти операции для типа complex<double> и выполните следующую программу:

#include <iostream> #include <complex>   // определения операций...   int main() { complex< double > cval( 4.0, 1.0 );   cout << cval << endl; ++cva1; cout << cval << endl;

}

Условное выражение

Условное выражение, или оператор выбора, предоставляет возможность более компактной записи текстов, включающих инструкцию if-else. Например, вместо:

bool is_equal; if (!strcmp(str1,str2)) is_equal = true;

else is_equal = false;

можно употребить более компактную запись:

bool is_equa1 = !strcmp( strl, str2 ) ? true : false;

Условный оператор имеет следующий синтаксис:

expr11 ? expr2 : expr3;

Вычисляется выражение expr1. Если его значением является true, оценивается expr2, если false, то expr3. Данный фрагмент кода:

int min( int ia, int ib )

{ return ( ia < ib ) ? ia : ib; }

эквивалентен

int min(int ia, int ib) { if (ia < ib) return ia; else return ib;

}

Приведенная ниже программа иллюстрирует использование условного оператора:

#include <iostream>   int main() { int i = 10, j = 20, k = 30; cout << "Большим из " << i << " и " << j << " является " << ( i > j ? i : j ) << end1;   cout << "Значение " << i << ( i % 2 ? " нечетно." : " четно." ) << endl;   /* условный оператор может быть вложенным, * но глубокая вложенность трудна для восприятия. * В данном примере max получает значение * максимальной из трех величин */ int max = ( (i > j) ? (( i > k) ? i : k) : ( j > k ) ? j : k);   cout << "Большим из " << i << ", " << j << " и " << k << " является " << max << endl;

}

Результатом работы программы будет:

 

Большим из 10 и 20 является 20

Значение 10 четно.

 

Оператор sizeof

Оператор sizeof возвращает размер в байтах объекта или типа данных. Синтаксис его таков:

sizeof ( type name ); sizeof ( object );

sizeof object;

Результат имеет специальный тип size_t, который определен как typedef в заголовочном файле cstddef. Вот пример использования обеих форм оператора sizeof:

#include <cstddef>   int ia[] = { 0, 1, 2 };   // sizeof возвращает размер всего массива size_t array_size = sizeof ia;   // sizeof возвращает размер типа int

size_t element_size = array_size / sizeof( int );

Применение sizeof к массиву дает количество байтов, занимаемых массивом, а не количество его элементов и не размер в байтах каждого из них. Так, например, в системах, где int хранится в 4 байтах, значением array_size будет 12. Применение sizeof к указателю дает размер самого указателя, а не объекта, на который он указывает:

int *pi = new int[ 3 ];

size_t pointer_size = sizeof ( pi );

Здесь значением pointer_size будет память под указатель в байтах (4 в 32-битных системах), а не массива ia.

Вот пример программы, использующей оператор sizeof:

#include <string> #include <iostream> #include <cstddef>   int main() { size_t ia;   ia = sizeof( ia ); // правильно ia = sizeof ia; // правильно   // ia = sizeof int; // ошибка ia = sizeof( int ); // правильно   int *pi = new int[ 12 ]; cout << "pi: " << sizeof( pi ) << " *pi: " << sizeof( pi ) << endl;   // sizeof строки не зависит от // ее реальной длины string stl( "foobar" ); string st2( "a mighty oak" );   string *ps = &stl;   cout << " st1: " << sizeof( st1 ) << " st2: " << sizeof( st2 ) << " ps: sizeof( ps ) << " *ps: " << sizeof( *ps ) << endl;   cout << "short :\t" << sizeof(short) << endl; cout << "shorf" :\t" << sizeof(short*) << endl; cout << "short& :\t" << sizeof(short&) << endl; cout << "short[3] :\t" << sizeof(short[3]) << endl;

}

Результатом работы программы будет:

 

pi: 4 *pi: 4

st1: 12 st2: 12 ps: 4 *ps:12

short : 2

short* : 4

short& : 2

short[3] : 6

 

Из данного примера видно, что применение sizeof к указателю позволяет узнать размер памяти, необходимой для хранения адреса. Если же аргументом sizeof является ссылка, мы получим размер связанного с ней объекта.

Гарантируется, что в любой реализации С++ размер типа char равен 1.

// char_size == 1

size_t char_size = sizeof( char );

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

// правильно: константное выражение

int array[ sizeof( some_type_T )];

Операторы new и delete

Каждая программа во время работы получает определенное количество памяти, которую можно использовать. Такое выделение памяти под объекты во время выполнения называется динамическим, а сама память выделяется из хипа (heap). (Мы уже касались вопроса о динамическом выделении памяти в главе 1.) Напомним, что выделение памяти объекту производится с помощью оператора new, возвращающего указатель на вновь созданный объект того типа, который был ему задан. Например:

int *pi = new int;

размещает объект типа int в памяти и инициализирует указатель pi адресом этого объекта. Сам объект в таком случае не инициализируется, но это легко изменить:

int *pi = new int( 1024 );

Можно динамически выделить память под массив:

int *pia = new int[ 10 ];

Такая инструкция размещает в памяти массив встроенного типа из десяти элементов типа int. Для подобного массива нельзя задать список начальных значений его элементов при динамическом размещении. (Однако если размещается массив объектов типа класса, то для каждого из элементов вызывается конструктор по умолчанию.) Например:

string *ps = new string;

размещает в памяти один объект типа string, инициализирует ps его адресом и вызывает конструктор по умолчанию для вновь созданного объекта типа string. Аналогично

string *psa = new string[10];

размещает в памяти массив из десяти элементов типа string, инициализирует psa его адресом и вызывает конструктор по умолчанию для каждого элемента массива.

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

После использования объекта, созданного таким образом, мы должны явно освободить память, применив оператор delete к указателю на этот объект. (Попытка применить оператор delete к указателю, не содержащему адрес объекта, полученного описанным способом, вызовет ошибку времени выполнения.) Например:

delete pi;

освобождает память, на которую указывает объект типа int, на который указывает pi. Аналогично

delete ps;

освобождает память, на которую указывает объект класса string, адрес которого содержится в ps. Перед уничтожением этого объекта вызывается деструктор. Выражение

delete [] pia;

освобождает память, отведенную под массив pia. При выполнении такой операции необходимо придерживаться указанного синтаксиса.

(Об операциях new и delete мы еще поговорим в главе 8.)

Упражнение 4.11

Какие из следующих выражений ошибочны?

(a) vector<string> svec( 10 );

(b) vector<string> *pvecl = new vector<string>(10);

(c) vector<string> **pvec2 = new vector<string>[10];

(d) vector<string> *pvl = &svec;

(e) vector<string> *pv2 = pvecl;

(f) delete svec;

(g) delete pvecl;

(h) delete [] pvec2;

(i) delete pvl;

(j) delete pv2;

4.10. Оператор “запятая”

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

int main() { // примеры оператора "запятая" // переменные ia, sz и index определены в другом месте ... int ival = (ia != 0) ? ix=get_va1ue(), ia[index]=ix : ia=new int[sz], ia[index]=0; // ...

}

Побитовые операторы

Таблица 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. При работе с битовыми векторами также можно применять подход, заимствованный из С, – использовать для представления такого вектора объект встроенного целого типа, обычно 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 в остальных. Для этого используем операцию сдвига:

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, его использование описано ниже.

Упражнение 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, а затем механизм шаблонов.

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

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 следующим набором значений:

 

 

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

bitset< 32 > bitvec3( 012 );

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

 

 

В качестве аргумента конструктору может быть передано и строковое значение, состоящее из нулей и единиц. Например, следующее определение инициализирует 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> для ее представления? Как присвоить значения для представления этой последовательности пустому битовому вектору? Напишите вариант инициализации и вариант с присваиванием значения каждому биту.

Приоритеты

Приоритеты операций задают последовательность вычислений в сложном выражении. Например, какое значение получит 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<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  
~ побитовое НЕ ~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  

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

Представим себе следующий оператор присваивания:

int ival = 0;   // обычно компилируется с предупреждением

ival = 3.541 + 3;

В результате ival получит значение 6. Вот что происходит: мы складываем литералы разных типов – 3.541 типа double и 3 типа int. C++ не может непосредственно сложить подобные операнды, сначала ему нужно привести их к одному типу. Для этого существуют правила преобразования арифметических типов. Общий принцип таков:

 








Не нашли, что искали? Воспользуйтесь поиском по сайту:



©2015 - 2024 stydopedia.ru Все материалы защищены законодательством РФ.