4.4. Операторы присвоения
Левым операндом оператора присвоения должно быть допускающее изменение l-значение. Ниже приведено несколько примеров недопустимых попыток присвоения.
int i = 0, j = 0, k = 0; // инициализация, а не присвоение
const int ci = i; // инициализация, а не присвоение
1024 = k; // ошибка: литерал является r-значением
i + j = k; // ошибка: арифметическое выражение - тоже r-значение
ci = k; // ошибка: ci - константа (неизменяемое l-значение)
Результат присвоения, левый операнд, является l-значением. Тип результата совпадает с типом левого операнда. Если типы левого и правого операндов отличаются, тип правого операнда преобразуется в тип левого.
k = 0; // результат: тип int, значение 0
k = 3.14159; // результат: тип int, значение 3
По новому стандарту с правой стороны можно использовать список инициализации (см. раздел 2.2.1):
k = {3.14}; // ошибка: сужающее преобразование
vector<int> vi; // первоначально пусто
vi = {0,1,2,3,4,5,6,7,8,9}; // теперь vi содержит десять элементов
// со значениями от 0 до 9
Если левый операнд имеет встроенный тип, список инициализации может содержать максимум одно значение, и это значение не должно требовать сужающего преобразования (narrowing conversion) (см. раздел 2.2.1).
Для типов классов происходящее зависит от подробностей класса. В случае вектора шаблон vector определяет собственную версию оператора присвоения, позволяющего использовать список инициализации. Этот оператор заменяет элементы вектора с левой стороны элементами списка с правой.
Независимо от типа левого операнда список инициализации может быть пуст. В данном случае компилятор создает инициализированный значением по умолчанию (см. раздел 3.3.1) временный объект и присваивает это значение левому операнду.
Оператор присвоения имеет правосторонний порядок
В отличие от других парных операторов, присвоение имеет правосторонний порядок:
int ival, jval;
ival = jval = 0; // ok: каждой переменной присвоено значение 0
Поскольку присвоение имеет правосторонний порядок, его крайняя правая часть, jval = 0, является правым операндом крайнего левого оператора присвоения. Поскольку присвоение возвращает свой левый операнд, результат крайнего правого присвоения (т.е. jval) присваивается переменной ival.
Каждый объект в множественном операторе присвоения должен иметь тип, совпадающий с типом соседа справа, или допускать преобразование в него (раздел 4.11):
int ival, *pval; // ival имеет тип int; pval имеет тип указателя на int
ival = pval = 0; // ошибка: переменной типа int нельзя присвоить
// значение указателя
string s1, s2;
s1 = s2 = "OK"; // строковый литерал "OK" преобразован в строку
Первое присвоение некорректно, поскольку объекты ival и pval имеют разные типы и не существует преобразования типа int* (pval) в тип int (ival). Оно некорректно, несмотря на то, что значение нуль может быть присвоено любому объекту.
Второе присвоение, напротив, вполне допустимо. Строковый литерал преобразуется в значение типа string, которое и присваивается переменной s2 типа string. Результат этого присвоения — строка s2 — имеет тот же тип, что и строка s1.
Оператор присвоения имеет низкий приоритет
Присвоения нередко происходят в условиях. Поскольку оператор присвоения имеет относительно низкий приоритет, его обычно заключают в скобки, чтобы он работал правильно. Чтобы продемонстрировать, чем присвоение может быть полезно в условии, рассмотрим следующий цикл. Здесь необходимо вызывать функцию до тех пор, пока она не возвратит желаемое значение, скажем 42.
// подробный, а потому более подверженный ошибкам
// способ написания цикла
int i = get_value(); // получить первое значение
while (i != 42) {
// выполнить действия ...
i = get_value(); // получить остальные значения
}
Код начинается с вызова функции get_value(), затем следует цикл, условие которого использует значение, возвращенное этим вызовом. Последним оператором этого цикла является еще один вызов функции get_value(), далее цикл повторяется. Этот код можно переписать более непосредственно:
int i;
// лучший вариант цикла, теперь вполне понятно, что делает условие
while ((i = get_value()) != 42) {
// выполнить действия ...
}
Теперь условие вполне однозначно выражает намерение разработчика: необходимо продолжать, пока функция get_value() не возвратит значение 42. В ходе вычисления условия результат вызова функции get_value() присваивается переменной i, значение которой затем сравнивается со значением 42.
Без круглых скобок операндами оператора != было бы значение, возвращенное функцией get_value() и 42, а результат проверки (true или false) был бы присвоен переменной i, чего явно не планировалось!
Поскольку приоритет оператора присвоения ниже, чем у операторов отношения, круглые скобки вокруг присвоений в условиях обычно необходимы.
Не перепутайте операторы равенства и присвоения
Тот факт, что присвоение возможно в условии, зачастую имеет удивительные последствия:
if (i = j)
Условие оператора if присваивает значение переменной j переменной i, а затем проверяет результат присвоения. Если значение переменной j отлично от нуля, то условие истинно. Однако автор этого кода почти наверняка намеревался проверить равенство значений переменных i и j так:
if (i == j)
Ошибки такого рода хоть и известны, но трудны для обнаружения. Некоторые, но не все компиляторы достаточно "любезны", чтобы предупредить о таком коде, как в этом примере.
Составные операторы присвоения
Довольно нередки случаи, когда оператор применяется к объекту, а полученный результат повторно присваивается тому же объекту. В качестве примера рассмотрим программу из раздела 1.4.2:
int sum = 0;
// сложить числа от 1 до 10 включительно
for (int val = 1; val <= 10; ++val)
sum += val; // эквивалентно sum = sum + val
Подобный вид операций характерен не только для сложения, но и для других арифметических и побитовых операторов, которые рассматриваются в разделе 4.8. Соответствующие составные операторы присвоения (compound assignment) существуют для каждого из этих операторов.
+= -= *= /= %= // арифметические операторы
<<= >>= &= ^= |= // побитовые операторы; см. p. 4.8
Каждый составной оператор по сути эквивалентен обычному, за исключением того, что, когда используется составное присвоение, левый операнд обрабатывается (оценивается) только однажды.
Но эти формы имеют одно очень важное различие: в составном операторе присвоения левый операнд вычисляется только один раз. По существу, он эквивалентен следующему:
а = а оператор b;
Если используется обычное присвоение, операнд а обрабатывается дважды: один раз в выражении с правой стороны и во второй раз — как операнд слева. В подавляющем большинстве случаев это различие несущественно, возможно, кроме тех, где критически важна производительность.
Упражнения раздела 4.4
Упражнение 4.13. Каковы значения переменных i и d после каждого присвоения?
int i; double d;
(a) d = i = 3.5; (b) i = d = 3.5;
Упражнение 4.14. Объясните, что происходит в каждом из следующих операторов if?
if (42 = i) // ...
if (i = 42) // ...
Упражнение 4.15. Следующее присвоение недопустимо. Почему? Как исправить ситуацию?
double dval; int ival; int *pi;
dval = ival = pi = 0;
Упражнение 4.16. Хотя ниже приведены вполне допустимые выражения, их поведение может оказаться не таким, как предполагалось. Почему? Перепишите выражения так, чтобы они стали более понятными.
(a) if (p = getPtr() != 0)
(b) if (i = 1024)
Более 800 000 книг и аудиокниг! 📚
Получи 2 месяца Литрес Подписки в подарок и наслаждайся неограниченным чтением
ПОЛУЧИТЬ ПОДАРОК