Как разбить программу на части, у каждой из которых есть имя, вход и результат
return, voidmain читался как план программыg++ -std=c++17 -Wall -Wextra файл.cpp -o файл.exe.В практической работе №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.
voidВызов digitCount(a) при a = 30572 выполняется так:
a, 30572;n, и в него копируется значение аргумента;return;return (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; пишется явно.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
bool, а имя начинается с is или has: if (isPrime(n)) читается как фраза. Сравнивать с true не нужно.main стал проще: в нём нет вложенного цикла, признака и разбора «почему кончился цикл». Что такое «простое», знает isPrime; что делать с простыми, знает main.return завершает только функцию, а не программу. Цикл в main продолжается со следующего n. return в main — особый случай: завершение main и есть завершение программы (раздел 6).Компилятор читает файл сверху вниз и должен знать функцию до первого вызова: её имя, типы параметров и тип результата. Если определение 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> в основном состоят из объявлений.Параметр, объявленный как 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
| Шаг | Стек — снизу вверх | Что происходит |
|---|---|---|
| 1 | main {s} | вызов sumOfSquares(3, 4) |
| 2 | main → sumOfSquares {a = 3, b = 4} | вызов square(3) |
| 3 | main → sumOfSquares → square {x = 3} | return 9 — кадр square снят |
| 4 | main → sumOfSquares → square {x = 4} | новый кадр для второго вызова; return 16 |
| 5 | main → sumOfSquares {a = 3, b = 4} | return 9 + 16 — кадр снят |
| 6 | main {s = 25} | выполнение продолжается в main |
Такую картину показывает отладчик (call stack) при остановке внутри функции. Размер стека ограничен — у программ Windows это единицы мегабайт; как стек устроен в памяти процесса — раздел 18. Функция может вызывать и саму себя — это рекурсия; каждый такой вызов добавляет кадр, и слишком глубокая рекурсия переполняет стек. В курсе рекурсия не нужна: все задачи решаются циклами.
constexpr; всё остальное функция получает через параметры и отдаёт через результат или параметры-ссылки.Ссылка 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&, если функция его меняет.Теперь блок проверки ввода из раздела 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.
Как делить программу на функции. Три правила, которых курс придерживается дальше:
readNumber, isPrime, digitCount. Если в имени просится «и» — это две функции.digitCount и isPrime получают всё через параметры и отдают через результат, поэтому их можно вызвать откуда угодно и проверить на десятке значений без клавиатуры. Ввод — в функциях ввода, вывод — в main или функциях вывода (printStars).main — план программы: ввод, вызовы, вывод. Если main длиннее экрана или в ней вложенные циклы — часть работы просится в функцию.Цену нужно вводить с проверкой так же, как количество, но она типа 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
readNumber(0.01, 100000.0, price) — все аргументы double: точное совпадение со второй версией. readNumber(1, 1000, quantity) — с первой.double-переменной вызвать нельзя: параметр int& не может означать double (пункт 06). Поэтому readNumber(1, 100000, price) тоже выберет версию для double — единственную подходящую.main занимает 16 строк и читается как план: цена, количество, стоимость. Три ввода с проверкой «в лоб» заняли бы 60.std::abs из <cmath> перегружена для int, long long, double: std::abs(-3) вернёт int, std::abs(-2.5) — double. Так же перегружены std::sqrt, std::pow и операция << у std::cout — поэтому один и тот же << печатает и числа, и строки. Когда перегружать: функции делают одно и то же для разных типов или разного числа данных. Функции с разным смыслом называют по-разному, даже если параметры позволяют дать одно имя.Если параметр почти всегда получает одно и то же значение, его можно указать в объявлении: 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(int width = 40, char fill) не собирается: default argument missing for parameter 2. Задать fill, не задав width, нельзя: printLine(, '=') — синтаксическая ошибка.defaulttwice.cpp:12:6: error: default argument given for parameter 1 of 'void printLine(int, char)' [-fpermissive]
defaulttwice.cpp:4:6: note: previous specification in 'void printLine(int, char)' here
defaulttwice.cpp:12:6: error: default argument given for parameter 2 of 'void printLine(int, char)' [-fpermissive]
defaulttwice.cpp:4:6: note: previous specification in 'void printLine(int, char)' here
В определении принято оставлять напоминание комментарием: void printLine(int width /* = 40 */, char fill /* = '-' */).printLine(int width = 40, char fill = '-') объявить printLine() без параметров, вызов printLine() подойдёт обеим — ошибка неоднозначности.Параметры по умолчанию — короткая запись той же идеи, что и перегрузка по числу параметров: printLine(), printLine(10) и printLine(10, '=') — как три функции, из которых две просто вызывают третью с готовыми значениями. Если «укороченная» версия должна делать не то же самое, а что-то другое — нужна перегрузка.
| Ошибка | Признак | Сообщение g++ | Как избежать |
|---|---|---|---|
| вызов выше определения, прототипа нет | не собирается | was not declared in this scope | прототипы всех функций перед main |
| прототип и определение различаются типами параметров | не компонуется | undefined reference to `имя(типы)' | копировать заголовок определения в прототип, а не набирать заново |
путь без return в функции с результатом | случайный результат | control reaches end of non-void function | return в конце каждой функции с результатом |
| функция должна изменить аргумент, а параметр — по значению | после вызова аргумент прежний | нет | параметр-ссылка 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 — план программыmain и код завершения; раздел 3 — типы, преобразования, область видимости, constexpr; разделы 4–6 — ветвления, циклы, проверка ввода, поиск во вложенных циклах. Нужен для: разделов 8–10 — обработка массивов и строк функциями, параметр-указатель и передача массива в функцию; раздела 12 — функции-члены классов и их перегрузка; разделов 14–19 — каждая функция Windows API вызывается по прототипу из <windows.h>, а функция потока пишется по шаблону, который задаёт Windows; раздела 18 — заголовочный файл с прототипами функций DLL и стек в памяти процесса.