Как писать код на с
Перейти к содержимому

Как писать код на с

  • автор:

Как начать писать программный код Си в ОС Linux (Руководство для совсем начинающих)

Добрый день. Этот материал рассчитан на людей, будущих программистов, которые только начинают разбираться в программировании под ОС Linux. Я попробую здесь показать прямое руководство к действию на примере тех простых инструментов, которые использовал некогда сам при изучении Си в процессе знакомства с Linux. На самом деле, с теми или иными поправками, это руководство можно использовать в большинстве дистрибутивов. Руководство однозначно подходит для всех deb-based дистрибутивов.

С установкой ОС, как я полагаю, проблем у Вас уже не возникло. Этому процессу посвящены просто тысячи статей на профильных сайтах.

Итак: у Вас сейчас установлен дистрибутив ОС, как говорится, «из коробки». Перед глазами пособие для разработчика/учебник/просто_хорошая_книга по «Языку программирования Си». И никакой вменяемой, полноценной подробной информации о том, как же собственно откомпилировать и выполнить, написанный в книге, исходный код. Быстрый осмотр тематических ресурсов уже показал Вам, что, необходимо установить компилятор Си, запустить его с нужными параметрами и потом запустить компилированный бинарный код. Примерно с этого момента мы и начнём.

Установка компилятора

Я имею ввиду, что Вы скорее всего (бывший) пользователь ОС Windows и действия в чёрном/синем окошке при помощи клавиатуры оканчивались где-то на команде ping, кажутся неким таинством. Однако отмечу, что всё банально просто и текстовой интерфейс предоставляет намного более гибкие возможности (скорее всего Вы неоднократно Вы слышали это ранее). Приступим:

Я подразумеваю, что с понятием компиляции и о том что такое компилятор Вас уже познакомила правильная книга.

На этом этапе всё будет очень быстро и просто. Открываем терминал и пишем:

sudo apt install gcc

(На всякий случай: вставка в gnome-terminal ctrl+shift+v)

Сразу поясню, что текст слева от курсора — это приглашение командного интерпретатора и оно выглядит следующим образом:

Далее я буду указывать только команды интерпретатору без приглашения.

Данная строка «говорит» интерпретатору: «от имени суперпользователя запустить менеджер пакетов для установки пакета gcc».

Система попросит Вас ввести пароль суперпользователя и приступит к установке компилятора.

Чтение списков пакетов… Готово Построение дерева зависимостей Чтение информации о состоянии… Готово Предлагаемые пакеты: gcc-multilib gcc-doc Следующие НОВЫЕ пакеты будут установлены: gcc Обновлено 0 пакетов, установлено 1 новых пакетов, для удаления отмечено 0 пакетов, и 44 пакетов не обновлено. Необходимо скачать 5 208 B архивов. После данной операции объём занятого дискового пространства возрастёт на 51,2 kB. Пол:1 http://ru.archive.ubuntu.com/ubuntu focal/main amd64 gcc amd64 4:9.3.0-1ubuntu2 [5 208 B] Получено 5 208 B за 0с (34,6 kB/s) Выбор ранее не выбранного пакета gcc. (Чтение базы данных … на данный момент установлено 371769 файлов и каталогов.) Подготовка к распаковке …/gcc_4%3a9.3.0-1ubuntu2_amd64.deb … Распаковывается gcc (4:9.3.0-1ubuntu2) … Настраивается пакет gcc (4:9.3.0-1ubuntu2) … Обрабатываются триггеры для man-db (2.9.1-1) …

Если же он уже установлен, то менеджер пакетов apt просто укажет на это примерно следующим образом:

Чтение списков пакетов… Готово Построение дерева зависимостей Чтение информации о состоянии… Готово Уже установлен пакет gcc самой новой версии (4:9.3.0-1ubuntu2).

Установка редактора

Обычно с дистрибутивом Ubuntu поставляется весьма интересный текстовой редактор gedit . Однако в других дистрибутивах возможно придётся установить этот редактор:

sudo apt install gedit

Создание файла с исходным кодом

Теперь пришло то самое время нашего классического «hello world»! Давайте сделаем это в стиле linux. Просто наберите в консоли:

gedit ~/hello_world.c

Более подробно Вы обязательно прочитайте в профильных ресурсах и в документации, я только отмечу, что символ «тильда» возвращает полный путь к домашнему каталогу пользователя ОС. Соответственно будет создан файл в вашем домашнем каталоге с указанным именем.

И далее наш программный код на языке Си в редакторе:

#include "stdio.h" int main() < printf ("\nHello world)\n"); for (int c=0; c<10;c++) < for (int i =0;ireturn 0; >

(Стоит отметить, что в редакторе gedit есть подсветка синтаксиса для различных языков программирования. Переключить режимы подсветки можно в нижней части окна редактора.)

Не забываем сохранить изменения нажатием ctrl+s. Обратите внимание, что вопросов об имени файла не последовало, так как имя было уже указано параметром при запуске редактора из командной строки терминала.

Компиляция и запуск

Закрываем окно редактора нажатием Alt+F4 и запустим же то сокровенное ради чего все тут и собрались:

gcc ./hello_world.c

И в ответ только новое приглашение. В отличие от стиля в ОС Windows, когда консоль, жутко подробно по-умолчанию, комментирует выполняемые действия — большинство программ в ОС семейства *nix сообщают только об исключительных ситуациях, ошибках и тому подобных вещах. То есть если «в ответ тишина» — то всё прошло хорошо.

Теперь в домашнем каталоге у нас появился файл a.out — он и есть файл с исполнимым кодом.

Для запуска этого файла на исполнение — назначим ему атрибут: «исполнимый»:

chmod +x a.out

и теперь запустим получившееся приложение:

./a.out

(Для запуска исполнимого файла интерпретатору требуется указать полный путь к файлу. Как в случае с «тильдой» символ «точка» возвращает полный путь к текущему каталогу. В данном конкретном случае правомерно так же запустить через ~/a.out Это не имеет значения здесь, так как файл создан в домашнем каталоге пользователя.)

И мы получаем вывод в терминале:

Hello world) # ## ### #### ##### ###### ####### ######## #########

Для выполнения всех повторных действий: изменение кода и снова компиляция, — Вы можете не вводить все эти команды каждый раз заново, а использовать стрелки вверх и вниз, для быстрого выбора команд из истории. И, кстати, вывод списка истории всех введённых команд можно выполнить командой (на самом деле программой) history .

Минутка автоматизации

Теперь приступим к очень интересному моменту связанному с творчеством в духе *nix. Каждый раз вводить много скучных команд неинтересно, возможно, даже вредно. Мы расширим функционал редактора gedit и доработаем его «напильником» до состояния примитивной среды разработки: запустим gedit и откроем меню параметров,

Главное меню gedit

где на вкладке «Расширения» добавляем «Внешние инструменты»

И затем, из того же главного меню gedit выбираем «Управление внешними инструментами».

Как Вы уже поняли — здесь можно выполнить доработку функциональности текстового редактора. Создадим новый инструмент: «Компиляция и запуск», В качестве вывода используем нижнюю область редактора. Инструмент назначим для файлов C и C++. Назначим клавишу F5 (дело вкуса) на применение инструмента и собственно сам код инструмента в виде скрипта bash:

#!/bin/bash gcc -o a.out $GEDIT_CURRENT_DOCUMENT_NAME chmod +x ./a.out ./a.out rm ./a.out

Разберёмся в том, что тут происходит:

#!/bin/bash — указание командного интерпретатора для выполнения скрипта.

gcc -o a.out $GEDIT_CURRENT_DOCUMENT_NAME — здесь мы запускаем компилятор, где в параметре -o указываем имя выходного файла. Пускай он будет таким же как и по-умолчанию.

$GEDIT_CURRENT_DOCUMENT_NAME — через эту переменную gedit передаёт имя файла.

Дальше Вы уже знаете — назначение атрибута «исполнения», запуск файла и потом:

rm ./a.out — удаление созданного исполнимого файла.

Попробуем инструмент в деле:

Теперь можно продолжать изучать пособие для разработчика/учебник/просто_хорошую_книгу по «Языку программирования Си» на практике.

Заключение

На самом деле в ОС Linux полно возможностей по доработке и использованию различного ПО. Само ПО является максимально гибким. Необязательно использовать предложенные мною средства, скорее методы, разработки.

В дальнейшем Вам обязательно понадобятся более серьёзные средства. А на первое время Вы можете дополнительно посмотреть другие редакторы, вплоть до больших и серьёзных сред разработки. Однако обязательно обратите внимание на редактор vim.

Как писать код и сразу видеть результат

Когда только начинаешь программировать и делать сайты, важно понимать, что вообще происходит. Вот изменил ты параметр объекта — а правильно или нет? Заработало это или нет? Красиво вышло или ужасно?

Чтобы разработчик сразу видел результат труда, боги создали для него IDE — integrated development environment, по-русски — среду разработки. Это программа, в которой программист пишет код, ловит ошибки и наблюдает результат.

