Язык программирования C++. Пятое издание - Стенли Липпман
Шрифт:
Интервал:
Закладка:
4.1.1. Фундаментальные концепции
Существуют унарные операторы (unary operator) и парные операторы (binary operator). Унарные операторы, такие как обращение к адресу (&) и обращение к значению (*), воздействуют на один операнд. Парные операторы, такие как равенство (==) и умножение (*), воздействуют на два операнда. Существует также (всего один) тройственный оператор (ternary operator), который использует три операнда, а также оператор вызова функции (function call), который получает неограниченное количество операндов.
Некоторые символы (symbol), например *, используются для обозначения как унарных (обращение к значению), так и парных (умножение) операторов. Представляет ли символ унарный оператор или парный, определяет контекст, в котором он используется. В использовании таких символов нет никакой взаимосвязи, поэтому их можно считать двумя разными символами.
Группировка операторов и операндовЧтобы лучше понять порядок выполнения выражений с несколькими операторами, следует рассмотреть концепцию приоритета (precedence), порядка (associativity) и порядка вычисления (order of evaluation) операторов. Например, в следующем выражении используются сложение, умножение и деление:
5 + 10 * 20/2;
Операндами оператора * могли бы быть числа 10 и 20, либо 10 и 20/2, либо 15 и 20, либо 15 и 20/2. Понимание таких выражений и является темой следующего раздела.
Преобразование операндовВ ходе вычисления выражения операнды нередко преобразуются из одного типа в другой. Например, парные операторы обычно ожидают операндов одинакового типа. Но операторы применимы и к операндам с разными типами, если они допускают преобразование (см. раздел 2.1.2) в общий тип.
Хотя правила преобразования довольно сложны, по большей части они очевидны. Например, целое число можно преобразовать в число с плавающей запятой, и наоборот, но преобразовать тип указателя в число с плавающей точкой нельзя. Немного неочевидным может быть то, что операнды меньших целочисленных типов (например, bool, char, short и т.д.) обычно преобразуются (promotion) в больший целочисленный тип, как правило int. Более подробная информация о преобразованиях приведена в разделе 4.11.
Перегруженные операторыЗначение операторов для встроенных и составных типов определяет сам язык. Значение большинства операторов типов классов мы можем определить самостоятельно. Поскольку такие определения придают альтернативное значение существующему символу оператора, они называются перегруженными операторами (overloaded operator). Операторы >> и << библиотеки ввода и вывода, а также операторы, использовавшиеся с объектами строк, векторов и итераторов, являются перегруженными операторами.
При использовании перегруженного оператора его смысл, а также тип операндов и результата зависят от того, как определен оператор. Однако количество операндов, их приоритет и порядок не могут быть изменены.
L- и r-значенияКаждое выражение в языке С++ является либо r-значением (r-value), либо l-значением (l-value). Эти названия унаследованы от языка С и первоначально имели простую мнемоническую цель: l-значения могли стоять слева от оператора присвоения, а r-значения не могли.
В языке С++ различие не так просто. В языке С++ выражение l-значения возвращает объект или функцию. Однако некоторые l-значения, такие как константные объекты, не могут быть левым операндом присвоения. Кроме того, некоторые выражения возвращают объекты, но возвращают их как r-, а не l-значения. Короче говоря, при применении объекта в качестве r-значения используется его значение (т.е. его содержимое). При применении объекта в качестве l-значения используется его идентификатор (т.е. его область в памяти).
Операторы различаются по тому, требуют ли они операндов l- или r-значения, а также по тому, возвращают ли они l- или r-значения. Важный момент здесь в том, что (за одним исключением, рассматриваемым в разделе 13.6) l-значение можно использовать там, где требуется r-значение, однако нельзя использовать r-значение там, где требуется l-значение (т.е. область). Когда l-значение применяется вместо r-значения, используется содержимое объекта (его значение). Мы уже использовали несколько операторов, которые задействовали l-значения.
• В качестве своего левого операнда оператор присвоения требует (неконстантного) l-значения и возвращает свой левый операнд как l-значение.
• Оператор обращения к адресу (см. раздел 2.3.2) требует в качестве операнда l-значение и возвращает указатель на свой операнд как r-значение.
• Встроенные операторы обращения к значению и индексирования (см. раздел 2.3.2 и раздел 3.5.2), а также обращение к значению итератора и операторы индексирования строк и векторов (см. раздел 3.4.1, раздел 3.2.3 и раздел 3.3.3) возвращают l-значения.
• Операторы инкремента и декремента, как встроенные, так и итератора (см. раздел 1.4.1 и раздел 3.4.1), требуют l-значения в качестве операндов. Их префиксные версии (которые использовались до сих пор) также возвращают l-значения.
Рассматривая операторы, следует обратить внимание на то, должен ли операнд быть l-значением и возвращает ли он l-значение.
L- и r-значения также различаются при использовании спецификатора decltype (см. раздел 2.5.3). При применении спецификатора decltype к выражению (отличному от переменной) результатом будет ссылочный тип, если выражение возвращает l-значение. Предположим, например, что указатель p имеет тип int*. Поскольку обращение к значению возвращает l-значение, выражение decltype(*p) имеет тип int&. С другой стороны, поскольку оператор обращения к адресу возвращает r-значение, выражение decltype(&p) имеет тип int**, т.е. указатель на указатель на тип int.
4.1.2. Приоритет и порядок
Выражения с двумя или несколькими операторами называются составными (compound expression). Результат составного выражения определяет способ группировки операндов в отдельных операторах. Группировку операндов определяют приоритет и порядок. Таким образом, они определяют, какие части выражения будут операндами для каждого из операторов в выражении. При помощи скобок программисты могут изменять эти правила, обеспечивая необходимую группировку.
Обычно значение выражения зависит от того, как группируются его части. Операнды операторов с более высоким приоритетом группируются прежде операндов операторов с более низким приоритетом. Порядок определяет то, как группируются операнды с тем же приоритетом. Например, операторы умножения и деления имеют одинаковый приоритет относительно друг друга, но их приоритет выше, чем у операторов сложения и вычитания. Поэтому операнды операторов умножения и деления группируются прежде операндов операторов сложения и вычитания. Арифметические операторы имеют левосторонний порядок, т.е. они группируются слева направо.
• Благодаря приоритету результатом выражения 3 + 4*5 будет 23, а не 35.
• Благодаря порядку результатом выражения 20-15-3 будет 2, а не 8.
В более сложном примере при вычислении слева направо следующего выражения получается 20:
6 + 3 * 4 / 2 + 2
Вполне возможны и другие результаты: 9, 14 и 36. В языке С++ результат составит 14, поскольку это выражение эквивалентно следующему:
// здесь круглые скобки соответствуют стандартному приоритету и порядку
((6 + ((3 * 4) / 2)) + 2)
Круглые скобки переопределяют приоритет и порядокКруглые скобки позволяют переопределить обычную группировку. Выражения в круглых скобках обрабатываются как отдельные модули, а во всех остальных случаях применяются обычные правила приоритета. Например, используя круглые скобки в предыдущем выражении, можно принудительно получить любой из четырех возможных вариантов:
// круглые скобки обеспечивают альтернативные группировки
cout << (6 + 3) * (4 / 2 + 2) << endl; // выводит 36
cout << ((6 + 3) * 4) / 2 + 2 << endl; // выводит 20
cout << 6 + 3 * 4 / (2 + 2) << endl; // выводит 9
Когда важны приоритет и порядокМы уже видели примеры, где приоритет влияет на правильность наших программ. Рассмотрим обсуждавшийся в разделе 3.5.3 пример обращения к значению и арифметических действий с указателями.
int ia[] = {0,2,4,6,8}; // массив из 5 элементов типа int
int last = *(ia + 4); // ok: инициализирует last значением