Поток управления - это порядок, в котором выполняются инструкции. Он определяет какие части кода будут выполнены, в какой последовательности и при каких условиях. Управление потоком позволяет с помощью кода принимать решения, повторять действия или переходить к определённым участкам кода в зависимости от логики или условий.
int main() {
int x = 5;
std::cout << x << std::endl;
x = x + 1;
std::cout << x << std::endl;
}
Тут у нас строго линейный порядок выполнения - без ветвления кода, условий или циклов.
Код будет выполнен сверху вниз, построчно.
Есть разные способы управления потоком:
- Условные операторы
if,else,else if - Оператор
switch - Циклы
for,while,do while - Исключения
trycatch - Оператор
goto
Условные операторы
Сейчас нас интересует первый пункт - условные операторы.
Эти операторы называются условными, потому что в зависимости от их условия будет выполнен или не выполнен определённый участок кода.
Оператор if
int x = 5;
if (x == 5)
{
std::cout << "x is 5" << std::endl;
}
x is 5
Так выглядит применение оператора if. Если условие в скобочках после него true, то будет выполнен следующий за ним блок кода.
Если x не был бы равен 5 в этом случае, то блок кода не был бы выполнен.
Важно то, что если после условия отсутствует блок кода, то будет выполнено только одно следующие действие.
int a = 1;
int b = 2;
int c = 3;
int d = 4;
if (false) a = b;
c = d;
std::cout << a << std::endl;
std::cout << c << std::endl;
1
4
В этом случае часть людей при изучении языка могут допустить ошибочное предположение.
Тут из-за false в условии выражение a = b не будет выполнено, но выражение c = d будет выполнено всегда, вне зависимости от условия после оператора if.
Это точно также будет работать и в такой ситуации:
int a = 1;
int b = 2;
int c = 3;
int d = 4;
if (false) a = b;
{
c = d;
}
std::cout << a << std::endl;
std::cout << c << std::endl;
1
4
То есть наличие блока кода ничего не меняет, а только запутывает и ухудшает читаемость кода.
В большинстве случаев такая запись получается по ошибке и уж точно не должна присутствовать у вас в коде.
Комбинирование условий
Используя логические операторы мы можем комбинировать условия, допустим, из операторов сравнения.
Классический пример - проверка нахождения значения в диапазоне, но есть одна типичная ошибка - писать цепочки сравнений так же как в математике или как в другом языке программирования
int x = -1;
if (0 < x < 10)
{
std::cout << "x between 0 and 10" << std::endl;
}
x between 0 and 10
Люди, приходящие из Python обычно так и пишут.
Проблема в том, что компилятор воспринимает это выражение как ((x < 0) < 10), то есть (false < 10), а далее после приведения bool к int мы получаем 0 < 10 и это выражение, естественно, является true.
Поэтому, чтобы записать такую логику корректно необходимо применить оператор && и разделить условия
int x = -1;
if (0 < x && x < 10)
{
std::cout << "x between 0 and 10" << std::endl;
}
Короткое замыкание
В контексте условных операторов, операторы && и || имеют короткое замыкание.
Этот момент, в большинстве случаев, разбирают в теме логических операторов, но проблема в том, что без контекста операторов потока управления поведение операторов && и || сложнее для понимания.
int a = 0;
bool b = false;
if (b && a++) {
// (<zero> && <expression>) is always zero.
// <expression> is never evaluated and might have side effects.
std::cout << "first if" << std::endl;
}
if (a == 1) {
std::cout << "second if" << std::endl;
}
std::cout << a << std::endl;
0
В этом случае ни одно из условий не является истинным.
Первое, потому что оператор && применяемый к false и любому другому значению всегда будет давать результат false.
Второе условие тоже не выполняется, потому что a имеет значение 0 при выводе.
Происходит это из-за того, что операция инкремента в условии не выполняется из-за короткого замыкания.
При вычислении выражения b && a++ проверяется сначала левая часть выражения, которая равна false и в этом случае результат операции && не может быть true.
То есть проверять и вычислять следующую часть выражения не имеет смысла.
Поэтому операция a++ никогда не будет выполнена.
Даже компилятор нам подсказывает и выдаёт предупреждение "<expression> is never evaluated and might have side effects", что означает "выражение никогда не вычисляется и может иметь побочные эффекты", что мы и наблюдаем со значением переменной a.
Это же касается оператора ||
int a = 0;
bool b = true;
if (b || a++) {
std::cout << "first if" << std::endl;
}
if (a == 1) {
std::cout << "second if" << std::endl;
}
std::cout << a << std::endl;
first if
0
В этом случае у нас очевидно b равна true и первая часть условия истина.
При этом оператор || возвращает true вне зависимости от второй части выражения.
Это применяется к условиям состоящим из любого количества операторов || или &&
int a = 0;
int b = 0;
bool c = false;
if (c && a++ && b++) {
std::cout << "first if" << std::endl;
}
if (a == 1 || b == 1) {
std::cout << "second if" << std::endl;
}
std::cout << a << std::endl;
0
Правило короткого замыкания бывает полезно в работе с указателями, но проще будет показать его полезность на том, что вы уже знаете
int x = 0;
int y = 10;
if (x != 0 && y / x > 1)
{
std::cout << (y / x) << std::endl;
}
else {
std::cout << "x is zero!" << std::endl;
}
x is zero!
Если здесь у нас x будет равен 0, то мы получим ошибку деления на ноль при вычислении второй части условия - y / x > 1.
Но эта часть условия выполнена не будет поскольку левая часть выражения x != 0 перед оператором && ложна и ошибка деления на ноль не возникает.
Такая же логика может использоваться для проверки валидности указателя перед последующей его проверкой.
Ошибка из-за использования оператора =
По случайности можно перепутать оператор сравнения == и оператор присвоения =.
int a = 1;
int b = 2;
if (a = b)
{
std::cout << "a = b!" << std::endl;
}
a = b!
Результатом выражения a = b будет значение переменной a после выполнения присвоения, то есть 2.
Это значение преобразуется к логическому типу и будет равно true, потому что двойка не равняется нулю.
Таким образом если вы перепутали операторы то следующий за if блок кода не будет выполнен только тогда, когда результат присвоения равен нулю.
Оператор else
Оператор else используется только в паре с if.
Он служит для того, чтобы указать блок кода или действие, которое будет выполнено, если условие, указанное после if ложно.
int a = 5;
if (a == 1)
{
std::cout << "a is 1" << std::endl;
}
else {
std::cout << "a is not 1" << std::endl;
}
a is not 1
То есть условие a == 1 ложно и выполняется блок кода не после этого условия, а после оператора else.
В этом случае также можно допустить такую ошибку из-за невнимательности.
int a = 1;
if (a == 1)
{
std::cout << "a is 1" << std::endl;
}
else a++;
{
std::cout << "a is not 1" << std::endl;
}
a is 1
a is not 1
Связка else и if
На самом деле else if это синтаксическая форма записи вложенного if внутри else.
Она существует для удобства чтения.
Точно как и else используется только в комбинации с if.
Эта конструкция позволяет после блока кода относящегося к предыдущему if задать ещё одно условие
int a = 0;
if (a > 0)
{
std::cout << "a greater than zero" << std::endl;
}
else if (a < 0)
{
std::cout << "a less than zero" << std::endl;
} else
{
std::cout << "a equal to zero" << std::endl;
}
a equal to zero
Неопределённое поведение
С логикой конструкций if и else связанные некоторые случая неопределённого поведения.
int main() {
int x = 5;
if (x < 10)
{
std::cout << "here!!!" << std::endl;
} else
return 1;
}
here!!!
Sandbox.exe (process 1640) exited with code 0 (0x0).
Поскольку после else нет блока кода - в случае, когда условие x < 10 ложно, будет выполнена одна инструкция идущая после оператора else - return 1.
В нашем случае условие истина и будет вывод текста here!!!, но return 1 после else вызван не будет.
При этом приложение всё равно завершается с кодом 0.
В C++ есть специальное правило - если выполнение достигает конца функции main, это эквивалентно return 0.
Из-за этого правила не возникает неопределённого поведения, чего нельзя сказать о следующем коде
int f(int a) {
if (a > 10)
{
return 1;
}
else {
}
}
int main() {
std::cout << "result: " << f(5) << std::endl;
return 123;
}
result: 0
Sandbox.exe (process 8760) exited with code 123 (0x7b).
Пока что мы не дошли до функций, но придётся использовать их концепцию для объяснения этого случая.
У нас функция f возвращает значение типа int, это значение зависит от аргумента a.
Если аргумент a больше 10, тогда результатом функции будет 1 в ином случае - у нас пустой блок кода и поведение не определено, при этом - код компилируется и выполняется.
Обратите внимание, что функция вызывается с аргументом 5, то есть a внутри функции равна 5.
Также я специально возвращаю значение 123 из функции main чтобы не возникало путаницы.
При выводе result: 0 мы явно видим, что результат выполнения функции это число 0, которое отсутствует в коде.
Такой результат не гарантирован стандартом C++ и может отличатся в зависимости от множества факторов.
В этом случае так совпало, что машинный код сгенерированный компилятором оставил один из регистров со значением 0, которое мы и увидели.
Когда вы пишите конструкцию if и else нужно учитывать в каком контексте это происходит и к какому результату приводит выполнение всех веток кода.
Нужно отметить что такой случай считается неопределённым поведением, по крайней мере до стандарта C++26, сейчас появляются отдельные термины для некоторых случаев
Комбинирование и вложенные условия
Мы можем комбинировать эти операторы как упоминалось выше, а так же писать вложенные условия.
Одно и то же условие можно записать двумя способами
bool a = true;
bool b = true;
if (a && b)
{
std::cout << "a and b is true" << std::endl;
}
a and b is true
bool a = true;
bool b = true;
if (a)
{
std::cout << "a is true" << std::endl;
if (b)
{
std::cout << "b also is true" << std::endl;
}
}
a is true
b also is true
В обоих случаях код выполнится только тогда, когда и a, и b имеют значение true.
Первый вариант более компактный и обычно используется, когда требуется проверить несколько простых условий.
Второй вариант удобен, если после проверки a необходимо выполнить несколько дополнительных действий или проверить другие условия.
Оператор switch
Оператор switch также является оператором выбора, но работает не через логическое условие, а через сопоставление значения с набором вариантов.
Оператор switch позволяет выполнить одну из веток кода на основании значения переменной.
Оформите подписку, чтобы иметь доступ ко всем публикациям без ограничений. Вы получите мгновенный доступ к полным версиям материалов, исходному коду и новым публикациям сразу после их выхода.
Я регулярно публикую материал и разборы ситуаций для различных игр, движков, а так же подходов разработки включая Internal, External и DMA.