Раздел 7 — Функции и перегрузка

Как разбить программу на части, у каждой из которых есть имя, вход и результат

Прогресс курса Раздел 7 из 20

Что вы освоите в этом разделе

3 академических часа теории и 3 часа практики: практическая работа №9 «Перегрузка функций» — на портале. Раздел открывает блок Б «Данные и функции». Все программы страницы — полные; собирайте их командой g++ -std=c++17 -Wall -Wextra файл.cpp -o файл.exe.
01

Зачем нужны функции

В практической работе №7 блок проверки ввода из раздела 6 повторялся для каждого числа — по 20 строк, которые отличаются только именем переменной и диапазоном. Исправить в нём ошибку значит исправить её во всех копиях и не пропустить ни одной. Функция — именованный фрагмент программы, который получает значения на вход, выполняет свою работу и возвращает результат. Написанная один раз, она вызывается столько раз, сколько нужно, одной строкой.

Функциями вы уже пользовались: main — функция, с которой начинается программа (раздел 2); std::sqrt и std::fabs — функции стандартной библиотеки; std::cin.fail() — функция потока. Теперь — свои. Количество цифр числа из раздела 6, оформленное функцией:

// Количество цифр двух чисел: одна функция, два вызова
#include <iostream>

int digitCount(int n)
{
    if (n < 0) {
        n = -n;
    }
    int count = 0;
    do {
        n /= 10;
        ++count;
    } while (n > 0);
    return count;
}

int main()
{
    int a, b;
    std::cout << "Two numbers: ";
    std::cin >> a >> b;
    const int lengthA = digitCount(a);
    const int lengthB = digitCount(b);
    std::cout << a << ": " << lengthA << ", " << b << ": " << lengthB << '\n';
    if (lengthA == lengthB) {
        std::cout << "Same length\n";
    } else if (lengthA > lengthB) {
        std::cout << "First is longer\n";
    } else {
        std::cout << "Second is longer\n";
    }
    return 0;
}
Two numbers: 30572 -408   ->  30572: 5, -408: 3 / First is longer
Two numbers: 0 7          ->  0: 1, 7: 1 / Same length
ЧастьВ примереЧто это
тип результатаintкакого типа значение вернёт функция
имяdigitCountпо правилам идентификаторов (раздел 2); в курсе — со строчной буквы, следующие слова с заглавной
параметры(int n)переменные функции, которые получают значения при вызове; через запятую, у каждого свой тип; скобки пустые, если параметров нет
тело{ … }инструкции функции
return выражение;return count;завершает функцию и передаёт значение туда, откуда её вызвали
вызовdigitCount(a)выражение: выполняет функцию и принимает значение, которое она вернула
аргументa, bзначение, переданное в вызове; им инициализируется параметр

Первые три части вместе — заголовок функции, заголовок с телом — определение. Слово «параметр» относится к функции, «аргумент» — к вызову: у digitCount один параметр n, а в двух вызовах — аргументы a и b.

02

Как выполняется вызов. void

Вызов digitCount(a) при a = 30572 выполняется так:

  1. вычисляются аргументы — здесь это просто значение a, 30572;
  2. создаётся параметр n, и в него копируется значение аргумента;
  3. выполняется тело функции — до return;
  4. значение выражения после return (5) становится значением вызова, параметр и переменные функции уничтожаются;
  5. выполнение продолжается в main — инициализируется lengthA.

Внутри функции n меняется — делится на 10 до нуля, — а a в main остаётся 30572: функция работала с копией. Поэтому в выводе печатается исходное a. Подробно об этом — в пункте 05.

Функция без результата объявляется с типом void («пусто»). Её вызов — отдельная инструкция; в выражении его использовать нельзя: значения нет. return; без выражения завершает её досрочно, а в конце тела он не нужен:

void printStars(int count)
{
    if (count <= 0) {
        return;                        // рисовать нечего
    }
    for (int i = 0; i < count; ++i) {
        std::cout << '*';
    }
    std::cout << '\n';
}

printStars(5);                          // *****

У функции с типом результата, наоборот, каждый путь выполнения обязан закончиться return со значением. Если какой-то путь доходит до закрывающей скобки, g++ предупреждает:

