Таблица 1.4. Побитовые операторы
Операция
Выражение
Дополнение до единицы
x
Левый сдвиг x << y
Правый сдвиг x >> y
Побитовое И
x & y
Побитовое исключающее ИЛИ
x ^ y
Побитовое включающее ИЛИ
x | y
Операция x << y сдвигает биты x влево на y позиций. И наоборот, x >> y сдвигает биты x на y позиций вправо. В большинстве случаев на свободные мес- та добавляются нулевые биты, за исключением отрицательных значений типов signed при сдвиге вправо — такой сдвиг зависит от реализации. Побитовое И можно использовать для проверки значения определенного бита значения. Поби- товое включающее ИЛИ может установить бит, а исключающее — изменить его значение на противоположное. Эти операции более важны для системного про- граммирования, чем для научного. В качестве алгоритмического развлечения мы используем их в разделе 3.6.1.
02_ch01.indd 41 14.07.2016 10:46:09
Основы C++
42
1.3.4. Присваивание
Значение объекта (модифицируемого lvalue) может быть установлено с помо- щью присваивания:
object = expr;
Если типы не совпадают, значение выражения expr преобразуется в тип объек- та object, если это возможно. Присваивание является правоассоциативной опе- рацией, так что можно последовательно присвоить значение нескольким объектам в одном выражении:
o3 = o2 = o1 = expr;
Составные операторы присваивания применяют арифметическую или побито- вую операцию к объекту слева от оператора с аргументом справа от него. Напри- мер, следующие две операции эквивалентны:
a += b; // Эквивалентно выражению в следующей строке a = a + b;
Все операторы присваивания имеют более низкий приоритет, чем все арифме- тические и побитовые операции, поэтому перед выполнением составного присва- ивания всегда вычисляется выражение справа от него:
a *= b + c; // Эквивалентно выражению в следующей строке a = a * (b + c);
Операторы присваивания перечислены в табл. 1.5. Все они правоассоциативны и имеют один и тот же приоритет.
Таблица 1.5. Операторы присваивания
Операция
Выражение
Простое присваивание x = y
Умножение и присваивание x *= y
Деление и присваивание x /= y
Деление по модулю и присваивание x %= y
Сложение и присваивание x += y
Вычитание и присваивание x -= y
Сдвиг влево и присваивание x <<= y
Сдвиг вправо и присваивание x >>= y
И и присваивание x &= y
Включающее ИЛИ и присваивание x |= y
Исключающее ИЛИ и присваивание x ^= y
1.3.5. Поток выполнения
Имеется три оператора управления потоком выполнения программы. Вызов функции в C++ обрабатывается как оператор. Подробное описание функций и их вызовов приведено в разделе 1.5.
02_ch01.indd 42 14.07.2016 10:46:09
1.3. Операторы
43Условный оператор c ? x : y вычисляет условие c, и, когда оно истинно, выра- жение имеет значение x, и y — в противном случае. Он может использоваться как альтернатива ветвлению с помощью конструкции if, особенно в тех местах, где разрешается только выражение, но не инструкция (см. раздел 1.4.3.1).
Очень специфичным оператором в C++ является
оператор запятой, который обеспечивает последовательное вычисление. Его смысл просто в том, что сначала вычисляется подвыражение слева от запятой, а затем — справа от нее. Значением всего выражения является значение правого подвыражения:
3+4
, 7*9.3
Результатом выражения является 65.1, а вычисление первого подвыражения со- вершенно не играет роли. Подвыражения также
могут содержать оператор запя- той, поэтому могут быть определены произвольно длинные последовательности выражений. С помощью оператора запятой можно вычислить несколько выраже- ний в тех местах программы, где допускается только одно выражение. Типичным примером является увеличение нескольких индексов в цикле for (раздел 1.4.4.2):
++i
, ++j
При использовании в качестве аргумента функции выражение с запятой нуж- дается в окружающих скобках; в противном случае запятая интерпретируется как разделитель аргументов функции.
1.3.6. Работа с памятьюОператоры new и delete соответственно выделяют и освобождают память
(см. раздел 1.8.2).
1.3.7. Операторы доступаC++ предоставляет несколько операторов для доступа к подструктурам, полу- чения адреса переменной и разыменования (обращения к памяти по указанному адресу). Обсуждать эти операторы до того, как мы рассмотрим указатели и клас- сы, не имеет никакого смысла. Так что мы просто отложим их описание до разде- лов, указанных в табл. 1.6.
Таблица 1.6. Операторы доступа
ОперацияВыражениеРазделВыбор члена x.m
2.2.3
Разыменующий выбор члена p->m
2.2.3
Индексация x[i]
1.8.1
Разыменование
*x
1.8.2
Разыменование члена x.*q
2.2.3
Разыменование разыменованного члена p->*q
2.2.3 02_ch01.indd 43 14.07.2016 10:46:09
Основы C++
441.3.8. Работа с типамиОператоры для работы с типами будут представлены в главе 5, “Метапрограм- мирование”, когда мы будем писать программы времени компиляции, работаю- щие с типами. Доступные операторы перечислены в табл. 1.7.
Обратите внимание, что оператор sizeof при использовании с выражением является единственным, применимым без скобок. Оператор alignof появил- ся в C++11; все прочие операторы имеются в языке по крайней мере начиная с
C++98.
Таблица 1.7. Операторы для работы с типами
ОперацияВыражениеИдентификация типа времени выполнения typeid(x)
Идентификация типа typeid(t)
Размер объекта sizeof(x) или sizeof x
Размер типа sizeof(t)
Количество аргументов sizeof...(p)
Количество аргументов типа sizeof...(P)
Выравнивание alignof(x)
Выравнивание типа alignof(t)
1.3.9. Обработка ошибокОператор throw используется для указания исключения во время выполнения
(например, нехватки памяти) (см. раздел 1.6.2).
1.3.10. ПерегрузкаОчень важным аспектом C++ является то, что программист может определить операторы для новых типов. Этот вопрос будет поясняться в разделе 2.7. Опе- раторы встроенных типов изменить нельзя. Однако мы можем определить, как встроенные типы взаимодействуют с новыми типами, т.е. мы можем перегрузить смешанные операции наподобие удвоения матрицы.
Большинство операторов могут быть перегружены. Исключениями являются следующие:
::
разрешение области видимости;
выбор члена (может быть добавлен в C++17);
.*
выбор
члена через указатель;
?:
условный оператор;
sizeof размер типа или объекта;
sizeof...
количество аргументов;
alignof выравнивание памяти типа или объекта;
typeid идентификатор типа.
02_ch01.indd 44 14.07.2016 10:46:09
1.3. Операторы
45
Перегрузка операторов в C++ дает нам очень большую свободу, и мы должны мудро ее использовать. Мы вернемся к этой теме в следующей главе, когда дейс- твительно будем перегружать операторы (подождите до раздела 2.7).
1.3.11. Приоритеты операторов
В табл. 1.8 дан краткий обзор приоритетов операторов. Для компактности мы объединяем обозначения для типов и выражений (например, typeid) и различ- ные записи для new и delete. Символ @= представляет все вычисляющие присва- ивания, такие как
+=, *= и т.д. Более подробно операторы и их семантика пред- ставлены в приложении В, “Определения языка”, в табл. В.1.
Таблица 1.8. Приоритеты операторов
Приоритеты операторов
class
::member
nspace
::member
::name
::qualified-name
object
.member
pointer
->member
expr
[expr]
expr(expr_list)
type(expr_list)
lvalue
++
lvalue
-- typeid(type/expr)
*_cast<type>(expr)
sizeof expr
sizeof(type)
sizeof...(pack)
alignof(type/expr)
++lvalue
--lvalue
expr
!expr
-expr
+expr
&lvalue
*expr
new… type…
delete []
opt
pointer
(type) expr
object
.*member_ptr
pointer
->*member_ptr
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
lvalue
= expr
lvalue
@= expr
throw expr
expr, expr
02_ch01.indd 45 14.07.2016 10:46:10
Основы C++
46
1.3.12. Избегайте побочных эффектов!
Безумие, делая одно и то же снова и снова,
ожидать увидеть разные результаты.
— Неизвестный
5
Ожидать разных результатов для одних и тех же входных данных в приложе- ниях с побочными эффектами — вовсе не безумие. Напротив, очень трудно пред- сказать поведение программы, компоненты которой мешают одни другим. Кроме того, пожалуй, лучше уж иметь детерминированную программу с неправильным результатом, чем программу, которая дает правильный результат только иногда, поскольку последнюю обычно намного сложнее исправить.
В стандартной библиотеке C имеется функция для копирования строки
(
strcpy). Эта функция принимает указатели на первые символы источника и це- левого объекта и копирует последующие символы до тех пор, пока не встретит нулевой символ. Ее можно реализовать с помощью единственного цикла, который имеет пустое тело (!) и выполняет копирование и инкремент как побочные эф- фекты теста продолжения выполнения цикла:
while(*tgt++ = *src++);
Выглядит страшно? Ну, самую малость. Однако это абсолютно законный код
C++, хотя некоторые компиляторы могут и пожаловаться на него в педантичном режиме работы. Это неплохая разминка для мозгов — потратить некоторое время на размышления о приоритетах операторов, типах подвыражений и порядке вы- числений.
Давайте рассмотрим кое-что попроще. Присвоим i-му элементу массива значе- ние i и увеличим значение i для следующей итерации:
v[i]= i++;
Выглядит так, как будто нет никаких проблем. Но они есть — поведение этого выражения не определено. Почему? Постфиксный инкремент i гарантирует, что мы присвоим старое значение i, а затем увеличим эту переменную. Однако этот шаг может быть выполнен до того, как будет вычислено выражение v[i], так что может быть выполнено присваивание значения i элементу v[i+1].
Последний пример должен показать вам, что побочные эффекты, на первый взгляд, не всегда очевидны. Некоторые довольно сложные трюки могут сработать, а гораздо более простые — нет. Еще хуже, что что-то может работать некоторое время, до тех пор, пока кто-то не скомпилирует этот код на другом компиляторе или на новой версии компилятора, в котором окажутся измененными некоторые детали реализации.
5
Ложно приписывалось Альберту Эйнштейну, Бенджамину Франклину и Марку Твену.
02_ch01.indd 46 14.07.2016 10:46:10
1.3. Операторы
47Первый фрагмент является примером отличных навыков программирования и доказательства того, что приоритет операторов имеет смысл — скобки в данном случае не нужны. Тем не менее такой стиль программирования не годится для современного C++. Стремление как можно больше сократить код восходит к вре- менам раннего C, когда ввод был более сложным, с использованием механических
(даже не электрических) клавиатур и перфораторов, да еще и без мониторов. При сегодняшних технологиях ввод немного большего количества букв проблемой быть не должен.
Еще одним неблагоприятным аспектом лаконичной реализации копирования является смешение различных задач: тестирования, модификации и обхода. В раз- работке программного обеспечения весьма важной концепцией является
разделение проблем. Оно способствует повышению гибкости и снижению сложности. В дан- ном случае мы хотим упростить понимание реализации копирования. Применение этого принципа к однострочной реализации копирования дает следующий код:
for (; *src; tgt++, src++)
*tgt= *src;
*tgt= *src; // Копирование завершающего нулевого символа
Так мы четко разделяем три проблемы:
тестирование:
•
*src;
изменение:
•
*tgt= *src;
обход:
•
tgt++, src++.
Становится также более очевидным, что
приращение выполняется над указа- телями, а тестирование и присваивание — над содержимым, на которое они ука- зывают. Реализация оказывается не столь компактной, как раньше, но зато стало гораздо проще проверять правильность ввода. Целесообразно также сделать более очевидной проверку на равенство нулю (
*src != 0).
Существует класс языков программирования, которые называются
функци-ональными языками. Значения в таких языках нельзя изменить после того, как они были установлены. Очевидно, что C++ к таким языкам программирования не относится. Но мы получим большое преимущество при программировании в функциональном стиле, там, где это имеет смысл. Например, когда мы записыва- ем присваивание, единственное, что должно измениться, — это переменная сле- ва от знака присваивания. Поэтому следует заменить изменения константными выражениями, например
++i на i+1. Правая сторона выражения без побочных эффектов облегчает как понимание поведения программы, так и оптимизацию кода компилятором. Как правило, более понятные программы обладают лучшим потенциалом для оптимизации.
02_ch01.indd 47 14.07.2016 10:46:11
Основы C++
481.4. Выражения и инструкцииC++ различает выражения и инструкции. Можно было бы вскользь заметить, что каждое выражение становится инструкцией после добавления точки с запя- той, однако мы хотели бы обсудить эту тему немного подробнее.
1.4.1. ВыраженияДавайте строить их рекурсивно снизу вверх. Любое имя переменной (
x, y, z, …), константа или литерал — это выражение. Одно или несколько выражений, объединенных оператором,
также представляют собой выражения, например x + y или x * y + z. В некоторых языках, таких как Pascal, присваивание является инструкцией. Но в C++ это выражение, например x = y + z. Поэтому оно может использоваться внутри другого присваивания: x2 = x = y + z. Присваивания вы- числяются справа налево. Также являются выражениями операции ввода-вывода, такие как std::cout << "x = " << x << "\n"
Вызов функции с аргументами, которые представляют собой выражения, так- же является выражением, например abs(x) или abs(x * y + z). Таким образом, вызовы функций могут быть вложенными: pow(abs(x),y). Обратите внимание, что вложенность была бы невозможна, если бы вызовы функций были инструк- циями.
Поскольку присваивание является выражением, его можно использовать в ка- честве аргумента функции: abs(x=y). Аргументами функции могут быть и опе- рации ввода-вывода, например print(std::cout << "x = " << x << "\n", "Я такой приколист!");
Нет нужды говорить, что это не особо удобочитаемо и вызовет больше путани- цы, чем принесет пользы. Выражение в скобках также является выражением, как, например,
(x+y). Поскольку приоритет скобок выше приоритета любого опера- тора, мы всегда можем изменить порядок вычисления для удовлетворения наших потребностей. Так, в x*(y+z) сначала вычисляется сумма.
1.4.2. ИнструкцииИнструкцией является любое из представленных выше выражений, за кото- рым следует точка с запятой, например x= y + z; y= f(x + z) * 3.5;
Инструкция наподобие y + z;
разрешена, несмотря на то что она (скорее всего) совершенно бесполезна. Во вре- мя выполнения программы вычисляется сумма y и z, которая затем игнорируется.
02_ch01.indd 48 14.07.2016 10:46:11
1.4. Выражения и инструкции
49Современные компиляторы оптимизируют код и выбрасывают такие бесполезные вычисления. Однако не гарантируется, что такая инструкция всегда будет опуще- на. Если y или z является объектом пользовательского типа, то операция сложе- ния также
определена пользователем и может изменять, например, y, z или что-то еще. Очевидно, что это плохой (хотя и вполне законный в C++) стиль программи- рования (наличие скрытого побочного эффекта).
Отдельная точка с запятой является пустой инструкцией, так что после выра- жения мы можем ставить столько точек с запятой, сколько захотим. Некоторые инструкции не заканчиваются точкой с запятой, например определения функций.
Если добавить к таким инструкциям точку с запятой, это не будет ошибкой — просто будет добавлена дополнительная пустая инструкция. Тем не менее неко- торые компиляторы в педантичном режиме могут вывести соответствующее пре- дупреждение. Любая последовательность инструкций, окруженная фигурными скобками, является
составной инструкцией.
Объявления переменных и констант, которые мы видели раньше, также явля- ются инструкциями. В качестве начального значения переменной или константы мы можем использовать любое выражение (за исключением другого присваива- ния или оператора запятой). Другие инструкции, которые мы рассмотрим, вклю- чают определения функций и классов, а также управляющие инструкции, с кото- рыми вы познакомитесь в следующем разделе.
За исключением условного оператора поток выполнения программы управля- ется инструкциями. Здесь мы различаем ветвления и циклы.
1.4.3. ВетвлениеВ этом разделе рассмотрим различные возможности, которые позволяют нам выбрать ветвь выполнения программы.
1.4.3.1. Инструкция ifЭто простейшая форма управления,
смысл которой интуитивно очевиден, на- пример
if (weight > 100.0) cout << " Достаточно тяжело.\n";
else cout << "Такой груз я донесу.\n";
Зачастую ветвь else не нужна и может быть опущена. Скажем, у нас есть зна- чение в переменной x и нам необходимо его абсолютное значение: