Главная страница

Язык программирования C Пятое издание


Скачать 1.85 Mb.
НазваниеЯзык программирования C Пятое издание
Дата15.07.2019
Размер1.85 Mb.
Формат файлаpdf
Имя файла620354-www.libfox.ru.pdf
ТипДокументы
#84130
страница10 из 54
1   ...   6   7   8   9   10   11   12   13   ...   54
Упражнение 4.1. Какое значение возвратит выражение 5 + 10 * 20/2?
Упражнение 4.2. Используя таблицу раздела 4.12, расставьте скобки в следующих выражениях, чтобы обозначить порядок группировки операндов:
(а) * vec.begin() (b) * vec.begin() + 1 4.1.3. Порядок вычисления
Приоритет определяет группировку операндов. Но он ничего не говорит о порядке, в котором обрабатываются операнды. В большинстве случаев порядок не определен. В следующем выражении известно, что функции f1() и f2() будут вызваны перед умножением: int i = f1() * f2();
В конце концов, умножаются именно их результаты. Тем не менее нет никакого способа узнать, будет ли функция f1() вызвана до функции f2(), или наоборот.
Для операторов, которые не определяют порядок вычисления, выражение, пытающееся обратиться к тому же объекту и
Page 177/1103
изменить его , было бы ошибочным. Выражения, которые действительно так поступают,
имеют непредсказуемое поведение (см. раздел 2.1.2). Вот простой пример: оператор <<
не дает никаких гарантий в том, как и когда обрабатываются его операнды. В результате следующее выражение вывода непредсказуемо: int i = 0; cout << i << " " << ++i << endl; // непредсказуемо
Непредсказуемость этой программы в том, что нет никакой возможности сделать выводы о ее поведении. Компилятор мог бы сначала обработать часть ++i, а затем часть i, тогда вывод будет 1 1. Но компилятор мог бы сначала обработать часть i, тогда вывод будет 0 1.
Либо компилятор мог бы сделать что-то совсем другое . Поскольку у этого выражения неопределенное поведение, программа ошибочна, независимо от того, какой код создает компилятор.
Четыре оператора действительно гарантируют порядок обработки операндов. В разделе 3.2.3
упоминалось о том, что оператор логического AND (&&) гарантирует выполнение сначала левого операнда. Кроме того, он гарантирует, что правый операнд обрабатывается только при истинности левого операнда. Другими операторами, гарантирующими порядок обработки операндов, являются оператор логического OR (||) (раздел 4.3), условный оператор
(? :) (раздел 4.7) и оператор запятая (,) (раздел 4.10). Порядок вычисления, приоритет и порядок операторов
Порядок вычисления операндов не зависит от приоритета и порядка операторов. Рассмотрим следующее выражение: f() + g() * h() + j()
• Приоритет гарантирует умножение результатов вызова функций g() и h().
• Порядок гарантирует добавление результата вызова функции f() к произведению g() и h(), а также добавление результата сложения к результату вызова функции j().
• Однако нет никаких гарантий относительно порядка вызова этих функций.
Если функции f(), g(), h() и j() являются независимыми и не влияют на состояние тех же объектов или выполняют ввод и вывод, то порядок их вызова несуществен. Но если любые из этих функций действительно воздействуют на тот же объект, то выражение ошибочно, а его поведение непредсказуемо. Упражнения раздела 4.1.3
Упражнение 4.3. Порядок вычисления большинства парных операторов оставляется неопределенным, чтобы предоставить компилятору возможность для оптимизации. Эта стратегия является компромиссом между созданием эффективного кода и потенциальными проблемами в использовании языка программистом. Полагаете этот компромисс приемлемым? Кто-то да, кто- то нет. Совет. Манипулирование составными выражениями
При написании составных выражений могут пригодиться два эмпирических правила.
1. В сомнительных случаях заключайте выражения в круглые скобки, чтобы явно сгруппировать операнды в соответствии с логикой программы.
2. При изменении значения операнда не используйте этот операнд в другом месте того же оператора.
Page 178/1103