int sign(int x)
{
    if (x > 0) {
        return 1;
    } else if (x < 0) {
        return -1;
    }
}                                       // при x == 0 — ни одного return
noreturn.cpp:9:1: warning: control reaches end of non-void function [-Wreturn-type]
Это предупреждение — ошибка. Вызов sign(0) вернёт случайное значение, а с оптимизацией (-O2) программа может повести себя как угодно: по стандарту это неопределённое поведение. Исправление — return 0; в конце. Исключение одно — main: без return она возвращает 0, но в курсе return 0; пишется явно.
03

return из середины функции

return завершает функцию из любого места — в том числе из середины цикла. В разделе 5 проверка на простоту требовала цикла с составным условием и разбора после него, почему цикл закончился. Функцией она записывается прямо: нашли делитель — ответ «нет», перебрали все — «да»:

// Простые числа на отрезке: функция isPrime, return из середины цикла
#include <iostream>

bool isPrime(int n)
{
    if (n < 2) {
        return false;
    }
    for (int divisor = 2; divisor * divisor <= n; ++divisor) {
        if (n % divisor == 0) {
            return false;               // делитель найден — дальше искать незачем
        }
    }
    return true;
}

int main()
{
    int a, b;
    std::cout << "a, b (1..100000): ";
    std::cin >> a >> b;
    if (std::cin.fail() || a < 1 || b > 100000 || a > b) {
        std::cout << "Error: need 1 <= a <= b <= 100000\n";
        return 1;
    }

    int count = 0;
    for (int n = a; n <= b; ++n) {
        if (isPrime(n)) {
            std::cout << n << ' ';
            ++count;
        }
    }
    std::cout << "\nPrimes: " << count << '\n';
    return 0;
}
a, b (1..100000): 1 30          ->  2 3 5 7 11 13 17 19 23 29 / Primes: 10
a, b (1..100000): 90 96         ->  (пустая строка) / Primes: 0
a, b (1..100000): 99990 100000  ->  99991 / Primes: 1
04

Объявление и определение. Прототип

Компилятор читает файл сверху вниз и должен знать функцию до первого вызова: её имя, типы параметров и тип результата. Если определение isPrime перенести ниже main, сборка остановится на вызове:

noproto.cpp:6:18: error: 'isPrime' was not declared in this scope

Чтобы main можно было поставить первой, функцию объявляют заранее — пишут заголовок с точкой с запятой вместо тела. Такое объявление называется прототипом; имена параметров в нём можно опустить, но с ними понятнее:

#include <iostream>

bool isPrime(int n);                 // прототип: объявление без тела

int main()
{
    ...                                 // вызовы isPrime — компилятор уже знает её заголовок
}

bool isPrime(int n)                  // определение — где угодно ниже
{
    ...
}

Объявлений одной функции может быть сколько угодно, определение — ровно одно. Если прототип и определение не совпадают по типам параметров, это две разные функции: одна объявлена, но не определена. Компилятор такую программу пропускает — ему достаточно прототипа, — а ошибку находит компоновщик, который ищет готовый код каждой вызванной функции (раздел 2, этапы сборки):

