К содержимому
ИС30

Поиск по сайту

Конспекты, лабы, квизы, ЧаВо и страницы

Войти

Лекция 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: беззнаковое переполнение определено по модулю 2642^{64}, но результат перестаёт быть математическим факториалом. Первое переполнение происходит уже на 21!21!.

Когда рекурсия уместна

Рекурсия удобна, когда задача сама устроена рекурсивно: обход дерева, вычисление выражения со скобками. Для простых циклов итеративная версия экономит стек и обычно читается не хуже.

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
Сокрытие имёнВнутренняя переменная временно скрывает внешнюю