Важнейшим исключением из второго правила является случай, когда часть выражения,
изменяющая операнд, сама является операндом другой части выражения. Например, в выражении *++iter инкремент изменяет значение итератора iter, а измененное значение используется как операнд оператора *. В этом и подобных выражениях порядок обработки операндов не является проблемным. Но в больших выражениях те части, которые изменяют операнд, должны обрабатываться в первую очередь. Такой подход не создает никаких проблем и применяется достаточно часто.
4.2. Арифметические операторы
Таблица 4.1. Арифметические операторы
(левосторонний порядок) Оператор Действие Применение + Унарный плюс + выражение - Унарный минус - выражение * Умножение выражение * выражение / Деление выражение / выражение % Остаток выражение % выражение + Сложение выражение + выражение - Вычитание выражение - выражение
В табл. 4.1 (и таблицах операторов последующих разделов) операторы сгруппированы по приоритету. Унарные арифметические операторы имеют более высокий приоритет, чем операторы умножения и деления, которые в свою очередь имеют более высокий приоритет,
чем парные операторы вычитания и сложения. Операторы с более высоким приоритетом группируются перед операторами с более низким приоритетом. Все эти операторы имеют левосторонний порядок, т.е. при равенстве приоритетов они группируются слева направо.
Если не указано иное, то арифметические операторы могут быть применены к любому арифметическому типу (см. раздел 2.1.1) или любому типу, который может быть преобразован в арифметический тип. Операнды и результаты этих операторов являются r-значениями. Как упоминается в разделе 4.11, в ходе вычисления операторов их операнды малых целочисленных типов преобразуются в больший целочисленный тип и все операнды могут быть преобразованы в общий тип.
Унарные операторы плюс и минус могут быть также применены к указателям. Использование парных операторов + и - с указателями рассматривалось в разделе 3.5.3. Будучи
Page 179/1103
примененным к указателю или арифметическому значению, унарный плюс возвращает
(возможно, преобразованную) копию значения своего операнда.
Унарный оператор минус возвращает отрицательную копию (возможно, преобразованную)
значения своего операнда. int i = 1024; int k = -i; // i равно -1024 bool b = true; bool b2 = -b; // b2 равно true!
В разделе 2.1.1 упоминалось, что значения типа bool не нужно использовать для вычислений.
Результат -b — хороший пример того, что имелось в виду.
Для большинства операторов операнды типа bool преобразуются в тип int. В данном случае значение переменной b, true, преобразуется в значение 1 типа int (см. раздел 2.1.2). Это
(преобразованное) значение преобразуется в отрицательное, -1. Значение -1 преобразуется обратно в тип bool и используется для инициализации переменной b2. Поскольку значение инициализатора отлично от нуля, при преобразовании в тип bool его значением станет true.
Таким образом, значением b2 будет true! Внимание! Переполнение переменной и другие арифметические особенности
Некоторые арифметические выражения возвращают неопределенный результат. Некоторые из этих неопределенностей имеют математический характер, например деление на нуль.
Причиной других являются особенности компьютеров, например, переполнение,
происходящее при превышении вычисленным значением размера области памяти,
представленной его типом.
Предположим, тип short занимает на машине 16 битов. В этом случае переменная типа short способна хранить максимум значение 32767. На такой машине следующий составной оператор присвоения приводит к переполнению. short short_value = 32767; // максимальное значение при short 16 битов short_value += 1; // переполнение cout << "short_value: " << short_value << endl;
Результат присвоения 1 переменной short_value непредсказуем. Для хранения знакового значения 32768 требуется 17 битов, но доступно только 16. Многие системы никак не предупреждают о переполнении ни во время выполнения, ни во время компиляции.
Подобно любой ситуации с неопределенностью, результат оказывается непредсказуем. На системе авторов программа завершилась с таким сообщением: short value: -32768
Page 180/1103

