4. ВыраженияВ главе 3 мы рассмотрели типы данных – как встроенные, так и предоставленные стандартной библиотекой. Здесь мы разберем предопределенные операции, такие, как сложение, вычитание, сравнение и т.п., рассмотрим их приоритеты. Скажем, результатом выражения 3+4*5 является 23, а не 35 потому, что операция умножения (*) имеет более высокий приоритет, чем операция сложения (+). Кроме того, мы обсудим вопросы преобразований типов данных – и явных, и неявных. Например, в выражении 3+0.7 целое значение 3 станет вещественным перед выполнением операции сложения. 4.1. Что такое выражение?Выражение состоит из одного или более операндов, в простейшем случае – из одного литерала или объекта. Результатом такого выражения является r-значение его операнда. Например: void mumble() { 3.14159; "melancholia"; upperBound; } Результатом вычисления выражения 3.14159 станет 3.14159 типа double, выражения
"melancholia" – адрес первого элемента строки типа const char*. Значение
выражения upperBound – это значение объекта upperBound, а его типом будет тип
самого объекта. salary + raise ivec[ size/2 ] * delta first_name + " " + 1ast_name Операции обозначаются соответствующими знаками. В первом примере сложение применяется
к salary и raise. Во втором выражении size делится на 2. Частное используется
как индекс для массива ivec. Получившийся в результате операции взятия индекса
элемент массива умножается на delta. В третьем примере два строковых объекта
конкатенируются между собой и со строковым литералом, создавая новый строковый
объект. ptr != 0 && *ptr != 0 Выражение состоит из трех подвыражений: проверку указателя ptr, разыменования ptr и проверку результата разыменования. Если ptr определен как int ival = 1024; int *ptr = &ival; то результатом разыменования будет 1024 и оба сравнения дадут истину. Результатом
всего выражения также будет истина (оператор && обозначает логическое
И). 4.2. Арифметические операцииТаблица 4.1. Арифметические операции
int ivall = 21 / 6; int iva12 = 21 / 7; И ival1, и ival2 в итоге получат значение 3. 3.14 % 3; // ошибка: операнд типа double 21 % 6; // правильно: 3 21 % 7; // правильно: 0 21 % -5; // машинно-зависимо: -1 или 1 int iva1 = 1024; double dval = 3.14159; iva1 % 12; // правильно: iva1 % dval; // ошибка: операнд типа double Иногда результат вычисления арифметического выражения может быть неправильным либо не определенным. В этих случаях говорят об арифметических исключениях (хотя они не вызывают возбуждения исключения в программе). Арифметические исключения могут иметь чисто математическую природу (скажем, деление на 0) или происходить от представления чисел в компьютере – как переполнение (когда значение превышает величину, которая может быть выражена объектом данного типа). Например, тип char содержит 8 бит и способен хранить значения от 0 до 255 либо от -128 до 127 в зависимости от того, знаковый он или беззнаковый. В следующем примере попытка присвоить объекту типа char значение 256 вызывает переполнение: #include <iostream> int main() { Для представления числа 256 необходимы 9 бит. Переменная byte_value получает
некоторое неопределенное (машинно-зависимое) значение. Допустим, на нашей рабочей
станции SGI мы получили 0. Первая попытка напечатать это значение с помощью: byte_value: После некоторого замешательства мы поняли, что значение 0 – это нулевой символ
ASCII, который не имеет представления при печати. Чтобы напечатать не представление
символа, а его значение, нам пришлось использовать весьма странно выглядящее
выражение: byte_value: 0 На самом деле нужно было изменить не значение, соответствующее byte_value,
а поведение операции вывода, которая действует по-разному для разных типов.
Объекты типа char представляются ASCII-символами (а не кодами), в то время как
для объектов типа int мы увидим содержащиеся в них значения. (Преобразования
типов рассмотрены в разделе 4.14.) Упражнение 4.1В чем разница между приведенными выражениями с операцией деления? Упражнение 4.2Напишите выражение, определяющее, четным или нечетным является данное целое число. Упражнение 4.3Найдите заголовочные файлы limits, climits и cfloat и посмотрите, что они содержат. 4.3. Операции сравнения и логические операции
Примечание. Все операции в результате дают значение типа bool Операции сравнения и логические операции в результате дают значение типа bool, то есть true или false. Если же такое выражение встречается в контексте, требующем целого значения, true преобразуется в 1, а false – в 0. Вот фрагмент кода, подсчитывающего количество элементов вектора, меньших некоторого заданного значения: vector<int>::iterator iter = ivec.beg-in() ; while ( iter != ivec.end() ) { // эквивалентно: e1em_cnt = e1em_cnt + (*iter < some_va1ue) // значение true/false выражения *iter < some_va1ue // превращается в 1 или 0 e1em_cnt += *iter < some_va1ue; ++iter; } Мы просто прибавляем результат операции “меньше” к счетчику. (Пара += обозначает
составной оператор присваивания, который складывает операнд, стоящий слева,
и операнд, стоящий справа. То же самое можно записать более компактно: elem_count
= elem_count + n. Мы рассмотрим такие операторы в разделе 4.4.) expr1 && expr2 expr1 || expr2 Если в первом из них expr1 равно false, значение всего выражения тоже будет
равным false вне зависимости от значения expr2, которое даже не будет вычисляться.
Во втором выражении expr2 не оценивается, если expr1 равно true, поскольку значение
всего выражения равно true вне зависимости от expr2. while ( ptr != О && ptr->va1ue < upperBound && ptr->va1ue >= 0 && notFound( ia[ ptr->va1ue ] )) { ... } Указатель с нулевым значением не указывает ни на какой объект, поэтому применение
к нулевому указателю операции доступа к члену вызвало бы ошибку (ptr->value).
Однако, если ptr равен 0, проверка на первом шаге прекращает дальнейшее вычисление
подвыражений. Аналогично на втором и третьем шагах проверяется попадание величины
ptr->value в нужный диапазон, и операция взятия индекса не применяется к
массиву ia, если этот индекс неправилен. bool found = false; // пока элемент не найден // и ptr указывает на объект (не 0) while ( ! found && ptr ) { found = 1ookup( *ptr ); ++ptr; } Подвыражение // Внимание! Порядок вычислений не определен! if ( ia[ index++ ] < ia[ index ] ) // поменять местами элементы Программист предполагал, что левый операнд оценивается первым и сравниваться будут элементы ia[0] и ia[1]. Однако компилятор не гарантирует вычислений слева направо, и в таком случае элемент ia[0] может быть сравнен сам с собой. Гораздо лучше написать более понятный и машинно-независимый код: if ( ia[ index ] < ia[ index+1 ] ) // поменять местами элементы ++index; Еще один пример возможной ошибки. Мы хотели убедиться, что все три величины ival, jval и kval различаются. Где мы промахнулись? // Внимание! это не сравнение 3 переменных друг с другом if ( ival != jva1 != kva1 ) // do something ... Значения 0, 1 и 0 дают в результате вычисления такого выражения true. Почему?
Сначала проверяется ival != jval, а потом итог этой проверки (true/false – преобразованной
к 1/0) сравнивается с kval. Мы должны были явно написать: Упражнение 4.4Найдите неправильные или непереносимые выражения, поясните. Как их можно изменить?
(Заметим, что типы объектов не играют роли в данных примерах.) Упражнение 4.5Язык С++ не диктует порядок вычисления операций сравнения для того, чтобы позволить компилятору делать это оптимальным образом. Как вы думаете, стоило бы в данном случае пожертвовать эффективностью, чтобы избежать ошибок, связанных с предположением о вычислении выражения слева направо? 4.4. Операции присваиванияИнициализация задает начальное значение переменной. Например: int ival = 1024; int *pi = 0; В результате операции присваивания объект получает новое значение, при этом старое пропадает: ival = 2048; pi = &iva1; Иногда путают инициализацию и присваивание, так как они обозначаются одним
и тем же знаком =. Объект инициализируется только один раз – при его определении.
В то же время операция может быть применена к нему многократно. 1024 = ival; // ошибка Возможно, имелось в виду следующее: int value = 1024; value = ival; // правильно Однако недостаточно потребовать, чтобы операнд слева от знака присваивания был l-значением. Так, после определений const int array_size = 8; int ia[ array_size ] = { 0, 1, 2, 2, 3, 5, 8, 13 }; int *pia = ia; выражение array_size = 512; // ошибка ошибочно, хотя array_size и является l-значением: объявление array_size константой не дает возможности изменить его значение. Аналогично ia = pia; // ошибка Неверна и инструкция extern char next_char(); int main() { char ch = next_char(); while ( ch != '\n' ) { // сделать что-то ... ch = next_char(); } // ... } может быть переписан так: extern char next_char(); int main() { char ch; while (( ch = next_char() ) != '\n' ) { // сделать что-то ... } // ... } Заметим, что вокруг выражения присваивания необходимы скобки, поскольку приоритет
этой операции ниже, чем операции сравнения. Без скобок первым выполняется сравнение: int main () { int ival, jval; ival = jval = 0; // правильно: присваивание 0 обеим переменным // ... } Обеим переменным ival и jval присваивается значение 0. Следующий пример неправилен, потому что типы pval и ival различны, и неявное преобразование типов невозможно. Отметим, что 0 является допустимым значением для обеих переменных: int main () { int ival; int *pval; ival = pval = 0; // ошибка: разные типы // ... } Верен или нет приведенный ниже пример, мы сказать не можем, поскольку определение jval в нем отсутствует: int main() { // ... int ival = jval = 0; // верно или нет? // ... } Это правильно только в том случае, если переменная jval определена в программе ранее и имеет тип, приводимый к int. Обратите внимание: в этом случае мы присваиваем 0 значение jval и инициализируем ival. Для того чтобы инициализировать нулем обе переменные, мы должны написать: int main() { // правильно: определение и инициализация int ival = 0, jval = 0; // ... } В практике программирования часты случаи, когда к объекту применяется некоторая операция, а результат этой операции присваивается тому же объекту. Например: int arraySum( int ia[], int sz ) { int sum = 0; for ( int i = 0; i < sz; ++i ) sum = sum + ia[ i ]; return sum; } Для более компактной записи С и С++ предоставляют составные операции присваивания. С использованием такого оператора данный пример можно переписать следующим образом: int arraySum( int ia[], int sz ) { int sum = 0; for ( int i =0; i < sz; ++i ) // эквивалентно: sum = sum + ia[ i ]; sum += ia[ i ]; return sum; } Общий синтаксис составного оператора присваивания таков: += -= *= /= %= <<= >>= &= ^= |= Запись a op= b в точности эквивалентна записи a = a op b. Упражнение 4.6Найдите ошибку в данном примере. Исправьте запись. int main() { float fval; int ival; int *pi; fval = ival = pi = 0; Упражнение 4.7Следующие выражения синтаксически правильны, однако скорее всего работают не так, как предполагал программист. Почему? Как их изменить? (a) if ( ptr = retrieve_pointer() != 0 ) (b) if ( ival = 1024 ) (c) ival += ival + 1; 4.5. Операции инкремента и декрементаОперации инкремента (++) и декремента (--) дают возможность компактной и удобной записи для изменения значения переменной на единицу. Чаще всего они используются при работе с массивами и коллекциями – для изменения величины индекса, указателя или итератора: #include <vector> #include <cassert> int main() Выражение // неверно: ошибки с границами индексов в // обоих случаях 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 = 10; while ( ix_vec < 10 ) ivec[ ++ix_vec ] = ia[ --ix_ia ]; В качестве последнего примера рассмотрим понятие стека. Это фундаментальная
абстракция компьютерного мира, позволяющая помещать и извлекать элементы в последовательности
LIFO (last in, fist out – последним вошел, первым вышел). Стек реализует две
основные операции – поместить (push) и извлечь (pop). stack[ top++ ] = value; Что делает операция pop? Уменьшает значение вершины (текущая вершина показывает на пустой элемент), затем извлекает значение. Это префиксная форма операции уменьшения: int value = stack[ --top ]; (Реализация класса stack приведена в конце этой главы. Стандартный класс stack рассматривается в разделе 6.16.) Упражнение 4.8Как вы думаете, почему язык программирования получил название С++, а не ++С? 4.6. Операции с комплексными числамиКласс комплексных чисел стандартной библиотеки С++ представляет собой хороший пример использования объектной модели. Благодаря перегруженным арифметическим операциям объекты этого класса используются так, как будто они принадлежат одному из встроенных типов данных. Более того, в подобных операциях могут одновременно принимать участие и переменные встроенного арифметического типа, и комплексные числа. (Отметим, что здесь мы не рассматриваем общие вопросы математики комплексных чисел. См. [PERSON68] или любую книгу по математике.) Например, можно написать: #inc1ude <complex> comp1ex< double > a; Комплексные и арифметические типы разрешается смешивать в одном выражении: 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Реализация стандартной библиотеки С++, доступная нам в момент написания книги,
не поддерживает составных операций присваивания, если правый операнд не является
комплексным числом. Например, подобная запись недопустима: #include <complex> inline complex<double>& operator+=( complex<double> &cval, double dval ) { return cval += complex<double>( dval ); } (Это пример перегрузки оператора для определенного типа данных, детально рассмотренной
в главе 15.) #include <iostream> #include <complex> // определения операций... int main() { Упражнение 4.10Стандарт С++ не специфицирует реализацию операций инкремента и декремента для
комплексного числа. Однако их семантика вполне понятна: если уж мы можем написать: #include <iostream> #include <complex> // определения операций... int main() { 4.7. Условное выражениеУсловное выражение, или оператор выбора, предоставляет возможность более компактной записи текстов, включающих инструкцию 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() { Результатом работы программы будет: Большим из 10 и 20 является 20 Значение 10 четно. 4.8. Оператор sizeofОператор sizeof возвращает размер в байтах объекта или типа данных. Синтаксис его таков: sizeof ( type name ); sizeof ( object ); sizeof object; Результат имеет специальный тип size_t, который определен как typedef в заголовочном файле cstddef. Вот пример использования обеих форм оператора sizeof: #include <cstddef> int ia[] = { 0, 1, 2 }; // sizeof возвращает размер всего массива Применение sizeof к массиву дает количество байтов, занимаемых массивом, а не количество его элементов и не размер в байтах каждого из них. Так, например, в системах, где int хранится в 4 байтах, значением array_size будет 12. Применение sizeof к указателю дает размер самого указателя, а не объекта, на который он указывает: int *pi = new int[ 3 ]; size_t pointer_size = sizeof ( pi ); Здесь значением pointer_size будет память под указатель в байтах (4 в 32-битных
системах), а не массива ia. #include <string> #include <iostream> #include <cstddef> int main() { Результатом работы программы будет: pi: 4 *pi: 4 st1: 12 st2: 12 ps: 4 *ps:12 short : 2 short* : 4 short& : 2 short[3] : 6 Из данного примера видно, что применение sizeof к указателю позволяет узнать
размер памяти, необходимой для хранения адреса. Если же аргументом sizeof является
ссылка, мы получим размер связанного с ней объекта. // char_size == 1 size_t char_size = sizeof( char ); Значение оператора sizeof вычисляется во время компиляции и считается константой. Оно может быть использовано везде, где требуется константное значение, в том числе в качестве размера встроенного массива. Например: // правильно: константное выражение int array[ sizeof( some_type_T )]; 4.9. Операторы 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
его адресом и вызывает конструктор по умолчанию для каждого элемента массива. delete pi; освобождает память, на которую указывает объект типа int, на который указывает pi. Аналогично delete ps; освобождает память, на которую указывает объект класса string, адрес которого содержится в ps. Перед уничтожением этого объекта вызывается деструктор. Выражение delete [] pia; освобождает память, отведенную под массив pia. При выполнении такой операции
необходимо придерживаться указанного синтаксиса. Упражнение 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.11. Побитовые операторыТаблица 4.3. Побитовые операторы
Побитовые операции рассматривают операнды как упорядоченные наборы битов,
каждый бит может иметь одно из двух значений – 0 или 1. Такие операции позволяют
программисту манипулировать значениями отдельных битов. Объект, содержащий набор
битов, иногда называют битовым вектором. Он позволяет компактно хранить
набор флагов – переменных, принимающих значение “да” “нет”. Например,
компиляторы зачастую помещают в битовые векторы спецификаторы типов, такие,
как const и volatile. Библиотека iostream использует эти векторы для хранения
состояния формата вывода. 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 Раз уж мы начали инкапсулировать действия с битовым вектором в функции, следующим шагом нужно создать класс. Стандартная библиотека С++ включает такой класс bitset, его использование описано ниже. Упражнение 4.12Даны два целых числа: Упражнение 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
Как мы уже говорили, необходимость создавать сложные выражения для манипуляции битовыми векторами затрудняет использование встроенных типов данных. Класс bitset упрощает работу с битовым вектором. Вот какое выражение нам приходилось писать в предыдущем разделе для того, чтобы “взвести” 27-й бит: quiz1 |= 1<<27; При использовании bitset то же самое мы можем сделать двумя способами: quiz1[27] = 1; или quiz1.set(27); (В нашем примере мы не используем нулевой бит, чтобы сохранить “естественную”
нумерацию. На самом деле, нумерация битов начинается с 0.) #include <bitset> Объект типа bitset может быть объявлен тремя способами. В определении по умолчанию мы просто указываем размер битового вектора: bitset<32> bitvec; Это определение задает объект bitset, содержащий 32 бита с номерами от 0 до 31. Все биты инициализируются нулем. С помощью функции any() можно проверить, есть ли в векторе единичные биты. Эта функция возвращает true, если хотя бы один бит отличен от нуля. Например: bool is_set = bitvec.any(); Переменная is_set получит значение false, так как объект bitset по умолчанию инициализируется нулями. Парная функция none() возвращает true, если все биты равны нулю: sbool 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 В результате определения 00000000000000000000000000001010 В качестве аргумента конструктору может быть передано и строковое значение, состоящее из нулей и единиц. Например, следующее определение инициализирует bitvec4 тем же набором значений, что и bitvec3: // эквивалентно bitvec3 string bitva1( "1010" ); bitset< 32 > bitvec4( bitval ); Можно также указать диапазон символов строки, выступающих как начальные значения для битового вектора. Например: // подстрока с шестой позиции длиной 4: 1010 string bitval ( "1111110101100011010101" ); bitset< 32 > bitvec5( bitval, 6, 4 ); Мы получаем то же значение, что и для bitvec3 и bitvec4. Если опустить третий
параметр, подстрока берется до конца исходной строки: 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.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. Приоритеты операций
4.14. Преобразования типовПредставим себе следующий оператор присваивания: int ival = 0; // обычно компилируется с предупреждением ival = 3.541 + 3; В результате ival получит значение 6. Вот что происходит: мы складываем литералы
разных типов – 3.541 типа double и 3 типа int. C++ не может непосредственно
сложить подобные операнды, сначала ему нужно привести их к одному типу. Для
этого существуют правила преобразования арифметических типов. Общий принцип
таков: перейти от операнда меньшего типа к большему, чтобы не потерять точность
вычислений. 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. Неявное преобразование типовЯзык определяет набор стандартных преобразований между объектами встроенного типа, неявно выполняющихся компилятором в следующих случаях:
4.14.2. Арифметические преобразования типовАрифметические преобразования приводят оба операнда бинарного арифметического выражения к одному типу, который и будет типом результата выражения. Два общих правила таковы:
Если один из операндов имеет тип long double, второй приводится к этому же типу в любом случае. Например, в следующем выражении символьная константа 'a' трансформируется в long double (значение 97 для представления ASCII) и затем прибавляется к литералу того же типа: 3.14159L + 'a'. Если в выражении нет операндов long double, но есть операнд double, все преобразуется к этому типу. Например: int ival; float fval; double dval; // fval и ival преобразуются к double перед сложением В том случае, если нет операндов типа double и long double, но есть операнд float, тип остальных операндов меняется на float: char cvat; int ival; float fval; // iva1 и cval преобразуются к float перед сложением Если у нас нет вещественных операндов , значит, все они представляют собой
целые типы. Прежде чем определить тип результата, производится преобразование,
называемое приведением к целому: все операнды с типом меньше, чем int, заменяются
на int. 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. char cval; long lval; // cval и 1024 преобразуются в long перед сложением Из этого правила есть одно исключение: преобразование unsigned int в long происходит
только в том случае, если тип long способен вместить весь диапазон значений
unsigned int. (Обычно это не так в 32-битных системах, где и long, и int представляются
одним машинным словом.) Если же тип long не способен представить весь диапазон
unsigned int, оба операнда приводятся к unsigned long. 4.14.3. Явное преобразование типовЯвное преобразование типов производится при помощи следующих операторов: static_cast,
dynamic_cast, const_cast и reinterpret_cast. Заметим, что, хотя иногда явное
преобразование необходимо, оно служит потенциальным источником ошибок,
поскольку подавляет проверку типов, выполняемую компилятором. Давайте сначала
посмотрим, зачем нужно такое преобразование. int iva1; int *pi = 0; char *pc = 0; void *pv; pv = pi; // правильно: неявное преобразование pv = pc; // правильно: неявное преобразование const int *pci = &iva1; pv = pci; // ошибка: pv имеет тип, отличный от const void*; const void *pcv = pci; // правильно Однако указатель void* не может быть разыменован непосредственно. Компилятор не знает типа объекта, адресуемого этим указателем. Но это известно программисту, который хочет преобразовать указатель void* в указатель определенного типа. С++ не обеспечивает подобного автоматического преобразования: #include <cstring> int ival = 1024; void *pv; int *pi = &iva1; const char *pc = "a casting call"; void mumble() { Компилятор выдает сообщение об ошибке, так как в данном случае указатель pv содержит адрес целого числа ival, и именно этот адрес пытаются присвоить указателю на строку. Если бы такая программа была допущена до выполнения, то вызов функции strcpy(), которая ожидает на входе строку символов с нулем в конце, скорее всего привел бы к краху, потому что вместо этого strcpy() получает указатель на целое число. Подобные ошибки довольно просто не заметить, именно поэтому С++ запрещает неявное преобразование указателя на void в указатель на другой тип. Однако такой тип можно изменить явно: void mumble 0 { // правильно: программа по-прежнему содержит ошибку, // но теперь она компилируется! // Прежде всего нужно проверить // явные преобразования типов... pc = static_cast< char* >( pv ); char *pstr = new char[ str1en( pc )+1 ]; // скорее всего приведет к краху Другой причиной использования явного преобразования типов может служить необходимость избежать стандартного преобразования или выполнить вместо него собственное. Например, в следующем выражении ival сначала преобразуется в double, потом к нему прибавляется dval, и затем результат снова трансформируется в int. double dval; int iva1; ival += dval; Можно уйти от ненужного преобразования, явно заменив dval на int: ival += static_cast< int >( dval ); Третьей причиной является желание избежать неоднозначных ситуаций, в которых
возможно несколько вариантов применения правил преобразования по умолчанию.
(Мы рассмотрим этот случай в главе 9, когда будем говорить о перегруженных функциях.) cast-name< type >( expression ); Здесь cast-name – одно из ключевых слов static_cast, const_cast, dynamic_cast
или reinterpret_cast, а type – тип, к которому приводится выражение expression. extern char *string_copy( char* ); const char *pc_str; char *pc = string_copy( const_cast< char* >( pc_str )); Любое иное использование const_cast вызывает ошибку компиляции, как и попытка
подобного приведения с помощью любого из трех других операторов. double d = 97.0; char ch = static_cast< char >( d ); Зачем использовать static_cast? Дело в том, что без него компилятор выдаст
предупреждение о возможной потере точности. Применение оператора static_cast
говорит и компилятору, и человеку, читающему программу, что программист знает
об этом. enum mumble { first = 1, second, third }; extern int ival; Трансформация ival в mumble будет правильной только в том случае, если ival
равен 1, 2 или 3. complex<double> *pcom; char *pc = reinterpret_cast< char* >( pcom ); Программист не должен забыть или упустить из виду, какой объект реально адресуется указателем char* pc. Формально это указатель на строку встроенного типа, и компилятор не будет препятствовать использованию pc для инициализации строки: string str( pc ); хотя скорее всего такая команда вызовет крах программы. char *pc = (char*) pcom; Эта запись эквивалентна применению оператора reinterpret_cast, однако выглядит
не так заметно. Использование операторов xxx_cast позволяет четко указать те
места в программе, где содержатся потенциально опасные трансформации типов. 4.14.4. Устаревшая форма явного преобразованияОператоры явного преобразования типов, представленные в предыдущем разделе,
появились только в стандарте С++; раньше использовалась форма, теперь считающаяся
устаревшей. Хотя стандарт допускает и эту форму, мы настоятельно не рекомендуем
ею пользоваться. (Только если ваш компилятор не поддерживает новый вариант.) // появившийся в C++ вид type (expr); // вид, существовавший в C (type) expr; и может применяться вместо операторов static_cast, const_cast и reinterpret_cast. const char *pc = (const char*) pcom; int ival = (int) 3.14159; extern char *rewrite_str( char* ); char *pc2 = rewrite_str( (char*) pc ); int addr_va1ue = int( &iva1 ); Эта форма сохранена в стандарте С++ только для обеспечения обратной совместимости с программами, написанными для С и предыдущих версий С++. Упражнение 4.21Даны определения переменных: char cval; int ival; float fval; double dva1; unsigned int ui; Какие неявные преобразования типов будут выполнены? (a) cva1 = 'a' + 3; (b) fval = ui - ival * 1.0; (c) dva1 = ui * fval; (d) cva1 = ival + fvat + dva1; Упражнение 4.22Даны определения переменных: void *pv; int ival; char *pc; double dval; const string *ps; Перепишите следующие выражения, используя операторы явного преобразования типов: (a) pv = (void*)ps; (b) ival = int( *pc ); (c) pv = &dva1; (d) pc = (char*) pv; 4.15. Пример: реализация класса StackОписывая операции инкремента и декремента, для иллюстрации применения их префиксной
и постфиксной формы мы ввели понятие стека. Данная глава завершается примером
реализации класса iStack – стека, позволяющего хранить элементы типа int. #include <vector> class iStack { В данном случае мы используем вектор фиксированного размера: для иллюстрации
использования префиксных и постфиксных операций инкремента и декремента этого
достаточно. (В главе 6 мы модифицируем наш стек, придав ему возможность динамически
меняться.) inline int iStack::size() { return _top; }; empty() возвращает true, если _top равняется 0; full() возвращает true, если _top равен _stack.size()-1 (напомним, что индексация вектора начинается с 0, поэтому мы должны вычесть 1). inline bool iStack::empty() { return _top ? false : true; } inline bool iStack::full() { return _top < _stack.size()-l ? false : true; } Вот реализация функций pop() и push(). Мы добавили операторы вывода в каждую из них, чтобы следить за ходом выполнения: bool iStack::pop( int &top_va1ue ) { if ( empty() ) return false; top_value = _stack[ --_top ]; Прежде чем протестировать наш стек на примере, добавим функцию display(), которая позволит напечатать его содержимое. Для пустого стека она выведет: ( 0 ) Для стека из четырех элементов – 0, 1, 2 и 3 – результатом функции display() будет: ( 4 )( bot: 0 1 2 3 :top ) Вот реализация функции display(): void iStack::display() { cout << "( " << size() << " )( bot: "; for ( int ix = 0; ix < _top; ++ix ) А вот небольшая программа для проверки нашего стека. Цикл for выполняется 50 раз. Четное значение (2, 4, 6, 8 и т.д.) помещается в стек. На каждой итерации, кратной 5 (5, 10, 15...), распечатывается текущее содержимое стека. На итерациях, кратных 10 (10, 20, 30...), из стека извлекаются два элемента и его содержимое распечатывается еще раз. #inc1ude <iostream> #inc1ude "iStack.h" int main() { Вот результат работы программы: ( 0 )( bot: :top ) iStack push( 2 ) iStack push( 4 ) ( 2 )( bot: 2 4 :top ) iStack push( 6 ) iStack push( 8 ) iStack push ( 10 ) ( 5 )( bot: 2 4 6 8 10 :top ) iStack pop(): 10 iStack pop(): 8 ( 3 )( bot: 2 4 6 :top ) iStack push( 12 ) iStack push( 14 ) ( 5 )( bot: 2 4 6 12 14 :top ) iStack::push( 16 ) iStack::push( 18 ) iStack::push( 20 ) ( 8 )( bot: 2 4 6 12 14 16 18 20 :top ) iStack::pop(): 20 iStack::pop(): 18 ( 6 )( bot: 2 4 6 12 14 16 :top ) iStack::push( 22 ) iStack::push( 24 ) ( 8 )( bot: 2 4 6 12 14 16 22 24 :top ) iStack::push( 26 ) iStack::push( 28 ) iStack::push( 30 ) ( 11 )( bot: 2 4 6 12 14 16 22 24 26 28 30 :top ) iStack::pop(): 30 iStack::pop(): 28 ( 9 )( bot: 2 4 6 12 14 16 22 24 26 :top ) iStack::push( 32 ) iStack::push( 34 ) ( 11 )( bot: 2 4 6 12 14 16 22 24 26 32 34 :top ) iStack::push( 36 ) iStack::push( 38 ) iStack::push( 40 ) ( 14 )( bot: 2 4 6 12 14 16 22 24 26 32 34 36 38 40 :top ) iStack::рор(): 40 iStack::popQ: 38 ( 12 )( bot: 2 4 6 12 14 16 22 24 26 32 34 36 :top ) iStack::push( 42 ) iStack::push( 44 ) ( 14 )( bot: 2 4 6 12 14 16 22 24 26 32 34 36 42 44 :top ) iStack::push( 46 ) iStack::push( 48 ) iStack::push( 50 ) ( 17 )( bot: 2 4 6 12 14 16 22 24 26 32 34 36 42 44 46 48 50 :top ) iStack::pop(): 50 iStack::pop(): 48 ( 15 )( bot: 2 4 6 12 14 16 22 24 26 32 34 36 42 44 46 :top ) Упражнение 4.23Иногда требуется операция peek(), которая возвращает значение элемента на вершине стека без извлечения самого элемента. Реализуйте функцию peek() и добавьте к программе main() проверку работоспособности этой функции. Упражнение 4.24В чем вы видите два основных недостатка реализации класса iStack? Как их можно исправить? Назад ВпередСодержание |
2011-11-05 18:04:04 Oleg Все очень понятно. 2011-11-30 19:23:09 Andriy Хорошая инфа. Спасибо. П.С. Порадовал пример с битовыми операциями, там где enum students 2018-04-12 15:57:44 Mike В параграфе 4.14.2 есть такая строка в первом примере кода: dval + fva1 + ival; Вопрос - в какую переменную записывается результат сложения? 2018-04-19 10:30:15 Alex Mike: Не присваивается. Это пример преобразования типа. Кое где вместо l стоит 1 Оставить комментарий: |