Ранее мы рассмотрели типы данных – как встроенные, так и предоставленные стандартной библиотекой. Здесь мы разберем предопределенные операции, такие, как сложение, вычитание, сравнение и т.п., рассмотрим их приоритеты. Скажем, результатом выражения 3+4*5 является 23, а не 35 потому, что операция умножения (*) имеет более высокий приоритет, чем операция сложения (+). Кроме того, мы обсудим вопросы преобразований типов данных – и явных, и неявных. Например, в выражении 3+0.7 целое значение 3 станет вещественным перед выполнением операции сложения.
§ 1.1. Что такое выражение?
§ 1.2. Арифметические операции
§ 1.3. Операции сравнения и логические операции
§ 1.4. Операции присваивания
§ 1.5. Операции инкремента и декремента
§ 1.6. Операции с комплексными числами
§ 1.7. Условное выражение
§ 1.8. Оператор sizeof
§ 1.9. Операторы new и delete
§ 1.10. Оператор запятая
§ 1.11. Побитовые операторы
§ 1.12. Класс bitset
§ 1.13. Приоритеты
§ 1.14. Преобразования типов
§ 1.15. Неявное преобразование типов
§ 1.16. Арифметические преобразования типов
§ 1.17. Явное преобразование типов
§ 1.18. Устаревшая форма явного преобразования
§ 1.19. Пример: реализация класса Stack
Выражение состоит из одного или более операндов, в простейшем случае – из одного литерала или объекта. Результатом такого выражения является 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
* представляет собой унарную операцию разыменования. Значением этого выражения является значение объекта, адрес которого содержится в ptr. Если же написать:
var1 * var2
то звездочка будет обозначать бинарную операцию умножения.
Результатом вычисления выражения всегда, если не оговорено противное, является r-значение. Тип результата арифметического выражения определяется типами операндов. Если операнды имеют разные типы, производится преобразование типов в соответствии с предопределенным набором правил. (Мы детально рассмотрим эти правила в разделе 4.14.)
Выражение может являться составным, то есть объединять в себе несколько подвыражений. Вот, например, выражение, проверяющее на неравенство нулю указатель и объект, на который он указывает (если он на что-то указывает) :
ptr != 0 && *ptr != 0
Выражение состоит из трех подвыражений: проверку указателя ptr, разыменования ptr и проверку результата разыменования. Если ptr определен как
int ival = 1024; int *ptr = &ival;
то результатом разыменования будет 1024 и оба сравнения дадут истину. Результатом всего выражения также будет истина (оператор && обозначает логическое И).
Если посмотреть на этот пример внимательно, можно заметить, что порядок выполнения операций очень важен. Скажем, если бы операция разыменования ptr производилась до его сравнения с 0, в случае нулевого значения ptr это скорее всего вызвало бы крах программы. В случае операции И порядок действий строго определен: сначала оценивается левый операнд, и если его значение равно false, правый операнд не вычисляется вовсе. Порядок выполнения операций определяется их приоритетами, не всегда очевидными, что вызывает у начинающих программистов на С и С++ множество ошибок. Приоритеты будут приведены в разделе 4.13, а пока мы расскажем обо всех операциях, определенных в С++, начиная с наиболее привычных.
Символ операции | Значение | Использование |
* | Умножение | expr*expr |
/ | Деление | expr / expr |
% | Остаток от деления | expr % expr |
+ | Сложение | expr + expr |
- | Вычитание | expr – expr |
Деление целых чисел дает в результате целое число. Дробная часть результата, если она есть, отбрасывается:
int ivall = 21 / 6; int iva12 = 21 / 7;
И ival1, и ival2 в итоге получат значение 3.
Операция остаток (%), называемая также делением по модулю, возвращает остаток от деления первого операнда на второй, но применяется только к операндам целого типа (char, short, int, long). Результат положителен, если оба операнда положительны. Если же один или оба операнда отрицательны, результат зависит от реализации, то есть машинно-зависим. Вот примеры правильного и неправильного использования деления по модулю:
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() { char byte_value = 32; int ival = 8; // переполнение памяти, отведенной под byte_value byte_value = ival * byte_value; std::cout << "byte_value: " <<static_cast<int>(byte_value) << std::endl; }
Для представления числа 256 необходимы 9 бит. Переменная byte_value получает некоторое неопределенное (машинно-зависимое) значение. Допустим, на нашей рабочей станции SGI мы получили 0. Первая попытка напечатать это значение с помощью:
std::cout << "byte_va1ue: " << byte_va1ue << std::endl;
привела к результату:
byte_value:
После некоторого замешательства мы поняли, что значение 0 – это нулевой символ ASCII, который не имеет представления при печати. Чтобы напечатать не представление символа, а его значение, нам пришлось использовать весьма странно выглядящее выражение:
static_cast<int>(byte_value)
которое называется явным приведением типа. Оно преобразует тип объекта или выражения в другой тип, явно заданный программистом. В нашем случае мы изменили byte_value на int. Теперь программа выдает более осмысленный результат:
byte_value: 0
На самом деле нужно было изменить не значение, соответствующее byte_value, а поведение операции вывода, которая действует по-разному для разных типов. Объекты типа char представляются ASCII-символами (а не кодами), в то время как для объектов типа int мы увидим содержащиеся в них значения. (Преобразования типов рассмотрены в разделе 4.14.)
Это небольшое отступление от темы – обсуждение проблем преобразования типов – вызвано обнаруженной нами погрешностью в работе нашей программы и в каком-то смысле напоминает реальный процесс программирования, когда аномальное поведение программы заставляет на время забыть о том, ради достижения какой, собственно, цели она пишется, и сосредоточиться на несущественных, казалось бы, деталях. Такая мелочь, как недостаточно продуманный выбор типа данных, приводящий к переполнению, может стать причиной трудно обнаруживаемой ошибки: из соображений эффективности проверка на переполнение не производится во время выполнения программы.
Стандартная библиотека С++ имеет заголовочный файл limits, содержащий различную информацию о встроенных типах данных, в том числе и диапазоны значений для каждого типа. Заголовочные файлы climits и cfloat также содержат эту информацию.
Арифметика вещественных чисел создает еще одну проблему, связанную с округлением. Вещественное число представляется фиксированным количеством разрядов (разным для разных типов – float, double и long double), и точность значения зависит от используемого типа данных. Но даже самый точный тип long double не может устранить ошибку округления. Вещественная величина в любом случае представляется с некоторой ограниченной точностью.
В чем разница между приведенными выражениями с операцией деления?
double dvall = 10.0, dva12 = 3.0;
int ivall = 10, iva12 = 3;
dvall / dva12;
ivall / iva12;
Напишите выражение, определяющее, четным или нечетным является данное целое число.
Найдите заголовочные файлы limits, climits и cfloat и посмотрите, что они содержат.
Символ операции | Значение | Использование |
---|---|---|
! | Логическое НЕ | !expr |
< | меньше | expr<expr |
<= | Меньше либо равно | expr<=expr |
> | больше | expr>expr |
>= | больше либо равно | expr>=expr |
== | равно | expr==expr |
!= | не равно | expr!=expr |
&& | логическое И | expr&&expr |
|| | логическое ИЛИ | expr||expr |
Примечание. Все операции в результате дают значение типа bool
Операции сравнения и логические операции в результате дают значение типа bool, то есть true или false. Если же такое выражение встречается в контексте, требующем целого значения, true преобразуется в 1, а false – в 0. Вот фрагмент кода, подсчитывающего количество элементов вектора, меньших некоторого заданного значения:
std::vector<int>::iterator iter = ivec.begin() ; 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.
Логическое И (&&) возвращает истину только тогда, когда истинны оба операнда. Логическое ИЛИ (||) дает истину, если истинен хотя бы один из операндов. Гарантируется, что операнды вычисляются слева направо и вычисление заканчивается, как только результирующее значение становится известно. Что это значит? Пусть даны два выражения:
expr1 && expr2 expr1 || expr2
Если в первом из них expr1 равно false, значение всего выражения тоже будет равным false вне зависимости от значения expr2, которое даже не будет вычисляться. Во втором выражении expr2 не оценивается, если expr1 равно true, поскольку значение всего выражения равно true вне зависимости от expr2.
Подобный способ вычисления дает возможность удобной проверки нескольких выражений в одном операторе AND:
while (ptr != О && ptr->va1ue < upperBound && ptr->va1ue >= 0 && notFound(ia[ptr->va1ue])) { ... }
Указатель с нулевым значением не указывает ни на какой объект, поэтому применение к нулевому указателю операции доступа к члену вызвало бы ошибку (ptr->value). Однако, если ptr равен 0, проверка на первом шаге прекращает дальнейшее вычисление подвыражений. Аналогично на втором и третьем шагах проверяется попадание величины ptr->value в нужный диапазон, и операция взятия индекса не применяется к массиву ia, если этот индекс неправилен.
Операция логического НЕ дает true, если ее единственный оператор равен false, и наоборот. Например:
bool found = false; // пока элемент не найден // и ptr указывает на объект (не 0) while (! found && ptr) { found = 1ookup(*ptr); ++ptr; }
Подвыражение !found дает true, если переменная found равна false. Это более компактная запись для found == false. Аналогично if (found) эквивалентно более длинной записи if (found == true). Использование операций сравнения достаточно очевидно. Нужно только иметь в виду, что, в отличие от И и ИЛИ, порядок вычисления операндов таких выражений не определен. Вот пример, где возможна подобная ошибка:
// Внимание! Порядок вычислений не определен! 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. Мы должны были явно написать:
if (ival != jva1 && ival != kva1 && jva1 != kva1)
// сделать что-то ...
Найдите неправильные или непереносимые выражения, поясните. Как их можно изменить? (Заметим, что типы объектов не играют роли в данных примерах.)
(a) ptr->iva1 != 0 (с) ptr != 0 && *ptr++; (e) vec[iva1++] <= vec[ival]; (b) ival != jva1 < kva1 (d) iva1++ && ival;
Язык С++ не диктует порядок вычисления операций сравнения для того, чтобы позволить компилятору делать это оптимальным образом. Как вы думаете, стоило бы в данном случае пожертвовать эффективностью, чтобы избежать ошибок, связанных с предположением о вычислении выражения слева направо?
Инициализация задает начальное значение переменной. Например:
int ival = 1024; int *pi = 0;
В результате операции присваивания объект получает новое значение, при этом старое пропадает:
ival = 2048; pi = &iva1;
Иногда путают инициализацию и присваивание, так как они обозначаются одним и тем же знаком =. Объект инициализируется только один раз – при его определении. В то же время операция может быть применена к нему многократно.
Что происходит, если тип объекта не совпадает с типом значения, которое ему хотят присвоить? Допустим,
ival = 3.14159; // правильно?
В таком случае компилятор пытается трансформировать тип объекта, стоящего справа, в тип объекта, стоящего слева. Если такое преобразование возможно, компилятор неявно изменяет тип, причем при потере точности обычно выдается предупреждение. В нашем случае вещественное значение 3.14159 преобразуется в целое значение 3, и это значение присваивается переменной ival.
Если неявное приведение типов невозможно, компилятор сигнализирует об ошибке:
pi = ival; // ошибка
Неявная трансформация типа int в тип указатель на int невозможна. (Набор допустимых неявных преобразований типов мы обсудим в разделе 4.14.)
Левый операнд операции присваивания должен быть l-значением. Очевидный пример неправильного присваивания:
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; // ошибка
ia – тоже l-значение, но оно не может быть значением массива.
Неверна и инструкция
pia + 2=1; // ошибка
Хотя pia+2 дает адрес ia[2], присвоить ему значение нельзя. Если мы хотим изменить элемент ia[2], то нужно воспользоваться операцией разыменования. Корректной будет следующая запись:
*(pia + 2) = 1; // правильно
Операция присваивания имеет результат – значение, которое было присвоено самому левому операнду. Например, результатом такой операции
ival = 0;
является 0, а результат
ival = 3.14159;
равен 3. Тип результата – int в обоих случаях. Это свойство операции присваивания можно использовать в подвыражениях. Например, следующий цикл
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') { // сделать что-то ... } // ... }
Заметим, что вокруг выражения присваивания необходимы скобки, поскольку приоритет этой операции ниже, чем операции сравнения. Без скобок первым выполняется сравнение:
next_char() != '\n'
и его результат, true или false, присваивается переменной ch. (Приоритеты операций будут рассмотрены в разделе 4.13.)
Аналогично несколько операций присваивания могут быть объединены, если это позволяют типы операндов. Например:
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;
где op= является одним из десяти операторов:
+= -= *= /= %= <<= >>= &= ^= |=
Запись a op= b в точности эквивалентна записи a = a op b.
Найдите ошибку в данном примере. Исправьте запись.
int main() { float fval; int ival; int *pi; fval = ival = pi = 0; }
Следующие выражения синтаксически правильны, однако скорее всего работают не так, как предполагал программист. Почему? Как их изменить?
(a) if (ptr = retrieve_pointer() != 0) (b) if (ival = 1024) (c) ival += ival + 1;
Операции инкремента (++) и декремента (--) дают возможность компактной и удобной записи для изменения значения переменной на единицу. Чаще всего они используются при работе с массивами и коллекциями – для изменения величины индекса, указателя или итератора:
#include <vector> #include <cassert> int main() { int ia[10] = {0,1,2,3,4,5,6,7,8,9}; std::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]; std::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 приведена в конце этой главы.)
Как вы думаете, почему язык программирования получил название С++, а не ++С?
Класс комплексных чисел стандартной библиотеки С++ представляет собой хороший пример использования объектной модели. Благодаря перегруженным арифметическим операциям объекты этого класса используются так, как будто они принадлежат одному из встроенных типов данных. Более того, в подобных операциях могут одновременно принимать участие и переменные встроенного арифметического типа, и комплексные числа. (Отметим, что здесь мы не рассматриваем общие вопросы математики комплексных чисел. См. [PERSON68] или любую книгу по математике.) Например, можно написать:
#inc1ude <complex> complex<double> a; complex<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); complex<double> complex1(complexO.real()); std::cout << complexO << " " << complex1 << std::endl;
выглядит так:
(3.14159, -2.171) (3.14159, 0.0)
Оператор ввода понимает любой из следующих форматов:
// допустимые форматы для ввода комплексного числа // 3.14159 ==> complex(3.14159); // (3.14159) ==> complex(3.14159); // (3.14, -1.0) ==> complex(3.14, -1.0); // может быть считано как // std::cin >> a >> b >> с; // где a, b, с - комплексные числа 3.14159 (3.14159) (3.14, -1.0)
Кроме этих операций, класс комплексных чисел имеет следующие функции-члены: sqrt(), abs(), polar(), sin(), cos(), tan(), exp(), log(), log10() и pow().
Реализация стандартной библиотеки С++, доступная нам в момент написания книги, не поддерживает составных операций присваивания, если правый операнд не является комплексным числом. Например, подобная запись недопустима:
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); std::cout << cval << std::endl; cval += 1; std::cout << cval << std::endl; cval -= 1; std::cout << cval << std::endl; cval *= 2; std::cout << cval << std::endl; std::cout /= 2; std::cout << cval << std::endl; }
Стандарт С++ не специфицирует реализацию операций инкремента и декремента для комплексного числа. Однако их семантика вполне понятна: если уж мы можем написать:
cval += 1;
что означает увеличение на 1 вещественной части cval, то и операция инкремента выглядела бы вполне законно. Реализуйте эти операции для типа complex<double> и выполните следующую программу:
#include <iostream> #include <complex> // определения операций... int main() { complex<double> cval(4.0, 1.0); std::cout << cval << std::endl; ++cva1; std::cout << cval << std::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; std::cout << "Большим из " << i << " и " << j << " является " << (i > j ? i : j) << end1; std::cout << "Значение " << i << (i % 2 ? " нечетно." : " четно.") << std::endl; /* условный оператор может быть вложенным, * но глубокая вложенность трудна для восприятия. * В данном примере max получает значение * максимальной из трех величин */ int max = ((i > j) ? ((i > k) ? i : k) : (j > k) ? j : k); std::cout << "Большим из " << i << ", " << j << " и " << k << " является " << max << std::endl; }
Результатом работы программы будет:
Большим из 10 и 20 является 20 Значение 10 четно.
Оператор 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]; std::cout << "pi: " << sizeof(pi) << " *pi: " << sizeof(pi) << std::endl; // sizeof строки не зависит от // ее реальной длины string stl("foobar"); string st2("a mighty oak"); string *ps = &stl; std::cout << " st1: " << sizeof(st1) << " st2: " << sizeof(st2) << " ps: sizeof(ps) << " *ps: " << sizeof(*ps) << std::endl; std::cout << "short :\t" << sizeof(short) << std::endl; std::cout << "shorf" :\t" << sizeof(short*) << std::endl; std::cout << "short& :\t" << sizeof(short&) << std::endl; std::cout << "short[3] :\t" << sizeof(short[3]) << std::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)];
Каждая программа во время работы получает определенное количество памяти, которую можно использовать. Такое выделение памяти под объекты во время выполнения называется динамическим, а сама память выделяется из хипа (heap). Напомним, что выделение памяти объекту производится с помощью оператора 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. При выполнении такой операции необходимо придерживаться указанного синтаксиса.
Упражнение 4.11: Какие из следующих выражений ошибочны?
(a) std::vector<std::string> svec(10); (b) std::vector<std::string> *pvecl = new std::vector<std::string>(10); (c) std::vector<std::string> **pvec2 = new std::vector<std::string>[10]; (d) std::vector<std::string> *pvl = &svec; (e) std::vector<std::string> *pv2 = pvecl; (f) delete svec; (g) delete pvecl; (h) delete [] pvec2; (i) delete pvl; (j) delete pv2;
запятая
Одно выражение может состоять из набора подвыражений, разделенных запятыми; такие подвыражения вычисляются слева направо. Конечным результатом будет результат самого правого из них. В следующем примере каждое из подвыражений условного оператора представляет собой список. Результатом первого подвыражения условного оператора является ix, второго – 0.
int main() { // примеры оператора "запятая" // переменные ia, sz и index определены в другом месте ... int ival = (ia != 0) ? ix=get_va1ue(), ia[index]=ix : ia=new int[sz], ia[index]=0; // ... }
Символ операции | Значение | Использование |
---|---|---|
~ | Побитовое НЕ | ~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 стандартной библиотеки – и два подхода к массивам:
массивы встроенного типа и объект std::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 bool has_passed_quiz[student_size+l]; for (int index = 1; index <= student_size; ++-index) has_passed_quiz[index] = bit_on(quiz1, index);
Раз уж мы начали инкапсулировать действия с битовым вектором в функции, следующим шагом нужно создать класс. Стандартная библиотека С++ включает такой класс bitset, его использование описано ниже.
Даны два целых числа:
unsigned int ui1 = 3, ui2 = 7;
Каков результат следующих выражений?
(a) ui1 & ui2 (c) uil | ui2
(b) ui1 && ui2 (d) uil || ui2
Используя пример функции bit_on(), создайте функции bit_turn_on() (выставляет бит в 1), bit_turn_off() (сбрасывает бит в 0), flip_bit() (меняет значение на противоположное) и bit_off() (возвращает true, если бит равен 0). Напишите программу, использующую ваши функции.
В чем недостаток функций из предыдущего упражнения, использующих тип unsigned int? Их реализацию можно улучшить, используя определение типа с помощью typedef или механизм функций-шаблонов. Перепишите функцию bit_on(),применив сначала typedef, а затем механизм шаблонов.
Операция | Значение | Использование |
---|---|---|
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.fli | p(4) |
set() | Выставить все биты в 1 | a.set() |
set(pos) | Выставить бит pos в 1 a.se | t(4) |
reset() | Выставить все биты в 0 | a.reset() |
reset(pos) | Выставить бит pos в 0 a.rese | t(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, если все биты равны нулю:
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 сработало!;
Значения битов с помощью индекса проверяются таким образом:
std::cout << "bitvec: включенные биты:\n\t"; for (int index = 0; index < 32; ++-index) if (bitvec[index]) std::cout << index << " "; std::cout << std::endl;
Следующая пара операторов демонстрирует сброс первого бита двумя способами:
bitvec.reset(0); bitvec[0] = 0;
Функции set() и reset() могут применяться ко всему битовому вектору в целом. В этом случае они должны быть вызваны без параметра. Например:
// сброс всех битов bitvec.reset(); if (bitvec.none() != true) // что-то не сработало // установить в 1 все биты вектора bitvec if (bitvec.any() != true) // что-то опять не сработало
Функция flip() меняет значение отдельного бита или всего битового вектора:
bitvec.f1ip(0); // меняет значение первого бита bitvec[0].flip(); // тоже меняет значение первого бита bitvec.flip(); // меняет значения всех битов
Существуют еще два способа определить объект типа bitset. Оба они дают возможность проинициализировать объект определенным набором нулей и единиц. Первый способ – явно задать целое беззнаковое число как аргумент конструктору. Начальные N позиций битового вектора получат значения соответствующих двоичных разрядов аргумента. Например:
bitset<32> bitvec2(Oxffff);
инициализирует bitvec2 следующим набором значений:
00000000000000001111111111111111
В результате определения
bitset<32> bitvec3(012);
у bitvec3 окажутся ненулевыми биты на местах 1 и 3:
00000000000000000000000000001010
В качестве аргумента конструктору может быть передано и строковое значение, состоящее из нулей и единиц. Например, следующее определение инициализирует bitvec4 тем же набором значений, что и bitvec3:
// эквивалентно bitvec3 string bitva1("1010"); bitset<32> bitvec4(bitval);
Можно также указать диапазон символов строки, выступающих как начальные значения для битового вектора. Например:
// подстрока с шестой позиции длиной 4: 1010 string bitval ("1111110101100011010101"); bitset<32> bitvec5(bitval, 6, 4);
Мы получаем то же значение, что и для bitvec3 и bitvec4. Если опустить третий параметр, подстрока берется до конца исходной строки:
// подстрока с шестой позиции до конца строки: 1010101
string bitva1("1111110101100011010101");
bitset<32> bitvec6(bitval, 6);
Класс bitset предоставляет две функции-члена для преобразования объекта bitset в другой тип. Для трансформации в строку, состоящую из символов нулей и единиц, служит функция to_string():
string bitva1(bitvec3.to_string());
Вторая функция, to_long(), преобразует битовый вектор в его целочисленное представление в виде unsigned long, если, конечно, оно помещается в unsigned long. Это видоизменение особенно полезно, если мы хотим передать битовый вектор функции на С или С++, не пользующейся стандартной библиотекой.
К объектам типа bitset можно применять побитовые операции. Например:
bitset<32> bitvec7 = bitvec2 & bitvec3;
Объект bitvec7 инициализируется результатом побитового И двух битовых векторов bitvec2 и bitvec3.
bitset<32> bitvec8 = bitvec2 | bitvec3;
Здесь bitvec8 инициализируется результатом побитового ИЛИ векторов bitvec2 и bitvec3. Точно так же поддерживаются и составные операции присваивания и сдвига.
Допущены ли ошибки в приведенных определениях битовых векторов?
(a) bitset<64> bitvec(32); (b) bitset<32> bv(1010101); (c) string bstr; std::cin >> bstr; bitset<8> bv(bstr); (d) bitset<32> bv; bitset<16> bvl6(bv);
Допущены ли ошибки в следующих операциях с битовыми векторами?
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());
Дана последовательность: 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.4.
(a) ! ptr == ptr->next (b) ~ uc ^ 0377 & ui << 4 (c) ch = buf[bp++] != '\n'
Все три выражения из предыдущего упражнения вычисляются не в той последовательности, какую, по-видимому, хотел задать программист. Расставьте скобки так, чтобы реализовать его первоначальный замысел.
Следующие выражения вызывают ошибку компиляции из-за неправильно понятого приоритета операций. Объясните, как их исправить, используя таблицу 4.4.
(a) int i = doSomething(), 0; (b) std::cout << ival % 2 ? "odd" : "even";
Оператор | Значение | Использование |
---|---|---|
:: | Глобальная область видимости | ::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<type>(expr) | |
преобразование типа | 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 | Выделение памяти под массив | все формы |
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++ не может непосредственно сложить подобные операнды, сначала ему нужно привести их к одному типу. Для этого существуют правила преобразования арифметических типов. Общий принцип таков: перейти от операнда меньшего типа к большему, чтобы не потерять точность вычислений.
В нашем случае целое значение 3 трансформируется в тип double, и только после этого производится сложение. Такое преобразование выполняется независимо от желания программиста, поэтому оно получило название неявного преобразования типов.
Результат сложения двух чисел типа double тоже имеет тип double. Значение равно 6.541. Теперь его нужно присвоить переменной ival. Типы переменной и результата 6.541 не совпадают, следовательно, тип этого значения приводится к типу переменной слева от знака равенства. В нашем случае это int. Преобразование double в int производится автоматически, отбрасыванием дробной части (а не округлением). Таким образом, 6.541 превращается в 6, и этот результат присваивается переменной ival. Поскольку при таком преобразовании может быть потеряна точность, большинство компиляторов выдают предупреждение.
Так как компилятор не округляет числа при преобразовании double в int, при необходимости мы должны позаботиться об этом сами. Например:
double dva1 = 8.6; int iva1 = 5; ival += dva1 + 0.5; // преобразование с округлением
При желании мы можем произвести явное преобразование типов:
// инструкция компилятору привести double к int ival = static_cast<int>(3.541) + 3;
В этом примере мы явно даем указание компилятору привести величину 3.541 к типу int, а не следовать правилам по умолчанию.
В этом разделе мы детально обсудим вопросы и неявного (как в первом примере), и явного преобразования типов (как во втором).
Язык определяет набор стандартных преобразований между объектами встроенного типа, неявно выполняющихся компилятором в следующих случаях:
int ival = 3; double dva1 = 3.14159; // ival преобразуется в double: 3.0 ival + dva1;
// 0 преобразуется в нулевой указатель типа int* int *pi = 0; // dva1 преобразуется в int: 3 ivat = dva1;
extern double sqrt(double); // 2 преобразуется в double: 2.0 std::cout << "Квадратный корень из 2: " << sqrt(2) << endt;
double difference(int ivati, int iva12) { // результат преобразуется в double return ivati - iva12; }
Арифметические преобразования приводят оба операнда бинарного арифметического выражения к одному типу, который и будет типом результата выражения. Два общих правила таковы:
Если один из операндов имеет тип long double, второй приводится к этому же типу в любом случае. Например, в следующем выражении символьная константа 'a' трансформируется в long double (значение 97 для представления ASCII) и затем прибавляется к литералу того же типа:
3.14159L + 'a'.
Если в выражении нет операндов long double, но есть операнд double, все преобразуется к этому типу. Например:
int iva1; float fval; double dval; // fva1 и iva1 преобразуются к double перед сложением dval + fva1 + ival;
В том случае, если нет операндов типа double и long double, но есть операнд float, тип остальных операндов меняется на float:
char cvat; int iva1; float fva1; // iva1 и cval преобразуются к float перед сложением cvat + fva1 + iva1;
Если у нас нет вещественных операндов , значит, все они представляют собой целые типы. Прежде чем определить тип результата, производится преобразование, называемое приведением к целому: все операнды с типом меньше, чем int, заменяются на int.
При приведении к целому типы char, signed char, unsigned char и short int преобразуются в int. Тип unsigned short int трансформируется в int, если этот тип достаточен для представления всего диапазона значений unsigned short int (обычно это происходит в системах, отводящих полслова под short и целое слово под int), в противном случае unsigned short int заменяется на unsigned int.
Тип wchar_t и перечисления приводятся к наименьшему целому типу, способному представить все их значения. Например, в перечислении
enum status { bad, ok };
значения элементов равны 0 и 1. Оба эти значения могут быть представлены типом char, значит char и станет типом внутреннего представления данного перечисления. Приведение к целому преобразует char в int.
В следующем выражении
char cval; bool found; enum mumble { ml, m2, m3 } mval; unsigned long ulong; cval + ulong; ulong + found; mval + ulong;
перед определением типа результата cval, found и mval преобразуются в int.
После приведения к целому сравниваются получившиеся типы операндов. Если один из них имеет тип unsigned long, то остальные будут того же типа. В нашем примере все три объекта, прибавляемые к ulong, приводятся к типу unsigned long.
Если в выражении нет объектов unsigned long, но есть объекты типа long, тип остальных операндов меняется на long. Например:
char cval; long lval; // cval и 1024 преобразуются в long перед сложением cval + 1024 + lval;
Из этого правила есть одно исключение: преобразование unsigned int в long происходит только в том случае, если тип long способен вместить весь диапазон значений unsigned int. (Обычно это не так в 32-битных системах, где и long, и int представляются одним машинным словом.) Если же тип long не способен представить весь диапазон unsigned int, оба операнда приводятся к unsigned long.
В случае отсутствия операндов типов unsigned long и long, используется тип unsigned int. Если же нет операндов и этого типа, то к int.
Может быть, данное объяснение преобразований типов несколько смутило вас. Запомните основную идею: арифметическое преобразование типов ставит своей целью сохранить точность при вычислении. Это достигается приведением типов всех операндов к типу, способному вместить любое значение любого из присутствующих в выражении операндов.
Явное преобразование типов производится при помощи следующих операторов: static_cast, dynamic_cast, const_cast и reinterpret_cast. Заметим, что, хотя иногда явное преобразование необходимо, оно служит потенциальным источником ошибок, поскольку подавляет проверку типов, выполняемую компилятором. Давайте сначала посмотрим, зачем нужно такое преобразование.
Указатель на объект любого неконстантного типа может быть присвоен указателю типа void*, который используется в тех случаях, когда действительный тип объекта либо неизвестен, либо может меняться в ходе выполнения программы. Поэтому указатель void* иногда называют универсальным указателем. Например:
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 = pi; // правильно: pv получает адрес ival pc = pv; // ошибка: нет стандартного преобразования char *pstr = new char[str1en(pc)+1]; strcpy(pstr, pc); }
Компилятор выдает сообщение об ошибке, так как в данном случае указатель pv содержит адрес целого числа ival, и именно этот адрес пытаются присвоить указателю на строку. Если бы такая программа была допущена до выполнения, то вызов функции strcpy(), которая ожидает на входе строку символов с нулем в конце, скорее всего привел бы к краху, потому что вместо этого strcpy() получает указатель на целое число. Подобные ошибки довольно просто не заметить, именно поэтому С++ запрещает неявное преобразование указателя на void в указатель на другой тип. Однако такой тип можно изменить явно:
void mumble() { // правильно: программа по-прежнему содержит ошибку, // но теперь она компилируется! // Прежде всего нужно проверить // явные преобразования типов... pc = static_cast<char*>(pv); char *pstr = new char[str1en(pc)+1]; // скорее всего приведет к краху strcpy(pstr, pc); }
Другой причиной использования явного преобразования типов может служить необходимость избежать стандартного преобразования или выполнить вместо него собственное. Например, в следующем выражении 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.
Четыре вида явного преобразования введены для того, чтобы учесть все возможные формы приведения типов. Так const_cast служит для трансформации константного типа в неконстантный и подвижного (volatile) – в неподвижный. Например:
extern char *string_copy(char*); const char *pc_str; char *pc = string_copy(const_cast<char*>(pc_str));
Любое иное использование const_cast вызывает ошибку компиляции, как и попытка подобного приведения с помощью любого из трех других операторов.
С применением static_cast осуществляются те преобразования, которые могут быть сделаны неявно, на основе правил по умолчанию:
double d = 97.0; char ch = static_cast<char>(d);
Зачем использовать static_cast? Дело в том, что без него компилятор выдаст предупреждение о возможной потере точности. Применение оператора static_cast говорит и компилятору, и человеку, читающему программу, что программист знает об этом.
Кроме того, с помощью static_cast указатель void* можно преобразовать в указатель определенного типа, арифметическое значение – в значение перечисления (enum), а базовый класс – в производный. (О преобразованиях типов базовых и производных классов говорится в главе 19.)
Эти изменения потенциально опасны, поскольку их правильность зависит от того, какое конкретное значение имеет преобразуемое выражение в данный момент выполнения программы:
enum mumble { first = 1, second, third }; extern int ival; mumble mums_the_word = static_cast<mumble>(ival);
Трансформация ival в mumble будет правильной только в том случае, если ival равен 1, 2 или 3.
reinterpret_cast работает с внутренними представлениями объектов (re-interpret – другая интерпретация того же внутреннего представления), причем правильность этой операции целиком зависит от программиста. Например:
complex<double> *pcom; char *pc = reinterpret_cast<char*>(pcom);
Программист не должен забыть или упустить из виду, какой объект реально адресуется указателем char* pc. Формально это указатель на строку встроенного типа, и компилятор не будет препятствовать использованию pc для инициализации строки:
string str(pc);
хотя скорее всего такая команда вызовет крах программы.
Это хороший пример, показывающий, насколько опасны бывают явные преобразования типов. Мы можем присваивать указателям одного типа значения указателей совсем другого типа, и это будет работать до тех пор, пока мы держим ситуацию под контролем. Однако, забыв о подразумеваемых деталях, легко допустить ошибку, о которой компилятор не сможет нас предупредить.
Особенно трудно найти подобную ошибку, если явное преобразование типа делается в одном файле, а используется измененное значение в другом.
В некотором смысле это отражает фундаментальный парадокс языка С++: строгая
проверка типов призвана не допустить подобных ошибок, в то же время наличие
операторов явного преобразования позволяет обмануть
компилятор и использовать
объекты разных типов на свой страх и риск. В нашем примере мы отключили
проверку
типов при инициализации указателя pc и присвоили ему адрес комплексного числа.
При инициализации строки str такая проверка производится снова, но компилятор
считает, что pc указывает на строку, хотя, на самом-то деле, это не так!
Четыре оператора явного преобразования типов были введены в стандарт С++ как наименьшее зло при невозможности полностью запретить такое приведение. Устаревшая, но до сих пор поддерживаемая стандартом С++ форма явного преобразования выглядит так:
char *pc = (char*) pcom;
Эта запись эквивалентна применению оператора reinterpret_cast, однако выглядит не так заметно. Использование операторов xxx_cast позволяет четко указать те места в программе, где содержатся потенциально опасные трансформации типов.
Если поведение программы становится ошибочным и непонятным, возможно, в этом виноваты явные видоизменения типов указателей. Использование операторов явного преобразования помогает легко обнаружить места в программе, где такие операции выполняются. (Другой причиной непредсказуемого поведения программы может стать нечаянное уничтожение объекта (delete), в то время как он еще должен использоваться в работе.)
Оператор dynamic_cast применяется при идентификации типа во время выполнения (run-time type identification). Мы вернемся к этой проблеме лишь в разделе 19.1.
Операторы явного преобразования типов, представленные в предыдущем разделе, появились только в стандарте С++; раньше использовалась форма, теперь считающаяся устаревшей. Хотя стандарт допускает и эту форму, мы настоятельно не рекомендуем ею пользоваться. (Только если ваш компилятор не поддерживает новый вариант.)
Устаревшая форма явного преобразования имеет два вида:
// появившийся в 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);
Эта форма сохранена в стандарте С++ только для обеспечения обратной совместимости с программами, написанными для С и предыдущих версий С++.
Даны определения переменных:
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;
Даны определения переменных:
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;
Описывая операции инкремента и декремента, для иллюстрации применения их префиксной и постфиксной формы мы ввели понятие стека. Данная глава завершается примером реализации класса iStack – стека, позволяющего хранить элементы типа int.
Как уже было сказано, с этой структурой возможны две основные операции – поместить элемент (push) и извлечь (pop) его. Другие операции позволяют получить информацию о текущем состоянии стека – пуст он (empty()) или полон (full()), сколько элементов в нем содержится (size()). Для начала наш стек будет предназначен лишь для элементов типа int. Вот объявление нашего класса:
#include <vector> class iStack { public: iStack(int capacity) : _stack(capacity), _top(0) {} bool pop(int &value); boot push(int value); bool full(); bool empty(); void display(); int size(); private: int _top; std::vector<int> _stack; };
В данном случае мы используем вектор фиксированного размера: для иллюстрации использования префиксных и постфиксных операций инкремента и декремента этого достаточно. (В главе 6 мы модифицируем наш стек, придав ему возможность динамически меняться.)
Элементы стека хранятся в векторе _stack. Переменная _top содержит индекс первой свободной ячейки стека. Этот индекс одновременно представляет количество заполненных ячеек. Отсюда реализация функции size(): она должна просто возвращать текущее значение _top.
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]; std::cout << "iStack::pop(): " << top_value << std::endl; return true; } bool iStack::push(int value) { std::cout << "iStack::push(" << value << ")\n"; if (full()) return false; _stack[_top++] = value; return true; }
Прежде чем протестировать наш стек на примере, добавим функцию display(), которая позволит напечатать его содержимое. Для пустого стека она выведет:
(0)
Для стека из четырех элементов – 0, 1, 2 и 3 – результатом функции display() будет:
(4)(bot: 0 1 2 3 :top)
Вот реализация функции display():
void iStack::display() { std::cout << "(" << size() << ")(bot: "; for (int ix = 0; ix < _top; ++ix) std::cout << _stack[ix] << " "; std::cout << " :top)\n"; }
А вот небольшая программа для проверки нашего стека. Цикл for выполняется 50 раз. Четное значение (2, 4, 6, 8 и т.д.) помещается в стек. На каждой итерации, кратной 5 (5, 10, 15...), распечатывается текущее содержимое стека. На итерациях, кратных 10 (10, 20, 30...), из стека извлекаются два элемента и его содержимое распечатывается еще раз.
#inc1ude <iostream> #inc1ude "iStack.h" int main() { iStack stack(32) ; stack.display(); for (int ix = 1; ix < 51; ++ix) { if (ix%2 == 0) stack.push(ix); if (ix%5 == 0) stack.display(); if (ix%10 == 0) { int dummy; stack.pop(dummy); stack.pop(dummy); stack.display(); } }
Вот результат работы программы:
(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? Как их можно исправить?