Комментарии 171
То есть, в проекте присутствует только одна версия библиотеки. Хочешь другую — нужно озаботиться этим и установить её в проекте.
В паре наших проектов видел такую практику: в папке с библиотекой хранятся исходники нескольких её версий в архивах, а в файле конфигурации сборки есть параметр, отвечающий за версию библиотеки. Указываешь версию и при сборке она будет разархивирована и собрана. Не знаю, насколько это часто используется, но можно рассмотреть как вариант.
А вообще есть clib, но я с ней не работал, ничего сказать о ней не могу:)
Ну или, вот это менее популярное решение.
С Rust я пока еще не знаком, с Go недавно начал возиться. Пока все очень нравится — система импорта пакетов после сей не перестает меня радовать :)
А вот как раз Cargo в Rust позволяет выбирать любую версию.
Так что, советую ознакомиться, если будет время.
Мне, например, очень нравится подход, который они используют.
Ну, надеюсь, что все еще впереди.
Rust недавно расхваливал один хороший знакомый, так что рано или поздно я к нему приду, думаю.
А Cargo — уже как получится. Пока вообще не представляю, что это за язык:)
Rust недавно расхваливал один хороший знакомый, так что рано или поздно я к нему приду, думаю.
А Cargo — уже как получится. Пока вообще не представляю, что это за язык:)
Если что, cargo это пакетный менеджер раста.
Область применения Rust или Go несколько отличается от системного программирования, особенно Go.
Интересно, почему область применения rust — не системное ПО? Он в этой нише, в первую очередь, и интересен.
По скорости Rust не дотягивает до C, насколько я могу судить.
В каких кейсах, при каких структурах данных, при каких используемых абстракциях?
http://benchmarksgame.alioth.debian.org/, если что, не аргумент, там много откровенно кривых бенчмарков.
Скажем так, узнать про inline проще, чем про restrict и volatile — вот я и решил это упомянуть :)
А register — да, потихоньку выходит из использования. Тем не менее, иногда — полезная вещь.
https://github.com/jemalloc/jemalloc/wiki/Use-Case%3A-Heap-Profiling
Еще есть более быстрый вариант. Это asan/ubsan. Он может проверять уже кое что, чего valgrind не может, но в тоже время чуть менее надежно.
Для использования *san надо собирать приложение с нужными флагами. Работа замедляется в несколько раз, но получается почти c/c++ с проверкой корректности работы с памятью.
Например?
Скажем у вас есть структура что-то вроде
struct
{
int a[15];
int b;
};
a[16] для valgrind скорее всего будет корректен, если не вставится паддинг после a. А ubsan или asan выведет ошибку.
Или тоже самое, только уже разные структуры или массивы, но по указателю из первой структуры через переполнение индекса вы попадете, во вторую. Тут тоже asan/ubsan должен сработать.
Пропустить может если там будут указатели а не массивы.
Другие случаи не помню, но они тоже есть.
P.S. А вот cppcheck не поймал, даже если индекс явно 16 влепить.
Возможно баг, или информации анализатору не хватает. Вообще он такое ловит:
(error) Array 's.a[15]' accessed at index 16, which is out of bounds.
Насчёт sanitizers — отличные инструменты. Замедляют программу значительно меньше valgrind, а находят зачастую больше проблем, хотя бы за счёт большего объёма информации о программе. Пересобирать надо, да, но оно того стоит.
Объявляйте переменные в начале функции.
Совет так себе. "Совершенный код" рекомендует объявлять локальные переменные как можно ближе к месту использования, чтобы при чтении не приходилось "вспоминать" типы увиденных переменных.
По возможности инициализируйте переменные при объявлении. Численные с помощью нуля, указатели — NULL.
В основном совет справедлив только для языков/компиляторов, где компилятор не проверяет инициализацию переменных. Небольшой оффтоп, но например в Java так лучше не делать (я понимаю, что эта статья для С-шников, но сейчас мало кто пишет только на С, а статью могут читать все).
Не инициализируем сразу:
int a;
if(something){
a = 3;
b = 5;
c = 9;
}else{
// тут забыли присвоить a=1;
b = 2;
c = 1;
}
f(a,b); // не компилируется (a не инициализирована), сразу исправляем.
Инициализируем сразу:
int a=0;
if(something){
a = 3;
b = 5;
c = 9;
}else{
// тут забыли присвоить a=1;
b = 2;
c = 1;
}
f(a,b); // компилируется (a инициализирована, но не тем, чем надо), ловим баги в рантайме.
Не уверен, но мне кажется, что в настоящее время ключевое слово register
не влияет ни на что, по крайней мере на самых распространенных платформах.
Объявляйте переменные в начале функции.
Как минимум это релевантно в Си, ибо не перечит стандарту ansi
В ansi c, насколько я помню, "не в начале" объявить даже нельзя, в С99 — можно. Если уже есть компилятор С99, не всегда нужно следовать ansi c.
Более того, хватает компиляторов с поддержкой c11
Но как правило последний стандарт не отражает стандарт который используется (требуется поддерживать)
На самом деле можно объявить и не в начале, если создать новый scope, то есть выглядить это будет что-то вроде:
void foo() {
uint32_t i;
uint8_t c;
/* do something */
{
uint32_t k = 1;
/* do something else */
}
/* do yet another thing */
}
И да, кстати
Не уверен, но мне кажется, что в настоящее время ключевое слово register не влияет ни на что, по крайней мере на самых распространенных платформах.
int sqr(int a) {
return a*a;
}
int sqr(register int a) {
return a*a;
}
Это с -O2
?
clang, icc компилируют одинаково; gcc начиная с -O1 — одинаково, а без оптимизаций — так, как на скринах.
https://godbolt.org/g/F5IFpG
Нет (: пример с оптимизацией будет придумать посложнее
На самом деле, в си есть такая вещь, как предварительное объявление функции.
То есть, если ты уверен, что это будет полезно для читаемости, ты можешь объявить переменную в начале, а инициализировать в коде, но так же с объявлением типа.
Например:
int foo;
/* ... спустя n строк в той же области видимости...*/
int foo = VALUE_FOR_FOO;
Конечно, это противоречить пункту о инициализации при объявлении, но каждый сам решает, чему отдавать приоритет.
Лучше вообще стараться иметь иммутабельные переменные, везде где можно (слово можно тут надо понимать с разных точек зрения). Это упрощает поведение кода и в итоге его анализ.
А это предполагает автоматически, что инициализация при объявлении, один раз и навсегда. Так что совет-то как раз годный. И для всех языков осмысленный. И именно в java есть смысл так делать почти всегда, добавив еще и final.
Мне не нравится идея писать макросы-функции капсом. Я рассуждаю логически (логика и программирование — это же стороны одной монеты, не так ли?): макрос-функция скрывает в себе некое законченное действие, т.е. по смыслу неотличим от функции. Так зачем выделять внешним видом его из ряда функций? Только для того, чтобы знать, что это макрос? И что дает это знание? Рано или поздно может возникнуть ситуация, когда макрос на самом деле придется заменить на функцию, и тогда придется править тонны кода, меняя капс-идентификатор на некапс-идентификатор…
С моей т.з. логично и правильно отличать идентификаторы переменных от констант, для чего и служит капс-выделение. Отличать функционально завешенный блок кода, решающий часто повторяющуюся или логически обособленную задачу, оформленный макросом или функцией смысла не вижу.
Что касается других «рекомендаций», то увы и ах, при работе с встраиваемыми системами некоторые «правильные» принципы не работают: «красиво завершиться, напоследок сказав, что именно пошло не по плану» программа попросту не может… Например, популярный нынче квадрокоптер: в случае ошибки «красиво завершиться» — это значит, перед падением на асфальт сделать мертвую петлю? Или осколки разбросать в виде кода ошибки? Во многом количестве встраиваемых систем НЕТ ОС, которая могла бы воспользоваться кодами ошибок. И так или иначе приходится писать программы, умело игнорирующие ошибки, т.е. допускающие деление на ноль без видимых для пользователя последствий.
Хотя сам принцип -да, разумен. Но не абсолютен.
И, таки-да, автор сам себе противоречит, выделяя функции-обертки капсом…
Так зачем выделять внешним видом его из ряда функций?Явное лучше неявного. Макрос — не функция (например, его нельзя передавать как аргумент в другие функции), и нет никакого смысла обманывать читающего код разработчика, делая вид, что это не так.
Рано или поздно может возникнуть ситуация, когда макрос на самом деле придется заменить на функциюНе надо писать макросов, которые можно заменить на функции.
Любая попытка давать ОДНОЗНАЧНЫЕ рекомендации будет ошибочной.
Функции, макросы — это все абстракции, призванные что-то от читающего скрыть, упростить, уменьшить количество анализируемых сущностей. С точки зрения человека их смысл именно в этом. Поэтому сущности с одинаковым смыслом (в человеческом понимании) не стоит явно разделять.
Вот объясните мне логику создателей GCC, которые спокойно сделали два макроса _BV(x) и bit_is_set(a,b) — какой логикой они руководствовались, делая один заглавными, а второй нет?
void do_things(void) {
...
}
void do_async(void (*what)(void)) {
...
}
...
do_async(do_things);
Макросы (в современном языке) нужны только там, где функций недостаточно. Там, где нужно генерировать названия функций, например. Или как-то ещё расширять синтаксис языка.
Логику создателей GCC я вам объяснить не смогу, я не принадлежу к их числу.
У новичка, читающего данную статью, появится представление о том, как обычно делают более опытные ребята в самых разных проектах :)
А по поводу оберток — они нужны не в эксплуатации — конечный пользователь не будет смотреть логи работы программы -, а в отладке. Надо же тестерам и разрабам понимать, что и где упало в случае ошибки.
о_О
Я пишу их с заглавной буквы.
Кто то использует _ перед именем обертки, кто-то — другие обозначения. Тут тоже есть свобода выбора :)
О_о
#define max(x,y) ((x)>(y))?(x):(y)
В Си это не раз выручает при сравнении чисел любых типов, в то время как функций пришлось бы писать гораздо больше и с не такими лаконичными именамиИли в «малых» встраиваемых системах: работу с портом ввода-вывода выгоднее делать макросом в силу большей скорости исполнения кода…
x = max(a++, b);
И удивляется результатам. Ну или правило о том, что так нельзя делать, пишется в Code Style Guide, который в итоге раздувается.
Или в «малых» встраиваемых системах: работу с портом ввода-вывода выгоднее делать макросом в силу большей скорости исполнения кода…Всегда пишу такие
#define CLR(a,b) ((a) &=~(1<<(b)))
static void clr(volatile uint8_t *PORT,uint8_t bitN)
{ *PORT &= ~ (1<<bitN); }
Результат один к одному. (Правда при использовании функции приходится писать лишний амперсант при вызове функции. В данной статье от разработчиков микроконтроллеров конечно сказано что макрос работает быстрее… Но я объявил функцию static и компилятор имеет полное право ее заоптимизировать, что он и сделал. Убрав static — получаю гораздо большую программу.)А с учетом что в коде у меня (и не только, не нахожу статью на хабре), присутствуют далее такие короткие функции:
static void Led_Off(void)
{ clr( &DDRC, 3 );
clr( &PORTC, 3 ); }
При этом компилятор и их заоптимизирует в две инструкции, но вот прежде чем писать такие трехэтажные макросы я бы трижды подумал.
P.S. В «малых» встраиваемых системах у меня все функции static (кроме main), сборка простая — в основной файл сначала все .h файлы инклудяются, потом все .c — все для того чтобы у компилятора были все возможности.
#define cmp(x, y) (x - y)
Вполне себе распространенное развлечение написано выше. Вот только беда: с беззнаковыми целыми работать не будет, хотя скомпилируется. Если я напишу это капсом, то это явный намек коллегам о том, что это макрос и надо осторожнее работать с этим.
По поводу операционных систем: скоро линукс и его собратья будут в каждом чайнике стоять. И это прекрасно. В mbed есть тоже есть всякие RTOS. Причем ошибки все равно обрабатываются, нередко для этого заводят обработчики по systick.
И там совсем другие подходы.
Поэтому статьи по C надо начинать с дифференциации.
Т.е. сразу предупредить, о программировании каких платформ идет речь и о каком организационном подходе к проектированию.
Например проблема сопровождения для малых микроконтроллеров у многих программистов отсутствует в принципе. Код пишется только для себя и не для кого больше.
Отсюда проистекает бессмысленность большинства советов в статье.
К примеру не надо зацикливаться на правилах названий функций и переменных, программист делает рефакторинг своего кода каждый день. Какая нибудь функция или переменная может поменять свое название десятки раз за время жизни проекта.
Постоянный рефакторинг на самом деле лучше закрепляет в памяти понимание работы программы, чем бесплодные попытки с самого начала дать правильные имена.
Более того, когда возвращаемся к старому проекту, то и тут можно сразу начать с рефакторинга. Это поможет быстрее восстановить в памяти структуру программы.
Соответственно код должен быть адаптирован к рефакторингу, вместо того чтобы подчиняться неким соглашениям об именованиях.
Ну и конечно о рефакторинге бессмысленно говорить не упомянув в какой среде разрабатывается код и в какой компилируется.
А по поводу адаптации к рефакторингу… Это очень сложная тема.
Когда я только начинал учить Си, меня часто тюкали по голове за то, что я пишу по-своему, игнорируя общепринятые вещи. Тогда же мне довелось участвовать в небольшом проекте, на последней стадии разработки которого одному опытному программисту поручили выполнить рефакторинг моего кода, ибо он не соответствовал кодстайлу проекта. А кодстайл проекта мы взяли такой же, как в ядре Linux.
Однако, каждый волен сам выбирать свой кодстайл, главное чтоб он не противоречил соглашениям, принятым в проекте:)
Разрешите поинтересоваться, это какие же подходы используются при разработке для малых микроконтроллеров, что указанные автором подходы не приенимы?
Хотя я такой подход не приветствую, но по своему опыту скажу что можно сдать код который «пора бы отрефакторить», но можно прицепить еще один костыль и будет работать.
Правда бывают и требовательные заказчики, один у меня уже обновления в течении пяти лет заказывает, вот там в исходнике — красота, хоть на выставку.
Редко, когда я, почти, полностью согласен с содержанием статьи. Автору спасибо!
потихоньку был вытеснен из десктопа и энтерпрайза
Да неужели?
https://habrahabr.ru/company/hh/blog/318450/
Да, у Java там почти в два раза отрыв по сравнению с С++, но что-то подсказывает, что это не десктоп с энтерпрайзом.
Я вот сейчас пишу с ПК на котором одно приложение на C# и штук пять на Java. И это не что-то такое особенное, вполне себе мейнстримовая ОС. Остальное, С++ и C. Странно, да?
И как много современных десктопных приложений написано на чистом С?
На С написано мало. Но вытеснил его отнюдь не Java и C#. А С++
Просто в статье и написано, что был вытеснен именно C. C++ от C уже совсем далеко ушел, их даже не стоит рядом упоминать.
Там ещё и ошибка была, вероятно. Если только в make текущий каталог не сбрасывается при переходе к следующей строке блока в Makefile, то make будет вызван в sound/
, sound/graphics/
, sound/graphics/engine/
.
В данном конкретном случае я бы использовал переменные (особенно когда количество директорий начинает расти), но это уже вопрос по make, а не по C.
Альтернативный вариант — исходники раскидать, а объектники свалить в одну кучу, используя VPATH. Но это опять вопрос сборки, не относящийся напрямую к языку.
SUBDIRS = sound graphics engine
.PHONY: all build clean
all: build
build clean:
for dir in $(SUBDIRS); do \
make -C $$dir $@; \
done
Или с макросами
SUBDIRS=sound graphics engine
define SUB
# DO NOT REMOVE THIS LINE
$(MAKE) -C $(1) $(2)
endef
.PHONY: all build clean
all: build
build clean:
$(foreach dir,$(SUBDIRS),$(call SUB, $(dir),$@))
Вобщем все печально, если не поступить примерно так:
SUBDIRS=sound graphics engine
SOURCES=$(foreach dir, $(SUBDIRS), $(wildcard $(dir)/*.c))
OBJECTS=$(subst .c,.o,$(SOURCES))
.PHONY: all clean
all: program
program: $(OBJECTS)
$(LD) -o $@ $^
clean:
-rm $(OBJECTS)
Мы не можем использовать имена каталогов в качестве таргетов (то есть можем, но будем при этом страдать), потому что они с одной стороны существуют, а с другой стороны ни от чего не зависят. Можно было бы для каждого каталога определить связанную с ним статическую либу (sound -> libsound.a), которую бы и собирал соответствующий sub-make, но это не спасет от необходимости делать clean все равно по каталогам (find -name '*.o;' это тоже не самый правильный путь).
Я бы предпочел иметь всю сборку в одном мэйкфайле, чтобы там иметь и список всех исходников, и список всех объектников и все на свете. Но это лично мои предпочтения. Так-то можно и automake использовать и иметь в каждом каталоге совсем крошечные Makefile.am.
Я бы очень хотел, чтобы описанная выше архитектура была преувеличением. Но нет, это суровая реальность на моем текущем месте работы.
Эскобар.jpg recursive make considered harmful.
Да-да, как раз хотел кинуть это. Пишут Makefile рекурсивные и потом жалуются на скорость работы make.
mybin: bin.c libsound.a:
cc $(^) -o $(@)
libsound/libsound.a:
make -C libsound
Поменяли файл локальный bin.c, не из libsound/. В этом примере make попытается пересобрать libsound.a, тк на этом уровне он не знает, что libsound.a не зависит от bin.c.
Если сделать это без вложенного make в libsound можно сделать полный граф зависимостей и тогда make-у будет понятно, что libsound.a не зависит от bin.c и его трогать не нужно.
Как можно тут указать правильно все от чего зависит libsound.a не дублирую Makefile из libsound?
Есть вариант, который используется в некоторых проектах, поставить libsound.a в зависимость libsound/stamp, на который делается touch в конце сборки libsound.a, а при любой правке в каталоге libsound придется делать touch на этот stamp. Такое имеет смысл, если предполагается, что содержимое libsound будет меняться редко и вообще это сторонний код, которому мы верим.
Я просто сталкивался в относительно большом проекте(2М строк), как раз с реккурсивным make и неправильным отслеживанием зависимостей отчасти из-за этого. Сборка была очень медленной(С++ с шаблонами, бустами всякими и подобным шлаком), плохо параллелилась и делала много лишнего на каждый чих, когда не нужно.
На самом деле я всеми руками за один единственный Makefile. Просто его чуть труднее правильно готовить, а многие разработчики, которых я видел, почему-то не хотят научиться это делать как следует.
The ‘-j’ option is a special case (see Parallel Execution). If you set it to some numeric value ‘N’ and your operating system supports it (most any UNIX system will; others typically won’t), the parent make and all the sub-makes will communicate to ensure that there are only ‘N’ jobs running at the same time between them all. Note that any job that is marked recursive (see Instead of Executing Recipes) doesn’t count against the total jobs (otherwise we could get ‘N’ sub-makes running and have no slots left over for any real work!)тыц
char c[IP_UDP_DHCP_SIZE == 576 ? 1 : -1];
Лично у меня, когда я это увидел, повис вопрос: «Чеееееееее?».
Это абсолютно необходимая проверка для компиляции на разные архитектуры и разными компиляторами. Дело в том, что отключение выравнивания в разных компиляторах выполняется по-разному. Опять-таки архитектуры бывают разные, вплоть до 32битных байтов.
Второй момент — почему так странно, а не просто assert. Дело в том, что обычный assert отрабатывает во время исполнения, а такой — во время компиляции. Сейчас обычно делают макрос CCASSERT, но тут, похоже, код древний, и на общепринятый макрос просто не перешли.
#define _x_CCASERT_LINE_CAT(predicate, line) typedef char constraint_violated_on_line_##line[2*((predicate) != 0)-1];
#define CCASSERT(predicate) _x_CCASERT_LINE_CAT(predicate, __LINE__)
// Usage: CCASSERT(1) to pass; CCASSERT(0) to fail
/*
typedef struct {
long x;
long y;
}foo ;
CCASSERT(sizeof(foo) < 10) // will not complain
*/
Это — пример того, как делать не стоит. Никогда. Иначе случится насилие. Рано или поздно.
В целом, когда у нас есть внутренняя структура, являющаяся образом какого-то внешнего пакета, то является хорошим тоном проверить хотя бы её длину (в идеале — ещё и смещение до ключевых полей).
Если вы когда-нибудь будете портировать ваш проект на пяток архитектур и пяток разных компиляторов, то сами нарветесь на проблемы со структурами. И на своем горьком опыте начнете ставить assert и CCASSERT.
Костыль, созданный с одной целью — вставить его, как палку, в колеса тому, кто будет расширять этот код.
Ну кто же мог подумать, что расширять код будет человек, который впервые сталкивается с проблемами портирования? :-))) Кстати, совет — держитесь в рамках ANSI C-88, все более новое, могут и не принять в код. Не на все платформы есть компиляторы, понимающие более новые стандарты Си.
Из рассказов коллег:
— И предоставьте исходный код в распечатанном виде.
— Там 600 тысяч строк, будет 15 тысяч листов, вес листа 80 грамм — итого 1200кг. Грузовик дадите?
— Ладно, давайте на CD.
:-)
Ну в общем, если сертификация важнее всего, то остальным можно пожертвовать.
По размеру это 6 коробок размером порядка 0.3х0.3х0.3 м (если спрессовать так плотно, как A4 обычно лежит в пачках). То есть, 30 пачек A4 по 500 листов.
Понял ошибку, исправился :)
В использовании оберток есть небольшой минус, который, если захотеть, можно решить костылем. А что это за минус — можете предположить в комментариях :)
Там минус очень существенный — на каком бы сокете не произошел сбой — сообщение об ошибке будет одинаковое. Так что надо как минимум добавлять вызываемые параметры. А ещё лучше — лишний параметр в обертке — текстовая строка с назначением сокета, которая выводится в сообщений.
А «небольшой» минус — это то, что как закроются сокеты — зависит от библиотеки. Если успели сделать connect по TCP/IP — сокет скорее всего закроется жестко и вторая сторона не будет в курсе, что мы уже отвалились. Так что лучше использовать atexit.
Храните заголовочники в директории include.
А не могли бы вы объяснить, какой смысл делать отдельную директорию include? Я в этом вижу только минусы:
- нужно прописывать еще один путь в настройках проекта
- дерево проекта увеличивается вдвое, ведь внутри папки include приходится повторять всю структуру папок с файлами .c
- в файле xx.c нельзя просто написать include «xx.h», приходится писать полный путь до него — #include «yyy/zzz/xx.h»
- чтобы скопировать какой-то «модуль», вам приходится копировать два файла из разных мест
Гораздо удобнее, на мой взгляд, группировать файлы в папке по принципу «модулей». Допустим, модуль Common — это отдельная папка, в ней common.c и common.h. Этот модуль приобретает «позиционную независимость», т.е. когда вы его копируете в другой проект, вам не надо переписывать инклуд в файле common.c. Ну и все минусы, описанные выше, пропадают.
Я уже писал — ко всему надо подходить с умом. Заголовки общего пользования, библиотечные заголовки, которые должны быть доступны во всем проекте. include — для них самое подходящее место.
Возьмем тот же busybox. В исходниках все группируется именно так, как вы сказали. Каждая утилита в отдельной папке. И .c, и локальные заголовки. Но тем не менее, директория include там тоже есть, и все библиотечные хэдеры живут там:)
Самый популярный файл во всем busybox — обитающий там заголовочник с интригующим названием «Libbb.h»
По поводу полного пути — тут вы не совсем правы. Когда мы указываем директорию include, то поиск заголовочников при линковке будет происходить в ней. Так что можно обойтись указанием имени файла в кавычках :)
По поводу полного пути — тут вы не совсем правы
Я просто подумал, что вы предлагаете в папке include дублировать всю структуру каталогов для исходников, а не держать там только глобальные заголовочники. Если в папку include не совать вообще все заголовочные файлы, а только глобальные, то никаких возражений у меня нет.
Например, если есть module1 использующий module2 который, в свою очередь использует module3, а во внешних include-ах всех модулей присутствуют включения друг-друга, то module1 не получится использовать без include-ов модулей 2 и 3, хотя на самом деле достаточно только .lib файлов.
Не совсем понятно почему часть про кодстайл описана в контексте Си. Сейчас, на мой взгляд, кодстайл должен быть неотъемлимой частью любого проекта.
Не понимаю в чем профит группирования заголовочных файлов в одном месте, если это не какая-то библиотека. Когда работал с одним самописным игровым движком (не моим) был бинарник и куча .h файов, который служили интерфейсами дял подключения. Когда не видишь реализации — это удобно. В остальных случаях скачки фокуса между папками include и src довольно неудобны и в чем польза такой группировки мне непонятно.
Не совсем понятно почему возникает необходимость использовать volatile переменные как в примере. Как минимум это усложняет код и добавляет магии в процесс разработки. Можно больше примеров когда такая магия оправдана?
По поводу заголовочников стоит написать отдельную статью. В идеале, все заголовочники должны быть оформлены так, чтобы из них можно было понять как можно больше интерфейсов взаимодействия внутри программы.
+ надо понимать, что ничего не стоит делать бездумно. В том же busybox заголовочники бывают 2х типов: общего пользования и локальные. Общего пользования лежат в include в корне, а локальные — в папках с утилитами (подпроектами).
Очень правильный комментарий вот тут, группировка — очень хорошая практика. Однако в системе вынос в инклуд я встречаю гораздо чаще.
Volatiole надо использовать, когда переменные могут быть изменены неявно. Например, если у нас многопоточное приложение, в котором потоки имеют общий доступ к каким-либо переменным.
Тогда любое изменение этих переменных, которое будет сделано из другого, будет неявным для компилятора. Отсутствие volatile в этом случае может положить весь механизм IPC в приложении.
Каюсь, про многопоточность не подумал.
Volatiole надо использовать, когда переменные могут быть изменены неявно. Например, если у нас многопоточное приложение, в котором потоки имеют общий доступ к каким-либо переменным.Насчет C я не знаю, но нас ведь могут читать люди, пишущие на разных языках.
В C++ использование volatile в многопоточной среде — это очень вредный совет! Он просто не имеет свойств, которые могут быть нужны в многопоточной среде — например, атомарности изменений. Подробное обсуждение есть на stackoverflow
Если нет, то, видимо, это я выразился неясно. Собственно, я то и имел в виду, что использовать volatile для доступа к разделяемой памяти без какой-либо внешней синхронизации небезопасно по большому количеству различных причин, и нужно применять существующие примитивы.
Да, я не прав. Многопоточное приложение тут не при чем, компилятор в состоянии отслеживать изменения переменных в потоках.
А вот когда несколько процессов могут менять одну переменную — другое дело :)
Когда речь идет о изменении переменной извне процесса, то тогда оно будет неявным для компилятора.
volatiole нужен в том случае если переменная изменяется снаружи.
Например у нас есть обработчик прерываний. И его вызова нету в тексте программы, так-как его вызывает(передает управление на него) процессор/контроллер в произвольный момент времени.
Или например у нас есть переменная которая связанна с некоторым физическим адресом(регистром периферии) в SoC.
И нам нужно записать а потом сразу считать значение так-как этот регистр обрабатывается особым образом.
И если мы напишем что-то типа
reg = 0x10;
if(reg == 1)
{
do_somthing;
}
и reg будет без volatile то копилятор вправе выкинуть этот код.
Или например мы пишем вот так
reg = 0x10;
reg = 0x11;
..
reg = 0x35;
Без volatile опять компилятор выкинет предыдущие операции и оставит только последнюю.
… а слова в названиях отделяются нижним подчеркиванием
А какие еще бывают подчеркивания?
static int CheckModemConnection()
На первый взгляд, всё выглядит понятным. На это и рассчитано. Возвращает 1, если параметры, связанные с модемным соединением изменились, и 0, если нет.Насколько я помню, это традиционно для Си возвращать именно такие значения, чтобы в месте вызова можно было бы реализовать обработку для случая произошедших изменений. Было бы логичнее иметь в качестве кода возврата
bool
, но это же Си! (NB: надо свериться с последним стандартом!)Далее, название функции:
CheckModemConnection
. А если придётся проверять ещё чего-нибудь, то надо будет писать отдельную функцию для вот этого самого чего-нибудь? Мне бы ужасно захотелось бы обобщить и иметь общую для всех функцию проверки. Даже, если в Си нет классов. (Всегда можно ввести.) К тому же, для того, чтобы иметь возможность проверять состояние, неплохо бы как-то формализовать сие понятие и сделать так, чтобы, например, пробегать по списку необходимых свойств в цикле, а не создавать длинное условие для оператора if
. В конце-концов, было бы крайне любопытно (и эффективно?) иметь текстовую строку, описывающую текущее состояние объекта, каждый символ которой связан с некоторым элементом описания модемного соединения. Делая простой проход по этой строке, можно было бы сразу получать ответ на нужный вопрос. А, если само символьное представление делать более сложным (XML?), то можно было бы автоматизировать выдачу диагностических сообщений (например, соединяя друг с другом строки состояния для различных элементов в единую строку).
И она бы вызывалась вместе со всеми проверками. CheckModemConnection() — одна из сотен функций, которые вызываются при обработке пришедшей конфигурации. То есть все — и сеть, и voip, и iptv имеет подобные проверки.
Нужно это, чтобы группировать параметры, и, когда надо добавить новый, знать, где находится проверка группы связанных параметров.
Конфигурация приходит в виде дерева, по конкретным нодам в цикле не пройтись. Разве что, можно создать массив/перечисление с указателями на параметры, и идти по нему… Но это не слишком целесообразно, ибо будет гораздо сложнее найти источник и сразу не скажешь (не смотря на массив), какие параметры подвергаются проверке.
"Традиционно" для Си функции действия add, get, write_ и т. д. должны возвращать отрицательный код ошибки, 0 или положительное значение в случае успеха. А функции предиката аналог bool — 0 и 1.
В данном примере CheckModemConnection() из названия должна возвращать 0 — всё ок или отрицательный код ошибки. Поэтому лучше её было бы назвать как-то так: isModemConnectionChanged(), тогда сразу бы было ясно, что она "Возвращает 1, если параметры, связанные с модемным соединением изменились, и 0, если нет.".
Слово традиционно я намеренно заключил в кавычки, т.к. если говорить о традициях, то язык Си неразрывно связан с историей Unix. Поэтому приверженцам традиций лучше следовать Linux kernel code style — пункт 16, либо KNF.
Костыль, созданный с одной целью — вставить его, как палку, в колеса тому, кто будет расширять этот код.
Вернее, не так.
Это — костыль, который ясно вам скажет: не надо повторять мою реализацию, хочешь пакет больше — указатели, динамическая память и динамическое вычисление размера тебе в помощь!
Это — пример того, как делать не стоит. Никогда. Иначе случится насилие. Рано или поздно.
Вы не поняли, и делаете неправильные выводы.
Это проверка, что компилятор корректно упаковал структуру с нужным размером.
См. https://git.busybox.net/busybox/commit/?id=6884f665bd7bc101f56ff9047afaffbc06dc99e2
если мы захотим сделать реализацию, в которой размер пакета будет вычисляться динамически (из соображений этики и следования стандартам, только такая реализация и имеет право на жизнь)
Это бессмысленное усложнение, т.к. нужна структура/буфер под максимальный размер DHCP пакета. Динамика — оверкил в условиях, когда пакеты обрабатываются строго последовательно и максимальный размер пакета заранее известен.
Если DHCP клиент не обозначил серверу максимальный размер (57 опция), то сервер не имеет права по RFC отвечать пакетами бОльшего размера. Однако, это только в идеальном мире, поэтому есть опция UDHCP_SLACK_FOR_BUGGY_SERVERS, в которой задается размер дополнительного места в буфере с максимумом 924, что дает возможность принимать пакеты до 1500 байт.
См. https://git.busybox.net/busybox/commit/?id=72e76044cfda377486a5199a0d35d71edf669a42
Такой небольшой размер — прямое противоречие RFC с описанной в нем 57й опцией.
Никакого противоречия нет. Такой небольшой размер находится в полном соответствии с минимальным MTU, ниже которого быть не может. Соответственно, т.к DHCP протокол основан на UDP без подтверждения доставки, этот небольшой размер дает "гарантию", что DHCP пакеты не потеряются по пути из-за более меньшего MTU.
Чтобы информировать сервер о максимально поддерживаемом размере, добавляется 57 опция, с размером — IP_UDP_DHCP_SIZE == 576, вне зависимости от дополнительного буфера UDHCP_SLACK_FOR_BUGGY_SERVERS.
См. коммиты 2007 и 2010 года:
https://git.busybox.net/busybox/commit/?id=35ff74676b54b1cae5a6324d2517568393fedbc8
https://git.busybox.net/busybox/commit/?id=b3af65b95de883e9be403e065f57b867d8ea8d43
Таким образом, чтобы "законно" получать пакеты больше стандартного размера, нужно
1) увеличить размер UDHCP_SLACK_FOR_BUGGY_SERVERS до максимально возможного
2) уведомлять сервер о реально поддерживаемом размере 57й опцией с учетом текущего MTU интерфейса.
И тут, всё давно придумано
https://github.com/wl500g/wl500g/commit/57fd93bd29399d6b08643bb79a3e41f330b6cd9a
Объявляйте переменные в начале функции.
Всегда страшно горело из-за этого правила. Вы можете обьяснить зачем это делать? В случае если переменная определена в месте первого использования, ее сразу можно инициализировать нужным значением, в случае рефакторинга кода ее тут-же можно удалить, не нужно искать тип переменной если видем присвоение ей значения, и еще много причин.
Даже Google думает не так, как написано:
C++ allows you to declare variables anywhere in a function. We encourage you to declare them in as local a scope as possible, and as close to the first use as possible.
Вы можете обьяснить зачем это делать?Чтобы код компилировался на старом компиляторе, например.
Или если вы по какой-то причине считаете, что именно в этой функции вам важнее уметь находить все переменные функции за один раз, чем удобство просмотра инициализации какой-нибудь одной.
Да мало ли причин может быть. Но, на мой взгляд, действительно не стоит преподносить это правило в качестве рекомендации на все случаи жизни.
ИМХО такое объявление добавляет читабельности: сразу прочитав список переменных, уже примерно понимаешь, с чем имеешь дело и что делает код.
С другой стороны, иногда gcc матерился в тех же автоматах, что переменная может быть использована без инициализации, хотя явно в это состояние можно было попасть только из того состояния, где переменная была инициализирована, в этом случае, после скурпулёзных проверок, переменная в начале цикла инициализировалось.
А ещё, начинающим сишникам я бы порекомендовал поизучать код nginx, отличный код, интересные решения.
Если вы постоянно работаете с трекерами (вроде RedMine), то при внесении правок в код можно указать номер задачи, в рамках которой эти правки были внесены. Если у кого-то при просмотре кода возникнет вопрос а-ля «Зачем тут этот функционал?», ему не придется далеко ходить. В нашей компании еще пишут фамилию программиста, чтобы если что знать, к кому идти с расспросами.
/* Muraviyov: #66770 */
Плохой совет. Плохой он оттого, что комментарии никто не поддерживает.
Мировой опыт (выраженный в книжках МакКоннелла, Р. Мартина и пр.), а равно и мой скромный, говорит, что комментарии редко бывают актуальными, тем более, такие.
«Кто, когда и почему?» — на эти вопросы достоверно отвечает система контроля версий (должна отвечать… у вас-то какая?).
+ даже зная, кто и каким каммитом добавил функционал, не всегда можно понять, в рамках какой задачи это было сделано. (не все и не везде указывают).
Да и в любом случае, просто вбить заранее указанный номер задачи в трекер будет в разы быстрее, чем сначала искать «кто, где и зачем».
По поводу того, что комментарии не поддерживают — не соглашусь.
Я бы посмотрел на того, кто бы взялся сопровождать тот же bb, не будь он так хорошо задокументирован)
Если комментарии не нужны, то почему так много рекомендаций из самой различной литературы и интернета отмечают умение грамотно писать комментарии одним из правил хорошего тона?
Когда мы делаем git blame в каком-нибудь огромном файле
я отсюда делаю вывод: файлы не должны быть огромными :)))
даже зная, кто и каким каммитом добавил функционал, не всегда можно понять, в рамках какой задачи это было сделано. (не все и не везде указывают).
это ничем не отличается от наличия/отсутствия (обсуждаемого нами) коммента
это вопрос дисциплины и гайдов оформления текста коммита, только вот в отличие от отсутствия комментария с причинами «забыл/лень/не хочу/не знаю» текст коммита легко валидируется хуками, что способствует гайдам
просто вбить заранее указанный номер задачи в трекер будет в разы быстрее
с «быстрее вбить» спорить даже не собираюсь (я сам люблю one-click переходы)
но Вы забываете про достоверность
Я бы посмотрел на того, кто бы взялся сопровождать тот же bb, не будь он так хорошо задокументирован)
я не знаю, что такое bb
но с высоты своего опыта поддержки многолетних активно меняющихся legacy-продуктов могу утверждать, что такие комментарии бессмысленны чуть менее, чем совсем
отмечают умение грамотно писать комментарии одним из правил хорошего тона
я выделил ключевое слово ;)
Согласен с
Названия макросов и макрофункций пишутся капсом, слова в названиях отделяются друг от друга нижним подчеркиванием.
Названия переменных записываются в нижнем регистре, а слова в названиях отделяются нижним подчеркиванием
Логически группируйте .c файлы в папки.
Не согласен с
Храните заголовочники в директории include
Остальное не важно
Вот мой реестр
51 Атрибут Хорошего С-кода (Хартия Си программистов)
https://habr.com/ru/articles/679256/
Пиши на C как джентльмен