Здесь произошло переполнение переменной: предназначенный для знака разряд содержал значение 0, но был заменен на 1, что привело к появлению отрицательного значения. На другой системе результат мог бы быть иным, либо программа могла бы повести себя по-другому, включая полный отказ.
Примененные к объектам арифметических типов, операторы +, -, * и / имеют вполне очевидные значения: сложение, вычитание, умножение и деление. Результатом деления целых чисел является целое число. Получаемая в результате деления дробная часть отбрасывается. int ival1 = 21/6; // ival1 равно 3; результат усекается
// остаток отбрасывается int ival2 = 21/7; // ival2 равно 3; остатка нет;
// результат - целочисленное значение
Оператор % известен как остаток (remainder), или оператор деления по модулю (modulus). Он позволяет вычислить остаток от деления левого операнда на правый. Его операнды должны иметь целочисленный тип. int ival = 42; double dval = 3.14; ival % 12; // ok: возвращает 6 ival % dval; // ошибка: операнд с плавающей запятой
При делении отличное от нуля частное позитивно, если у операндов одинаковый знак, и отрицательное в противном случае. Прежние версии языка разрешали округление отрицательного частного вверх или вниз; однако новый стандарт требует округления частного до нуля (т.е. усечения).
Оператор деления по модулю определен так, что если m и n целые числа и n отлично от нуля, то (m/n)*n + m%n равно m. По определению, если m%n отлично от нуля, то у него тот же знак, что и у m. Прежние версии языка разрешали результату выражения m%n иметь тот же знак, что и у m, причем на реализациях, у которых отрицательный результат выражения m/n округлялся не до нуля, но такие реализации сейчас запрещены. Кроме того, за исключением сложного случая, где -m приводит к переполнению, (-m)/n и m/(-n) всегда эквивалентны -(m/n),
m%(-n) эквивалентно m%n и (-m)%n эквивалентно -(m%n). А конкретно:
Page 181/1103

21 % 6; /* результат 3 */ 21 / 6; /* результат 3 */
21 % 7; /* результат 0 */ 21 / 7; /* результат 3 */
-21 % -8; /* результат -5 */ -21 / -8; /* результат 2 */
21 % -5; /* результат 1 */ 21 / -5; /* результат -4 */Упражнения раздела 4.2
Упражнение 4.4. Расставьте скобки в следующем выражении так, чтобы продемонстрировать порядок его обработки. Проверьте свой ответ, откомпилировав и отобразив результат выражения без круглых скобок.
12 / 3 * 4 + 5 * 15 + 24 % 4 / 2
Упражнение 4.5. Определите результат следующих выражений:
(а) -30 * 3 + 21 / 5 (b) -30 + 3 * 21 / 5
(с) 30 / 3 * 21 % 5 (d) -30 / 3 * 21 % 4
Упражнение 4.6. Напишите выражение, чтобы определить, является ли значение типа int четным или нечетным.
Упражнение 4.7. Что значит переполнение? Представьте три выражения, приводящих к переполнению.
4.3. Логические операторы и операторы отношения
Операторам отношения передают операторы арифметического типа или типа указателя, а логическим операторам — операнды любого типа, допускающего преобразование в тип bool.
Все они возвращают значение типа bool. Арифметические операнды и указатели со значением нуль рассматриваются как значение false, а все другие как значение true.
Операнды для этих операторов являются r-значениями, а результат — r-значение.
Таблица 4.2. Логические операторы и операторы отношения Порядок Оператор Действие
Применение Правосторонний ! Логическое NOT !
Page 182/1103
выражение Левосторонний < Меньше выражение < выражение Левосторонний <= Меньше или равно выражение <= выражение Левосторонний > Больше выражение > выражение Левосторонний >= Больше или равно выражение >= выражение Левосторонний == Равно выражение == выражение Левосторонний != Не равно выражение != выражение Левосторонний && Логическое AND выражение && выражение Левосторонний || Логическое OR выражение || выражение Операторы логического AND и OR
Общим результатом оператора логического AND (&&) является true, если и только если оба его операнда рассматриваются как true. Оператор логического OR (||) возвращает значение true, если любой из его операндов рассматривается как true.
Операторы логического AND и OR всегда обрабатывают свой левый операнд перед правым.
Кроме того, правый операнд обрабатывается, если и только если левый операнд не определил результат. Эта стратегия известна как вычисление по сокращенной схеме (short-circuit evaluation).
• Правая сторона оператора && вычисляется, если и только если левая сторона истинна.
• Правая сторона оператора || вычисляется, если и только если левая сторона ложна.
Оператор логического AND использовался в некоторых из программ главы 3. Эти программы использовали левый операнд для проверки, безопасно ли выполнять правый операнд.
Например, условие цикла for в разд 3.2.3: сначала проверялось, что index не достиг конца строки: index != s.size() && ! isspace(s[index])
Это гарантировало, что правый операнд не будет выполнен, если индекс уже вышел из диапазона.
Page 183/1103