Чисто технически работать можно и без IDE: писать код в блокноте и просматривать его в специальных программах или браузере. Но это бывает медленно и требует дополнительных телодвижений. Лучше научиться пользоваться IDE и писать в сто раз быстрее.

Выбор среды разработки (IDE) — дело вкуса и привычки. Какие-то из них — универсальные, другие заточены под конкретные языки программирования. IDE часто похожи по своим возможностям и позволяют увеличивать функциональность за счёт внешних дополнений.

Visual Studio Code

Программу можно скачать с официального сайта. Несмотря на то, что VS Code делает Микрософт, это бесплатный продукт с открытым исходным кодом, доступный на всех платформах. Благодаря этому и своим возможностям VS Code стал одной из самых популярных сред для разработки в мире.

Как писать код и сразу видеть результат

VS Code распознаёт почти все существующие языки программирования, самостоятельно или с помощью плагинов, и форматирует их соответствующим образом. Кроме этого, у него глубокая поддержка HTML, CSS, JavaScript и PHP — он проследит за парными тегами, закрытыми скобками и ошибками в командах.

Вот самые интересные возможности VS Code.

Умное автодополнение. Программа анализирует, какую команду вы хотите ввести, и предлагает закончить фразу за вас, с подсказками и объяснением. Удобно, если вы забыли порядок следования переменных или как точно звучит нужная команда:

Выполнение скриптов по шагам. Иногда нужно иметь возможность выполнить скрипт не сразу, а по шагам, например, чтобы понять, не зациклилась ли наша программа. Для этого используйте встроенный дебаггер — это программа, которая следит за кодом, ищет в нём ошибки и позволяет выполнять его поэтапно.

Множественное выделение и поиск. Чтобы поменять много одинаковых значений переменных или найти все одинаковые слова или команды, VS Code использует свой алгоритм обработки. Благодаря этому редактировать код становится проще, а замена функций или переменных происходит быстрее.

Мультикурсор помогает вводить одинаковые значения сразу на нескольких строках

Найденные одинаковые слова и команды можно тут же заменить на другие

Навигация по коду и описания функций. Когда пишешь большую программу, легко забыть то, что делал в начале — как работает функция или какого типа переменная используется в этом месте. Чтобы этого избежать, VS Code может показывать саму функцию, описание переменной или какие параметры передаются при вызове команды. Ещё это пригодится, если код достался вам по наследству от прошлого разработчика и нужно быстро понять, какие куски кода за что отвечают и как работают:

Как писать код и сразу видеть результат

Сразу после установки VS Code не умеет показывать результаты работы кода, когда мы делаем веб-страницы. Это можно исправить с помощью расширения Live HTML Previewer. Для этого заходим в раздел «Extensions», щёлкая на последнем значке на панели слева или нажимая Ctrl+Shift+X, и начинаем писать «Live HTML Previewer» в строке поиска.

После установки и запуска расширения можно будет сразу видеть, как ваш HTML-код и CSS влияют на внешний вид и поведение страницы. Это особенно удобно, когда строишь сайт с нуля и хочешь сразу понимать, что происходит.

WebStorm

Среда разработки для JavaScript от компании jetBrains стоит денег, но есть пробный период в 30 дней. Вполне хватит, чтобы попробовать и понять, нужна эта программа или нет.

Как писать код и сразу видеть результат

Автоподстановка. Некоторые IDE с автоподстановкой тормозят и не предлагают сразу все варианты переменных или команд — но не WebStorm. Здесь всё работает с первой буквы и понимает, когда надо предложить переменную, а когда команду или служебное слово:

Как писать код и сразу видеть результат Как писать код и сразу видеть результат

Встроенная система задач. Полезно, когда в работе много проектов и нужно по каждому помнить, что ты хотел в нём сделать. Здесь это реализовано сразу и доступно для любого файла:

Как писать код и сразу видеть результат

Проверка ошибок. WebStorm умеет сам проверять код на ошибки и пояснять каждую из них. Не всегда это работает идеально, но когда работает — экономит кучу времени:

Как писать код и сразу видеть результат

Чтобы сразу видеть, что получается на странице, нам понадобится плагин LiveEdit. По умолчанию он выключен, но его можно включить или поставить отдельно в любое время. После активации нужно будет в настройках плагина поставить галочку «Update application in Chrome on changes in» — она как раз отвечает за обновление информации в браузере Chrome. Теперь можно писать код и сразу видеть результат:

Как писать код и сразу видеть результат

Sublime Text 3

Бесплатный редактор, который назойливо предлагает занести денег разработчикам. Про Sublime Text у нас есть отдельная и более подробная статья — почитайте, там тоже интересно.

Sublime Text — потрясающе мощный текстовый редактор. Его сила — в скорости работы, он одинаково быстро обработает простую веб-страничку или программу на сто тысяч строк кода. Подсветка синтаксиса всех возможных языков программирования, автоподстановка, умное закрытие тегов — всё это доступно сразу после установки.

Вот что ещё умеет программа сразу после установки:

  • показывать разными цветами команды и переменные в популярных языках программирования;
  • автоматически завершать команды;
  • выделять сразу все одинаковые слова;
  • сворачивать код для удобства чтения;
  • использовать любые горячие клавиши, какие только понадобятся;
  • разделять рабочую область на несколько окон, в каждой из которых можно редактировать свой код.

Вторая суперспособность, которая превращает Sublime Text из простого текстового редактора в универсальное решение, — плагины. По принципу действия они такие же, как и в других программах из обзора, но они совершенно не влияют на скорость работы. Когда начинаешь плотно работать с Sublime Text, может показаться, что у него есть плагины для всего. Нужно редактировать одновременно один и тот же код, но в разных панелях — пожалуйста, написать быстро HTML-код — само собой, проверить код на ошибки и недочёты — без проблем.

Emmet сокращает время на написание кода, подставляя вместо стандартных команд целые куски готового кода

JavaScript & NodeJS Snippets упрощает написание кода на JavaScript и работает по тому же принципу, что и Emmet

SublimeCodeIntel помогает быстро разобраться в коде со множеством функций. Если щёлкнуть на название любой функции, плагин сразу покажет вам её описание.

Так как эта статья — для начинающих программистов, которым важно сразу видеть изменения в коде, то посмотрим, как это делает Sublime Text.

Сразу после установки он этого делать не умеет, но нам поможет плагин LiveReload. Он показывает все изменения в браузере, как только мы сохраняем рабочий файл с кодом. Это не так изящно, как в VS Code, но в случае с Sublime Text простительно. Дело в том, что привыкнув однажды писать в нём код, сложно пересесть на что-то другое, что работает с той же скоростью. Установка LiveReload состоит из двух компонентов — плагин для Sublime Text и расширение для браузера.

После установки давайте посмотрим, что у нас получилось. Создадим файл tested.html в Sublime Text, разметим его внутри стандартным шаблоном как HTML-документ, а рядом откроем окно браузера.

В реальном времени мы не увидим на странице те изменения, которые вносим в код, как это было в VS Code. Но если нажать Ctrl+S, чтобы сохранить все данные, то браузер моментально показывает то, что мы сделали.

Если вы серьёзно настроены программировать, присмотритесь к Visual Studio Code. Почти со всем он справляется сам или с плагинами, не нужно подключать дополнительно браузеры или сторонний софт.

Любите, чтобы после установки были доступны почти все нужные функции? Попробуйте WebStorm — платную, но мощную среду разработки.

Если вам важна скорость работы в любых ситуациях, то Sublime Text — лучший выбор. Он очень быстрый, и для него есть плагины почти на все случаи жизни.

Получите ИТ-профессию

В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.

Как писать код на C++

1. Этот текст носит рекомендательный характер.

2. Если вы редактируете код, то имеет смысл писать так, как уже написано.

3. Стиль нужен для единообразия. Единообразие нужно, чтобы было проще (удобнее) читать код. А также, чтобы было легче осуществлять поиск по коду.

4. Многие правила продиктованы не какими либо разумными соображениями, а сложившейся практикой.

Форматирование​

1. Большую часть форматирования сделает автоматически clang-format .

2. Отступы — 4 пробела. Настройте среду разработки так, чтобы таб добавлял четыре пробела.

3. Открывающая и закрывающие фигурные скобки на отдельной строке.

inline void readBoolText(bool & x, ReadBuffer & buf)   char tmp = '0'; readChar(tmp, buf);  x = tmp != '0'; > 

4. Если всё тело функции — один statement , то его можно разместить на одной строке. При этом, вокруг фигурных скобок ставятся пробелы (кроме пробела на конце строки).

inline size_t mask() const  return buf_size() - 1; > inline size_t place(HashValue x) const  return x & mask(); > 

5. Для функций. Пробелы вокруг скобок не ставятся.

void reinsert(const Value & x) 
memcpy(&buf[place_value], &x, sizeof(x)); 

6. В выражениях if , for , while и т.д. перед открывающей скобкой ставится пробел (в отличие от вызовов функций).

