Лекция 2. Выражения, управление потоком и функции
Выражения и инструкции, блоки, ветвления if и switch, циклы while, do-while и for, break и continue, функции и рекурсия, аргументы командной строки, область видимости и сокрытие имён.
На этой лекции программа начинает «жить»: появляются решения, повторения и подпрограммы. Разберём, чем выражение отличается от инструкции, как устроены ветвления и циклы, почему фигурные скобки здесь не просто украшение, и как написать функцию так, чтобы её можно было использовать без страха.
Все фрагменты кода на лекции — самостоятельные примеры. Попробуйте скомпилировать каждый и поменять условия, чтобы проверить своё понимание. Фрагменты с пометкой «схема» — это части программы, а не законченные программы.
1. Выражения, инструкции и блоки
Выражение задаёт вычисление и обычно даёт значение: a + b — сумма, i++ — значение i до увеличения, а заодно и изменение самой переменной. Выражение типа void значения не даёт.
Инструкция задаёт шаг выполнения программы: вычислить выражение, объявить переменную, выбрать ветку, запустить цикл.
Если к выражению добавить точку с запятой, получится инструкция-выражение:
c = a + b;
i++;Не каждая инструкция является выражением. Например, if и while управляют выполнением других инструкций, но сами значения не возвращают.
Несколько инструкций можно объединить в блок с помощью фигурных скобок. Блок задаёт область видимости: объявленные внутри переменные живут до его конца.
{
PrintMessage();
++message_count;
}2. Условный оператор if
if (condition) {
StatementWhenTrue();
} else {
StatementWhenFalse();
}- ветка
elseнеобязательна; - в условии число
0означаетfalse, любое ненулевое —true; - на курсе фигурные скобки ставим всегда, даже когда ветка состоит из одной инструкции.
2.1. Висячий else
Если опустить скобки во вложенных if, возникает неоднозначность: к какому if относится else? Пример:
bool outer_condition = false;
bool inner_condition = false;
int result = 0;
if (outer_condition)
if (inner_condition)
result = 1;
else
result = 2;Отступ подсказывает, что else относится к внешнему if, но компилятор читает код без отступов. Правило простое: else относится к ближайшему if, который ещё не получил свою ветку. Здесь это внутренний if, поэтому при ложном внешнем условии else не выполняется, и result остаётся нулём.
Чтобы структура читалась однозначно, поставьте фигурные скобки:
if (outer_condition) {
if (inner_condition) {
result = 1;
}
} else {
result = 2;
}Теперь при outer_condition == false выполнится result = 2, и отступы больше не обманывают.
2.2. Цепочка else if
if (score >= 90) {
grade = 'A';
} else if (score >= 75) {
grade = 'B';
} else if (score >= 60) {
grade = 'C';
} else {
grade = 'F';
}Проверки идут сверху вниз и останавливаются на первой подходящей ветке. Порядок условий поэтому важен: сначала более строгие.
3. Циклы
3.1. while
int number = 5;
while (number > 0) {
std::cout << number << '\n';
--number;
}Условие проверяется до каждого прохода. Если оно сразу ложно, тело не выполнится ни разу.
3.2. do-while
unsigned long number = 0;
do {
std::cout << "Enter number (0 to end): ";
std::cin >> number;
std::cout << "You entered: " << number << '\n';
} while (number != 0);Условие проверяется после прохода, поэтому тело выполнится хотя бы раз. Удобно для ввода, который нужно повторять до особого значения.
Конструкция do { ... } while (false); выполняется один раз. Внутри можно использовать break как «выход из блока». Это удобно, когда нужно проверить несколько условий подряд и при первом сбое прекратить работу:
do {
if (!OpenFile()) break;
if (!AllocateMemory()) break;
ProcessData();
} while (false);3.3. for
for (initialization; condition; step) {
Statement();
}initializationвыполняется один раз до начала цикла;conditionпроверяется перед каждым проходом;stepвыполняется после тела.
Для цикла без continue схема эквивалентна while, где инициализация стоит перед ним в отдельном блоке, а шаг — в конце тела. Это нужно для того, чтобы переменная цикла оставалась видна только внутри цикла.
Заголовок цикла может содержать несколько инициализаций и шагов через запятую:
for (int left = 0, right = 100; left != right; ++left, --right) {
std::cout << left << ", ";
}3.4. continue внутри for
continue пропускает остаток тела и переходит к следующему шагу. В цикле for шаг выполняется в любом случае, поэтому следующая итерация не зацикливается:
for (int i = 0; i < 3; ++i) {
if (i == 1) {
continue;
}
std::cout << i << ' ';
}
// Вывод: 0 2Наивная перепись на while, где ++i стоит в конце тела, зациклилась бы на i == 1: continue пропустил бы инкремент. Поэтому шаг в while нужно дублировать перед каждым continue.
Конструкция for (; i --> 0;) работает, но читается как шутка. Выражение i --> 0 на самом деле означает i-- > 0. Её можно показать как курьёз на паре, но в рабочем коде пишите for (int i = 10; i > 0; --i).
3.5. Range-based for
Для перебора элементов массива или контейнера удобна форма с двоеточием:
int values[] = {0, 1, 2, 3, 4, 5};
for (int value : values) {
std::cout << value << ' ';
}Цикл последовательно берёт каждый элемент диапазона. Индексы не нужны, и выход за границы исключён.
3.6. break и continue
breakнемедленно завершает ближайший цикл илиswitch.continueпропускает остаток текущей итерации.
for (int number = 10; number > 0; --number) {
if (number == 3) {
std::cout << "countdown aborted!\n";
break;
}
std::cout << number << ", ";
}4. switch
switch сравнивает одно значение с набором констант:
switch (value) {
case 1:
HandleOne();
break;
case 2:
HandleTwo();
break;
default:
HandleOther();
break;
}Правила, которые стоит запомнить:
- выражение должно иметь целочисленный или перечислимый тип;
- метки
case— различные константы; - выполнение начинается с совпавшей метки и идёт вниз;
- если совпадения нет, выполняется
default; - без
breakвыполнение «проваливается» в следующую ветку.
Проваливание иногда полезно, чтобы объединить несколько меток в одну ветку:
switch (value) {
case 1:
case 2:
case 3:
std::cout << "value is 1, 2 or 3\n";
break;
default:
std::cout << "another value\n";
break;
}Когда нужно сравнить значение с несколькими константами, switch читается лучше цепочки if. Для диапазонов и сложных условий лучше подходит if.
5. Функции
Функция — именованный блок кода с параметрами и результатом:
int Addition(int left, int right) {
return left + right;
}- параметры доступны внутри тела;
returnзавершает функцию и возвращает значение;- выражение в
returnдолжно неявно преобразовываться к типу результата.
5.1. Объявление и определение
Функцию нужно объявить до первого вызова. Объявление задаёт сигнатуру, определение содержит тело:
int Maximum(int left, int right); // объявление
int main() {
int result = Maximum(10, 2);
std::cout << result << '\n';
return 0;
}
int Maximum(int left, int right) { // определение
return left > right ? left : right;
}5.2. Функция без значения
Тип void означает, что функция ничего не возвращает. Такая функция может завершиться оператором return; без значения или просто дойти до конца тела.
void PrintMessage() {
std::cout << "I'm a function!\n";
}5.3. Функция main
Стандарт допускает две формы:
int main() { /* ... */ }
int main(int argc, char* argv[]) { /* ... */ }- достижение конца
mainэквивалентноreturn 0;; - значения
EXIT_SUCCESSиEXIT_FAILUREобъявлены в<cstdlib>; - дополнительные параметры
mainмогут зависеть от реализации.
5.4. Аргументы командной строки
argc— количество строк вargv;argv— массив указателей на строки с аргументами;- если
argc > 0, тоargv[0]содержит имя программы или пустую строку; argv[argc]равенnullptr.
С указателями разберёмся подробно на следующей лекции. Пока достаточно понимать, что каждый элемент argv — это строка.
6. Рекурсия
Рекурсивная функция вызывает саму себя с меньшим аргументом и имеет условие остановки. Факториал:
unsigned long long Factorial(unsigned int number) {
if (number == 0) {
return 1;
}
return number * Factorial(number - 1);
}Итеративная версия вычисляет то же значение без цепочки вызовов:
unsigned long long Factorial(unsigned int number) {
unsigned long long result = 1;
for (unsigned int factor = 2; factor <= number; ++factor) {
result *= factor;
}
return result;
}Обе версии дают одинаковый результат. Для достаточно большого number оба переполнят unsigned long long: беззнаковое переполнение определено по модулю , но результат перестаёт быть математическим факториалом. Первое переполнение происходит уже на .
Рекурсия удобна, когда задача сама устроена рекурсивно: обход дерева, вычисление выражения со скобками. Для простых циклов итеративная версия экономит стек и обычно читается не хуже.
7. Область видимости и сокрытие имён
Объявление в более внутренней области временно скрывает одноимённую переменную из внешней:
int x = 0;
int y = 0;
void PrintLocalValues(double x) {
double y = 3.14;
std::cout << "local: x = " << x << ", y = " << y << '\n';
}
int main() {
x = 21;
y = 239;
{
int x = 10;
std::cout << "block: x = " << x << ", y = " << y << '\n';
PrintLocalValues(y);
}
std::cout << "global: x = " << x << ", y = " << y << '\n';
return 0;
}Вывод программы:
block: x = 10, y = 239
local: x = 239, y = 3.14
global: x = 21, y = 239Внутри блока x — локальная переменная со значением 10. Глобальный y при этом виден, потому что y локально не объявлен. Параметр функции x скрывает глобальный x, а локальный y скрывает глобальный y внутри функции.
Одинаковые имена в разных областях компилятор принимает, но читатель может перепутать переменные. Используйте разные имена или хотя бы включите предупреждения компилятора -Wshadow.
Частые ошибки
elseбез фигурных скобок при вложенныхif.- Забытый
breakвswitch, который «проваливает» выполнение. - Шаг цикла, который пропускается из-за
continueвwhile. - Рекурсия без условия остановки.
- Переполнение при вычислении больших факториалов.
Чек-лист
- Могу объяснить, чем выражение отличается от инструкции.
- Ставлю фигурные скобки у каждой ветки и каждого цикла.
- Понимаю, когда
breakиcontinueвforиwhileведут себя по-разному. - Могу написать функцию с объявлением и определением.
- Понимаю, почему рекурсия без условия остановки не работает.
Шпаргалка
| Понятие | Суть |
|---|---|
| Выражение | Вычисление, которое обычно даёт значение |
| Блок | Инструкции в фигурных скобках и своя область видимости |
else | Относится к ближайшему if без ветки |
while | Проверка условия до прохода |
do-while | Проверка условия после прохода, тело выполнится хотя бы раз |
for | Инициализация, условие и шаг в заголовке |
break | Выход из цикла или switch |
continue | Переход к следующей итерации |
switch | Выбор по целочисленной константе, нужен break |
| Сокрытие имён | Внутренняя переменная временно скрывает внешнюю |
Актуальная версия: https://m3105.ru/notes/osnovy-programmirovaniya/lektsiya-2-vyrazheniya-upravlenie-potokom-i-funktsii

Комментарии0
Пока никто ничего не написал.
Войдите, чтобы оставить комментарий