Синтаксис ветвлений и циклов C++ унаследовал от C, а оттуда его переняли Java, C#, JavaScript и
Go. Если вы писали на любом из них, читать эту главу будет легко – ловушки в другом: в
целочисленном делении, в неявных преобразованиях и в том, что if тут принимает не только
bool.
Арифметика
int a = 7;
int b = 2;
a + b; // 9
a - b; // 5
a * b; // 14
a / b; // 3 – не 3.5
a % b; // 1 – остаток
Деление двух целых даёт целое. Дробная часть отбрасывается, а не округляется: 7 / 2 – это
3, -7 / 2 – это -3. Чтобы получить дробь, дробным должен быть хотя бы один операнд:
static_cast<double>(a) / b; // 3.5
a / 2.0; // 3.5
Самая частая версия этой ошибки – проценты: count / total * 100 при count = 1, total = 4 даст ноль, потому что 1 / 4 посчиталось до умножения. Порядок спасает: count * 100 / total.
Деление на ноль – неопределённое поведение для целых (обычно аварийное завершение) и определённое
для дробных: получится inf или nan.
Инкремент
int i = 5;
++i; // префиксный: увеличить, вернуть новое значение
i++; // постфиксный: запомнить старое, увеличить, вернуть старое
Когда результат не используется, разницы для int нет. Для пользовательских типов и итераторов
есть: постфиксная форма обязана сохранить копию «себя до изменения», чтобы её вернуть.
Отсюда привычка писать ++i в заголовке цикла. С int компилятор всё равно сгенерирует
одинаковый код, но привычка бесплатна, а на итераторе тяжёлого контейнера – уже нет.
Несколько изменений одной переменной в одном выражении (i = i++ + ++i;) – классический пример неопределённого поведения. Не потому что сложно, а потому что стандарт не задаёт порядок. Не пишите так, и не верьте задачам на собеседованиях, где такое спрашивают.
Сравнение и логика
a == b; a != b; a < b; a <= b;
x && y; // и: если x ложно, y не вычисляется
x || y; // или: если x истинно, y не вычисляется
!x; // не
Логические операторы ленивы – это не оптимизация, а гарантия стандарта, на которой строят проверки:
if (ptr != nullptr && ptr->IsValid()) { ... } // безопасно: разыменования не будет при nullptr
Побитовые & и | ленью не обладают и вычисляют оба операнда всегда. Опечатка в одну черту превращает безопасную проверку в падение.
Всё, что не ноль, – истина
if принимает не bool, а любое значение, приводимое к bool. Ноль и nullptr – ложь, всё
остальное – истина.
int count = 5;
if (count) { ... } // истина
if (!errors) { ... } // «ошибок нет»
Из-за этого живёт классическая ошибка, которую в C# и Java компилятор не пропустил бы:
if (x = 5) { ... } // присваивание, а не сравнение: условие всегда истинно
Компиляторы предупреждают об этом (-Wall), но по умолчанию собирают.
if и его формы
if (temperature > 30) {
std::cout << "жарко";
} else if (temperature > 15) {
std::cout << "нормально";
} else {
std::cout << "холодно";
}
С C++17 в if можно объявить переменную, видимую только внутри ветвления:
if (auto it = users.find(id); it != users.end()) {
std::cout << it->second;
} // it здесь уже не существует
Полезно не ради экономии строк, а ради области видимости: переменная, нужная только для проверки, не торчит наружу.
Фигурные скобки при одной инструкции формально не обязательны. Их всё равно ставят: слишком легко дописать вторую строку и получить тело, которое выглядит вложенным, а не является им.
switch
switch (command) {
case 'a':
Add();
break;
case 'd':
Delete();
break;
default:
std::cout << "неизвестная команда";
}
Работает только с целыми и перечислениями – строку в switch подать нельзя, в отличие от C# и
Java.
break обязателен. Без него выполнение проваливается в следующий case и идёт дальше, пока
не встретит break или конец. Иногда это то, что нужно:
switch (level) {
case 3:
case 2:
EnableExtras();
[[fallthrough]]; // C++17: «я знаю, что делаю»
case 1:
EnableBasics();
break;
}
Атрибут [[fallthrough]] ничего не меняет в поведении – он гасит предупреждение компилятора и
сообщает читателю, что провал намеренный.
Циклы
for – когда известны границы:
for (int i = 0; i < 10; ++i) {
std::cout << i << " ";
}
range-for – когда нужны все элементы:
std::vector<std::string> names = {"Anya", "Boris", "Vera"};
for (const auto& name : names) { // const& – без копирования
std::cout << name << "\n";
}
for (auto& name : names) { // без const – можно менять элементы
name += "!";
}
foreach (var name in names) { ... }
for _, name := range names { ... }
Обратите внимание на &. for (auto name : names) копирует каждую строку на каждой итерации – для int неважно, для строк и объектов уже заметно. Привычка писать const auto& по умолчанию экономит копии.
while – когда границы неизвестны:
while (std::getline(input, line)) {
Process(line);
}
do-while – когда тело обязано выполниться хотя бы раз. Встречается редко.
break, continue и почему не goto
for (const auto& user : users) {
if (user.banned) continue; // к следующей итерации
if (user.id == target) break; // выйти из цикла
Process(user);
}
break выходит из одного цикла – самого внутреннего. Из вложенных выходят либо флагом, либо
вынесением цикла в отдельную функцию с return. Второе обычно чище: если внутренностям цикла
нашлось имя, его стоило дать.
goto в языке есть, работает и почти никогда не нужен. Единственное живое применение – выход из
глубоко вложенного цикла в коде на C; в C++ для этого есть функции, исключения и RAII.