for (size_t i = 0; i  rows; i += storage.index_granularity) 

7. Вокруг бинарных операторов ( + , — , * , / , % , …), а также тернарного оператора ?: ставятся пробелы.

UInt16 year = (s[0] - '0') * 1000 + (s[1] - '0') * 100 + (s[2] - '0') * 10 + (s[3] - '0'); UInt8 month = (s[5] - '0') * 10 + (s[6] - '0'); UInt8 day = (s[8] - '0') * 10 + (s[9] - '0'); 

8. Если ставится перенос строки, то оператор пишется на новой строке, и перед ним увеличивается отступ.

if (elapsed_ns)  message  " ("  rows_read_on_server * 1000000000 / elapsed_ns  " rows/s., "  bytes_read_on_server * 1000.0 / elapsed_ns  " MB/s.) "; 

9. Внутри строки можно, выполнять выравнивание с помощью пробелов.

dst.ClickLogID = click.LogID; dst.ClickEventID = click.EventID; dst.ClickGoodEvent = click.GoodEvent; 

10. Вокруг операторов . , -> не ставятся пробелы.

При необходимости, оператор может быть перенесён на новую строку. В этом случае, перед ним увеличивается отступ.

11. Унарные операторы — , ++ , * , & , … не отделяются от аргумента пробелом.

12. После запятой ставится пробел, а перед — нет. Аналогично для точки с запятой внутри выражения for .

13. Оператор [] не отделяется пробелами.

14. В выражении template <. >, между template и < ставится пробел, а после < и до >не ставится.

template typename TKey, typename TValue> struct AggregatedStatElement > 

15. В классах и структурах, public , private , protected пишется на том же уровне, что и class/struct , а остальной код с отступом.

template typename T> class MultiVersion   public: /// Version of object for usage. shared_ptr manage lifetime of version. using Version = std::shared_ptrconst T>; ... > 

16. Если на весь файл один namespace и кроме него ничего существенного нет, то отступ внутри namespace не нужен.

17. Если блок для выражения if , for , while , … состоит из одного statement , то фигурные скобки не обязательны. Вместо этого поместите statement на отдельную строку. Это правило справедливо и для вложенных if , for , while , …

Если внутренний statement содержит фигурные скобки или else , то внешний блок следует писать в фигурных скобках.

/// Finish write. for (auto & stream : streams)  stream.second->finalize(); 

18. Не должно быть пробелов на концах строк.

19. Исходники в кодировке UTF-8.

20. В строковых литералах можно использовать не-ASCII.

 ", "  (timer.elapsed() / chunks_stats.hits)  " μsec/hit."; 

21. Не пишите несколько выражений в одной строке.

22. Внутри функций группируйте блоки кода, отделяя их не более, чем одной пустой строкой.

23. Функции, классы, и т. п. отделяются друг от друга одной или двумя пустыми строками.

24. const (относящийся к значению) пишется до имени типа.

//correct const char * pos const std::string & s //incorrect char const * pos 

25. При объявлении указателя или ссылки, символы * и & отделяются пробелами с обеих сторон.

//correct const char * pos //incorrect const char* pos const char *pos 

26. При использовании шаблонных типов, пишите using (кроме, возможно, простейших случаев).

То есть, параметры шаблона указываются только в using и затем не повторяются в коде.

using может быть объявлен локально, например, внутри функции.

//correct using FileStreams = std::mapstd::string, std::shared_ptrStream>>; FileStreams streams; //incorrect std::mapstd::string, std::shared_ptrStream>> streams; 

27. Нельзя объявлять несколько переменных разных типов в одном выражении.

//incorrect int x, *y; 

28. C-style cast не используется.

//incorrect std::cerr  (int)c ; std::endl; //correct std::cerr  static_castint>(c)  std::endl; 

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

30. Для не очень большого класса/структуры, можно не отделять объявления методов от реализации.

Аналогично для маленьких методов в любых классах/структурах.

Для шаблонных классов/структур, лучше не отделять объявления методов от реализации (так как иначе они всё равно должны быть определены в той же единице трансляции).

31. Не обязательно умещать код по ширине в 80 символов. Можно в 140.

32. Всегда используйте префиксный инкремент/декремент, если постфиксный не нужен.

for (Names::const_iterator it = column_names.begin(); it != column_names.end(); ++it) 

Комментарии​

1. Необходимо обязательно писать комментарии во всех нетривиальных местах.

Это очень важно. При написании комментария, можно успеть понять, что код не нужен вообще, или что всё сделано неверно.

/** Part of piece of memory, that can be used.  * For example, if internal_buffer is 1MB, and there was only 10 bytes loaded to buffer from file for reading,  * then working_buffer will have size of only 10 bytes  * (working_buffer.end() will point to position right after those 10 bytes available for read).  */ 

2. Комментарии могут быть сколь угодно подробными.

3. Комментарии пишутся до соответствующего кода. В редких случаях после, на той же строке.

