--- tags: прога, зачёт по проге --- # Подготовка к зачёту (2020-12-09, среда) ## 1. Чтение сложного типа в Си Если вы мне дадите тип `void *(*f[])(void *(*)(void *(*)()[]))`, я вам без проблем скажу, что это тип массива указателей на функции, которые возвращают `void *` и принимают `void *(*)(void *(*)()[])` (указатель на функцию, которая возвращает `void *` и принимает указатель на функцию, которая принимает неопределённое число аргументов и возвращает массив указателей на `void *`) единственным аргументом. Придержите свою челюсть, это не магия, и тут всё очень просто. Сейчас расскажу как. Дано: `void *(*f[])(void *(*)(void *(*)()[]))`. Надо понять, откуда начинать развёртывать тип. Делается это так. 1. Читаем тип слева направо. Первым мы должны встретить какой-то **основной тип**. В примере это `void`. Может быть стракт, юнион, енам, инт какой-нибудь, каждый может быть вместе с `const`. 2. Дальше у нас идёт декларатор. > Формально говорить если (вдруг поможет), задаётся он так: > > - идентификатор — это декларатор > - `(декларатор)` — это декларатор > - `*декларатор` — это декларатор > - `декларатор-кроме-понтеров[размер]` — декларатор (`размер` может отсутствовать) > - `декларатор-кроме-поинтеров(тип без идентификаторов, тип без идентификаторов, ⋯)` — декларатор > > `декларатор-кроме-поинтеров` означает, что это любой декларатор, кроме вида `*декларатор`. 3. Если встретили идентификатор, останавливаемся здесь. 4. Если видим `*`, идём дальше. 5. Если видим `(`, идём внутрь. 6. Если встретили квадратную скобку, стопаемся перед ней. С места, где остановились, начинаем читать тип. В нашем примере: `void *(* | f[])(void *(*)(void *(*)()[]))` — на месте чёрточки. Будем тип описывать постепенно. Кусочки я в кавычки записывать буду. Если справа идентификатор, то "переменная с этим именем имеет тип...". Иначе — "наш тип — это..." 1. Смотрим направо от уже прочитанного. - Там может быть `[]` — "...массив..." - Уходим в шаг 1. - Там может быть `(...)` — "...функция с параметрами внутри скобок и возвращающей..." - Уходим в шаг 1. - Там может быть закрывающаяся скобка `)` — тогда переходим ко 2 шагу. - Или мы могли дойти до конца типа, тогда тоже ко 2 шагу переходим. 2. Смотрим налево от уже прочитанного. - Там может быть `*` — "...указатель на..." - Уходим в шаг 2 - Там может быть `(` — goto step 1. - Там может стоять основной тип. "...основной тип." Мы дошли до конца, выходим. Попробуем. Прочитано | Номер шага | Видим | Кусочек описания типа :---------------------------------------------------------------------------------|:----------:|:--------------------|:---------------------------------------------------------- <code>void \*(\*<u>f</u>[])(void \*(\*)(void \*(\*)()[]))</code> | 0 | Идентификатор `f` | Переменная с именем `f` имеет тип... <code>void \*(\*<strong>f</strong><u>[]</u>)(void \*(\*)(void \*(\*)()[]))</code> | 1 | `[]` | ...массива... <code>void \*(\*<strong>f[]</strong><u>)</u>(void \*(\*)(void \*(\*)()[]))</code> | 1 → 2 | `)` | ... <code>void \*(<u>\*</u><strong>f[])</strong>(void \*(\*)(void \*(\*)()[]))</code> | 2 | `*` | ...указателей на... <code>void \*<u>(</u><strong>\*f[])</strong>(void \*(\*)(void \*(\*)()[]))</code> | 2 → 1 | `(` | ... <code>void \*<strong>(\*f[])</strong><u>(void \*(\*)(void \*(\*)()[]))</u></code> | 1 | `(...)` | ...функции с параметрами внутри скобок и возвращающих... <code>void \*<strong>(\*f[])(void \*(\*)(void \*(\*)()[]))</strong></code> | 1 → 2 | Конец типа | ... <code>void <u>\*</u><strong>(\*f[])(void \*(\*)(void \*(\*)()[]))</strong></code> | 2 | `*` | ...указатель на... <code><u>void</u> <strong>\*(\*f[])(void \*(\*)(void \*(\*)()[]))</strong></code> | 2 → конец | Основной тип `void` | ...`void`. Итак, переменная с именем `f` имеет тип массива указателей на функции с параметрами внутри скобок и возвращающих указатель на `void`. Чтобы узнать типы параметров, мы рекурсивно проходимся алгоритмом выше. Потренироваться: https://cdecl.org/ > Подсказывают, что невозможно в си создать функцию, которая возвращает массив. > > ```c > int id(int x[])[] { > return x; > } > ``` > > Этот код будет невалидным. Но тип корректен: функция, возвращающая `int[]`. ### 1.2. Запись сложного типа в Си Используя логику и алгоритм выше, мы можем записать тип любой сложности. Например, нужна функция `foo`, которая берёт указатель на функцию `(int x[]) -> void *` и возвращает его же. Как это записать? - Начинаем с имени: `foo`. - Дальше нужно сказать, что это функция, и мы добавляем: `foo(...)`. Вернёмся к содержимому позже, пока пойдём дальше. - Парсер типа поедет дальше вправо, чтобы узнать возвращаемый тип `foo`. Но чтобы подставить звёздочку, нужно его вернуть налево. Поэтому ставим в конце `)`: `foo(...))` — и парсер пойдёт налево от прочитанного до соответствующей `(`. - Ставим слева звёздочку: `*foo(...))`. У нас уже записано "foo — функция, возвращающая указатель на...". Теперь надо сказать, что указатель возвращает `foo` на функцию, а для этого парсер придётся отбросить вправо — скобочкой. `(*foo(...))`. - Парсер будет смотреть вправо от прочтённого, и туда мы закинем функцию: `(*foo(...))(...)`. - Теперь нам нужно снова записать указатель, для чего парсер должен вернуться назад. Можно снова поставить `)`, но если подумать, то вправо тип мы наращивать уже не будем, поэтому если ничего не ставить, то парсер и так влево возращаться будет. - Накидываем теперь влево оставшееся. Сначала "...возвращающую указатель на..." — `*(*foo(...))(...)`. Парсер всё ещё на 2 шаге алгоритма, что нам и нужно. Допишем основной тип: `void *(*foo(...))(...)`. - Мы получили такое: "`foo` — функция, возвращающая указатель на функцию, возвращающую указатель на `void`". Заполним пропуски на месте `...`. - Параметр у `foo` единственный. Назовём его `func`. Это указатель (`*func`) на функцию (`(*func)(...)`), берущую массив интов (`(*func)(int [])`) и возвращающую `void *` (`void *(*func)(int [])`). - Во втором пропуске параметры функции, которую плюёт `foo`, — это `int []`, поэтому так и запишем: `void *(*foo(void *(*func)(int [])))(int [])`. Вуаля. Мы получили нужный тип. Можем теперь дописать содержимое: ```c void *(*foo(void *(*func)(int [])))(int []) { return func; } ``` ## 2. BFS Код, который написали, внизу. Поговорили про такие вещи: - BFS = Breadth-first search — обход в ширину — дерево обходит послойно: сначала первый слой, потом второй и т. д. - Есть вот такая взаимосвязь: - Для DFS нужен **стэк**. - Стэк — это LIFO (last in, first out) структура данных. - Мы кладём (push) данные в один конец и достаём (pop) их оттуда же. - Получается, что забираем последний добавленный элемент. - Бытовая визуализация — стопка тарелок: добавляем новые мы поверх и сверху же берём их. - Для BFS нужна **очередь**. - Очередь — это FIFO (first in, first out) структура данных. - Мы кладём (enqueue) данные в один конец, а достаём (dequeue) с другого. - Получается, что первый элемент, который в очередь заехал, первым же оттуда и выйдет. - Бытовая визуализация — обычная линейная очередь: кто первый пришёл, тот первый попадёт. <table> <thead> <tr> <th>Очередь</th> <th>Стэк</th> </tr> </thead> <tbody> <tr> <td><p><a href="https://commons.wikimedia.org/wiki/File:Data_Queue.svg#/media/File:Data_Queue.svg"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/5/52/Data_Queue.svg/1200px-Data_Queue.svg.png" alt="Data Queue.svg"></a><br>By <a href="https://upload.wikimedia.org/wikipedia/commons/5/52/Data_Queue.svg" class="internal" title="Data Queue.svg">This Image</a> was created by <a href="https://en.wikipedia.org/wiki/User:Vegpuff" class="extiw" title="en:User:Vegpuff">User:Vegpuff</a>. - <span class="int-own-work" lang="en">Own work</span>, <a href="https://creativecommons.org/licenses/by-sa/3.0" title="Creative Commons Attribution-Share Alike 3.0">CC BY-SA 3.0</a>, <a href="https://commons.wikimedia.org/w/index.php?curid=7586271">Link</a></p></td> <td><p><a href="https://commons.wikimedia.org/wiki/File:Tallrik_-_Ystad-2018.jpg#/media/File:Tallrik_-_Ystad-2018.jpg"><img src="https://upload.wikimedia.org/wikipedia/commons/1/19/Tallrik_-_Ystad-2018.jpg" alt="Tallrik - Ystad-2018.jpg"></a><br>By Foto: Jonn Leffmann, <a href="https://creativecommons.org/licenses/by/3.0" title="Creative Commons Attribution 3.0">CC BY 3.0</a>, <a href="https://commons.wikimedia.org/w/index.php?curid=67631269">Link</a></p></td> </tr> </tbody> </table> - В DFS мы обычно стэк вручную не делаем потому, что используем рекурсию. - Есть понятие стэка вызовов: при вызове функции она кладётся в стэк и в любой момент времени исполняется та функция, которая в его верхушке. - Пример: `foo() -> bar() -> baz() -> eggs()`. Верхушка — `eggs()`, и она же и будет исполняться, остальные спят. - Чтобы неявно задать очередь, нужно использовать `goto` или корутину. Первое — моветон, а второго в сишечке нету. - Поэтому очередь придётся делать вручную. - Очередь можно соорудить двумя способами: - Через связные списки. - Довольно удобно по сравнению с другим способом. - Достаточно односвязного списка. - Нужно хранить указатель на начало списка, откуда удалять элементы, и на конец, куда добавлять. - Через дек — сложно, но если дек уже есть, то можно использовать. - Мы сделаем очередь на связном списке. - Алгоритм BFS: 1. Берём корень, кладём в очередь. 2. В цикле, пока очередь не пуста: 3. Берём первую ноду из очереди. 4. Помещаем детей ноды в очередь: сначала левый, потом правый. 5. Обрабатываем ноду — произвольная логика. 6. GOTO 2. ```c typedef struct tree { struct tree *left; struct tree *right; int value; } tree_t; // Нужен BFS (breadth-first search). // // DFS — стэк (юзаем рекурсию вместо явного стэка) // BFS — очередь (корутина или goto) // // Стэк вызовов: foo() -> bar() -> baz() -> eggs() // Чтобы сделать BFS, нам по-любому нужна очередь. // Очередь можно делать 2 путями: через связный список и через кольцевой буфер. // Делаем через связные списки, потому что это проще. typedef struct node_list { tree_t const *node; struct node_list *next; } node_list_t; // Вставить значение в связный список после данного. void list_insert_after(node_list_t **head, int value); // Удалить значение из списка. void list_remove(node_list_t **head); // Очередь — структура данных, в которой: // - элементы *помещаются* в один конец // - элементы *выдёргиваются* с другого конца // // queue_new(): ø // enqueue(1): 1 // enqueue(2): 1 → 2 // enqueue(3): 1 → 2 → 3 // dequeue(): 2 → 3, получаем 1 // dequeue(): 3, получаем 2 // dequeue(): ø, получаем 3 // // FIFO = First-in-first-out // (Стэк = LIFO — Last-in-first-out) typedef struct { node_list_t *start; node_list_t *end; size_t len; } queue_t; queue_t queue_new() { queue_t result = { .start = NULL, .end = NULL, .len = 0, }; return result; } void enqueue(queue_t *self, int value) { assert(self != NULL); list_insert_after(&self->end, value); ++self->len; if (self->len == 1) { // Было ø, стал список из 1 элемента self->start = self->end; } // Иначе всё ок: // 1 │ 1 → 2 // ├ end │⇒ │ └ end // └ start │ └ start } tree_t const *dequeue(queue_t *self) { assert(self != NULL); int result = self->start->value; list_remove(&self->start); --self->len; if (self->len == 0) { // Список опустел, потому что удалили элемент, на который указывали // self->start и self->end. Занулим self->end. self->end = NULL; } return result; } bool queue_is_empty(queue_t const *self) { return self->len == 0; } // Алгоритм: // 1. Берём корень, кладём в очередь. // 2. В цикле, пока очередь не пуста: // 3. Берём первую ноду из очереди. // 4. Помещаем детей ноды в очередь: сначала левый, потом правый. // 5. Обрабатываем ноду — произвольная логика. // 6. GOTO 2. void bfs(tree_t const *root) { queue_t *queue = queue_new(); // читается как "кью", спасибо французам enqueue(&queue, root); // ① while (!queue_is_empty(&queue)) { // ② tree_t const *node = dequeue(&queue); // ③ enqueue(&queue, node->left); // ④ enqueue(&queue, node->right); // ④ printf("%d\n", node->value); // ⑤ } // ⑥ } ``` ## 3. Обзор задачек ### 3.1. Поиск подстроки в строке Как делать `O(n²)`, понятно. `O(n)` — сложно, до экзамена доживёт. ### 3.2. Правильно ли расставлены скобки? **Дано:** `((()())))()()(()()()((())))` - Храним глубину — число открытых скобочек. - Идём по строке слева направо. - Видим `(` — увеличиваем глубину. - `)` — уменьшаем. - Если глубина 0 и встречаем `)`, то скобки расставлены неправильно. - Закрыли скобку, которую не открывали. - Если дошли до конца и глубина ненулевая, то скобки расставлены неправильно. - Не закрыли открытую скобку. ### 3.2.1. Правильно ли расставлены скобки разного типа? **Дано:** `(([[{{()}}]]))` **Или:** `(}` - Делать это стэком. - Каждую открывающую скобку пихаем в стэк. - Видим закрывающую — вытаскиваем из стэка и сверяем, что это тот же тип скобки. - Если не тот же, то скобки расставлены неправильно. - Закрыли не тот тип скобки. - Если стэк пуст, скобки расставлены неправильно. - Закрыли скобку, которую не открывали. - Когда дошли до конца, проверяем, что стэк пуст. - Если не пуст, то скобки расставлены неправильно. - Наоткрывали скобок, но не закрыли их. #### Стэк Структура данных, также известная как LIFO = Last in, first out. Пихаем (push) в один конец и оттуда же достаём (pop). Пример: - push(1): 1 - push(2): 1 → 2 - push(3): 1 → 2 → 3 - pop(): 1 → 2, получаем 3 - pop(): 1, получаем 2 - pop(): ø, получаем 1 Стэк делать обычным массивом динамическим. В конец массива добавляем новые элементы, оттуда же удаляем. ### 3.3. RLE Run-length encoding: `aaabbbbbbccd` ⇒ `3a6b2cd`. Вспоминаем, как делали (делали же?) вторую лабу, и делаем то же самое. Возможны вариации с форматом, но алгоритм тот же. Если надо читать из файла, то обработка ошибок — самая сложная часть: диск может накрыться, заблокироваться, быть удалённым, в файле может быть не цифра, где должна быть цифра, цифры может не быть, файл после числа внезапно может оборваться, число может не влезть в лонг-лонг... ### 3.4 Удаление заданных символов `testasdfasd34bsaeb` → `testasdfasdbsaeb` Самое сложное — понять, когда остановиться. `strlen` юзать не надо, проверять лучше на `'\0'`. ### 3.5 Сортировка (связного) списка Массивы — это 4 лаба. Связный список сортируется merge sort'ом. Массив делится рекурсивно пополам, пока не получим 2 массива по 1 элементу в каждому (или 1 массив из 1 элемента). Возвращаясь назад из рекурсии, мы сливаем по 2 упорядоченных списка за раз. ```c void merge(list_t *list1, list_t *list2); ``` Не забыть про то, что один из список может быть нулевым. Понять, какой из них такой, и вернуть другой. Если оба списка непусты, то выбираем один из них и сливаем другой в него. Правый опустеет, а левый потолстеет. Или наоборот. ### 3.6. Объединение двух упорядоченных связных списков См. `merge` сверху. Это он и есть. ### 3.7. Построение дерева по списку Есть такая вещь — двоичный поиск. Двоичное дерево поиска запечатлевает все пути алгоритма во время двоичного поиска. > Двоичное дерево поиска (BST) — это дерево, в котором **все** элементы левого поддерева меньше значению сверху, а **все** элементы правого поддерева больше этого значения. > > ``` > 3 > 1─┘ > └5 > ``` > > Это не BST, потому что пятёрка в левом поддереве тройки, хотя 5 ≮ 3. > > > Возможны вариации на тему одинаковых элементов. Обычно дупликаты не хранят, но можно всунуть в одно из поддеревьев. Алгоритм следующий: 1. Берём середину упорядоченного списка. 2. Рекурсивно обрабатываем часть слева и часть справа. Слева — в левое поддерево, справа — в правое поддерево помещаем. Не забыть вовремя остановиться. ### 3.8. Сравнение 2 деревьев Без понятия, что за задача такая. Возможно, нужно сказать, являются ли все элементы первого дерева меньшими всех элементов правого дерева. На проге в среду разберёмся. ### 3.9. Копирование дерева Есть одно дерево, надо сделать второе с теми же элементами. Делается обходом в глубину. Можно префиксный использовать, так, наверное, красивее. - Префиксный (pre-order DFS): ``` process(x) dfs(left(x)) dfs(right(x)) ``` - Инфиксный (in-order DFS): ``` dfs(left(x)) process(x) dfs(right(x)) ``` - Постфиксный (post-order DFS): ``` dfs(left(x)) dfs(right(x)) process(x) ``` Сигнатура функции-копировальщика такая: ```c tree_t *copy(tree_t const *original); ``` ### 3.10 Задачи про `void *` Просто 4 лаба. Если надо, могу рассказать подробнее. ### 3.10.1. Сортировка простая Пузырьком, insertion sort, selection sort. Вспомнить алгоритм и записать в обобщённом виде. Полезно выделить индексацию и своп в отдельные функции. ### 3.11 Вопросы по теории #### 3.11.1. Раздельная компиляция, использование заголовочных файлов. Объявления и определения функций и переменных Сурс-файлы компилируются отдельно и затем только объединяются в один бинарь. Чтобы код из одного файла можно было юзать в другом, не копируя объявления постоянно, используем хедеры. **Хедер должен начинаться с инклюд-гарда (include guard)!** Есть два варианта: 1. ```c #ifndef SORT_H // ← #include <stddef.h> void sort(void *array, size_t size, size_t len); // ... #define SORT_H // ← #endif // ← ``` 2. ```c #pragma once // ← #include <stddef.h> void sort(void *array, size_t size, size_t len); // ... ``` Оба варианта проверяют, инклюдился ли уже в этой единице трансляции хедер. Если да, то инклюд раскроется в пустой код. Если инклюдится впервые, то содержмое хедера подставится. #### 3.11.2 Ошибки компиляции, линковки, понятие типа и ошибки типизации Сборка бинаря из сырцов — процесс многоступенчатый. ``` [C source files] ---preprocessing---> [translation unit] ---compilation---> [object files] ---linking---> [executable] ``` ##### Препроцессор Обрабатывает директивы препроцессора: ```c #define ANSWER 42 #define SUM(x, x) ((x) + (x)) #pragma once #error "error message" #include "..." #include <...> #if #ifdef #undef #else #elseif ``` Дефайны подставляются, комментарии удаляются. Строки с `\` в конце объединяются. Например, вот такой код: ```c int \ main( \ ) { return 0; } ``` ...после препроцессора превратится в ```c int main( ) { return 0; } ``` > С помощью директив препроцессора можно собирать разный код в зависимости от платформы. > Например, если при сборке для винды компилятор запускать как-то так: > > ``` > gcc -DWINDOWS input.c -o input.exe > ``` > > ...то внутри кода можно использовать наличие дефайна. > > ```c > #ifdef WINDOWS > int do_something() { > // this code will only be run on Windows > return 1; > } > #else > int do_something() { > // on other platforms > return 0; > } > #endif > ``` > Директива `#include` тупо берёт содержимое файла и подставляет. > > Допустим, в `data.txt` мы написали: > > ``` > 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 > ``` > > Теперь можно сделать так: > > ```c > int const data[] = { > #include "data.txt" // ← > }; > > int main() { > printf("%d\n", data[3]); //> 4 > return 0; > } > ``` Файл после обработки препроцессором называется **единицей трансляции**. В нём нет директив препроцесса (что начинается с `#`), `\` и комментов. ##### Компиляция Компилятор делает семантический анализ: например, проверяет типы. Здесь он замечает ошибки типизации: неверный тип используется. ```c int x = printf; // ☝ ошибка типизации: // в `int` пихаем `int (*)(const char *restrict format, ...)`. ``` Возможна ещё огромная куча разных ошибок, которые компилятор может встретить и выдать. Например, синтаксическую ошибку из-за кода вроде такого: ```c int привет, вася!() = 42. ``` Все эти ошибки называются **ошибками компиляции**. После компиляции мы получаем объектный файл — по одному на каждую единицу трансляции. ```c int test(); // вот эта строка приехала к нам из #include <stdio.h> // (вместе с кучей других, конечно) int printf( const char *restrict format, ... ); ``` В объектном файле на месте адресов, про реализации которых мы ничего пока не знаем (например, `test` и `printf`), стоят затычки. ##### Линковка Линкер берёт кучу объектных файлов, склеивает их в один бинарник, заменяя затычки на реальные адреса. Есть 2 вида линковки: - **статическая:** берётся библиотека и копипастится в бинарь - **динамическая:** линкер оставляет заметку в бинарнике, чтобы при запуске его операционная система автоматом подключила библиотеку, но содержимого этой библиотеки нету Если линковщик видит затычку, а функции под неё не видит, он выдаст ошибку. Например, если мы в коде сверху `test` так и не определим, линкер на нас нажалуется. Эти ошибки называются **ошибками линковки**. И их обычно весьма сложно читать.