Рассмотрим пример применения оператора логического OR. Предположим, что в векторе строк имеется некоторый текст, который необходимо вывести, добавляя символ новой строки после каждой пустой строки или после строки, завершающейся точкой. Для отображения каждого элемента используем серийный оператор for (раздел 3.2.3):
// обратите внимание, s - ссылка на константу; элементы не копируются и
// не могут быть изменены for (const auto &s : text) { // для каждого элемента text cout << s; // вывести текущий элемент
// пустые строки и строки, завершающиеся точкой, требуют новой строки if (s.empty() || s[s.size() - 1] == '.') cout << endl; else cout << " "; // в противном случае отделить пробелом
}
После вывода текущего элемента выясняется, есть ли необходимость выводить новую строку. Условие оператора if сначала проверяет, не пуста ли строка s. Если это так, то необходимо вывести новую строку независимо от значения правого операнда. Только если строка не пуста, обрабатывается второе выражение, которое проверяет, не заканчивается ли строка точкой. Это выражение полагается на вычисление по сокращенной схеме оператора ||,
гарантирующего индексирование строки s, только если она не пуста.
Следует заметить, что переменная s объявлена как ссылка на константу (см. раздел 2.5.2).
Элементами вектора text являются строки, и они могут быть очень большими, а использование ссылки позволяет избежать их копирования. Поскольку запись в элементы не нужна, объявляем s ссылкой на константу. Оператор логического NOT
Оператор логического NOT (!) возвращает инверсию исходного значения своего операнда.
Этот оператор уже использовался в разделе 3.2.2. В следующем примере подразумевается,
что vec — это вектор целых чисел, для проверки наличия значений в элементах которого используется оператор логического NOT для значения, возвращенного функцией empty().
// отобразить первый элемент вектора vec, если он есть
Page 184/1103
if (!vec.empty()) cout << vec[0];
Подвыражение !vec.empty() возвращает значение true, если вызов функции empty()
возвращает значение false. Операторы отношения
Операторы отношения (<, <=, >, <=) имеют свой обычный смысл и возвращают значение типа bool. Эти операторы имеют левосторонний порядок.
Поскольку операторы отношения возвращают логическое значение, их сцепление может дать удивительный результат:
//
Упс! это условие сравнивает k с результатом сравнения i < j if (i < j < k) // true, если k больше 1!
Условие группирует i и j в первый оператор <. Результат этого выражения (типа bool)
является левым операндом второго оператора <. Таким образом, переменная k сравнивается с результатом (true или false) первого оператора сравнения! Для реализации той проверки, которая и предполагалась, выражение нужно переписать следующим образом:
// условие истинно, если i меньше, чем j, и j меньше, чем k if (i < j && j < k) { /* ... */ } Проверка равенства и логические литералы
Если необходимо проверить истинность арифметического значения или объекта указателя,
то самый простой способ подразумевает использование этого значения как условия. if (val) { /* ... */ } // true, если val - любое не нулевое значение if (!val) { /* ... */ } // true, если val - нуль
В обоих условиях компилятор преобразовывает val в тип bool. Первое условие истинно, пока значение переменной val отлично от нуля; второе истинно, если val — нуль.
Казалось бы, условие можно переписать так: if (val == true) { /* ... */ } // true, только если val равно 1!
У этого подхода две проблемы. Прежде всего, он длинней и менее непосредствен, чем предыдущий код (хотя по общему признанию в начале изучения языка С++ этот код понятней). Но важней всего то, что если тип переменной val отличен от bool, то это сравнение работает не так, как ожидалось.
Если переменная val имеет тип, отличный от bool, то перед применением оператора ==
Page 185/1103
значение true преобразуется в тип переменной val. Таким образом, получается код,
аналогичный следующему: if (val == 1) { /*...*/ }
Как уже упоминалось, при преобразовании значения типа bool в другой арифметический тип false преобразуется в 0, a true — в 1 (см. раздел 2.1.2). Если бы нужно было действительно сравнить значение переменной val со значением 1, то условие так и следовало бы написать.
Использование логических литералов true и false в качестве операндов сравнения — обычно плохая идея. Эти литералы следует использовать только для сравнения с объектами типа bool. Упражнения раздела 4.3
Упражнение 4.8. Объясните, когда обрабатываются операнды операторов логического AND,
логического OR и оператора равенства.
Упражнение 4.9. Объясните поведение следующего условия оператора if: const char *cp = "Hello World"; if (cp && *cp)
Упражнение 4.10. Напишите условие цикла while, который читал бы целые числа со стандартного устройства ввода, пока во вводе не встретится значение 42.
Упражнение 4.11. Напишите выражение, проверяющее четыре значения а, b, с и d и являющееся истинным, если значение а больше b, которое больше c, которое больше d.
Упражнение 4.12. С учетом того, что i, j и k имеют тип int, объясните значение выражения i != j
< k.
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-значение)
Page 186/1103

