В этом параграфе мы познакомимся с операторами ветвления if
и switch
, циклами while
, do-while
и for
, а также с оператором goto
.
Оператор if
Условный оператор if
записывается так:
if (condition) {
// код, который исполнится в случае, когда условие condition истинно
}
Дополнительно можно добавить ветку кода для случая, когда условие ложно:
if (condition) {
// код, который исполнится, если condition истинно
} else {
// код, который исполнится, если condition ложно
}
Также можно выстроить цепочку условных операторов:
if (condition1) {
// случай, когда condition1 истинно
} else if (condition2) {
// случай, когда condition1 ложно, а condition2 истинно
} else if (condition3) {
// случай, когда condition1 и condition2 ложны, а condition3 истинно
} else {
// случай, когда condition1, condition2 и condition3 ложны
}
Если код внутри фигурных скобок состоит из одной инструкции, то фигурные скобки можно не писать. Однако рекомендуется ставить фигурные скобки всегда во избежание ошибок.
На месте condition
может стоять любое выражение логического типа. Простейшие примеры таких выражений — это проверка на равенство (==
) и неравенство (!=
), а также сравнения на меньше / больше (<
, <=
, >
и >=
):
#include <iostream>
int main() {
int x;
std::cin >> x;
if (x <= 0) {
std::cout << "zero or negative\n";
} else if (x == 1) {
std::cout << "one\n";
} else if (x == 2) {
std::cout << "two\n";
} else {
std::cout << "many\n";
}
}
Сложные условия
Условия можно комбинировать с помощью логических операторов &&
(и), ||
(или) и !
(не). Рассмотрим пример, где проверяется принадлежность точки разным интервалам на прямой.
int main() {
int a, b, x;
/* Тут должна быть логика заполнения объявленных переменных,
но мы её опустили, чтобы не отвлекаться */
if (a <= x && x <= b) {
// точка x лежит на отрезке [a; b]
} else {
// точка x лежит вне отрезка [a; b]
}
// то же самое можно было бы проверить так:
if (!(x < a || x > b)) { // отрицание
// точка x лежит на отрезке [a; b]
} else {
// точка x лежит вне отрезка [a; b]
}
}
Обратите внимание, что двойное неравенство некорректно проверять через a <= x <= b
. Так можно написать, но смысл будет совсем другим: результат сравнения a <= x
будет приведён к нулю или единице, и полученное число будет сравниваться с b
.
Операторы сравнения имеют больший приоритет, чем логические операторы, поэтому скобки вокруг элементарных сравнений в сложных условиях не обязательны. А вот оператор отрицания имеет высокий приоритет, поэтому в последнем примере он применяется к условию в скобках. Таблицу приоритета операторов C++ можно посмотреть здесь.
Операторы &&
и ||
ведут себя лениво: если первого аргумента уже достаточно для ответа, то второй аргумент вычисляться не будет. Например, в выражении condition1 && condition2
второе условие не вычисляется, если первое ложно. Это часто используют для проверок корректности:
int a, b;
// ...
if (a != 0 && b % a == 0) {
// b делится на a
}
Сравнение чисел с плавающей точкой
Рассмотрим программу, которая проверяет равенство 0.1 + 0.2 == 0.3
:
#include <iostream>
int main() {
double x = 0.1, y = 0.2;
if (x + y == 0.3) {
std::cout << "EQUAL ";
} else {
std::cout << "NOT EQUAL ";
}
std::cout << x + y << "\n";
}
Логично было бы предположить, что программа выведет EQUAL 0.3
, потому что . Однако программа напечатает NOT EQUAL 0.3
. Данная «ошибка вычисления» встречается в большинстве современных языков программирования и обусловлена погрешностью представления этих чисел. Если повысить число знаков дробной части в выводе, мы увидим, что 0.1 + 0.2 == 0.30000000000000004
. Подробнее об этом можно прочитать здесь и в этой статье.
Поскольку операции над числами с плавающей точкой могут содержать погрешность, обычное сравнение через ==
некорректно. Поэтому правильнее сравнивать модуль разности величин с некой допустимой для нас погрешностью. Модуль дробного числа можно получить с помощью функции std::abs
из заголовочного файла cmath
. Исходную программу можно было бы переписать так:
#include <cmath>
#include <iostream>
int main() {
double delta = 0.000001;
double x = 0.1, y = 0.2;
double sum = x + y;
if (std::abs(sum - 0.3) < delta) {
std::cout << "EQUAL ";
} else {
std::cout << "NOT EQUAL ";
}
std::cout << sum << "\n";
}
Теперь программа выведет EQUAL 0.3
.
Оператор switch
Рассмотрим простейший калькулятор, считывающий число, затем знак арифметической операции, а затем другое число и печатающий результат. Напишем сначала программу с помощью if
и else
.
#include <cstdint>
#include <iostream>
int main() {
int64_t a, b;
char operation;
std::cin >> a >> operation >> b;
int64_t result = 0;
if (operation == '+') {
result = a + b;
} else if (operation == '-') {
result = a - b;
} else if (operation == '*') {
result = a * b;
} else if (operation == '/' || operation == ':') {
result = a / b;
} else if (operation == '%') { // остаток от деления
result = a % b;
}
std::cout << result << "\n";
}
Вопросы для самопроверки
Что будет, если ввести 2 / 3
?
- Так как всё вычисление происходит в целых числах, то напечатается
0
. Деление целых положительных чисел в C++ — это всегда неполное частное.
Что будет, если ввести 2 @ 3
?
- Обработку значка
@
мы не предусмотрели. Программа напечатает0
, но тут необходимо напомнить, что у локальных переменных типаint
нет дефолтных значений, и если бы мы не присвоили изначальноresult = 0
, то программа напечатала бы какое-то неизвестное заранее число, лежащее в ячейках памяти, в которых поселилсяresult
.
Перепишем эту программу через оператор switch
. Этот оператор следует рассматривать как условный прыжок на соответствующую метку в зависимости от значения выражения.
#include <cstdint>
#include <iostream>
int main() {
int64_t a, b;
char operation;
std::cin >> a >> operation >> b;
int64_t result;
switch (operation) {
case '+':
result = a + b;
break; // если не написать этот break, программа просто пойдёт дальше в код следующего блока case
case '-':
result = a - b;
break;
case '*':
result = a * b;
break;
case '/':
case ':':
result = a / b;
break;
case '%':
result = a % b;
break;
default: // здесь обрабатывается случай, когда ни один case не сработал.
result = 0;
}
std::cout << result << "\n";
}
Выражения внутри скобок оператора switch
и в блоках case
должны быть простого целочисленного или символьного типа. В приведённой выше программе значение +
относится к типу char
. Использование сложных типов (например, строк) приведёт к ошибке компиляции:
int main() {
std::string name;
std::cin >> name;
switch (name) { // ошибка компиляции
case "Alice":
std::cout << "Hello, Alice!\n";
break;
}
}
Оператор goto
Есть шутка, что оператор безусловного перехода goto
— это ругательное слово из четырёх букв. Безусловные переходы ломают иерархичность программы, затрудняют чтение и отладку. Доказано, что любую программу можно переписать без оператора goto
. Поэтому во многих современных языках goto
отсутствует.
Однако в C++ этот оператор есть по следующим причинам:
- обратная совместимость с языком С;
- удобный выход из вложенных циклов;
- применение в автосгенерированном коде (например, коде конечного автомата), не предназначенном для чтения человеком.
Мы познакомимся с его синтаксисом, но дальше использовать нигде не будем — и вам не советуем.
int main() {
again: // метка — это произвольное имя с двоеточием
std::cout << "How old are you?\n";
int age;
std::cin >> age;
if (age < 0 || age >= 128) {
std::cout << "Wrong age...\n";
goto again; // безусловный прыжок в место, помеченное меткой
}
std::cout << "Your age is " << age << ".\n";
// ...
}
Здесь вводится метка again
, на которую осуществляется переход, если возраст введён некорректно. Ниже мы покажем, как можно избавиться от оператора goto
.
С помощью оператора goto
нельзя выйти из функции или зайти в неё, а также нельзя перепрыгнуть через объявления переменных (кроме тривиальных случаев):
#include <iostream>
int main() {
goto label;
int x = 42;
label: // ошибка компиляции!
std::cout << x << "\n";
}
Цикл while
В C++ существует несколько видов циклов. Цикл while
— это цикл с предусловием. Перед очередной итерацией проверяется условие, и если оно истинно, то цикл продолжается. Рассмотрим пример печати таблицы квадратов чисел от 1 до 10:
#include <iostream>
int main() {
int n = 1;
while (n <= 10) {
std::cout << n << "\t" << n * n << "\n"; // выводим число и его квадрат через табуляцию
++n;
}
}
Здесь мы намеренно разделяем числа и их квадраты не пробелом, а знаком табуляции \t
. В консоли такой вывод будет выглядеть выровненным по колонкам с фиксированной шириной:
1 1 2 4 3 9 4 16 5 25 6 36 7 49 8 64 9 81 10 100
Цикл do-while
Это цикл с постусловием. Отличие от цикла while
заключается в том, что первая итерация всегда выполняется безусловно. Только после её завершения проверяется условие цикла. Если оно истинно, то цикл продолжается.
#include <iostream>
int main() {
int n = 1;
do {
std::cout << n << "\t" << n * n << "\n";
++n;
} while (n <= 10);
}
Без особых причин пользоваться этим видом циклов не стоит, старайтесь использовать циклы while
или for
.
Цикл for
Цикл for
— самый гибкий. Он записывается так:
for (initialization; condition; action) {
// тело цикла
}
Как правило, с циклом ассоциируется некоторый параметр, который меняется от итерации к итерации, а цикл выполняется до тех пор, пока некоторое условие на этот параметр истинно.
Начальное значение такого параметра можно задать в разделе initialization
, условие — в condition
, а действие над параметром, выполняющееся после каждой итерации, — в action
.
Напечатаем таблицу квадратов через цикл for
:
#include <iostream>
int main() {
for (int i = 1; i <= 10; ++i) {
std::cout << i << "\t" << i * i << "\n";
}
}
Напомним, что ++i
— традиционная краткая форма записи для выражения i = i + 1
.
Цикл for
эквивалентен такому циклу while
:
{
initialization;
while (condition) {
// тело цикла
action;
}
}
Цикл range-based for
Этот цикл применим к контейнерам разной природы (массивам, векторам, спискам и т. д.), с которыми мы познакомимся позже. Пока рассмотрим его на примере строк. Цикл позволяет удобно проитерироваться по символам строки, не используя индексов. В этом примере мы считываем строку и печатаем отдельно все символы строки и их ASCII-коды:
#include <iostream>
#include <string>
int main() {
std::string line;
std::getline(std::cin, line);
for (char symbol : line) {
std::cout << symbol << "\t" << static_cast<int>(symbol) << "\n";
}
}
Здесь оператор static_cast
преобразует символ к числовому типу int
, чтобы получить его код. Результат для строки Hello, world!
выглядит так:
H 72 e 101 l 108 l 108 o 111 , 44 32 w 119 o 111 r 114 l 108 d 100 ! 33
Обратите внимание, что std::string
хранит внутри байты. Если вы вводите символы русского алфавита и у вас используется кодировка UTF-8, ставшая де-факто стандартом, то эти символы будут кодироваться парами байтов. И при такой итерации вы увидите отдельные байты, а не символы.
Вложенные циклы
Циклы могут быть вложенными. Напечатаем таблицу умножения:
#include <iostream>
int main() {
for (int i = 1; i <= 10; ++i) {
for (int j = 1; j <= 10; ++j) {
std::cout << i * j << "\t";
}
std::cout << "\n";
}
}
Результат:
1 2 3 4 5 6 7 8 9 10 2 4 6 8 10 12 14 16 18 20 3 6 9 12 15 18 21 24 27 30 4 8 12 16 20 24 28 32 36 40 5 10 15 20 25 30 35 40 45 50 6 12 18 24 30 36 42 48 54 60 7 14 21 28 35 42 49 56 63 70 8 16 24 32 40 48 56 64 72 80 9 18 27 36 45 54 63 72 81 90 10 20 30 40 50 60 70 80 90 100
Операторы break
и continue
Оператор break
досрочно заканчивает текущий цикл. Оператор continue
прыгает в самый конец тела цикла и, если условие цикла позволяет, переходит на следующую итерацию.
Типичный пример использования оператора break
— выход из формально бесконечного цикла:
while (true) {
// ...
if (condition) {
break;
}
// ...
}
Кстати, рассмотрим другие способы записать бесконечный цикл.
Через цикл do-while
:
do {
// ...
} while (true);
Через цикл for
:
for (;;) {
// ...
}
Напишем программу, которая считывает числа с клавиатуры до тех пор, пока пользователь не введёт ноль, а затем печатает их сумму:
#include <iostream>
int main() {
int sum = 0;
while (true) {
int x;
std::cin >> x;
if (x == 0) {
break;
}
sum += x;
}
std::cout << sum << "\n";
}
(Здесь, конечно, неявно предполагается, что и сами числа, и результат суммирования помещаются в тип int
, и в ходе вычислений не происходит переполнений.)
Считывание до конца ввода
В предыдущем примере мы считывали числа до тех пор, пока на вход не поступит ноль. Это не очень удобно. Предположим, что вместо ввода с клавиатуры мы читали бы эти числа из файла. Это легко сделать, перенаправив файл на стандартный ввод при запуске программы из консоли:
./a.out < input.txt
Следующий цикл считывает числа до тех пор, пока поступающие данные не закончатся:
#include <iostream>
int main() {
int sum = 0;
int x;
while (std::cin >> x) {
sum += x;
}
std::cout << sum << "\n";
}
Здесь вместо условия цикла подставлено выражение std::cin >> x
. Кроме считывания x
это выражение преобразуется к логическому типу, показывающему, есть ли ещё данные в потоке ввода.
При вводе данных не из файла, а с клавиатуры можно сымитировать конец ввода комбинацией клавиш Ctrl+D
в Linux и macOS или Ctrl+Z
в Windows.
Аналогично можно прочитать строки до конца ввода с помощью std::getline
:
#include <iostream>
#include <string>
int main() {
std::string name;
while (std::getline(std::cin, name)) {
std::cout << "Hello, " << name << "!\n";
}
}