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


    «Code Monkey like Fritos
    Code Monkey like Tab and Mountain Dew
    Code Monkey very simple man
    With big warm fuzzy secret heart:
    Code Monkey like you
    Code Monkey like you»

    — Jonathan Coulton — Code Monkey


    Я думаю, многим знакома эта шикарная песня 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]);
          }
      }

    • i, j, k — стандартные названия для итераторов цикла

      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 { dont_do_something(); } 

    • Объявляйте переменные в начале функции. Если это глобальные переменные, то в начале файла.

      По возможности инициализируйте переменные при объявлении. Численные с помощью нуля, указатели — 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 */



    P.S. Для тех кто устраивается на работу: так же не следует забывать, что в каждой компании, как правило, используется свой Code Style, и ему нужно следовать. В противном сулучае можно получить как минимум укоризненные взгляды товарищей-разрабов или втык от начальства.

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


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

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

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

    2. Храните заголовочники в директории include.

      Рассмотрим пример:

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

      В принципе, проект как проект. Но давайте на секунду представим, что у нас не 9 файлов, а, скажем, 39. Что-то быстро найти будет проблематично. Да, в консоли — пара пустяков, но что если человек работает с GUI, или, что еще хуже, пытается найти файл в Github/Gitlab/Bitbucket?

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

      • project/
        • include/
          • common.h
          • network.h
          • packet.h

        • common.c
        • main.c
        • networking.c
        • networking_v6.c
        • packet.c
        • Makefile

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

      @$(CC) $(OBJS) -o networkd -L$(ROMFS)/lib -linteraction -Wall -lpthread -I ./include

    3. Логически группируйте .c файлы в папки.

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

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

      .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)) <= 0) { 
        perror("socket one");
        exit(EXIT_ERROR_CODE);
    }
    if ((socket_two = socket(AF_INET , SOCK_DGRAM , 0)) <= 0) { 
        perror("socket two");
        exit(EXIT_ERROR_CODE);
    }
    if ((socket_three = socket(PF_INET , SOCK_RAW , 0)) <= 0) { 
        perror("socket three");
        exit(EXIT_ERROR_CODE);
    } 

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

    /* Где-то в коде... */
    int Socket(int domain, int type, int proto) {
        int desk = socket(domain, type, proto);
        if (desk <= 0) {
            perror("socket");
            exit(EXIT_ERROR_CODE);
        }
        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);

    • restrict — при объявлении указателя дает компилятору гарантию (вы, как программист, гарантируете), что ни один указатель не будет указывать на область памяти, на которую указывает целевой указатель. Профит этого модификатора в том, что компилятору не придется проверять, не указывает ли какой-то еще указатель на целевой блок памяти. Если у вас внутри функции несколько указателей одного типа — возможно, он вам пригодится.

      void updatePtrs(size_t *restrict ptrA, size_t *restrict ptrB, size_t *restrict val);
      

    • volatile — указывает компилятору, что переменная может быть изменена неявным для него образом. Даже если компилятор пометит код, зависимый от волатильной переменной, как dead code (код, который никогда не будет выполнен), он не будет выброшен, и в рантайме выполнится в полном объеме.

      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);
      }
      

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

    Заключение




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

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

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

    Удачи, Хабр!
    Share post

    Similar posts

    Comments 169

      0
      Спасибо за статью.У меня есть вопрос, по поводу вендоринга в C и в C++, какая лучше всего практика по использованию и подключению сторонних библиотек и контроля их версий? И смотрели ли Вы в сторону таких молодых языков как Rust или Go?
        +1
        В крупных проектах (во всяком случае, в тех, в которых я участвовал) все необходимые библиотеки (включая libc-библиотеки) содержатся в исходниках и компилируются при сборке.
        То есть, в проекте присутствует только одна версия библиотеки. Хочешь другую — нужно озаботиться этим и установить её в проекте.

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

        А вообще есть clib, но я с ней не работал, ничего сказать о ней не могу:)
        Ну или, вот это менее популярное решение.

        С Rust я пока еще не знаком, с Go недавно начал возиться. Пока все очень нравится — система импорта пакетов после сей не перестает меня радовать :)
          +4
          Да, но Go пока позволяет использовать только последнюю версию библиотеки.
          А вот как раз Cargo в Rust позволяет выбирать любую версию.
          Так что, советую ознакомиться, если будет время.
          Мне, например, очень нравится подход, который они используют.
            +1
            Ну, я только начал знакомство с Go, у меня еще не было проектов на нем, где требовалась бы не последняя версия библиотеки:)
            Ну, надеюсь, что все еще впереди.

            Rust недавно расхваливал один хороший знакомый, так что рано или поздно я к нему приду, думаю.
            А Cargo — уже как получится. Пока вообще не представляю, что это за язык:)
              +1
              Rust недавно расхваливал один хороший знакомый, так что рано или поздно я к нему приду, думаю.
              А Cargo — уже как получится. Пока вообще не представляю, что это за язык:)

              Если что, cargo это пакетный менеджер раста.

                +1
                Спасибо за поправку, не знал :)
                +1
                Cargo — это не язык, это утилита для Rust — пакетный менеджер, который отвечает за структуру проекта, сборку и за библиотеки.
                  0
                  Ахах, не знал.
                  Спасибо за просвещение :)
                0
                Вендоринг в go позволяет заморозить библиотеку на нужном коммите (если только там не разветвленная цепь зависимостей, которые вытянуть проблематично. Еще есть мой любимый gopkg.in (от создателей mgo), позволяющий использовать даже несколько версий одного пакета в одном приложении. Но тут проблема доверия и оф.поддержки для многих может стать решающим фактором.
              –1

              Область применения Rust или Go несколько отличается от системного программирования, особенно Go.

                +2

                Интересно, почему область применения rust — не системное ПО? Он в этой нише, в первую очередь, и интересен.

                  –2

                  По скорости Rust не дотягивает до C, насколько я могу судить.

                    0

                    В каких кейсах, при каких структурах данных, при каких используемых абстракциях?


                    http://benchmarksgame.alioth.debian.org/, если что, не аргумент, там много откровенно кривых бенчмарков.

                      0
                      А есть альтернативные бенчмарки?
                +1
                Если в проекте используется CMake, то есть ещё один вариант: таки указать в CMakeLists.txt репозитории и конкретные версии, которые нужно тянуть. К примеру, в мануалах ко многим библиотекам от Google рекомендуется подключать их именно так.
                +1
                Если уж упоминается restrict, то можно было бы вспомнить и про inline-функции, константы (в разделе про макросы), про именованую инициализацию структур и расширеную массивов (особенно в последнем разделе) etc.
                А вот register на этом фоне смотрится уже какой-то тенью прошлого :)

                За valgrind отдельное спасибо, почему-то до сих пор есть люди, которые про него не знают.
                  0
                  Да, возможно. Выбрал довольно абстрактную тему для статьи — сложно охватить все и разом. Правда, inline я вижу постоянно, а те же volatile и restrict — довольно редко, даже в тех местах, где их использование напрашивается.
                  Скажем так, узнать про inline проще, чем про restrict и volatile — вот я и решил это упомянуть :)

                  А register — да, потихоньку выходит из использования. Тем не менее, иногда — полезная вещь.
                    0
                    register вообще ни на что не влияет в современных компиляторах при уровне оптимизации -O1 и выше. (современные — это где-то с конца 1980х). Но на -O0 может влиять. Вот например обсуждение в рассылке GCC: https://gcc.gnu.org/ml/gcc/2010-05/msg00098.html
                      +2
                      Когда писал программы на streetinterview (сейчас hackerrank.com) моя программа не вкладывалась по времени буквально на процент в отведенный лимит. Я исключительно с помощью register (скорее методом итеративного поиска, чем логически, потому что были и очень не очевидные последствия) подобрал так чтобы программа на моем AMD работала процентов на 5 быстрее. Отправил программу на streetinterview и увидел падание производительности на все 10% на их XEON. Для убедительности запустил пару раз и с тех пор register не использую, чтобы не снился.
                    +1
                    Да, только он только на однопоточный простейших приложений и работает. В более-менее сложном чем-то он тупо замедляет все раз в 100 примерно и ничего толком не работает. Мне больше нравится вариант использовать jemalloc с опцией дебага heap-а. Там можно и в рантайпе профили снимать, графы смотреть, что откуда пришло. Различные графики строить и тд. Ну и утечки тоже ищет. И оно почти не замедляет ничего(по крайней мере приложение приемлемо работает с ним), памяти только больше жрет, что ожидаемо.
                    https://github.com/jemalloc/jemalloc/wiki/Use-Case%3A-Heap-Profiling
                      0
                      Правильно ли я понимаю, что для отлова утечек/выходов за границы нужно сначала полностью пересобрать проект с другой версией библиотеки, а потом разглядывать логи? Не назвал бы это удобным подходом.

                      Но в любом случае попробую попозже, интересно проверить насколько оно справится с теми случаями, которые valgrind точно отслеживает.
                        0
                        Можно без перезборки.
                        Достаточно через LD_PRELOAD подгрузить so-ку, jemalloc-а собранного с поддержкой поиска утечек.
                        Так через переменные окружения можно настроить, а настроек там тьма.
                        +1
                        Как я понимаю jmalloc это нестандартный аллокатор и отладчик для хипа. А valgrind это проверка всего и сразу. Хотя часть можно отключить. Он проверяет переполнения, обращения по некорректным адресам, утечки и д.р.

                        Еще есть более быстрый вариант. Это asan/ubsan. Он может проверять уже кое что, чего valgrind не может, но в тоже время чуть менее надежно.
                        Для использования *san надо собирать приложение с нужными флагами. Работа замедляется в несколько раз, но получается почти c/c++ с проверкой корректности работы с памятью.
                          0
                          Он может проверять уже кое что, чего valgrind не может,

                          Например?
                          Для использования *san надо собирать приложение с нужными флагами
                          В valgrind мне нравится как раз то, что для проверок не нужны никакие вмешательства в сборку и прочие дополнительные телодвижения. Но при этом он удобно интегрируется в IDE. Например, в том же Eclipse (да-да, я им вовсю пользуюсь) живёт в профайлерах:

                          image

                          С остальными ещё пока руки не дошли поразбираться, но интересно.
                            0
                            Например?


                            Скажем у вас есть структура что-то вроде
                            struct
                            {
                              int a[15];
                              int b;
                            };
                            


                            a[16] для valgrind скорее всего будет корректен, если не вставится паддинг после a. А ubsan или asan выведет ошибку.
                            Или тоже самое, только уже разные структуры или массивы, но по указателю из первой структуры через переполнение индекса вы попадете, во вторую. Тут тоже asan/ubsan должен сработать.
                            Пропустить может если там будут указатели а не массивы.

                            Другие случаи не помню, но они тоже есть.

                              0
                              Ну, если в лоб, то gcc и сам отловит :) И если индекс задаётся переменной, значение которой компилятору известно. Но идея понятна, спасибо.

                              P.S. А вот cppcheck не поймал, даже если индекс явно 16 влепить. Надо ещё PVS замучить, но позже.

                              P.P.S. Кстати, немного не про это, но про С. Вроде бы мелочь, но приятно, чёрт возьми :)
                                0
                                да, конечно я имел в виду что индекс задается в рантайме. Скажем читается из файлы, или как-то вычисляется.
                                  0
                                  P.S. А вот cppcheck не поймал, даже если индекс явно 16 влепить.

                                  Возможно баг, или информации анализатору не хватает. Вообще он такое ловит:


                                  (error) Array 's.a[15]' accessed at index 16, which is out of bounds.


                                  Насчёт sanitizers — отличные инструменты. Замедляют программу значительно меньше valgrind, а находят зачастую больше проблем, хотя бы за счёт большего объёма информации о программе. Пересобирать надо, да, но оно того стоит.

                        +9
                        Объявляйте переменные в начале функции.

                        Совет так себе. "Совершенный код" рекомендует объявлять локальные переменные как можно ближе к месту использования, чтобы при чтении не приходилось "вспоминать" типы увиденных переменных.


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

                        В основном совет справедлив только для языков/компиляторов, где компилятор не проверяет инициализацию переменных. Небольшой оффтоп, но например в Java так лучше не делать (я понимаю, что эта статья для С-шников, но сейчас мало кто пишет только на С, а статью могут читать все).


                        примеры (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 не влияет ни на что, по крайней мере на самых распространенных платформах.

                          0
                          Объявляйте переменные в начале функции.

                          Как минимум это релевантно в Си, ибо не перечит стандарту ansi

                            +3

                            В ansi c, насколько я помню, "не в начале" объявить даже нельзя, в С99 — можно. Если уже есть компилятор С99, не всегда нужно следовать ansi c.

                              0

                              Более того, хватает компиляторов с поддержкой c11
                              Но как правило последний стандарт не отражает стандарт который используется (требуется поддерживать)

                            +1
                            объявлять локальные переменные как можно ближе к месту использования, чтобы при чтении не приходилось «вспоминать» типы увиденных переменных.

                            Если функция не растягивается на хрелиард строк, то ничего вспоминать и не придётся. Одна из причин такого объявления в C++ или Java в том, что для не-POD-типов существуют конструкторы и деструкторы. И нет никакого смысла их дёргать (а они могут быть далеко на самыми лёгкими), если код пойдёт по ветке, в которой эти переменные просто не используются. В C такой проблемы нет (если не учитывать инициализацию). Привычка, скорее, тянется из ISO C :-)

                              0

                              И да, кстати


                              Не уверен, но мне кажется, что в настоящее время ключевое слово register не влияет ни на что, по крайней мере на самых распространенных платформах.

                              src без register

                              int sqr(int a) {
                              return a*a;
                              }


                              image


                              src c register

                              int sqr(register int a) {
                              return a*a;
                              }


                              image

                                +2

                                Это с -O2?

                                  0

                                  clang, icc компилируют одинаково; gcc начиная с -O1 — одинаково, а без оптимизаций — так, как на скринах.
                                  https://godbolt.org/g/F5IFpG

                                    0

                                    Не писал тэг "сарказм", думал и так поймут. Естественно, при включенной оптимизации будет что-то вида


                                    mov eax, edi
                                    imul eax, eax
                                    ret

                                    или функциональный аналог.

                                    +1

                                    Нет (: пример с оптимизацией будет придумать посложнее

                                      +2
                                      К тому же, в соответствии с ABI, указание register в параметрах функции вообще бессмысленно. А если брать «игрушечные» примеры, то и вовсе:

                                      static int sqr(int a) {
                                          return a*a;
                                      }
                                      
                                      int main(void) {
                                          return sqr(123);
                                      }

                                      превратится в:

                                      main:
                                      .LFB61:
                                      .cfi_startproc
                                      movl $15129, %eax
                                      ret


                                      Или это:

                                      int main(int argv, char **argc) {
                                          return sqr(argv);
                                      }

                                      станет

                                      main:
                                      .LFB61:
                                      .cfi_startproc
                                      movl %edi, %eax
                                      imull %edi, %eax
                                      ret

                                  0
                                  Не уверен, но мне кажется, что в настоящее время ключевое слово register не влияет ни на что, по крайней мере на самых распространенных платформах.
                                  По крайей мере нельзя взять адрес регистровой переменной, независимо от того, поместил ли ее компилятор в регистр.
                                    –1
                                    Дельное замечание.
                                    На самом деле, в си есть такая вещь, как предварительное объявление функции.
                                    То есть, если ты уверен, что это будет полезно для читаемости, ты можешь объявить переменную в начале, а инициализировать в коде, но так же с объявлением типа.
                                    Например:
                                    int foo;
                                    /* ... спустя n строк в той же области видимости...*/
                                    int foo = VALUE_FOR_FOO;
                                    

                                    Конечно, это противоречить пункту о инициализации при объявлении, но каждый сам решает, чему отдавать приоритет.
                                      0
                                      Что-то я не припомню аналога прототипов функций, но для переменных. Это какое то дополнение в стандарте позволяющее два раза объявить переменную?
                                        0
                                        Скорее всего пропущено extern :)
                                          0
                                          не, нельзя так, автор сморозил просто.
                                          будет либо «error: non-extern declaration of 'foo' follows extern declaration»
                                          либо «error: 'extern' variable cannot have an initializer»
                                            +1
                                            Почему же? Вот вполне валидный код:

                                            #include <stdio.h>
                                            
                                            extern int foo;
                                            /* ... спустя n строк в той же области видимости...*/
                                            int foo = 0xDEAFBEEF;
                                            
                                            int main(void) 
                                            {
                                                return printf( "%08X\n", foo );
                                            }
                                      +1

                                      Лучше вообще стараться иметь иммутабельные переменные, везде где можно (слово можно тут надо понимать с разных точек зрения). Это упрощает поведение кода и в итоге его анализ.


                                      А это предполагает автоматически, что инициализация при объявлении, один раз и навсегда. Так что совет-то как раз годный. И для всех языков осмысленный. И именно в java есть смысл так делать почти всегда, добавив еще и final.

                                        0
                                        чтобы при чтении не приходилось «вспоминать» типы увиденных переменных.

                                        IDE же подскажет типы при наведении.
                                          0

                                          Но нужно делать лишние действия с каждой такой переменной

                                        +4
                                        По-моему, каждый программист Си в своем развитии обязательно проходит через этап, когда зуд в каких-то частях тела (то ли мозга, то ли пятой точки) становится нестерпимым и побуждает сесть и написать очередной баян про то, как следует писать программы на Си.

                                        Мне не нравится идея писать макросы-функции капсом. Я рассуждаю логически (логика и программирование — это же стороны одной монеты, не так ли?): макрос-функция скрывает в себе некое законченное действие, т.е. по смыслу неотличим от функции. Так зачем выделять внешним видом его из ряда функций? Только для того, чтобы знать, что это макрос? И что дает это знание? Рано или поздно может возникнуть ситуация, когда макрос на самом деле придется заменить на функцию, и тогда придется править тонны кода, меняя капс-идентификатор на некапс-идентификатор…
                                        С моей т.з. логично и правильно отличать идентификаторы переменных от констант, для чего и служит капс-выделение. Отличать функционально завешенный блок кода, решающий часто повторяющуюся или логически обособленную задачу, оформленный макросом или функцией смысла не вижу.

                                        Что касается других «рекомендаций», то увы и ах, при работе с встраиваемыми системами некоторые «правильные» принципы не работают: «красиво завершиться, напоследок сказав, что именно пошло не по плану» программа попросту не может… Например, популярный нынче квадрокоптер: в случае ошибки «красиво завершиться» — это значит, перед падением на асфальт сделать мертвую петлю? Или осколки разбросать в виде кода ошибки? Во многом количестве встраиваемых систем НЕТ ОС, которая могла бы воспользоваться кодами ошибок. И так или иначе приходится писать программы, умело игнорирующие ошибки, т.е. допускающие деление на ноль без видимых для пользователя последствий.
                                        Хотя сам принцип -да, разумен. Но не абсолютен.

                                        И, таки-да, автор сам себе противоречит, выделяя функции-обертки капсом…
                                          +10
                                          Так зачем выделять внешним видом его из ряда функций?
                                          Явное лучше неявного. Макрос — не функция (например, его нельзя передавать как аргумент в другие функции), и нет никакого смысла обманывать читающего код разработчика, делая вид, что это не так.

                                          Рано или поздно может возникнуть ситуация, когда макрос на самом деле придется заменить на функцию
                                          Не надо писать макросов, которые можно заменить на функции.
                                            0
                                            Функции с результатом void тоже нельзя передавать, как аргумент в другие функции, — и что?
                                            Любая попытка давать ОДНОЗНАЧНЫЕ рекомендации будет ошибочной.
                                            Функции, макросы — это все абстракции, призванные что-то от читающего скрыть, упростить, уменьшить количество анализируемых сущностей. С точки зрения человека их смысл именно в этом. Поэтому сущности с одинаковым смыслом (в человеческом понимании) не стоит явно разделять.
                                            Вот объясните мне логику создателей GCC, которые спокойно сделали два макроса _BV(x) и bit_is_set(a,b) — какой логикой они руководствовались, делая один заглавными, а второй нет?
                                              +3
                                              Функции с результатом void можно передавать. Я имел в виду что-то вроде
                                              void do_things(void) {
                                                  ...
                                              }
                                              
                                              void do_async(void (*what)(void)) {
                                                  ...
                                              }
                                              
                                              ...
                                              
                                              do_async(do_things);
                                              

                                              Макросы (в современном языке) нужны только там, где функций недостаточно. Там, где нужно генерировать названия функций, например. Или как-то ещё расширять синтаксис языка.

                                              Логику создателей GCC я вам объяснить не смогу, я не принадлежу к их числу.
                                                0
                                                Обидно, что мои слова вы поняли буквально.
                                                «Передавать функцию» я, разумеется, имел ввиду «передавать результат функции», а не ее адрес.
                                                • UFO just landed and posted this here
                                                    0
                                                    Вот чесслово, не понимаю: зачем в одном языке делать как в другом. Эдак можно договориться и до упрощённого клингонского, или того круче, «ку» и «кю» :-)
                                                    • UFO just landed and posted this here
                                                        0
                                                        В других языках это понятно зачем. Но ответа на вопрос «зачем это в C» вы так и не дали.
                                                        • UFO just landed and posted this here
                                                            0
                                                            Что именно в ней кажется вам сложным?
                                                            • UFO just landed and posted this here
                                                                +1
                                                                Нет, мне не кажется. Я знаю C, я много пишу на нём, и не понимаю зачем притягивать за уши юзкейсы из других языков и совсем других областей применения.

                                                                Функции могут возвращать void
                                                                Нет, не могут. void-функция в C не возвращает ничего, никакого «возвращаемого значения» у неё нет, и сама мысль о его использовани не должна возникать, by design.

                                                                P.S. Не путать с void *
                                            0
                                            Я не нес в своей статье посыл «Пишите только так и никак иначе» — каждый волен выбирать, какого стиля придерживаться. Те рекомендации по оформлению кода, что я привел — сборка наиболее часто используемых принципов как из проектов компании, где я работаю, так и из opensource проектов.

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

                                            А по поводу оберток — они нужны не в эксплуатации — конечный пользователь не будет смотреть логи работы программы -, а в отладке. Надо же тестерам и разрабам понимать, что и где упало в случае ошибки.
                                              0
                                              Да, кстати, капсом я обертки не выделяю
                                              о_О
                                              Я пишу их с заглавной буквы.
                                              Кто то использует _ перед именем обертки, кто-то — другие обозначения. Тут тоже есть свобода выбора :)
                                              О_о
                                                +2
                                                Если у вас есть сомнения: написать макрос или функцию — пишите функцию, не прогадаете. При соблюдении этого простого правила макрос никогда в жизни не выродится в функцию.
                                                  –5
                                                  Увы, и Ваша рекомендация не панацея. Частенько приходится делать как переход от макроса к функции, так и наоборот. Это же не С++ с его шаблонами… Простой пример
                                                  #define max(x,y) ((x)>(y))?(x):(y)
                                                  
                                                  В Си это не раз выручает при сравнении чисел любых типов, в то время как функций пришлось бы писать гораздо больше и с не такими лаконичными именами
                                                  Или в «малых» встраиваемых системах: работу с портом ввода-вывода выгоднее делать макросом в силу большей скорости исполнения кода…
                                                    +3
                                                    Ага, а потом кто-то пишет
                                                    x = max(a++, b);
                                                    

                                                    И удивляется результатам. Ну или правило о том, что так нельзя делать, пишется в Code Style Guide, который в итоге раздувается.
                                                      0
                                                      Еще один пример того, что абсолютно правильных рекомендаций не бывает.
                                                      +2
                                                      Или в «малых» встраиваемых системах: работу с портом ввода-вывода выгоднее делать макросом в силу большей скорости исполнения кода…
                                                      Всегда пишу такие макросы функции со словом static, это позволяет компилятору самому принимать решение, как оптимальнее — заинлайнить как макрос или сделать функцией. Только что сравнил:
                                                      #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 — все для того чтобы у компилятора были все возможности.
                                                        +2
                                                        Убрав static — получаю гораздо большую программу.)

                                                        Ну так это понятно. У меня тоже давно выработан автопилот: все функции внутри единицы компиляции объявляются как static, если внешнее связывание не требуется явно. И это не зависит от цели сборки.
                                                    +2
                                                    Зуд происходит неспроста. Человек который +- постоянно пишет на си, прекрасно знает, какое там раздолье на стрельбу по конечностям.

                                                    #define cmp(x, y) (x - y)
                                                    

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

                                                    По поводу операционных систем: скоро линукс и его собратья будут в каждом чайнике стоять. И это прекрасно. В mbed есть тоже есть всякие RTOS. Причем ошибки все равно обрабатываются, нередко для этого заводят обработчики по systick.
                                                    +1
                                                    Системщикам неплохо бы помнить, что C является основным языком при программировании встраиваемых систем и узлов IoT на малых микроконтроллерах.
                                                    И там совсем другие подходы.
                                                    Поэтому статьи по C надо начинать с дифференциации.
                                                    Т.е. сразу предупредить, о программировании каких платформ идет речь и о каком организационном подходе к проектированию.

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


                                                      0
                                                      Как я писал в комментариях выше, я не говорю, что нужно писать только так. Это — рекомендации, сделанные на основе самых распространенных практик оформления кода в самых разных проектах.

                                                      А по поводу адаптации к рефакторингу… Это очень сложная тема.
                                                      Когда я только начинал учить Си, меня часто тюкали по голове за то, что я пишу по-своему, игнорируя общепринятые вещи. Тогда же мне довелось участвовать в небольшом проекте, на последней стадии разработки которого одному опытному программисту поручили выполнить рефакторинг моего кода, ибо он не соответствовал кодстайлу проекта. А кодстайл проекта мы взяли такой же, как в ядре Linux.

                                                      Однако, каждый волен сам выбирать свой кодстайл, главное чтоб он не противоречил соглашениям, принятым в проекте:)
                                                        0

                                                        Разрешите поинтересоваться, это какие же подходы используются при разработке для малых микроконтроллеров, что указанные автором подходы не приенимы?

                                                          0
                                                          Не знаю что имел в виду автор, но встречал как «тяп ляп и впродакшн» так и «я художник, я так вижу» (ну с очень индивидуальным подходом в программировании). Такой код write only и стоит его почитать, но ровно с той целью с которой стоит посмотреть картины авангардизма.

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

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

                                                        Редко, когда я, почти, полностью согласен с содержанием статьи. Автору спасибо!

                                                          +1
                                                          потихоньку был вытеснен из десктопа и энтерпрайза

                                                          Да неужели?
                                                          https://habrahabr.ru/company/hh/blog/318450/
                                                          Да, у Java там почти в два раза отрыв по сравнению с С++, но что-то подсказывает, что это не десктоп с энтерпрайзом.
                                                          Я вот сейчас пишу с ПК на котором одно приложение на C# и штук пять на Java. И это не что-то такое особенное, вполне себе мейнстримовая ОС. Остальное, С++ и C. Странно, да?
                                                            0

                                                            И как много современных десктопных приложений написано на чистом С?

                                                              0
                                                              С или С++?
                                                              На С написано мало. Но вытеснил его отнюдь не Java и C#. А С++
                                                                +1

                                                                Просто в статье и написано, что был вытеснен именно C. C++ от C уже совсем далеко ушел, их даже не стоит рядом упоминать.

                                                                  +1
                                                                  В статье:
                                                                  «был вытеснен из десктопа и энтерпрайза такими высокоуровневыми гигантами как Java и C#»
                                                                  А С++ он не был вытеснен. Он был достаточно плавно заменен, причем часто с сохранением кодовой базы.
                                                          • UFO just landed and posted this here
                                                              +2

                                                              Там ещё и ошибка была, вероятно. Если только в make текущий каталог не сбрасывается при переходе к следующей строке блока в Makefile, то make будет вызван в sound/, sound/graphics/, sound/graphics/engine/.

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

                                                                  Я бы, всё же, очень попросил исправить вас этот Makefile к виду с вызовами $(MAKE) вместо make. Иначе это ломает многопоточную сборку.


                                                                  А ещё лучше — совместить это с makefile'ом от Sirikid. :)

                                                                    0

                                                                    поправка: может сломать в некоторых случаях

                                                                  0
                                                                  В make каждая строка выполняется в отдельном сабшелле, поэтому все сбрасывается.

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

                                                                  Альтернативный вариант — исходники раскидать, а объектники свалить в одну кучу, используя VPATH. Но это опять вопрос сборки, не относящийся напрямую к языку.
                                                                  • UFO just landed and posted this here
                                                                      +1
                                                                      С конкретно такими «подпроектами» сильно красиво не получается, поэтому через шелл циклы:

                                                                      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.
                                                                      • UFO just landed and posted this here
                                                                          +1
                                                                          Потому что make умеет из коробки в параллельность и инкрементальные сборки. А мне почему-то регулярно приходится разбирать конструкции вида «тут мы из мэйка вызовем шелл, а в нем мэйк, а там опять шелл, а уже он окончательно последний мэйк».

                                                                          Я бы очень хотел, чтобы описанная выше архитектура была преувеличением. Но нет, это суровая реальность на моем текущем месте работы.
                                                                  +2
                                                                    0

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

                                                                      0
                                                                      Тут не скорость работы make-а, а проблема отслеживания зависимостей. Тк make тупо не знает про вложенные и не может правильно понять, что нужно пересобрать.
                                                                        0
                                                                        Знает, если ему объяснить. В приведенном примере make ничего не объяснили о зависимостях.
                                                                          0
                                                                          А каким образом ему можно сообщить? Простой пример.
                                                                          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?
                                                                            0
                                                                            Он зайдет в libsound, запустит там make, увидит, что делать ничего не надо, выйдет. Да, уйти в sub-make придется, но и все.

                                                                            Есть вариант, который используется в некоторых проектах, поставить libsound.a в зависимость libsound/stamp, на который делается touch в конце сборки libsound.a, а при любой правке в каталоге libsound придется делать touch на этот stamp. Такое имеет смысл, если предполагается, что содержимое libsound будет меняться редко и вообще это сторонний код, которому мы верим.
                                                                              0
                                                                              Если представить проект, где, например, с десяток таких библиотек и они по-разном зависят от каких то хедеров. Если Makefile общий с include вместо вызова вложенного make, тогда есть полный граф зависимостей и будет правильно работать -j например. Сразу будет понятно, что можно распараллелить, а что нет. В случае с раздельными же, нужно либо както этот -j передавать ниже, либо всегда с ним запускать. Так же, подобные внешние вызовы будут своего рода барьерами, мешая make-у работать параллельно иногда и ожидая вложенного завершения. Короче в статье, которую привели выше все хорошо описано и почему не нужно так делать, если хочется нормальной сборки. Минусов у единого Makefile, если это один проект, я не вижу. Некой модульности можно достичить include-ами, если хочется собрать только одну либо, то можно написать make libsound/libsound.a
                                                                              Я просто сталкивался в относительно большом проекте(2М строк), как раз с реккурсивным make и неправильным отслеживанием зависимостей отчасти из-за этого. Сборка была очень медленной(С++ с шаблонами, бустами всякими и подобным шлаком), плохо параллелилась и делала много лишнего на каждый чих, когда не нужно.
                                                                                0
                                                                                -j передается в sub-make автоматически (в отличие от некоторых других флагов), если внешний make понял, что это sub-make. Для этого стоит либо использовать $(MAKE) вместо make, либо (надежнее) ставить + перед соответствующей строкой. На самом деле в этом случае sub-make даже идет параллельно с «внешним», джобы общие и вообще все шикарно работает.

                                                                                На самом деле я всеми руками за один единственный Makefile. Просто его чуть труднее правильно готовить, а многие разработчики, которых я видел, почему-то не хотят научиться это делать как следует.
                                                                                  0
                                                                                  -j передается в sub-make автоматически

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

                                                                                    +1
                                                                                    Как раз наоборот.

                                                                                    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!)
                                                                                    тыц
                                                                  +3
                                                                  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, все более новое, могут и не принять в код. Не на все платформы есть компиляторы, понимающие более новые стандарты Си.
                                                                    +2
                                                                    Не на все платформы есть компиляторы, понимающие более новые стандарты Си.

                                                                    C99 уже 18 лет… Допускаю, что есть такие компиляторы, но примеры можно?
                                                                      +1
                                                                      MS-DOS, МСВС 3.0 (там gcc 2.95.4 и обновлять нельзя), думаю что PDP-11, VAX-11 и так далее… Плюс вагон военных машинок с нестандартной архитектурой…
                                                                        0
                                                                        Под МСВС собирал последний раз году в 2005, неужели они с тех пор так и заморожены? Вот это стабильность :)

                                                                        Впрочем, привычка кодировать ближе к ISO всё равно осталась. Но без некоторых новых вещей уже обходиться не хочется :)
                                                                          0
                                                                          Суть в том, что в МСВС сертифицируется конкретная сборка под конкретную машину. Поставить более новую версию — это заново потратить год (и пару миллионов) на сертификацию. Так что если сертифицирована 3.0 — себе дороже затевать сертификацию 5.0.
                                                                            0
                                                                            Ну да, у нас для сертификации программы считали MD5 исходников/утилит/мейкфайлов etc, потом контрольные суммы того, что получилось после сборки. Плюс-минус байт уже не катит. Но когда это было и какое тогда было железо — с тех пор же многое изменилось. Неужели у нх не возникло желание использовать ОС на чём-то посвежей?
                                                                              +1
                                                                              Новые версии МСВС есть. Но кто будет сертифицировать новую версию на то железо, на которое уже сертифицирована старая? А сертифицируется именно бинарная сборка, если пересобрали из сорцов -уже сертификации нет. Принесли любой бинарнкик — опять сертификация слетела. Провели компиляцию не на МСВС — опять сертификации нет.

                                                                              Из рассказов коллег:
                                                                              — И предоставьте исходный код в распечатанном виде.
                                                                              — Там 600 тысяч строк, будет 15 тысяч листов, вес листа 80 грамм — итого 1200кг. Грузовик дадите?
                                                                              — Ладно, давайте на CD.

                                                                              :-)

                                                                              Ну в общем, если сертификация важнее всего, то остальным можно пожертвовать.
                                                                                +2
                                                                                Вообще-то, лист весит около 5 граммов, получается всего 75 кг.
                                                                                  0
                                                                                  Вы правы. Значит там по размеру получался грузовик
                                                                                    0
                                                                                    Если в одну стопку сложить, то при стандартной плотности (80 г — толщина 0.1 мм) получится около 1.5 метров — тоже никак не гузовик. Может, у вас листов было в 10 раз больше?
                                                                                      0
                                                                                      Не, 1.5 метра — это до распечатки. Далее до такой толщины обратно в условиях офиса не сжать. Прессов-то нет…

                                                                                      Но даже 5 метров — в багажник влезет струдом. Газель нужна.
                                                                                      0

                                                                                      По размеру это 6 коробок размером порядка 0.3х0.3х0.3 м (если спрессовать так плотно, как A4 обычно лежит в пачках). То есть, 30 пачек A4 по 500 листов.

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

                                                                                    А с тех пор и железо другое, и софтины сильно меняются, не sh ведь :) Но хозяин — барин, конечно.
                                                                            0

                                                                            tms320, snes (стандарт не нашел, но выглядит старым), v810 и даже x86

                                                                            0
                                                                            Посмотрите вот этот мой комментарий, и комментарий выше.
                                                                            Понял ошибку, исправился :)
                                                                              0
                                                                              Ну у нас Linux, FreeBSD, MS-DOS, Windows, QNX. Из компиляторов — gcc, lcc, clang, BC++, BCB, VC++… В итоге ровно к такому же решению и пришли. И именно потому, что не раз налетали.
                                                                            +2
                                                                            В использовании оберток есть небольшой минус, который, если захотеть, можно решить костылем. А что это за минус — можете предположить в комментариях :)

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

                                                                            А «небольшой» минус — это то, что как закроются сокеты — зависит от библиотеки. Если успели сделать connect по TCP/IP — сокет скорее всего закроется жестко и вторая сторона не будет в курсе, что мы уже отвалились. Так что лучше использовать atexit.
                                                                              0
                                                                              Да, все абсолютно верно.
                                                                              Пометил себе, как появится заряд — с меня плюс в карму :)
                                                                              +4
                                                                              Храните заголовочники в директории include.

                                                                              А не могли бы вы объяснить, какой смысл делать отдельную директорию include? Я в этом вижу только минусы:
                                                                              • нужно прописывать еще один путь в настройках проекта
                                                                              • дерево проекта увеличивается вдвое, ведь внутри папки include приходится повторять всю структуру папок с файлами .c
                                                                              • в файле xx.c нельзя просто написать include «xx.h», приходится писать полный путь до него — #include «yyy/zzz/xx.h»
                                                                              • чтобы скопировать какой-то «модуль», вам приходится копировать два файла из разных мест

                                                                              Гораздо удобнее, на мой взгляд, группировать файлы в папке по принципу «модулей». Допустим, модуль Common — это отдельная папка, в ней common.c и common.h. Этот модуль приобретает «позиционную независимость», т.е. когда вы его копируете в другой проект, вам не надо переписывать инклуд в файле common.c. Ну и все минусы, описанные выше, пропадают.
                                                                                +1
                                                                                Возможно, я выбрал не очень удачный пример в статье. Да, группировка — очень хорошая практика. И она прекрасно дополняет метод с include'ом.

                                                                                Я уже писал — ко всему надо подходить с умом. Заголовки общего пользования, библиотечные заголовки, которые должны быть доступны во всем проекте. include — для них самое подходящее место.

                                                                                Возьмем тот же busybox. В исходниках все группируется именно так, как вы сказали. Каждая утилита в отдельной папке. И .c, и локальные заголовки. Но тем не менее, директория include там тоже есть, и все библиотечные хэдеры живут там:)

                                                                                Самый популярный файл во всем busybox — обитающий там заголовочник с интригующим названием «Libbb.h»

                                                                                По поводу полного пути — тут вы не совсем правы. Когда мы указываем директорию include, то поиск заголовочников при линковке будет происходить в ней. Так что можно обойтись указанием имени файла в кавычках :)
                                                                                  0
                                                                                  По поводу полного пути — тут вы не совсем правы

                                                                                  Я просто подумал, что вы предлагаете в папке include дублировать всю структуру каталогов для исходников, а не держать там только глобальные заголовочники. Если в папку include не совать вообще все заголовочные файлы, а только глобальные, то никаких возражений у меня нет.
                                                                                  0
                                                                                  В папку include нужно класть только внешние заголовки библиотеки. Все внутренние заголовки удобнее держать рядом с компилируемыми файлами (.c), тут вы правы. Делается это для того, чтобы избежать роста сложности в настройках проектов использующих ваш модуль. Представьте, что в ваших внешних includa-x есть ссылки на include-ы сторонних библиотек, которые вы используете. В таком случае, пользователь вашей библиотеки просто не сможет использовать ее без настойки правильных путей к внешним библиотекам.
                                                                                  Например, если есть module1 использующий module2 который, в свою очередь использует module3, а во внешних include-ах всех модулей присутствуют включения друг-друга, то module1 не получится использовать без include-ов модулей 2 и 3, хотя на самом деле достаточно только .lib файлов.
                                                                                  0

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


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


                                                                                  Не совсем понятно почему возникает необходимость использовать volatile переменные как в примере. Как минимум это усложняет код и добавляет магии в процесс разработки. Можно больше примеров когда такая магия оправдана?

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


                                                                                    Там явно написано почему, в первом же предложении. Во втором — более развёрнуто.
                                                                                      0
                                                                                      Скажу снова: по поводу оформления кода — это не «абсолютные истины», а рекомендации. Практика, которая встречается чаще всего. Кодстайл всегда зависит от конкретного проекта.

                                                                                      По поводу заголовочников стоит написать отдельную статью. В идеале, все заголовочники должны быть оформлены так, чтобы из них можно было понять как можно больше интерфейсов взаимодействия внутри программы.
                                                                                      + надо понимать, что ничего не стоит делать бездумно. В том же busybox заголовочники бывают 2х типов: общего пользования и локальные. Общего пользования лежат в include в корне, а локальные — в папках с утилитами (подпроектами).
                                                                                      Очень правильный комментарий вот тут, группировка — очень хорошая практика. Однако в системе вынос в инклуд я встречаю гораздо чаще.

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

                                                                                      Тогда любое изменение этих переменных, которое будет сделано из другого, будет неявным для компилятора. Отсутствие volatile в этом случае может положить весь механизм IPC в приложении.
                                                                                        0

                                                                                        Каюсь, про многопоточность не подумал.

                                                                                          0

                                                                                          И не только, может и адрес из mmio-диапазона, как вариант. Можно, конечно, сказать, что это частный случай многопоточности, но потоков на том уровне может и не быть.

                                                                                          +1
                                                                                          Volatiole надо использовать, когда переменные могут быть изменены неявно. Например, если у нас многопоточное приложение, в котором потоки имеют общий доступ к каким-либо переменным.
                                                                                          Насчет C я не знаю, но нас ведь могут читать люди, пишущие на разных языках.

                                                                                          В C++ использование volatile в многопоточной среде — это очень вредный совет! Он просто не имеет свойств, которые могут быть нужны в многопоточной среде — например, атомарности изменений. Подробное обсуждение есть на stackoverflow
                                                                                            +2
                                                                                            могут читать люди, пишущие на разных языках

                                                                                            В C++

                                                                                            Настоящий фидошник Subject не читает?
                                                                                              +2
                                                                                              Вам смешно, а начинающий разработчик прочитает совет, подумает «ну C++ — это почти тот же C», и будет бездумно применять volatile там, где не надо. Навстречался я уже с такими, так что всегда не лишним будет предупредить лишний раз.
                                                                                              +1
                                                                                              Вы не правы. Модификатор volatile не имеет к многопоточности никакого отношения. Это абсолютно перпендикулярные вещи. В общем случае, кроме чтения/записи байта, вы не сможете атомарно менять переменную, не используя специальные функции (примитивы) предназначенные для этого. volatile никак не влияет на барьеры памяти в С, в отличие, например, от Java-ы.
                                                                                                0

                                                                                                Ещё забавно, как сочетается volatile и переупорядочивание строк компилятором. Тут можно придумать что-то кроме вставки на асме или специфичных для процессора инструкций?

                                                                                                  0
                                                                                                  Вы веткой не ошиблись?
                                                                                                  Если нет, то, видимо, это я выразился неясно. Собственно, я то и имел в виду, что использовать volatile для доступа к разделяемой памяти без какой-либо внешней синхронизации небезопасно по большому количеству различных причин, и нужно применять существующие примитивы.
                                                                                                    +1
                                                                                                    Да, извините. Я отвечал Indever2. Не пойму как так вышло.
                                                                                                      0
                                                                                                      Я прочитал ваш комментарий.
                                                                                                      Да, я не прав. Многопоточное приложение тут не при чем, компилятор в состоянии отслеживать изменения переменных в потоках.
                                                                                                      А вот когда несколько процессов могут менять одну переменную — другое дело :)
                                                                                                      Когда речь идет о изменении переменной извне процесса, то тогда оно будет неявным для компилятора.
                                                                                                        +2
                                                                                                        Да. Но если совсем точно, то компилятор ничего не знает про потоки, абсолютно. Не знаю как в C, а в C++ только-только стали появляться TLS в стандарте, а вся работа с многопоточностью реализована в библиотеке. В любом случае на месте обращения к разделяемой потоками переменной будет нетривиальная функция или intrinsinc и компилятор нечего не выкинет. С volatile все по старинке.
                                                                                                          +1
                                                                                                          del. Ниже комментарий более подробный: https://habrahabr.ru/post/325678/#comment_10158130
                                                                                                  +1
                                                                                                  Вы неправы. Точнее не совсем правы. Компилятор в состоянии отследить изменение в разных потоках.
                                                                                                  volatiole нужен в том случае если переменная изменяется снаружи.
                                                                                                  Например у нас есть обработчик прерываний. И его вызова нету в тексте программы, так-как его вызывает(передает управление на него) процессор/контроллер в произвольный момент времени.
                                                                                                  Или например у нас есть переменная которая связанна с некоторым физическим адресом(регистром периферии) в SoC.
                                                                                                  И нам нужно записать а потом сразу считать значение так-как этот регистр обрабатывается особым образом.
                                                                                                  И если мы напишем что-то типа

                                                                                                  reg = 0x10;
                                                                                                  if(reg == 1)
                                                                                                  {
                                                                                                  do_somthing;
                                                                                                  }
                                                                                                  

                                                                                                  и reg будет без volatile то копилятор вправе выкинуть этот код.
                                                                                                  Или например мы пишем вот так
                                                                                                  reg = 0x10;
                                                                                                  reg = 0x11;
                                                                                                  ..
                                                                                                  reg = 0x35;
                                                                                                  

                                                                                                  Без volatile опять компилятор выкинет предыдущие операции и оставит только последнюю.
                                                                                                0
                                                                                                del
                                                                                                  –1
                                                                                                  … а слова в названиях отделяются нижним подчеркиванием

                                                                                                  А какие еще бывают подчеркивания?

                                                                                                    0
                                                                                                    Никакие :)
                                                                                                    Отчего-то все называют этот символ именно так :)
                                                                                                    Изначально хотел написать «нижний дефис», но меня раскритиковали.
                                                                                                    0
                                                                                                    У меня вызывает внутреннее противление приведённая в статье реализация функции
                                                                                                    static int CheckModemConnection()
                                                                                                    
                                                                                                    На первый взгляд, всё выглядит понятным. На это и рассчитано.
                                                                                                    Возвращает 1, если параметры, связанные с модемным соединением изменились, и 0, если нет.
                                                                                                    Насколько я помню, это традиционно для Си возвращать именно такие значения, чтобы в месте вызова можно было бы реализовать обработку для случая произошедших изменений. Было бы логичнее иметь в качестве кода возврата bool, но это же Си! (NB: надо свериться с последним стандартом!)

                                                                                                    Далее, название функции: CheckModemConnection. А если придётся проверять ещё чего-нибудь, то надо будет писать отдельную функцию для вот этого самого чего-нибудь? Мне бы ужасно захотелось бы обобщить и иметь общую для всех функцию проверки. Даже, если в Си нет классов. (Всегда можно ввести.) К тому же, для того, чтобы иметь возможность проверять состояние, неплохо бы как-то формализовать сие понятие и сделать так, чтобы, например, пробегать по списку необходимых свойств в цикле, а не создавать длинное условие для оператора if.

                                                                                                    В конце-концов, было бы крайне любопытно (и эффективно?) иметь текстовую строку, описывающую текущее состояние объекта, каждый символ которой связан с некоторым элементом описания модемного соединения. Делая простой проход по этой строке, можно было бы сразу получать ответ на нужный вопрос. А, если само символьное представление делать более сложным (XML?), то можно было бы автоматизировать выдачу диагностических сообщений (например, соединяя друг с другом строки состояния для различных элементов в единую строку).
                                                                                                      0
                                                                                                      Было бы логичнее иметь в качестве кода возврата bool

                                                                                                      Для C, скорее, традиционней ERROR_SUCCESS (0 в общем случае) или код ошибки (!= 0), или что изменилось, etc.

                                                                                                      NB: надо свериться с последним стандартом!

                                                                                                      C99 — stdbool.h

                                                                                                        0
                                                                                                        Если бы надо было проверить что-то еще — да, мы бы написали отдельную функцию.
                                                                                                        И она бы вызывалась вместе со всеми проверками. CheckModemConnection() — одна из сотен функций, которые вызываются при обработке пришедшей конфигурации. То есть все — и сеть, и voip, и iptv имеет подобные проверки.
                                                                                                        Нужно это, чтобы группировать параметры, и, когда надо добавить новый, знать, где находится проверка группы связанных параметров.

                                                                                                        Конфигурация приходит в виде дерева, по конкретным нодам в цикле не пройтись. Разве что, можно создать массив/перечисление с указателями на параметры, и идти по нему… Но это не слишком целесообразно, ибо будет гораздо сложнее найти источник и сразу не скажешь (не смотря на массив), какие параметры подвергаются проверке.
                                                                                                          0
                                                                                                          Сказанное Вами побуждает меня попросить Вас сообщить новые подробности. Всё это воспринимается как задачка, при решении которой можно было бы проверить в деле много разных «гитик».
                                                                                                            0
                                                                                                            Эта функция — одна из рядовых проверок в одном крупном проекте.
                                                                                                            Так как пункт про комменты самый первый и ориентирован больше для новичков, то я просто взял эту функцию (в которой изначально комментариев не было), и для наглядности придумал пару поясняющих комментариев.
                                                                                                          0

                                                                                                          "Традиционно" для Си функции действия add, get, write_ и т. д. должны возвращать отрицательный код ошибки, 0 или положительное значение в случае успеха. А функции предиката аналог bool — 0 и 1.


                                                                                                          В данном примере CheckModemConnection() из названия должна возвращать 0 — всё ок или отрицательный код ошибки. Поэтому лучше её было бы назвать как-то так: isModemConnectionChanged(), тогда сразу бы было ясно, что она "Возвращает 1, если параметры, связанные с модемным соединением изменились, и 0, если нет.".


                                                                                                          Слово традиционно я намеренно заключил в кавычки, т.к. если говорить о традициях, то язык Си неразрывно связан с историей Unix. Поэтому приверженцам традиций лучше следовать Linux kernel code style — пункт 16, либо KNF.

                                                                                                          +3
                                                                                                          Костыль, созданный с одной целью — вставить его, как палку, в колеса тому, кто будет расширять этот код.
                                                                                                          Вернее, не так.
                                                                                                          Это — костыль, который ясно вам скажет: не надо повторять мою реализацию, хочешь пакет больше — указатели, динамическая память и динамическое вычисление размера тебе в помощь!
                                                                                                          Это — пример того, как делать не стоит. Никогда. Иначе случится насилие. Рано или поздно.

                                                                                                          Вы не поняли, и делаете неправильные выводы.
                                                                                                          Это проверка, что компилятор корректно упаковал структуру с нужным размером.
                                                                                                          См. 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

                                                                                                            0
                                                                                                            Спасибо за проделанную работу по поиску информации по теме.

                                                                                                            Да, вы абсолютно правы, я не правильно понял замысел и интерфейсы bb.
                                                                                                            Извиняюсь.
                                                                                                            Сейчас отредактирую статью.
                                                                                                            +3
                                                                                                            Объявляйте переменные в начале функции.

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

                                                                                                            Даже 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.


                                                                                                              0
                                                                                                              Вы можете обьяснить зачем это делать?
                                                                                                              Чтобы код компилировался на старом компиляторе, например.
                                                                                                              Или если вы по какой-то причине считаете, что именно в этой функции вам важнее уметь находить все переменные функции за один раз, чем удобство просмотра инициализации какой-нибудь одной.

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

                                                                                                                  Жизнь очень многогранна, то, что для одного — укуренный бред, для другого — реальная ситуация.
                                                                                                                –1
                                                                                                                Как минимум для совместимости с ANSI / ISO стандартами.

                                                                                                                Даже Google думает не так

                                                                                                                И о другом языке.

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

                                                                                                                https://habrahabr.ru/post/325678/#comment_10157022
                                                                                                                  –1
                                                                                                                  Если мы говорим о совместимости, то стоит добавить советы типа «использовать битовый сдвиг вместо умножения»
                                                                                                                    0
                                                                                                                    Для современных (и даже не очень) компиляторов совет абсолютно бесполезный — они сами разберутся как оптимизировать арифметику. А к совместимости он вообще не имеет никакого отношения.
                                                                                                                  +2
                                                                                                                  Сюда же в правило обычно добавляються переменные в `for loop`-ах. От этого горит еще больше.
                                                                                                                    0
                                                                                                                    Это плюсовая привычка, многим сишникам просто непонятная :)
                                                                                                                    0

                                                                                                                    ИМХО такое объявление добавляет читабельности: сразу прочитав список переменных, уже примерно понимаешь, с чем имеешь дело и что делает код.

                                                                                                                      0
                                                                                                                      Ну как читабельности… Если функцию можно разделить на логические части, а для каждой части свои переменные. То читабельнее будет объявлять переменные хотя-бы перед каждой частью, но уж точно не в начале функции.
                                                                                                                    0
                                                                                                                    Отличное руководство. Как раз для таких начинающих как я. Спасибо большое.
                                                                                                                      +1
                                                                                                                      По поводу инициализации переменных не соглашусь. Если переменные не инициализировать, компилятор будет ругаться, если она была использована без инициализации. Несколько раз это серьёзно выручало при написании конечных автоматов на case-ах.

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

                                                                                                                      А ещё, начинающим сишникам я бы порекомендовал поизучать код nginx, отличный код, интересные решения.
                                                                                                                        0
                                                                                                                        Плюсик за nginx.
                                                                                                                        Очень изящная вещь.
                                                                                                                          0
                                                                                                                          Да, я бы порекомендовал вместо инициализации переменных включить это (и не только) в сообщения об ошибках и реагировать на них!
                                                                                                                          +2
                                                                                                                          Если вы постоянно работаете с трекерами (вроде RedMine), то при внесении правок в код можно указать номер задачи, в рамках которой эти правки были внесены. Если у кого-то при просмотре кода возникнет вопрос а-ля «Зачем тут этот функционал?», ему не придется далеко ходить. В нашей компании еще пишут фамилию программиста, чтобы если что знать, к кому идти с расспросами.
                                                                                                                          /* Muraviyov: #66770 */


                                                                                                                          Плохой совет. Плохой он оттого, что комментарии никто не поддерживает.
                                                                                                                          Мировой опыт (выраженный в книжках МакКоннелла, Р. Мартина и пр.), а равно и мой скромный, говорит, что комментарии редко бывают актуальными, тем более, такие.
                                                                                                                          «Кто, когда и почему?» — на эти вопросы достоверно отвечает система контроля версий (должна отвечать… у вас-то какая?).
                                                                                                                            –1
                                                                                                                            Когда мы делаем git blame в каком-нибудь огромном файле, в который каммитили все, кому не лень, найти изменения по какой-то конкретной фигне может быть довольно долго.
                                                                                                                            + даже зная, кто и каким каммитом добавил функционал, не всегда можно понять, в рамках какой задачи это было сделано. (не все и не везде указывают).
                                                                                                                            Да и в любом случае, просто вбить заранее указанный номер задачи в трекер будет в разы быстрее, чем сначала искать «кто, где и зачем».

                                                                                                                            По поводу того, что комментарии не поддерживают — не соглашусь.
                                                                                                                            Я бы посмотрел на того, кто бы взялся сопровождать тот же bb, не будь он так хорошо задокументирован)
                                                                                                                            Если комментарии не нужны, то почему так много рекомендаций из самой различной литературы и интернета отмечают умение грамотно писать комментарии одним из правил хорошего тона?
                                                                                                                              +1
                                                                                                                              Когда мы делаем git blame в каком-нибудь огромном файле

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

                                                                                                                              это ничем не отличается от наличия/отсутствия (обсуждаемого нами) коммента

                                                                                                                              это вопрос дисциплины и гайдов оформления текста коммита, только вот в отличие от отсутствия комментария с причинами «забыл/лень/не хочу/не знаю» текст коммита легко валидируется хуками, что способствует гайдам

                                                                                                                              просто вбить заранее указанный номер задачи в трекер будет в разы быстрее

                                                                                                                              с «быстрее вбить» спорить даже не собираюсь (я сам люблю one-click переходы)
                                                                                                                              но Вы забываете про достоверность

                                                                                                                              Я бы посмотрел на того, кто бы взялся сопровождать тот же bb, не будь он так хорошо задокументирован)

                                                                                                                              я не знаю, что такое bb
                                                                                                                              но с высоты своего опыта поддержки многолетних активно меняющихся legacy-продуктов могу утверждать, что такие комментарии бессмысленны чуть менее, чем совсем

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

                                                                                                                              я выделил ключевое слово ;)
                                                                                                                                +2
                                                                                                                                Соглашусь с Zapped. Очень уж часто у моей практике такие комментарии протухали. Или трекер менялся, и получалось как на bash:
                                                                                                                                комментарий перед злобной реализацией некого алгоритма на несколько страниц: «описание алгоритма смотри в тетрадке у Чуня».
                                                                                                                              0
                                                                                                                              del (не туда)

                                                                                                                              Only users with full accounts can post comments. Log in, please.