Результат присвоения, левый операнд, является 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.
Page 187/1103

Каждый объект в множественном операторе присвоения должен иметь тип, совпадающий с типом соседа справа, или допускать преобразование в него (раздел 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(); // получить остальные значения
}
Page 188/1103

Код начинается с вызова функции 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)
Page 189/1103
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)
Page 190/1103

4.5. Операторы инкремента и декремента
Операторы инкремента (++) и декремента (--) позволяют в краткой и удобной форме добавить или вычесть единицу из объекта. Эта форма записи обеспечивает не только удобство, она весьма популярна при работе с итераторами, поскольку большинство итераторов не поддерживает арифметических действий.
Эти операторы существуют в двух формах: префиксной и постфиксной. До сих пор использовался только префиксный оператор инкремента (prefix increment). Он осуществляет инкремент (или декремент) своего операнда и возвращает измененный объект как результат.
Постфиксный оператор инкремента (postfix increment) (или декремента) возвращает копию первоначального операнда неизменной , а затем изменяет значение операнда. int i = 0, j; j = ++i; // j = 1, i = 1: префикс возвращает увеличенное значение j = i++; // j = 1, i = 2: постфикс возвращает исходное значение
Операндами этих операторов должны быть l-значения. Префиксные операторы возвращают сам объект как l-значение. Постфиксные операторы возвращают копию исходного значения объекта как r-значение. Совет. Используйте постфиксные операторы только по мере необходимости
Читатели с опытом языка С могли бы быть удивлены тем, что в написанных до сих пор программах использовался префиксный оператор инкремента. Причина проста: префиксная версия позволяет избежать ненужной работы. Она увеличивает значение и возвращает результат. Постфиксный оператор должен хранить исходное значение, чтобы возвратить неувеличенное значение как результат. Но если в исходном значении нет никакой потребности, то нет необходимости и в дополнительных действиях, осуществляемых постфиксным оператором.
Для переменных типа int и указателей компилятор способен оптимизировать код и уменьшить количество дополнительных действий. Для более сложных типов итераторов подобные дополнительные действия могут обойтись довольно дорого. При использовании префиксных версий об эффективности можно не волноваться. Кроме того, а возможно и важнее всего то,
что так можно выразить свои намерения более непосредственно. Объединение операторов обращения к значению и инкремента в одном выражении
Постфиксные версии операторов ++ и -- используются в случае, когда в одном составном выражении необходимо использовать текущее значение переменной, а затем увеличить его.
В качестве примера используем постфиксный оператор инкремента для написания цикла,
выводящего значения вектора до, но не включая, первого отрицательного значения.
Page 191/1103
auto pbeg = v.begin();
// отображать элементы до первого отрицательного значения while (pbeg != v.end() && *beg >= 0) cout << *pbeg++ << endl; // отобразить текущее значение и
// переместить указатель pbeg
Выражение *pbeg++ обычно малопонятно новичкам в языках С++ и С. Но поскольку эта схема весьма распространена, программисты С++ должны понимать такие выражения.
Приоритет постфиксного оператора инкремента выше, чем оператора обращения к значению,
поэтому код *pbeg++ эквивалентен коду *(pbeg++). Часть pbeg++ осуществляет инкремент указателя pbeg и возвращает как результат копию предыдущего значения указателя pbeg.
Таким образом, операндом оператора * будет неувеличенное значение указателя pbeg.
Следовательно, оператор выводит элемент, на который первоначально указывал указатель pbeg, а затем осуществляет его инкремент.
Этот подход основан на том, что постфиксный оператор инкремента возвращает копию своего исходного, не увеличенного операнда. Если бы он возвратил увеличенное значение,
то обращение к элементу вектора по такому увеличенному значению привело бы к плачевным результатам: первым оказался бы незаписанный элемент вектора. Хуже того,
если бы у последовательности не было никаких отрицательных значений, то в конце произошла бы попытка обращения к значению несуществующего элемента за концом вектора. Совет. Краткость может быть достоинством
Такие выражения, как *iter++, могут быть не очевидны, однако они весьма популярны.
Следующая форма записи проще и менее подвержена ошибкам: cout << *iter++ << endl; чем ее более подробный эквивалент: cout << *iter << endl;
++iter;
Поэтому примеры подобного кода имеет смысл внимательно изучать, чтобы они стали совершенно понятны. В большинстве программ С++ используются краткие формы выражений, а не их более подробные эквиваленты. Поэтому программистам С++ придется привыкать к ним. Кроме того, научившись работать с краткими формами, можно заметить, что они существенно менее подвержены ошибкам. Помните, что операнды могут быть обработаны в любом порядке
Большинство операторов не гарантирует последовательности обработки операндов (см.
раздел 4.1.3). Отсутствие гарантированного порядка зачастую не имеет значения. Это действительно имеет значение в случае, когда выражение одного операнда изменяет значение, используемое выражением другого. Поскольку операторы инкремента и декремента изменяют свои операнды, очень просто неправильно использовать эти
Page 192/1103
операторы в составных выражениях.
Для иллюстрации проблемы перепишем цикл из раздела 3.4.1, который преобразует в верхний регистр символы первого введенного слова: for (auto it = s.begin(); it != s.end() && !isspace(*it) ; ++it) it = toupper(*it); // преобразовать в верхний регистр
Этот пример использует цикл for, позволяющий отделить оператор обращения к значению beg от оператора его приращения. Замена цикла for, казалось бы, эквивалентным циклом while дает неопределенные результаты:
// поведение следующего цикла неопределенно! while (beg != s.end() && !isspace(*beg)) beg = toupper(*beg++); // ошибка: это присвоение неопределенно
Проблема пересмотренной версии в том, что левый и правый операнды оператора =
используют значение, на которое указывает beg, и правый его изменяет. Поэтому присвоение неопределенно. Компилятор мог бы обработать это выражение так:
*beg = toupper(*beg); // сначала обрабатывается левая сторона
*(beg + 1) = toupper(*beg); // сначала обрабатывается правая сторона
Или любым другим способом. Упражнения раздела 4.5
Упражнение 4.17. Объясните различие между префиксным и постфиксным инкрементом.
Упражнение 4.18. Что будет, если цикл while из последнего пункта этого раздела,

1   ...   6   7   8   9   10   11   12   13   ...   54


написать администратору сайта