bool isPrime(int n);                 // объявлена isPrime(int)
...
bool isPrime(long long n)            // определена isPrime(long long) — другая функция
...ld.exe: ...: undefined reference to `isPrime(int)'
collect2.exe: error: ld returned 1 exit status
Правило курса: прототипы всех функций — перед main, определения — после неё, в том же порядке, у каждого определения — комментарий: что функция делает и что возвращает. Тогда main читается первым, как оглавление, а список прототипов — как перечень того, что умеет программа. В разделе 18 прототипы функций DLL переедут в заголовочный файл .h — это и есть главное назначение прототипов: <iostream> и <windows.h> в основном состоят из объявлений.
05

Параметры по значению, локальные переменные, стек вызовов

Параметр, объявленный как int x, — параметр по значению: при вызове в него копируется значение аргумента, и дальше это самостоятельная переменная функции. Что бы функция с ней ни делала, аргумент не изменится. Попытка написать обмен двух переменных функцией это показывает:

// Обмен значений: параметры по значению не меняют аргументы, по ссылке — меняют
#include <iostream>

void swapCopy(int a, int b);
void swapRef(int& a, int& b);

int main()
{
    int x = 1;
    int y = 2;
    swapCopy(x, y);
    std::cout << "after swapCopy: x = " << x << ", y = " << y << '\n';
    swapRef(x, y);
    std::cout << "after swapRef:  x = " << x << ", y = " << y << '\n';
    return 0;
}

void swapCopy(int a, int b)
{
    const int temp = a;
    a = b;
    b = temp;
}

void swapRef(int& a, int& b)
{
    const int temp = a;
    a = b;
    b = temp;
}
after swapCopy: x = 1, y = 2
after swapRef:  x = 2, y = 1

swapCopy честно поменяла местами свои a и b — копии, которые исчезли при выходе из функции. swapRef отличается одним символом & — о нём пункт 06.

Локальные переменные. Параметры и переменные, объявленные в теле, — локальные переменные функции (раздел 3, область видимости): их видно только внутри неё. Имена в разных функциях не связаны — a в swapCopy и a в swapRef — разные переменные, и count в digitCount никак не мешает count в main. Функция не видит переменных main: всё, что ей нужно, она получает через параметры.

Стек вызовов. Локальные переменные создаются при каждом вызове заново и уничтожаются при выходе — поэтому они не помнят значений от прошлого вызова. Хранятся они в области памяти, которая называется стеком: вызов кладёт на вершину стека кадр — параметры, локальные переменные и адрес, куда вернуться; return снимает кадр. Последним вызван — первым завершается:

int square(int x)
{
    return x * x;
}

int sumOfSquares(int a, int b)
{
    return square(a) + square(b);
}

int s = sumOfSquares(3, 4);           // в main
ШагСтек — снизу вверхЧто происходит
1main {s}вызов sumOfSquares(3, 4)
2main → sumOfSquares {a = 3, b = 4}вызов square(3)
3main → sumOfSquares → square {x = 3}return 9 — кадр square снят
4main → sumOfSquares → square {x = 4}новый кадр для второго вызова; return 16
5main → sumOfSquares {a = 3, b = 4}return 9 + 16 — кадр снят
6main {s = 25}выполнение продолжается в main

Такую картину показывает отладчик (call stack) при остановке внутри функции. Размер стека ограничен — у программ Windows это единицы мегабайт; как стек устроен в памяти процесса — раздел 18. Функция может вызывать и саму себя — это рекурсия; каждый такой вызов добавляет кадр, и слишком глубокая рекурсия переполняет стек. В курсе рекурсия не нужна: все задачи решаются циклами.

Глобальные переменные. Переменная вне функций видна всем функциям файла (раздел 3). Кажется, что так проще передавать данные, но тогда любая функция может её изменить, и чтобы понять значение переменной в одной строке, приходится читать всю программу. Правило курса: вне функций — только константы constexpr; всё остальное функция получает через параметры и отдаёт через результат или параметры-ссылки.
06

Параметры по ссылке

Ссылка int& a — другое имя уже существующей переменной. Параметр-ссылка не создаёт копию: при вызове swapRef(x, y) имя a внутри функции означает саму x из main, а b — саму y. Присваивание a = b меняет x — поэтому swapRef в пункте 05 работает.

По значению int xПо ссылке int& x
Что получает функциякопию значениясаму переменную под другим именем
Изменение параметрааргумент не меняетсяменяет аргумент
Каким может быть аргументлюбое выражение: x, 5, a + 1, f(a)только переменная того же типа
Когда выбиратьфункция только читает значение — почти всегдафункция должна изменить переменную вызывающего или вернуть больше одного результата

Аргументом параметра-ссылки может быть только переменная: у выражения a + 1 нет места в памяти, которое функция могла бы изменить.

void twiceRef(int& x)
{
    x *= 2;
}
...
twiceRef(a + 1);
rvalueref.cpp:10:16: error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'
rvalueref.cpp:2:20: note:   initializing argument 1 of 'void twiceRef(int&)'

Переменная другого типа тоже не подойдёт: double d; twiceRef(d); — та же ошибка, потому что ссылка на int не может означать переменную типа double.

Несколько результатов. return возвращает одно значение. Если результатов больше, функция возвращает главный — например, «нашлось ли» — а остальные записывает в параметры-ссылки. Так решается поиск во вложенных циклах, который в разделе 6 требовал goto или признака: return выходит сразу из всех циклов, потому что завершает функцию.

// n = a^2 + b^2: поиск во вложенных циклах — функция, выход из обоих циклов — return
#include <iostream>

bool twoSquares(int n, int& a, int& b);

int main()
{
    int n;
    std::cout << "n (1..1000000): ";
    std::cin >> n;
    if (std::cin.fail() || n < 1 || n > 1000000) {
        std::cout << "Error: n must be 1..1000000\n";
        return 1;
    }

    int a = 0;
    int b = 0;
    if (twoSquares(n, a, b)) {
        std::cout << n << " = " << a << "^2 + " << b << "^2\n";
    } else {
        std::cout << n << ": no such a, b\n";
    }
    return 0;
}

// n = a^2 + b^2, 1 <= a <= b, с наименьшим a. Нашла — true и ответ в a, b; нет — false, a и b не меняются
bool twoSquares(int n, int& a, int& b)
{
    for (int i = 1; i * i <= n; ++i) {
        for (int j = i; i * i + j * j <= n; ++j) {
            if (i * i + j * j == n) {
                a = i;
                b = j;
                return true;
            }
        }
    }
    return false;
}
n (1..1000000): 25        ->  25 = 3^2 + 4^2
n (1..1000000): 1000000   ->  1000000 = 280^2 + 960^2
n (1..1000000): 999999    ->  999999: no such a, b

Вывод тот же, что у обеих версий из раздела 6, но нет ни goto, ни признака found. Параметры-ссылки a и b здесь — выходные: функция их только записывает, и в комментарии к ней сказано, когда именно. В main они обнулены до вызова, чтобы у переменных всегда было определённое значение, даже если ответа нет.

Бывает и ссылка на константу — const int& x: функция получает переменную без копирования, но изменить её не может. Для int и double это бессмысленно — скопировать число дешевле. Она понадобится для больших значений — строк и структур (разделы 10–11). В этом разделе параметр либо по значению, либо int& / double&, если функция его меняет.
07

Функция ввода с проверкой. Программа из функций

Теперь блок проверки ввода из раздела 6 становится функцией. У неё два результата: прочитанное число и признак, удалось ли его прочитать, — ведь при конце ввода числа нет. По пункту 06 признак — результат функции, число — параметр-ссылка. Диапазон — параметры по значению:

// Читает целое из [low; high]: не число или вне диапазона — сообщение и повтор.
// Конец ввода — сообщение и false
bool readNumber(int low, int high, int& value)
{
    bool ok = false;
    do {
        std::cin >> value;
        if (std::cin.fail()) {
            if (std::cin.eof()) {
                std::cout << "\nNo input\n";
                return false;
            }
            std::cin.clear();
            std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
            std::cout << "Not a number, again: ";
        } else if (value < low || value > high) {
            std::cout << "Must be " << low << ".." << high << ", again: ";
        } else {
            ok = true;
        }
    } while (!ok);
    return true;
}

Приглашение печатает вызывающий — он знает, что именно спрашивает; функция при ошибке пишет «again» и ждёт повтора. В main каждый ввод теперь занимает две строки, а конец ввода — третью:

int quantity;
std::cout << "Quantity (1..1000): ";
if (!readNumber(1, 1000, quantity)) {
    return 1;                           // ввод кончился — код 1, как в разделе 6
}

Полная программа — в конце пункта 08: ей нужна ещё одна readNumber, для цены типа double.

Как делить программу на функции. Три правила, которых курс придерживается дальше:

08

Перегрузка функций

Цену нужно вводить с проверкой так же, как количество, но она типа double. Можно назвать вторую функцию readDouble, но в C++ у функций, которые делают одно и то же с разными типами, может быть одно имя. Это перегрузка: несколько функций с одинаковым именем, различающихся количеством или типами параметров. При каждом вызове компилятор выбирает ту, чьи параметры лучше всего подходят к аргументам.

// Перегрузка: одно имя — несколько функций с разными параметрами
#include <iostream>

int maxOf(int a, int b);
int maxOf(int a, int b, int c);
double maxOf(double a, double b);

int main()
{
    std::cout << maxOf(3, 7) << '\n';           // maxOf(int, int)
    std::cout << maxOf(3, 9, 5) << '\n';        // maxOf(int, int, int)
    std::cout << maxOf(2.5, 1.5) << '\n';       // maxOf(double, double)
    std::cout << maxOf('a', 'b') << '\n';       // char -> int: maxOf(int, int)
    return 0;
}

int maxOf(int a, int b)
{
    if (a > b) {
        return a;
    }
    return b;
}

int maxOf(int a, int b, int c)
{
    return maxOf(maxOf(a, b), c);
}

double maxOf(double a, double b)
{
    if (a > b) {
        return a;
    }
    return b;
}
7
9
2.5
98

Последняя строка — не опечатка: у maxOf нет версии для char, символы преобразованы в int, и напечатан код 'b' — 98 (раздел 3). Функция для трёх чисел вызывает версию для двух — одна перегрузка может опираться на другую.

Как компилятор выбирает. Сначала отбираются функции с подходящим числом параметров. Затем для каждого аргумента оценивается, насколько просто привести его к типу параметра:

РангПреобразованиеПример
1 — лучшийточное совпадение типаint → int, переменная → ссылка того же типа
2повышение: «малый» тип в «свой» большойchar, bool, short → int; float → double
3стандартное преобразованиеint → double, double → int, int → long long, long → int

Выигрывает функция, у которой каждый аргумент подходит не хуже, чем у остальных, и хотя бы один — лучше. Если такой нет — ошибка неоднозначности. Вызов maxOf(3, 2.5): для maxOf(int, int) первый аргумент точный, второй требует преобразования; для maxOf(double, double) — наоборот. Ни одна не лучше:

ambiguous.cpp:9:23: error: call of overloaded 'maxOf(int, double)' is ambiguous
ambiguous.cpp:4:5: note: candidate: 'int maxOf(int, int)'
ambiguous.cpp:5:8: note: candidate: 'double maxOf(double, double)'

Исправление — сделать аргументы одного типа: maxOf(3.0, 2.5). Если же подходящая версия одна, компилятор преобразует аргументы молча, даже с потерей: при единственной maxOf(int, int) вызов maxOf(3, 2.5) собрался бы и вернул 3.

Тип результата не различает перегрузки. По вызову half(4) нельзя понять, какой результат нужен, поэтому две функции, различающиеся только типом результата, — ошибка уже при объявлении:

int half(int x);
double half(int x);
returnonly.cpp:3:8: error: ambiguating new declaration of 'double half(int)'
returnonly.cpp:2:5: note: old declaration 'int half(int)'

Теперь полная программа с двумя readNumber — для int и для double. Тело второй отличается от первой только типами; компилятор выбирает версию по типам аргументов:

// Покупка со скидкой: ввод с проверкой — две перегруженные функции readNumber
#include <iostream>
#include <iomanip>
#include <limits>

constexpr int DISCOUNT_FROM = 10;       // штук: с этого количества — скидка
constexpr double DISCOUNT = 0.05;       // 5 %

bool readNumber(int low, int high, int& value);
bool readNumber(double low, double high, double& value);
double total(double price, int quantity);

int main()
{
    double price;
    int quantity;
    std::cout << "Price (0.01..100000): ";
    if (!readNumber(0.01, 100000.0, price)) {
        return 1;
    }
    std::cout << "Quantity (1..1000): ";
    if (!readNumber(1, 1000, quantity)) {
        return 1;
    }
    std::cout << std::fixed << std::setprecision(2)
              << "Total: " << total(price, quantity) << '\n';
    return 0;
}

// readNumber(int, int, int&) — как выше

// То же для вещественного числа: другой тип параметров — другая функция с тем же именем
bool readNumber(double low, double high, double& value)
{
    ...                                 // тело как у версии для int
}

// Стоимость покупки: от DISCOUNT_FROM штук — со скидкой
double total(double price, int quantity)
{
    double sum = price * quantity;
    if (quantity >= DISCOUNT_FROM) {
        sum *= 1 - DISCOUNT;
    }
    return sum;
}
Price (0.01..100000): abc                   Price (0.01..100000): 99.9
Not a number, again: -5                     Quantity (1..1000): 12
Must be 0.01..100000, again: 250            Total: 1138.86
Quantity (1..1000): 0
Must be 1..1000, again: 3                   Price (0.01..100000): ^Z
Total: 750.00
                                            No input
Перегрузка в стандартной библиотеке. std::abs из <cmath> перегружена для int, long long, double: std::abs(-3) вернёт int, std::abs(-2.5) — double. Так же перегружены std::sqrt, std::pow и операция << у std::cout — поэтому один и тот же << печатает и числа, и строки. Когда перегружать: функции делают одно и то же для разных типов или разного числа данных. Функции с разным смыслом называют по-разному, даже если параметры позволяют дать одно имя.
09

Параметры по умолчанию

Если параметр почти всегда получает одно и то же значение, его можно указать в объявлении: int width = 40. Тогда аргумент для него в вызове можно не писать — подставится значение по умолчанию:

// Параметры по умолчанию: printLine() рисует линию из 40 минусов
#include <iostream>

void printLine(int width = 40, char fill = '-');

int main()
{
    printLine();                // printLine(40, '-')
    printLine(10);              // printLine(10, '-')
    printLine(10, '=');
    return 0;
}

void printLine(int width, char fill)
{
    for (int i = 0; i < width; ++i) {
        std::cout << fill;
    }
    std::cout << '\n';
}
----------------------------------------
----------
==========

Правила:

Параметры по умолчанию — короткая запись той же идеи, что и перегрузка по числу параметров: printLine(), printLine(10) и printLine(10, '=') — как три функции, из которых две просто вызывают третью с готовыми значениями. Если «укороченная» версия должна делать не то же самое, а что-то другое — нужна перегрузка.

10

Ловушки раздела

ОшибкаПризнакСообщение g++Как избежать
вызов выше определения, прототипа нетне собираетсяwas not declared in this scopeпрототипы всех функций перед main
прототип и определение различаются типами параметровне компонуетсяundefined reference to `имя(типы)'копировать заголовок определения в прототип, а не набирать заново
путь без return в функции с результатомслучайный результатcontrol reaches end of non-void functionreturn в конце каждой функции с результатом
функция должна изменить аргумент, а параметр — по значениюпосле вызова аргумент прежнийнетпараметр-ссылка int&
выражение или переменная другого типа как аргумент ссылкине собираетсяcannot bind non-const lvalue referenceпередать переменную того же типа
вызов подходит двум перегрузкам одинаковоне собираетсяcall of overloaded … is ambiguousаргументы одного типа: 3.0, а не 3
единственная подходящая версия с потерей данныхдробная часть молча отброшенанетперегрузка для double или аргумент нужного типа
перегрузки различаются только типом результатане собираетсяambiguating new declarationразные имена
значение по умолчанию повторено в определениине собираетсяdefault argument given for parameterзначение — только в прототипе
значение по умолчанию не у последнего параметране собираетсяdefault argument missing for parameterпараметры по умолчанию — в конце списка
функция меняет глобальную переменнуюзначение «само» меняется между вызоваминетвне функций — только constexpr
Функцию, которая только считает, проверяйте отдельно от ввода: в main — десяток вызовов с известным ответом, включая границы, и вывод «вызов = результат». Ошибку в такой функции видно сразу, а не после десяти минут ввода с клавиатуры. Так устроено задание 2 практической работы №9.