/** Parses and executes the query. */ void executeQuery(  ReadBuffer & istr, /// Where to read the query from (and data for INSERT, if applicable)  WriteBuffer & ostr, /// Where to write the result  Context & context, /// DB, tables, data types, engines, functions, aggregate functions.  BlockInputStreamPtr & query_plan, /// Here could be written the description on how query was executed  QueryProcessingStage::Enum stage = QueryProcessingStage::Complete /// Up to which stage process the SELECT query ) 

4. Комментарии следует писать только на английском языке.

5. При написании библиотеки, разместите подробный комментарий о том, что это такое, в самом главном заголовочном файле.

6. Нельзя писать комментарии, которые не дают дополнительной информации. В частности, нельзя писать пустые комментарии вроде этого:

/* * Procedure Name: * Original procedure name: * Author: * Date of creation: * Dates of modification: * Modification authors: * Original file name: * Purpose: * Intent: * Designation: * Classes used: * Constants: * Local variables: * Parameters: * Date of creation: * Purpose: */ 

7. Нельзя писать мусорные комментарии (автор, дата создания…) в начале каждого файла.

8. Однострочные комментарии начинаются с трёх слешей: /// , многострочные с /** . Такие комментарии считаются «документирующими».

Замечание: такие комментарии могут использоваться для генерации документации с помощью Doxygen. Но, фактически, Doxygen не используется, так как для навигации по коду гораздо удобнее использовать возможности IDE.

9. В начале и конце многострочного комментария, не должно быть пустых строк (кроме строки, на которой закрывается многострочный комментарий).

10. Для закомментированных кусков кода, используются обычные, не «документирующие» комментарии.

11. Удаляйте закомментированные куски кода перед коммитом.

12. Не нужно писать нецензурную брань в комментариях или коде.

13. Не пишите прописными буквами. Не используйте излишнее количество знаков препинания.

/// WHAT THE FAIL. 

14. Не составляйте из комментариев строки-разделители.

15. Не нужно писать в комментарии диалог (лучше сказать устно).

/// Why did you do this stuff? 

16. Не нужно писать комментарий в конце блока о том, что представлял собой этот блок.

Имена​

1. В именах переменных и членов класса используйте маленькие буквами с подчёркиванием.

size_t max_block_size; 

2. Имена функций (методов) camelCase с маленькой буквы.

std::string getName() const override  return "Memory"; > 

3. Имена классов (структур) — CamelCase с большой буквы. Префиксы кроме I для интерфейсов — не используются.

class StorageMemory : public IStorage 

4. using называются также, как классы, либо с _t на конце.

5. Имена типов — параметров шаблонов: в простых случаях — T ; T , U ; T1 , T2 .

В более сложных случаях — либо также, как имена классов, либо можно добавить в начало букву T .

template typename TKey, typename TValue> struct AggregatedStatElement 

6. Имена констант — параметров шаблонов: либо также, как имена переменных, либо N в простом случае.

template bool without_www> struct ExtractDomain 

7. Для абстрактных классов (интерфейсов) можно добавить в начало имени букву I .

class IBlockInputStream 

8. Если переменная используется достаточно локально, то можно использовать короткое имя.

В остальных случаях используйте имя, описывающее смысл.

bool info_successfully_loaded = false; 

9. В именах define и глобальных констант используется ALL_CAPS с подчёркиванием.

#define MAX_SRC_TABLE_NAMES_TO_STORE 1000 

10. Имена файлов с кодом называйте по стилю соответственно тому, что в них находится.

Если в файле находится один класс, назовите файл, как класс (CamelCase).

Если в файле находится одна функция, назовите файл, как функцию (camelCase).

11. Если имя содержит сокращение, то:

  • для имён переменных, всё сокращение пишется маленькими буквами mysql_connection (не mySQL_connection ).
  • для имён классов и функций, сохраняются большие буквы в сокращении MySQLConnection (не MySqlConnection ).

12. Параметры конструктора, использующиеся сразу же для инициализации соответствующих членов класса, следует назвать также, как и члены класса, добавив подчёркивание в конец.

FileQueueProcessor( const std::string & path_, const std::string & prefix_,  std::shared_ptrFileHandler> handler_) : path(path_), prefix(prefix_), handler(handler_), log(&Logger::get("FileQueueProcessor"))   > 

Также можно называть параметры конструктора так же, как и члены класса (не добавлять подчёркивание), но только если этот параметр не используется в теле конструктора.

13. Именование локальных переменных и членов класса никак не отличается (никакие префиксы не нужны).

timer (not m_timer) 

14. Константы в enum — CamelCase с большой буквы. Также допустим ALL_CAPS. Если enum не локален, то используйте enum class .

enum class CompressionMethod   QuickLZ = 0,  LZ4 = 1, >; 

15. Все имена — по-английски. Транслит с русского использовать нельзя.

не Stroka 

16. Сокращения (из нескольких букв разных слов) в именах можно использовать только если они являются общепринятыми (если для сокращения можно найти расшифровку в английской википедии или сделав поисковый запрос).

`AST`, `SQL`. Не `NVDH` (что-то неведомое) 

Сокращения в виде обрезанного слова можно использовать, только если такое сокращение является широко используемым.

Впрочем, сокращения также можно использовать, если расшифровка находится рядом в комментарии.

17. Имена файлов с исходниками на C++ должны иметь расширение только .cpp . Заголовочные файлы — только .h .

Как писать код​

1. Управление памятью.

Ручное освобождение памяти ( delete ) можно использовать только в библиотечном коде.

В свою очередь, в библиотечном коде, оператор delete можно использовать только в деструкторах.

В прикладном коде следует делать так, что память освобождается каким-либо объектом, который владеет ей.

  • проще всего разместить объект на стеке, или сделать его членом другого класса.
  • для большого количества маленьких объектов используйте контейнеры.
  • для автоматического освобождения маленького количества объектов, выделенных на куче, используйте shared_ptr/unique_ptr .

2. Управление ресурсами.

Используйте RAII и см. пункт выше.

3. Обработка ошибок.

Используйте исключения. В большинстве случаев, нужно только кидать исключения, а ловить — не нужно (потому что RAII ).

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

В серверах, обрабатывающих пользовательские запросы, как правило, достаточно ловить исключения на самом верху обработчика соединения.

В функциях потока, следует ловить и запоминать все исключения, чтобы выкинуть их в основном потоке после join .

/// Если вычислений ещё не было - вычислим первый блок синхронно if (!started)   calculate();  started = true; > else /// Если вычисления уже идут - подождём результата  pool.wait();  if (exception)  exception->rethrow(); 

Ни в коем случае не «проглатывайте» исключения без разбора. Ни в коем случае, не превращайте все исключения без разбора в сообщения в логе.

//Not correct catch (...) > 

Если вам нужно проигнорировать какие-то исключения, то игнорируйте только конкретные, а остальные кидайте обратно.

catch (const DB::Exception & e)   if (e.code() == ErrorCodes::UNKNOWN_AGGREGATE_FUNCTION) return nullptr; else throw; > 

При использовании функций, использующих коды возврата или errno , проверяйте результат и кидайте исключение.

if (0 != close(fd)) throwFromErrno("Cannot close file " + file_name, ErrorCodes::CANNOT_CLOSE_FILE); 

assert не используются.

4. Типы исключений.

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

5. Исключения, вылетающие из деструкторов.

Использовать не рекомендуется, но допустимо.

Используйте следующие варианты:

  • Сделайте функцию ( done() или finalize() ), которая позволяет заранее выполнить всю работу, в процессе которой может возникнуть исключение. Если эта функция была вызвана, то затем в деструкторе не должно возникать исключений.
  • Слишком сложную работу (например, отправку данных по сети) можно вообще не делать в деструкторе, рассчитывая, что пользователь заранее позовёт метод для завершения работы.
  • Если в деструкторе возникло исключение, желательно не «проглатывать» его, а вывести информацию в лог (если в этом месте доступен логгер).
  • В простых программах, если соответствующие исключения не ловятся, и приводят к завершению работы с записью информации в лог, можно не беспокоиться об исключениях, вылетающих из деструкторов, так как вызов std::terminate (в случае noexcept по умолчанию в C++11), является приемлемым способом обработки исключения.

6. Отдельные блоки кода.

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

Block block = data.in->read();    std::lock_guardstd::mutex> lock(mutex);  data.ready = true;  data.block = block; > ready_any.set(); 

7. Многопоточность.

В программах офлайн обработки данных:

  • cначала добейтесь более-менее максимальной производительности на одном процессорном ядре, потом можно распараллеливать код, но только если есть необходимость.

В программах — серверах:

  • используйте пул потоков для обработки запросов. На данный момент, у нас не было задач, в которых была бы необходимость использовать userspace context switching.

Fork для распараллеливания не используется.

8. Синхронизация потоков.

Часто можно сделать так, чтобы отдельные потоки писали данные в разные ячейки памяти (лучше в разные кэш-линии), и не использовать синхронизацию потоков (кроме joinAll ).

Если синхронизация нужна, то в большинстве случаев, достаточно использовать mutex под lock_guard .

В остальных случаях, используйте системные примитивы синхронизации. Не используйте busy wait.

Атомарные операции можно использовать только в простейших случаях.

Не нужно писать самостоятельно lock-free структуры данных, если вы не являетесь экспертом.

9. Ссылки и указатели.

В большинстве случаев, предпочитайте ссылки.

10. const.

Используйте константные ссылки, указатели на константу, const_iterator , константные методы.

Считайте, что const — вариант написания «по умолчанию», а отсутствие const только при необходимости.

Для переменных, передающихся по значению, использовать const обычно не имеет смысла.

11. unsigned.

Используйте unsigned , если нужно.

12. Числовые типы.

Используйте типы UInt8 , UInt16 , UInt32 , UInt64 , Int8 , Int16 , Int32 , Int64 , а также size_t , ssize_t , ptrdiff_t .

Не используйте для чисел типы signed/unsigned long , long long , short , signed/unsigned char , char .

13. Передача аргументов.

Сложные значения передавайте по ссылке (включая std::string ).

Если функция захватывает владение объектом, созданным на куче, то сделайте типом аргумента shared_ptr или unique_ptr .

14. Возврат значений.

В большинстве случаев, просто возвращайте значение с помощью return . Не пишите return std::move(res) .

Если внутри функции создаётся объект на куче и отдаётся наружу, то возвращайте shared_ptr или unique_ptr .

В некоторых редких случаях, может потребоваться возвращать значение через аргумент функции. В этом случае, аргументом будет ссылка.

using AggregateFunctionPtr = std::shared_ptrIAggregateFunction>;  /** Позволяет создать агрегатную функцию по её имени.  */ class AggregateFunctionFactory   public: AggregateFunctionFactory();  AggregateFunctionPtr get(const String & name, const DataTypes & argument_types) const; 

15. namespace.

Для прикладного кода отдельный namespace использовать не нужно.

Для маленьких библиотек — не требуется.

Для не совсем маленьких библиотек — поместите всё в namespace .

Внутри библиотеки в .h файле можно использовать namespace detail для деталей реализации, не нужных прикладному коду.

В .cpp файле можно использовать static или анонимный namespace для скрытия символов.

Также, namespace можно использовать для enum , чтобы соответствующие имена не попали во внешний namespace (но лучше использовать enum class ).

16. Отложенная инициализация.

Обычно, если для инициализации требуются аргументы, то не пишите конструктор по умолчанию.

Если потом вам потребовалась отложенная инициализация, то вы можете дописать конструктор по умолчанию (который создаст объект с некорректным состоянием). Или, для небольшого количества объектов, можно использовать shared_ptr/unique_ptr .

Loader(DB::Connection * connection_, const std::string & query, size_t max_block_size_);  /// Для отложенной инициализации Loader() > 

17. Виртуальные функции.

Если класс не предназначен для полиморфного использования, то не нужно делать функции виртуальными зря. Это относится и к деструктору.

18. Кодировки.

Везде используется UTF-8. Используется std::string , char * . Не используется std::wstring , wchar_t .

19. Логирование.

См. примеры везде в коде.

Перед коммитом, удалите всё бессмысленное и отладочное логирование, и другие виды отладочного вывода.

Не должно быть логирования на каждую итерацию внутреннего цикла, даже уровня Trace.

При любом уровне логирования, логи должно быть возможно читать.

Логирование следует использовать, в основном, только в прикладном коде.

Сообщения в логе должны быть написаны на английском языке.

Желательно, чтобы лог был понятен системному администратору.

Не нужно писать ругательства в лог.

В логе используется кодировка UTF-8. Изредка можно использовать в логе не-ASCII символы.

20. Ввод-вывод.

Во внутренних циклах (в критичных по производительности участках программы) нельзя использовать iostreams (в том числе, ни в коем случае не используйте stringstream ).

Вместо этого используйте библиотеку DB/IO .

21. Дата и время.

См. библиотеку DateLUT .

22. include.

В заголовочном файле используется только #pragma once , а include guards писать не нужно.

23. using.

using namespace не используется. Можно использовать using что-то конкретное. Лучше локально, внутри класса или функции.

24. Не нужно использовать trailing return type для функций, если в этом нет необходимости.

auto f() -> void 

25. Объявление и инициализация переменных.

//right way std::string s = "Hello"; std::string s"Hello">;  //wrong way auto s = std::string"Hello">; 

26. Для виртуальных функций, пишите virtual в базовом классе, а в классах-наследниках, пишите override и не пишите virtual .

Неиспользуемые возможности языка C++​

2. Спецификаторы исключений из C++03 не используются.

Сообщения об ошибках​

Сообщения об ошибках — это часть пользовательского интерфейса программы, предназначенная для того, чтобы позволить пользователю:

  • замечать ошибочные ситуации,
  • понимать их смысл и причины,
  • устранять эти ситуации.

Форма и содержание сообщений об ошибках должны способствовать достижению этих целей.

Есть два основных вида ошибок:

  • пользовательская или системная ошибка,
  • внутренняя программная ошибка.

Пользовательская ошибка​

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

  • что произошло. Это должно объясняться в пользовательских терминах ( Function pow() is not supported for data type UInt128 ), а не загадочными конструкциями из кода ( runtime overload resolution failed in DB::BinaryOperationBuilder::Impl, UInt128, Int8>::kaboongleFastPath() ).
  • почему/где/когда — любой контекст, который помогает отладить проблему. Представьте, как бы её отлаживали вы (программировать и пользоваться отладчиком нельзя).
  • что можно предпринять для устранения ошибки. Здесь можно перечислить типичные причины проблемы, настройки, влияющие на это поведение, и так далее.

Пример нормального сообщения:

No alias for subquery or table function in JOIN (set joined_subquery_requires_alias=0 to disable restriction). While processing '(SELECT 2 AS a)'. 

Сказано что не хватает алиаса, показано, для какой части запроса, и предложена настройка, позволяющая ослабить это требование.

Пример катастрофически плохого сообщения:

The dictionary is configured incorrectly. 

Из него не понятно:

  • какой словарь?
  • в чём ошибка конфигурации?

Что может сделать пользователь в такой ситуации: применять внешние отладочные инструменты, спрашивать совета на форумах, гадать на кофейной гуще, и, конечно же, ненавидеть софт, который над ним так издевается. Не нужно издеваться над пользователями, это плохой UX.

Внутренняя программная ошибка​

Такая ошибка вызвана нарушением внутренних инвариантов программы: например, внутренняя функция вызвана с неверными параметрами, не совпадают размеры колонок в блоке, произошло разыменование нулевого указателя, и так далее. Сигналы типа SIGSEGV относятся к этой же категории.

Появление такой ошибки всегда свидетельствует о наличии бага в программе. Пользователь не может исправить такую ошибку самостоятельно, и должен сообщить о ней разработчикам.

Есть два основных варианта проверки на такие ошибки:

  • Исключение с кодом LOGICAL_ERROR . Его можно использовать для важных проверок, которые делаются в том числе в релизной сборке.
  • assert . Такие условия не проверяются в релизной сборке, можно использовать для тяжёлых и опциональных проверок.

Пример сообщения, у которого должен быть код LOGICAL_ERROR : Block header is inconsistent with Chunk in ICompicatedProcessor::munge(). It is a bug! По каким признакам можно заметить, что здесь говорится о внутренней программной ошибке?

  • в сообщении упоминаются внутренние сущности из кода,
  • в сообщении написано it’s a bug,
  • непосредственные действия пользователя не могут исправить эту ошибку. Мы ожидаем, что пользователь зарепортит её как баг, и будем исправлять в коде.

Как выбрать код ошибки?​

Код ошибки предназначен для автоматической обработки некоторых видов ошибок, подобно кодам HTTP. SQL стандартизирует некоторые коды, но на деле ClickHouse не всегда соответствует этим стандартам. Лучше всего выбрать существующий код из ErrorCodes.cpp , который больше всего подходит по смыслу. Можно использовать общие коды типа BAD_ARGUMENTS или TYPE_MISMATCH . Заводить новый код нужно, только если вы чётко понимаете, что вам нужна специальная автоматическая обработка конкретно этой ошибки на клиенте. Для внутренних программных ошибок используется код LOGICAL_ERROR .

Как добавить новое сообщение об ошибке?​

Когда добавляете сообщение об ошибке:

  1. Опишите, что произошло, в пользовательских терминах, а не кусками кода.
  2. Добавьте максимум контекста (с чем произошло, когда, почему, и т.д.).
  3. Добавьте типичные причины.
  4. Добавьте варианты исправления (настройки, ссылки на документацию).
  5. Вообразите дальнейшие действия пользователя. Ваше сообщение должно помочь ему решить проблему без использования отладочных инструментов и без чужой помощи.
  6. Если сообщение об ошибке не формулируется в пользовательских терминах, и действия пользователя не могут исправить проблему — это внутренняя программная ошибка, используйте код LOGICAL_ERROR или assert.

Платформа​

1. Мы пишем код под конкретные платформы.

Хотя, при прочих равных условиях, предпочитается более-менее кроссплатформенный или легко портируемый код.

2. Язык — C++20 (см. список доступных C++20 фич).

3. Компилятор — clang . На данный момент (апрель 2021), код собирается версией 11. (Также код может быть собран gcc версии 10, но такая сборка не тестируется и непригодна для продакшена).

Используется стандартная библиотека (реализация libc++ ).

4. ОС — Linux, Mac OS X или FreeBSD.

5. Код пишется под процессоры с архитектурой x86_64, AArch64 и ppc64le.

6. Используются флаги компиляции -Wall -Wextra -Werror и -Weverything с некоторыми исключениями.

7. Используется статическая линковка со всеми библиотеками кроме libc.

Инструментарий​

1. Хорошая среда разработки — KDevelop.

2. Для отладки используется gdb , valgrind ( memcheck ), strace , -fsanitize=. , tcmalloc_minimal_debug .

3. Для профилирования используется Linux Perf , valgrind ( callgrind ), strace -cf .

4. Исходники в Git.

5. Сборка с помощью CMake .

6. Программы выкладываются с помощью deb пакетов.

7. Коммиты в master не должны ломать сборку проекта.

А работоспособность собранных программ гарантируется только для отдельных ревизий.

8. Коммитьте как можно чаще, в том числе и нерабочий код.

Для этого следует использовать бранчи.

Если ваш код в ветке master ещё не собирается, исключите его из сборки перед push , также вы будете должны его доработать или удалить в течение нескольких дней.

9. Для нетривиальных изменений, используются бранчи. Следует загружать бранчи на сервер.

10. Ненужный код удаляется из исходников.

Библиотеки​

1. Используются стандартные библиотеки C++20 (допустимо использовать экспериментальные расширения), а также фреймворки boost , Poco .

2. Библиотеки должны быть расположены в виде исходников в директории contrib и собираться вместе с ClickHouse. Не разрешено использовать библиотеки, доступные в пакетах ОС, или любые другие способы установки библиотек в систему. Подробнее смотрите раздел Рекомендации по добавлению сторонних библиотек и поддержанию в них пользовательских изменений.

3. Предпочтение отдаётся уже использующимся библиотекам.

Общее​

1. Пишите как можно меньше кода.

2. Пробуйте самое простое решение.

3. Не нужно писать код, если вы ещё не знаете, что будет делать ваша программа, и как будет работать её внутренний цикл.

4. В простейших случаях, используйте using вместо классов/структур.

5. Если есть возможность — не пишите конструкторы копирования, операторы присваивания, деструктор (кроме виртуального, если класс содержит хотя бы одну виртуальную функцию), move-конструкторы и move-присваивания. То есть, чтобы соответствущие функции, генерируемые компилятором, работали правильно. Можно использовать default .

6. Приветствуется упрощение и уменьшение объёма кода.

Дополнительно​

1. Явное указание std:: для типов из stddef.h .

Рекомендуется не указывать. То есть, рекомендуется писать size_t вместо std::size_t , это короче.

При желании, можно дописать std:: , этот вариант допустим.

2. Явное указание std:: для функций из стандартной библиотеки C.

Не рекомендуется. То есть, пишите memcpy вместо std::memcpy .

Причина — существуют похожие нестандартные функции, например, memmem . Мы можем использовать и изредка используем эти функции. Эти функции отсутствуют в namespace std .

Если вы везде напишете std::memcpy вместо memcpy , то будет неудобно смотреться memmem без std:: .

Тем не менее, указывать std:: тоже допустимо, если так больше нравится.

3. Использование функций из C при наличии аналогов в стандартной библиотеке C++.

Допустимо, если это использование эффективнее.

Для примера, для копирования длинных кусков памяти, используйте memcpy вместо std::copy .

4. Перенос длинных аргументов функций.

Допустимо использовать любой стиль переноса, похожий на приведённые ниже:

function(  T1 x1,  T2 x2) 
function(  size_t left, size_t right, const & RangesInDataParts ranges,  size_t limit) 
function(size_t left, size_t right, const & RangesInDataParts ranges,  size_t limit) 
function(size_t left, size_t right, const & RangesInDataParts ranges,  size_t limit) 
function(  size_t left,  size_t right, const & RangesInDataParts ranges,  size_t limit) 

Пиши на C как джентльмен

Я думаю, многим знакома эта шикарная песня Jonathan Coulton’а, и эта жизненная ситуация, когда «Rob say Code Monkey very diligent», но «his output stink» и «his code not ‘functional’ or ‘elegant’».

Язык Си, подаривший нам столько полезного софта, потихоньку был вытеснен из десктопа и энтерпрайза такими высокоуровневыми гигантами как Java и C# и занял нишу системного программирования. И все бы хорошо, но системщики — очень отбитые своеобразные ребята. Задачи, которые порой возникают перед ними даже своей формулировкой способны вогнать в ужас простых смертных. Собственно говоря, так же, как и некоторые решения.

Сегодня мы поговорим о некоторых полезных практиках, которые я вынес из глубин системного программирования на Си. Поехали.

Пункты будут располагаться от самых фундаментальных и очевидных (ориентированных на новичков в языке Си) до самых специфичных, но полезных. Если чувствуете, что вы это знаете — листайте дальше.

Практика I: Соблюдайте единый Code Style и фундаментальные принципы «хорошего тона»

Функция принимает в качестве аргумента переменную INPUT, парсит её в массив IncomingValues и возвращает result_to_return? Отставить быдлокод!

То, что в первую очередь выдает новичка — несоблюдение единого стиля написания кода в рамках конкретного приложения. Следом идет игнорирование правил «хорошего тона».

Вот несколько самых распространенных рекомендаций к оформлению кода на Си:

    Названия макросов и макрофункций пишутся капсом, слова в названиях отделяются друг от друга нижним подчеркиванием.

#define MAX_ARRAY_SIZE 32 #define INCORRECT_VALUE -1 #define IPC_FIND_NODE(x) ipc_find_node(config.x) 
int my_int_variable = 0; char *hello_str = "hello_habrahabr"; pid_t current_pid = fork();

Вообще, этот пункт спорный. Мне доводилось видеть проекты, где имена переменных и функций пишутся в camelCase и PascalCase соответственно.

UPD: Спасибо пользователю fogree за то, что он обнаружил косяк с перепутанным PascalCase и camelCase.

Кстати, рекомендую взять за привычку писать код так, чтобы одна функция делала только одну вещь. Именно это должно отразиться в названии.

То же можно сказать и про переменные — никаких a, b, c — в названии должен быть отражен смысл (итераторы не в счет). Самодокументируемый код — очень хорошая практика.

Как правило, можно выбрать между стилем написания названия: PascalCase и under_score, тут уже зависит от вас.

/* пример функции общего пользования */ static void dgtprint(char *str) < int i; for (i = 0; i < strlen(str); i++) < if (isdigit(str[i])) printf("%c", str[i]); else print("_"); >> /* пример функции для работы со специфичным контекстом */ /* PascalCase */ void EnableAllVlans(struct vlan_cfg *vp) < int i; for (i = 0; i < VLAN_COUNT; i++) < EnableVlanByProto(vp.vlan[i]); >> /* under_score */ void enable_all_vlans(struct vlan_cfg *vp) < int i; for (i = 0; i < VLAN_COUNT; i++) < enable_vlan_by_proto(vp.vlan[i]); >>
int array[MAX_ARRAY_SIZE] = arrinit(); register int i, j, k; for (i = 0; i < MAX_ARRAY_SIZE; i++) for (j = 0; j < MAX_ARRAY_SIZE; j++) for (k = MAX_ARRAY_SIZE; k >= 0; k--) dosmthng(i, j, k, array[i]);
if (condition) < dosmthng(); >else < dont_do_something(); >/* Не делайте так */ if (condition) < dosmthng(); >else < dont_do_something(); >/* Гораздо правильнее будет следовать одному правилу переноса скобок, как тут */ if (condition) < dosmthng(); >else < dont_do_something(); >/* Или как тут */ /* Ну, или как тут, но это уже совсем экзотика */ if (condition) < dosmthng(); >else

По возможности инициализируйте переменные при объявлении. Численные с помощью нуля, указатели — NULL:

int counter = 0, start_position = 0, unknown_position = 0; struct dhcp_header * dhcp = NULL, * dhcp_temp = NULL; char input_string[32] = < 0 >;

Ну оставили мы переменные неинициализированными, и что?

А то. Если смотреть их (до инициализации) в отладке (в том же gdb), там будет лежать мусор. Это нередко сбивает с толку (особенно, если мусор «похож на правду»). Про указатели я вообще молчу.

Не надо комментировать каждую строчку кода — если вы пишите самодокументируемый код, большая часть его будет простой для понимания.

Оптимальное решение — писать описания функций, если из аргументов и названия сложно понять весь её функционал. Для переменных — правила те же, в пояснении нуждаются только какие-то нелинейный вещи, где одного названия мало.

На самом деле, в вопросе документации у вас есть полная свобода действий — надо лишь следить, чтобы комментариев было не много, но достаточно, чтобы человек, видящий ваш код в первый раз, не задавал вопросов.

/* Возвращает 1, если параметры, связанные с модемным * соединением изменились, и 0, если нет. */ static int CheckModemConnection() < int i = 0; /* Проверка сети отдельно - меняется чаще всего */ if (CHECK_CFG_STR(Network.LanIpAddress) || CHECK_CFG_STR(Network.LanNetmask)) return 1; for(i = 0; i < MAX_MODEM_IDX; i++) < if (CHECK_CFG_INT(Modems.Modem[i].Proto) || CHECK_CFG_INT(Modems.Modem[i].MTU) || CHECK_CFG_STR(Modems.Modem[i].Username) || CHECK_CFG_STR(Modems.Modem[i].Password) || CHECK_CFG_STR(Modems.Modem[i].Number) || CHECK_CFG_STR(Modems.Modem[i].AdditionalParams) || CHECK_CFG_STR(Modems.Modem[i].PIN) || CHECK_CFG_STR(Modems.Modem[i].MRU) || CHECK_CFG_STR(Modems.Modem[i].PppoeIdle) || CHECK_CFG_STR(Modems.Modem[i].USBPort) || CHECK_CFG_STR(Reservation.Prefer) || CHECK_CFG_STR(Modems.Modem[i].PppoeConnectType) || CHECK_CFG_INT(Modems.Mode) || CHECK_CFG_INT(Aggregation.usb1) || CHECK_CFG_INT(Aggregation.usb2)) return 1; >return 0; >

Если вы постоянно работаете с трекерами (вроде RedMine), то при внесении правок в код можно указать номер задачи, в рамках которой эти правки были внесены. Если у кого-то при просмотре кода возникнет вопрос а-ля «Зачем тут этот функционал?», ему не придется далеко ходить. В нашей компании еще пишут фамилию программиста, чтобы если что знать, к кому идти с расспросами.

/* Muraviyov: #66770 */

Практика II: Оптимизируйте структуру вашего проекта

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

    Называйте файлы так, чтобы всем было ясно, какой файл за что отвечает.

Не следует называть файлы file1.c, mySUPER_COOL_header.h и т.д.
main.c — для файла с точкой входа, graph_const.h — для заголовочника с графическими константами будет в самый раз.

  • project/
    • common.c
    • common.h
    • main.c
    • network.h
    • networking.c
    • networking_v6.c
    • packet.c
    • packet.h
    • Makefile

    Если он точно не знает, какой файл ему нужен? Можно сберечь много нервов, если сделать так:

    • project/
      • include/
        • common.h
        • network.h
        • packet.h
        @$(CC) $(OBJS) -o networkd -L$(ROMFS)/lib -linteraction -Wall -lpthread -I ./include

        Если у вас игра, в которой есть файлы, отвечающие за движок/звук/графику — будет удобно раскидать их по папкам. Звук, графику и движок — отдельно друг от друга.

        .PHONY clean build build: cd sound/ && make clean && make cd graphics/ && make clean && make cd engine/ && make clean && make sound: cd sound/ && make clean && make graphics: cd graphics/ && make clean && make engine: cd engine/ && make clean && make clean: cd sound/ && make clean cd engine/ && make clean cd greaphics/ && make clean

        Практика III: Используйте враппер-функции для обработки возвращаемых значений

        Враппер-функция (функция-обертка) в языке Си используется как функция со встроенной обработкой возвращаемого значения. Как правило, в случае ошибки в работе функции, возвращаемое значение вам об этом скажет, а глобальная переменная errno примет в себя код ошибки.

        Если вы пишите в системе (а сейчас большинство программ на си — именно системные программы), то нет ничего хуже, чем «немое» падение программы. По-хорошему, она должна красиво завершиться, напоследок сказав, что именно пошло не по плану.

        Но обрабатывать значение от каждой функции в коде — такое себе решение. Тут же упадет читаемость, и объем (+ избыточность) кода увеличится в пару раз.

        Тут и помогают врапперы. Рассмотрим первый пример — безопасный код без врапперов:

        int sock_one = 0, sock_two = 0, sock_three = 0; /* операция сравнения имеет больший приоритет, чем операция присваивания, * поэтому присваивание выполняется в скобках */ if ((socket_one = socket(AF_INET , SOCK_STREAM , 0)) if ((socket_two = socket(AF_INET , SOCK_DGRAM , 0)) if ((socket_three = socket(PF_INET , SOCK_RAW , 0))

        Ну, такое себе, не правда ли? Теперь попробуем с обертками.

        /* Где-то в коде. */ int Socket(int domain, int type, int proto) < int desk = socket(domain, type, proto); if (desk return desk; > /* . n строчек спустя - наш предыдущий пример . */ int socket_one = 0, socket_two = 0, soket_three = 0; socket_one = Socket(AF_INET , SOCK_STREAM , 0); socket_two = Socket(AF_INET , SOCK_DGRAM , 0); socket_three = Socket(PF_INET , SOCK_RAW , 0);

        Как видите, код по-прежнему безопасен (не будет «немого» падения), но теперь его функциональная часть гораздо компактнее.

        Я называю обертки именем самих функций, но с большой буквы. Каждый сам волен выбрать, как их оформлять.

        В использовании оберток есть небольшой минус, который, если захотеть, можно решить костылем. А что это за минус — можете предположить в комментариях 🙂

        Практика IV: Используйте keywords как профи

        Хорошее знание keywords никогда не будет лишним. Да, и без них ваш код будет работать, не спорю. Но когда речь зайдет об экономии места, быстродействии и оптимизации — это именно то, чего вам будет не хватать.

        К тому же, мало кто может похвастаться хорошим знанием ключевых слов, поэтому их повседневное использование может быть шансом блеснуть знаниями перед коллегами. Однако, не надо бездумно пихать кейворды всюду, куда только можно. Вот вам несколько фич:

          register — дает компилятору указание по возможности хранить переменную в регистрах процессора, а не в оперативной памяти. Использование модификатора register при объявлении переменной-итератора цикла с небольшим телом может повысить скорость работы всего цикла в несколько раз.

        register byte i = 0; for (i; i < 256; i++) check_value(i);
        void updatePtrs(size_t *restrict ptrA, size_t *restrict ptrB, size_t *restrict val); 
        int var = 1; if (!var) /* Эти 2 строчки будут отброшены компилятором */ dosmthng(); volatile int var = 1; if (!var) /* А вот эти - нет */ dosmthng(); 

        Практика V: Не доверяйте себе. Доверяйте valgrind.

        Если у вас в программе есть работа со строками, динамическое выделение памяти и все, где замешаны указатели, то не будет лишним проверить себя.

        Valgrind — программа, которая создана для того, чтобы помочь программисту выявить утечки памяти и ошибки контекста. Не буду вдаваться в подробности, скажу лишь, что даже в небольших программах он нередко находит косяки, которые совсем не очевидны для большинства программистов, но, тем не менее, в эксплуатации могут повлечь за собой большие проблемы. За всем не уследишь.
        + у нее есть и другой полезный функционал.

        Более подробно о нем можно узнать тут.

        Практика VI: Помогайте тем, кто хочет улучшить ваш софт

        Пример будет взят из исходников busybox 1.21. Для тех кто не знает, что такое busybox, можете посмотреть эту вики-статью.

        UPD: до этого здесь был пример «плохого» кода из busybox. Спасибо пользователю themiron за то, что показал, что этот код был понят мною неправильно — это были лишь тонкости реализации, причем реализации очень хорошей. В качестве извинения за свою «клевету» на busybox, здесь будет пример хорошего кода.

        Причем, все так же из busybox.

        Код busybox очень эллегантен, пусть и совсем не прост. Всем, кто хочет взглянуть на язык си под другим углом — рекомендую ознакомиться с исходниками.

        Теперь обобщения по этому пункту на примерах из busybox. Все примеры взяты из udhcpc — крохотного DHCP клиента:

          Оставляй комментарии, там где они нужны.

        Протокол DHCP имеет полную документацию в RFC, там описаны все возможные поля dhcp-пакета. Но, тем не менее, ребята озаботились и полностью задокументировали даже поля структуры. Эта структура — первое, на что посмотрит программист, расширяющий функционал программы (DHCP-клиент DHCP-пакет).

        (файл networking/udhcp/common.h)

        struct dhcp_packet < uint8_t op; /* BOOTREQUEST or BOOTREPLY */ uint8_t htype; /* hardware address type. 1 = 10mb ethernet */ uint8_t hlen; /* hardware address length */ uint8_t hops; /* used by relay agents only */ uint32_t xid; /* unique id */ uint16_t secs; /* elapsed since client began acquisition/renewal */ uint16_t flags; /* only one flag so far: */ #define BROADCAST_FLAG 0x8000 /* "I need broadcast replies" */ uint32_t ciaddr; /* client IP (if client is in BOUND, RENEW or REBINDING state) */ uint32_t yiaddr; /* 'your' (client) IP address */ /* IP address of next server to use in bootstrap, returned in DHCPOFFER, DHCPACK by server */ uint32_t siaddr_nip; uint32_t gateway_nip; /* relay agent IP address */ uint8_t chaddr[16]; /* link-layer client hardware address (MAC) */ uint8_t sname[64]; /* server host name (ASCIZ) */ uint8_t file[128]; /* boot file name (ASCIZ) */ uint32_t cookie; /* fixed first four option bytes (99,130,83,99 dec) */ uint8_t options[DHCP_OPTIONS_BUFSIZE + CONFIG_UDHCPC_SLACK_FOR_BUGGY_SERVERS]; >PACKED; 

        Листинг выше частично вошел сюда, т.к. подходит для еще одного примера.

        Посмотрите: описание упакованных структур идет перед перечислением, отражающим размеры этих структур.

        (файл networking/udhcp/common.h)

        struct dhcp_packet < uint8_t op; /* BOOTREQUEST or BOOTREPLY */ uint8_t htype; /* hardware address type. 1 = 10mb ethernet */ uint8_t hlen; /* hardware address length */ uint8_t hops; /* used by relay agents only */ uint32_t xid; /* unique id */ uint16_t secs; /* elapsed since client began acquisition/renewal */ uint16_t flags; /* only one flag so far: */ #define BROADCAST_FLAG 0x8000 /* "I need broadcast replies" */ uint32_t ciaddr; /* client IP (if client is in BOUND, RENEW or REBINDING state) */ uint32_t yiaddr; /* 'your' (client) IP address */ /* IP address of next server to use in bootstrap, returned in DHCPOFFER, DHCPACK by server */ uint32_t siaddr_nip; uint32_t gateway_nip; /* relay agent IP address */ uint8_t chaddr[16]; /* link-layer client hardware address (MAC) */ uint8_t sname[64]; /* server host name (ASCIZ) */ uint8_t file[128]; /* boot file name (ASCIZ) */ uint32_t cookie; /* fixed first four option bytes (99,130,83,99 dec) */ uint8_t options[DHCP_OPTIONS_BUFSIZE + CONFIG_UDHCPC_SLACK_FOR_BUGGY_SERVERS]; >PACKED; #define DHCP_PKT_SNAME_LEN 64 #define DHCP_PKT_FILE_LEN 128 #define DHCP_PKT_SNAME_LEN_STR "64" #define DHCP_PKT_FILE_LEN_STR "128" struct ip_udp_dhcp_packet < struct iphdr ip; struct udphdr udp; struct dhcp_packet data; >PACKED; struct udp_dhcp_packet < struct udphdr udp; struct dhcp_packet data; >PACKED; enum < IP_UDP_DHCP_SIZE = sizeof(struct ip_udp_dhcp_packet) - CONFIG_UDHCPC_SLACK_FOR_BUGGY_SERVERS, UDP_DHCP_SIZE = sizeof(struct udp_dhcp_packet) - CONFIG_UDHCPC_SLACK_FOR_BUGGY_SERVERS, DHCP_SIZE = sizeof(struct dhcp_packet) - CONFIG_UDHCPC_SLACK_FOR_BUGGY_SERVERS, >;

        Если в этом же файле мы спустимся чуть пониже, то увидим, что объявления функций работы с опциями так же находятся в одном месте:

        (файл networking/udhcp/common.h)

        unsigned FAST_FUNC udhcp_option_idx(const char *name); uint8_t *udhcp_get_option(struct dhcp_packet *packet, int code) FAST_FUNC; int udhcp_end_option(uint8_t *optionptr) FAST_FUNC; void udhcp_add_binary_option(struct dhcp_packet *packet, uint8_t *addopt) FAST_FUNC; void udhcp_add_simple_option(struct dhcp_packet *packet, uint8_t code, uint32_t data) FAST_FUNC; #if ENABLE_FEATURE_UDHCP_RFC3397 char *dname_dec(const uint8_t *cstr, int clen, const char *pre) FAST_FUNC; uint8_t *dname_enc(const uint8_t *cstr, int clen, const char *src, int *retlen) FAST_FUNC; #endif struct option_set *udhcp_find_option(struct option_set *opt_list, uint8_t code) FAST_FUNC;

        В udhcpc есть огромное количество опций, которые по умолчанию не используются. Каждая опция соответствует макросу, который закрывает ее номер.

        Если бы они были раскомментированы — то это были бы макросы, которые не всплывают нигде в коде. Человек, который спросил бы «а зачем все эти опции?» искал бы ответ очень долго. И не нашел бы.

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

        (файл networking/udhcp/common.h)

        #define DHCP_PADDING 0x00 #define DHCP_SUBNET 0x01 //#define DHCP_TIME_OFFSET 0x02 /* (localtime - UTC_time) in seconds. signed */ //#define DHCP_ROUTER 0x03 //#define DHCP_TIME_SERVER 0x04 /* RFC 868 time server (32-bit, 0 = 1.1.1900) */ //#define DHCP_NAME_SERVER 0x05 /* IEN 116 _really_ ancient kind of NS */ //#define DHCP_DNS_SERVER 0x06 //#define DHCP_LOG_SERVER 0x07 /* port 704 UDP log (not syslog) //#define DHCP_COOKIE_SERVER 0x08 /* "quote of the day" server */ //#define DHCP_LPR_SERVER 0x09 #define DHCP_HOST_NAME 0x0c /* either client informs server or server gives name to client */ //#define DHCP_BOOT_SIZE 0x0d //#define DHCP_DOMAIN_NAME 0x0f /* server gives domain suffix */ //#define DHCP_SWAP_SERVER 0x10 //#define DHCP_ROOT_PATH 0x11 //#define DHCP_IP_TTL 0x17 //#define DHCP_MTU 0x1a //#define DHCP_BROADCAST 0x1c //#define DHCP_ROUTES 0x21 //#define DHCP_NIS_DOMAIN 0x28 //#define DHCP_NIS_SERVER 0x29 //#define DHCP_NTP_SERVER 0x2a //#define DHCP_WINS_SERVER 0x2c #define DHCP_REQUESTED_IP 0x32 /* sent by client if specific IP is wanted */ #define DHCP_LEASE_TIME 0x33 #define DHCP_OPTION_OVERLOAD 0x34 #define DHCP_MESSAGE_TYPE 0x35 #define DHCP_SERVER_ID 0x36 /* by default server's IP */ #define DHCP_PARAM_REQ 0x37 /* list of options client wants */ //#define DHCP_ERR_MESSAGE 0x38 /* error message when sending NAK etc */ #define DHCP_MAX_SIZE 0x39 #define DHCP_VENDOR 0x3c /* client's vendor (a string) */ #define DHCP_CLIENT_ID 0x3d /* by default client's MAC addr, but may be arbitrarily long */ //#define DHCP_TFTP_SERVER_NAME 0x42 /* same as 'sname' field */ //#define DHCP_BOOT_FILE 0x43 /* same as 'file' field */ //#define DHCP_USER_CLASS 0x4d /* RFC 3004. set of LASCII strings. "I am a printer" etc */ #define DHCP_FQDN 0x51 /* client asks to update DNS to map its FQDN to its new IP */ //#define DHCP_DOMAIN_SEARCH 0x77 /* RFC 3397. set of ASCIZ string, DNS-style compressed */ //#define DHCP_SIP_SERVERS 0x78 /* RFC 3361. flag byte, then: 0: domain names, 1: IP addrs */ //#define DHCP_STATIC_ROUTES 0x79 /* RFC 3442. (mask,ip,router) tuples */ #define DHCP_VLAN_ID 0x84 /* 802.1P VLAN ID */ #define DHCP_VLAN_PRIORITY 0x85 /* 802.1Q VLAN priority */ //#define DHCP_MS_STATIC_ROUTES 0xf9 /* Microsoft's pre-RFC 3442 code for 0x79? */ //#define DHCP_WPAD 0xfc /* MSIE's Web Proxy Autodiscovery Protocol */ #define DHCP_END 0xff

        Довольно сложный для восприятия аспект, требующий пояснения. В двух словах: если у вас есть функция, которая внутри проекта вызывается с n комбинациями различных параметров (где n — небольшое число), причем каждая комбинация вызывается по нескольку раз, имеет смысл сделать для каждой комбинации отдельную функцию, вызывающую внутри себя целевую функцию, но уже с нужными параметрами. Например. У нас есть функция, отправляющая пакеты на определенный порт и адрес:

        (файл networking/udhcp/packet.c)

        /* Construct a ip/udp header for a packet, send packet */ int FAST_FUNC udhcp_send_raw_packet(struct dhcp_packet *dhcp_pkt, uint32_t source_nip, int source_port, uint32_t dest_nip, int dest_port, const uint8_t *dest_arp, int ifindex) < struct sockaddr_ll dest_sll; struct ip_udp_dhcp_packet packet; unsigned padding; int fd; int result = -1; const char *msg; fd = socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_IP)); if (fd < 0) < msg = "socket(%s)"; goto ret_msg; >memset(&dest_sll, 0, sizeof(dest_sll)); memset(&packet, 0, offsetof(struct ip_udp_dhcp_packet, data)); packet.data = *dhcp_pkt; /* struct copy */ dest_sll.sll_family = AF_PACKET; dest_sll.sll_protocol = htons(ETH_P_IP); dest_sll.sll_ifindex = ifindex; dest_sll.sll_halen = 6; memcpy(dest_sll.sll_addr, dest_arp, 6); if (bind(fd, (struct sockaddr *)&dest_sll, sizeof(dest_sll)) < 0) < msg = "bind(%s)"; goto ret_close; >/* We were sending full-sized DHCP packets (zero padded), * but some badly configured servers were seen dropping them. * Apparently they drop all DHCP packets >576 *ethernet* octets big, * whereas they may only drop packets >576 *IP* octets big * (which for typical Ethernet II means 590 octets: 6+6+2 + 576). * * In order to work with those buggy servers, * we truncate packets after end option byte. */ padding = DHCP_OPTIONS_BUFSIZE - 1 - udhcp_end_option(packet.data.options); packet.ip.protocol = IPPROTO_UDP; packet.ip.saddr = source_nip; packet.ip.daddr = dest_nip; packet.udp.source = htons(source_port); packet.udp.dest = htons(dest_port); /* size, excluding IP header: */ packet.udp.len = htons(UDP_DHCP_SIZE - padding); /* for UDP checksumming, ip.len is set to UDP packet len */ packet.ip.tot_len = packet.udp.len; packet.udp.check = inet_cksum((uint16_t *)&packet, IP_UDP_DHCP_SIZE - padding); /* but for sending, it is set to IP packet len */ packet.ip.tot_len = htons(IP_UDP_DHCP_SIZE - padding); packet.ip.ihl = sizeof(packet.ip) >> 2; packet.ip.version = IPVERSION; packet.ip.ttl = IPDEFTTL; packet.ip.check = inet_cksum((uint16_t *)&packet.ip, sizeof(packet.ip)); udhcp_dump_packet(dhcp_pkt); result = sendto(fd, &packet, IP_UDP_DHCP_SIZE - padding, /*flags:*/ 0, (struct sockaddr *) &dest_sll, sizeof(dest_sll)); msg = "sendto"; ret_close: close(fd); if (result < 0) < ret_msg: bb_perror_msg(msg, "PACKET"); >return result; > 

        Но dhcp не всегда нуждается в отправке пакета на один IP адрес. В основном используется широковещательная (BROADCAST) рассылка.

        Но широковещательная отправка пакета — всего лишь отправка пакета по адресу, зарезервированному под broadcast. Собственно, для того, чтоб отправить широковещательный запрос, достаточно использовать описанную выше функцию, но в качестве адреса указать тот, что зарезервирован про бродкаст. Отсюда получаем функцию:

        (файл networking/udhcp/dhcpc.c)

        static int raw_bcast_from_client_config_ifindex(struct dhcp_packet *packet) < return udhcp_send_raw_packet(packet, /*src*/ INADDR_ANY, CLIENT_PORT, /*dst*/ INADDR_BROADCAST, SERVER_PORT, MAC_BCAST_ADDR, client_config.ifindex); >

        Заключение

        Пиши код так, чтобы те, кто будет его сопровождать любили тебя, а не ненавидели. Сложная гибкая реализация гораздо лучше простого костыля.

        Описывай интерфейсы доступа, комментируй проблемные моменты. Не делай констант, от изменения которых придется переписывать весь код. Не допускай утечек памяти. Следи за безопасностью и отказоустойчивостью кода.

        Пиши на Си как джентльмен.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *