--- tags: си, прога, ошибки --- # Частые ошибки Короче, в лабах из раза в раз встречаются одни и те же ошибки. Поэтому перед лабой окиньте взором эту доку сверху вниз и проверьте, всё ли у вас на месте. ## 0. Компиляция Используйте ключи `-Wall -Wextra -Werror -Wpedantic -fsanitize=address`. Это поможет увидеть многие ошибки. - `-Wall -Wextra -Wpedantic` — это значит включить тучу ворнингов - `-Werror` — это сделать все ворнинги ошибками, чтобы вы точно их не пропустили (может бесить, но во благо!) - `-fsanitize=address` будет орать, когда вы попытаете незаконно пересечь границу массива, записать что-то в никуда (или прочесть оттуда же), за`free`шить что-то и кучу всего. - **На винде может не работать.** Можно в WSL попробовать гнать. - К слову, чтобы отсечь варианты отсечения ноги, можно приправить опции следующим: - `-fsanitize=leak` — поиск утечек памяти - `-fsanitize=undefined` — поиск undefined behavior в коде - А ещё `valgrind` есть, очень крутая вещь. Про утечки памяти пишет. ## 1. Перед основным кодом - Дефайны/инклюды ненужные или закинутые в `.h` вместо `.c` - Нет include guard в хедерах - Как надо: ```c code.h: #ifndef CODE_H #define CODE_H // code #endif // CODE_H ``` - Есть глобальные переменные - Нужно: ```c static const int FOO = 5; #define FOO 5 ``` - `static` обязателен, когда переменная **вне функций** объявляется, кроме случаев, когда это не так. - Низя: ```c int foo = 5; ``` - Это про переменные **вне функций**. Внутри можно и нужно так. - Любые неявные константы (например, длина экспоненты, количество соседей в "Жизни" или одна и та же строка с ошибкой, которую вы выводите в трёх разных случаях) нужно объявить как константы / задефайнить ## 2. Функции - Не должно быть длинных функций. Всё длинное стоит разбить на маленькие функции - Функция должна заниматься небольшим числом вещей - Каждая строка должна помещаться в экран - Стандарт: 80 символов / строка. 81 — расстрел. - Если что-то повторяется много раз — вынести как отдельную функцию - Если вы передаёте множество связанных данных раз за разом, сделайте структуру, где всё это будет лежать. ## 3. Проверки Как программисты, надо учитывать все случаи. Все. Пограничные тоже. Можно некоторые проигнорировать из прагматическо-инженерных соображений (которые придётся доказывать, если спросят), но учесть нужно. - Максимально вариативные тесты: - целые числа - дробные числа - `NULL` > Технически компилятор считает, что если мы разыменовываем переменную, > то там не `NULL` обязательно (иначе UB), и оптимизирует соответственно. > > Но до проверки на `NULL` поинтер — это на самом деле тип, который > может не иметь определённого значения. > Поэтому при получении поинтеров нужен хотя бы `assert(NULL != ptr)`. - обратить внимание на `fopen`: часто не проверяется возвращаемое значение - NaN, infinity - отрицательные числа - экспоненты - буквы - строки - пустая строка - переполнение `int`/`long long`/`size_t` - в том числе во время вычислений: в `(min + max) / 2` может быть переполнение при суммировании, даже если результат всегда влезет в результирующий тип - в том числе при юзании литералов: `1 << 60` — выстрел в упор себе в ногу, потому что у `1` тип `int` длиной (обычно) 32 бита - фиксить юзанием шестнадцатеричных литералов: `0x1` - ноль и единица - пограничные случаи (например, ровно `INT_MAX`) - Используйте `<assert.h>` **только** для того, что точно не должно произойти. «Штатные» ошибки выводите, например: - нельзя открыть файл - неверные вводные данные Список зависит от проги. Интерфейс должен быть достаточно юзер-френдли - Вместо одного `assert` со сложным условием лучше написать несколько. Если сломается что-то одно, прога напишет строку с ошибкой, и будет проще чинить. - Плохо: ```c assert(NULL != left && NULL != right); ``` - Хорошо: ```c assert(NULL != left); assert(NULL != right); ``` - Сколько было вызвано `calloc`/`malloc`, столько же должно быть вызовов `free` ## 4. Небольшие упрощения Золотое правило писателя криптосистем — **код должен быть скучным**. Это значит, что не надо в коде юзать какие-то шаманские трюки, которые бы сводили с ума читателей. Код должен быть последовательным и логичным. ```c if (0 == foo); if (!foo); ``` ```c void foo() { if (true) { return 1; } return 0; } /* Так понятней при чтении и можно использовать где угодно * * */ enum ERRORS {IS_OK, SOME_ERROR}; void foo() { if (true) { return SOME_ERROR; } return IS_OK; } ``` ## 5. Стиль кода Писать код надо красиво и единообразно. Если не знаете, как это, то поставьте форматтер. - `clang-format` — универсальный вариант (можно настроить под себя) - **Code::Blocks:** `Plugins` → `Source code formatter` - **Sublime Text:** можно поставить любой плагин-форматтер - **CLion:** форматтер, как обычно, встроен Если осилите, включите автоформатирование при коммите. ## 6. Юзать нужные типы - Если функции нужно неотрицательное число, то пишем `unsigned` (или `size_t`, если это размер / кол-во). - `size_t` живёт в хедере `<stddef.h>`, но его ещё тащит `<stdlib.h>`. - Счётчик в цикле тоже лучше `size_t`. Но осторожно с переполнением и циклами вида ```c for (size_t i = NUMBER; i >= 0; i--) { // code } ``` - Если функция возвращает код ошибки, все их лучше запихать в `enum` и возвращать элементы енама. - Указатели надо делать константными (кроме случаев, когда надо что-то менять): `void const *`, `int const *const *array`. - Если куча одинаковых аргументов таскаются из функции в функцию, можно их запихать в один стракт и передавать уже его. - В функцию стракты (особенно большие) передавать по указателю, иначе будут копироваться. - Если по смыслу буля, юзать `bool` (живёт в `<stdbool.h>`). - Функцию, которая юзается только в одном файле, нужно отметить `static`: ```c static bool is_positive(int value) { return value > 0; } ``` - Функцию, которая юзается во многих, статиком не отмечать, и декларацию добавить в хедер ## 7. Файлы - Просто для удобства закиньте пару файлов с примерами ввода в репозиторий. Ускорит проверку на пару минут (и на формат, и на разнообразные данные, которые программа успешно скушает и начнёт обрабатывать) - Прогу можно разбить на несколько файлов. Например, в один файл поместить ваш вектор (структура данных такая), а в другом читать ввод. ## 8. Смысловое - Каждая функция должна быть простой как три рубля. Не нужно впихивать несколько задач в одну функцию, в таком случае лучше поделить её на несколько. - Логические выражения: избегать двойных отрицаний, использовать правила де Моргана. Воспринимать ДНФ/КНФ проще. - Читать код должно быть легко. Будто книгу. ## Thoughts Хотелось бы ещё сказать пару слов о: - [ ] Если файл не открывается, то в fprintf(stderr, ...) пихать название файла; - [ ] Больше комментировать; - [x] Разбивать ассерты (если там несколько выражений через ИЛИ); - [x] ~~perror, errno~~ - [x] указатель до assert на NULL — это опциональный тип, который может не иметь значения, и разыменовывать его — семантическая ошибка - [x] ~~слои абстракций~~ - [x] разбивку проги на файлы - [x] (мета) настроить форматтер кода в редакторе - [x] bool - [ ] pragma - [ ] ссылку на разбор сложных типов - [ ] оформить красиво - [ ] пару слов о дефайнах - [ ] SOLID, Open-close, KISS, DRY, DIE etc. - [ ] typedef для удобства - [ ] пару слов о сроках и их срывах