Ключевые выводы раздела

Запомните главное

  • Функция: тип результата, имя, параметры, тело; return завершает её и возвращает значение; void — функция без результата
  • Функцию нужно объявить до вызова; прототип — объявление без тела; не совпавшие прототип и определение находит компоновщик
  • Параметр по значению — копия аргумента; параметр по ссылке & — сам аргумент под другим именем, аргументом может быть только переменная того же типа
  • Локальные переменные создаются при каждом вызове в кадре стека и исчезают при выходе
  • Несколько результатов — главный через return, остальные через параметры-ссылки; return выходит сразу из всех циклов функции
  • Перегрузка — одно имя, разные количество или типы параметров; тип результата не считается; неоднозначный вызов — ошибка
  • Параметры по умолчанию — только в конце списка и только в прототипе
  • Функция, которая считает, не читает и не печатает; main — план программы
Связи раздела. Опирается на раздел 2 — этапы сборки, компоновщик, main и код завершения; раздел 3 — типы, преобразования, область видимости, constexpr; разделы 4–6 — ветвления, циклы, проверка ввода, поиск во вложенных циклах. Нужен для: разделов 8–10 — обработка массивов и строк функциями, параметр-указатель и передача массива в функцию; раздела 12 — функции-члены классов и их перегрузка; разделов 14–19 — каждая функция Windows API вызывается по прототипу из <windows.h>, а функция потока пишется по шаблону, который задаёт Windows; раздела 18 — заголовочный файл с прототипами функций DLL и стек в памяти процесса.
Практическая работа №9 — на учебном портале. Задания по вариантам, критерии оценивания и сдача — в курсе на portal.nevabit.ru. Учётную запись выдаёт преподаватель.
Раздел 6: Цикл do while и инструкции перехода Практика на портале