Я по жизни самоучка: если не считать FORTRAN и PL/1, которым меня «учили» в ВУЗе (надеюсь, понятно, как давно это было?), основную часть своих знаний по программированию я получил исключительно методом самообразования. Начинал, как водится, с Pascal и логичным его продолжением Delphi. По мере изменения приоритетов осваивал ассемблер 86-го семейства (MS DOS), а затем, по мере увлечения микроконтроллерами освоил ASM51 и AVR Assembler. Все это давалось мне достаточно просто, т.к. все перечисленное поддается весьма простому и, главное, четко структурированному логичному описанию — я имею ввиду синтаксис и принципы языка.

Закономерно неизбежным был и следующий шаг — переход на Си (подчеркну — в моем случае речь именно о программировании микроконтроллеров), и тут, как говорится, процесс застопорился. Синтаксис Си — та еще штучка, книги по этому языку — отдельная песня. Даже у классиков K&R с первой же главы на неокрепший мозг начинающего обрушивается «работающий helloword», при том что ни единого слова о синтаксисе, об операторах и т.п. сказано не было! Когда я начал изучать стандарт языка Си, у меня возникали суицидально-депрессивные состояния, ибо ранее устаканившееся в мозгу понятие стандарта, как четкого регламента, было разрушено почти полностью!

И поэтому я сам для себя подготовил некоторые «правила приведения к логически непротиворечивым определениям», которые позволили мне более-менее спокойно освоить язык Си и даже иногда (!) поучать других начинающих.
Мне кажется, что начинающим осваивать Си не помешает узнать хотя бы об одном таком «правиле».

Старался использовать имеющееся чувство юмора — рекомендую при чтении его не отключать.

Для начала речь пойдет о, казалось бы, самом простом: типах данных и описании переменных этих типов.
Все знают, что описание int var; описывает переменную var типа int, соответственно расширенное описание int var1, var2, var3; описывает уже три переменных одинакового типа. Здравый смысл и жизненный опыт подсказывают, что описание переменной или нескольких переменных состоит из двух частей: на первом месте стоит идентификатор типа (int), а затем, через пробел, один или более (списком через запятую) идентификаторов переменных.
Все так, и пока все понятно. Но Си позволяет нам добавлять к описанию переменных еще и некоторые префиксы, например, const, означающий, что переменная не должна менять свое значение. Кстати, есть и другие префиксы, например, auto, но не о них сейчас речь. Причем как это ни удивительно, префикс может стоять как перед типом, так и после него:
const int var1;
и
int const var1;
полностью идентичные записи.

Не знаю, как у вас, но мой мозг начинает недовольно ворочать извилинами, т.к. не выходит составить четкое правило описания переменных, как было сказано ранее (см. выделенный текст выше). Получается что-то типа: описание переменной заключается в том, что перед ее идентификатором или списком идентифкаторов разделенные пробелами могут находиться идентификаторы типа и/или префикс, указывающий на особенности применения и использования переменной. Обратите внимание, что я написал могут находиться и использовал союз или – это на самом деле так: язык Си допускает отсутствие идентификатора типа переменной, в этом случае тип по умолчанию считается int. Т.е. допустима и такая запись: const var;

Пробуем плавно перейти к указателям.
const int *var; — указатель на переменную, которая не может менять значение.
int const *var; — указатель, который не может менять свое значение, на переменную int, которая может…

Ваш мозг еще не взорвался? Мой уцелел только благодаря охлаждающему компрессу: только что казалось, что тип и префикс могут располагаться друг относительно друга в произвольном порядке, а теперь выясняется, что порядок существенно меняет смысл описания! Да еще и правильное прочтение строки int const *var; не слева направо, не справа налево, а какой-то спиралью: начинаем с середины (указатель, т.е. *), потом идем налево (const, т.е. указатель не меняющий значение), потом направо (вспоминаем идентификатор переменной var), а потом снова налево (тип переменой int). Бррр…

А теперь попробуйте описать без ошибок указатель, не меняющий свое значение, на указатель, который может меняться, указывающий на неизменяемую переменную типа int… Каково?! Думаю, на этом этапе немало начинающих бросали в стенку книжку по Си и шли пить пиво.

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

Я, конечно, понимаю, что Си – инструмент широких возможностей, и, наверное, каждая из возможностей и каждый из нюансов служат для достижения каких-то по-настоящему важных целей, но ведь не каждый должен быть гуру! Как же быть обычному человеку, для которого программирование – хобби?

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

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

На практике это выглядит так.
const int var; — для описания приходится использовать 2 идентификатора, т.е. наш случай. Вводим новый тип для этого:
typedef const int const_int;
B определение нашей переменной превращается в идеальное
const_int var;
строго по нашему правилу.

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

Если мы хотим определить указатель на этот новый тип, можем записать так
const_int *var; и это будет так же просто и однозначно, но можем и ввести новый тип:
typedef const_int* const_int_ptr;
и получить «идеальную» запись
const_int_ptr var;

Теперь вспомним о том описании, которое вводило в ступор ранее: указатель, не меняющий свое значение, на указатель, который может меняться, указывающий на неизменяемую переменную типа int. С учетом только что сделанного получится так:
typedef const int const_int; // неизменяемая переменная int
typedef const_int* const_int_ptr; // указатель (изменяемый) на неизменяемую переменную int
typedef const const_int_ptr* const_ptr; // неизменяемый указатель на предыдущий указатель
тут уже можно остановиться, но можно и продолжить:
const_ptr var; // а это уже конечное описание переменной

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

К чему я призываю? Да вот к чему: коллеги-программисты и те, кто хочет ими стать! Будьте проще, не кичитесь своими знаниями глубин Си и не погружайтесь в эти глубины без необходимости, ограничивайтесь самыми простыми (как в записи, так и в восприятии) конструкциями для достижения своих целей! И тогда люди к вам потянутся!

Комментарии (36)


  1. sim-dev
    12.08.2015 12:13

    Уже после нажатия «опубликовать» увидел кое-какие ошибки — исправил.


    1. sim-dev
      12.08.2015 14:31

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


  1. a553
    12.08.2015 12:54
    +12

    Во-первых, вы ошиблись:

    typedef const const_int_ptr* const_ptr; // неизменяемый указатель на предыдущий указатель
    Это изменяемый указатель на неизменяемый указатель на неизменяемый int.

    Во-вторых, в корне не согласен. Вы добавили ненужный слой абстракции над нотацией типа переменной. Каждому программисту, который прочитает ваш код, придётся распарсивать вашу собственную нотацию вида const_ptr или const_int_ptr. Заметьте, что const и ptr у вас в обоих случаях значат совершенно разные вещи: в первом случае у вас константный указатель на указатель, а во втором случае — указатель на константный int.

    Если не брать объявления указателей на функции (их, как раз, лучше выносить в typedef), Си имеет прекрасную нотацию. Главное её использовать правильно; например, писать const справа:

    int const * const * const ptr;
    

    Читайте справа налево: ptr — это константный указатель на константный указатель на константный int.

    Ничего сложного.


  1. encyclopedist
    12.08.2015 12:56
    +11

    Ваша статья не имеет смысла, потому что вы ошиблись в самом начале:

    const int *var; — указатель на переменную, которая не может менять значение.
    int const *var; — указатель, который не может менять свое значение, на переменную int, которая может…
    — эти 2 объявления означают абсолютно одно и то же. Чтобы получить неконстантный указатель на константные данные, нужно написать:

    int * const z;
    

    Все на самом деле логично: то, что находится до символа *, относится к тому на что указываем, а то что после * — к самому указателю. Демо


    1. encyclopedist
      12.08.2015 13:02
      +4

      Ох, вы и меня запутали. z в моём комментарии — это конечно же константный указатель на неконстантные данные.


  1. ServPonomarev
    12.08.2015 13:04
    -11

    К чему я призываю? Да вот к чему: коллеги-программисты и те, кто хочет ими стать! Будьте проще, не кичитесь своими знаниями глубин Си


    Просто не используйте костыль «const» всуе а только там, где он действительно необходим.


    1. poxu
      12.08.2015 13:21
      +13

      А я бы сказал так. Убирайте «const» только тогда, когда это необходимо. А вообще суйте его везде, где только можно.


    1. VioletGiraffe
      12.08.2015 21:10
      +3

      Как сказал когда-то мой друг: «Лучше переконстить, чем недоконстить» :)


  1. potan
    12.08.2015 13:30
    +1

    Полюбить очень просто. Есть такой эффект — «Стокгольмский синдром» называется. Очень помогает в подобных случаях.


    1. kolu4iy
      12.08.2015 14:27

      Вот согласен на 100%. После некоторого времени с Си привыкаешь к указателям и прочим радостям. Потом берешь язык уровня повыше (С++ не берём, он другой и инопланетный) — и тут-то и понимаешь как тебе не хватает тех примитивных конструкций, которые можно было сложить в три этажа и обернуть скобками для лучшей читаемости, а не для компилятора…


      1. The_Floyd
        12.08.2015 15:19
        +3

        Очень сомневаюсь, что нечто трехэтажное обернутое в скобки улучшит читаемость.


  1. saboteur_kiev
    12.08.2015 16:06

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


    1. a553
      12.08.2015 16:13
      +1

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


      1. a553
        12.08.2015 16:22
        +1

        Насчёт С++ — я имею ввиду, что в С++ можно выражать типы с указателями каким-то таким образом:

        ptr<const ptr<volatile ptr<const int> > >
        
        Что в сложных случаях будет читабельнее, чем Си вариант. Или с привкусом D:
        ptr ! const ptr ! volatile ptr ! const int
        


      1. FoxCanFly
        12.08.2015 17:48

        В C++ вообще не нужны многоэтажные объявления указателей — там есть инкапсуляция. Если проще —

        const std::string& var
        вместо
        const char* const* var
        — char* скрыт в объекте std::string. Да еще и ссылка вместо указателя.


        1. a553
          12.08.2015 17:54
          +1

          Это-то всё фигня. Самое сложное — это функции, и там будет либо многоэтажные объявления, либо куча typedef-ов.


          1. The_Floyd
            12.08.2015 18:05
            +1

            Откройте для себя std::function и не будет вам никаких многоэтажных объявлений с кучей тайпдефов.


            1. a553
              12.08.2015 18:07
              +1

              Тот же самый typedef, вид сбоку.


              1. The_Floyd
                12.08.2015 18:26

                Как Вы в таком случае себе представляете сферическое объявление указателя на функцию в вакууме?



              1. FoxCanFly
                12.08.2015 18:27

                Приведите пример сложного и непонятного объявления std::function


              1. FoxCanFly
                12.08.2015 18:34

                Даже в сложных случаях типа

                std::function< std::function<int (int)> (std::function<int (int, int)>) >
                

                — «функция, принимающая функцию, принимающую два аргумента типа int и возвращающую int, возвращающая функцию, принимающую int и возвращающую int» — все понятно, читабельно и логично. А как будет выглядеть это на Сишных указателях? Мне например и пробовать сейчас написать это страшно.


                1. a553
                  12.08.2015 18:51
                  +1

                  «сферическое объявление указателя на функцию в вакууме»:

                  void(*(*(*(*f())(void(*)()))(void(*)()))(void(*)()))();
                  Разбавить квалификаторами по вкусу.

                  Выделим типы:
                  typedef void(*g0)();
                  typedef g0(*g1)(g0);
                  typedef g1(*g2)(g0);
                  typedef g2(*g3)(g0);
                  
                  g3 g();
                  Теперь читаемо.

                  Если пофантазировать и сказать, что у нас весь код написан с использованием std::function, то код выглядит вот так:
                  std::function<
                  	std::function<
                  		std::function<
                  			std::function<
                  				void()
                  			>(std::function<void()>)
                  		>(std::function<void()>)
                  	>(std::function<void()>)
                  > h();
                  
                  Я бы сказал, что никаких плюсов перед typedef в плане выразительности std::function не представляет. Наоборот, я мало того, что указываю, что тип — функция, я ещё и указываю сигнатуру функции рядом используя Си нотацию. Многословненько.


                  1. FoxCanFly
                    12.08.2015 18:55
                    +1

                    В вашем примере для читающего код сигнатуры скрыты за кучей typedef, и без их разбора ему вообще не понятно, что таки туда передавать. Если std::function кажется слишком длинно, можете с помощью using хоть fn ее называть, и будет

                    fn< fn<int (int)>  ( fn<int (int, int)> ) >


                    1. a553
                      12.08.2015 18:57
                      +1

                      Дело не в том, что длинно. Дело в том, что используя std::function я пишу то, что я бы написал, используя typedef или инлайн-нотацию, ПЛЮС описываю непосредственно шаблон.


                      1. FoxCanFly
                        12.08.2015 19:01

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


                        1. a553
                          12.08.2015 19:04
                          +1

                          Видимо, вы просто фанат std::function. :) На мой взгляд, вариант с std::function, в котором почти в два раза больше скобочек и ровно в два раза больше упоминаний функций, выглядит достаточно уродливо. Я бы использовал typedef.


                          1. encyclopedist
                            12.08.2015 19:09
                            +3

                            Ещё лучше использовать using:

                            using func_t = int(int*);
                            using func2_t = float(func1_t);
                            


                            1. a553
                              12.08.2015 19:14
                              +1

                              Согласен, да.


            1. encyclopedist
              12.08.2015 18:38
              +1

              Стоит признать, что std::function это не равнозначная замена, у нее есть рантайм издержки.


              1. FoxCanFly
                12.08.2015 18:48

                Она не хранит в себе ничего кроме указателя, и издержки такие же. А clang их даже инлайнить умеет.


                1. encyclopedist
                  12.08.2015 19:07

                  Она type-erased. Хранит указатель на базовый класс с виртуальным operator(). Поэтому один лишний косвенный вызов нужен. (Это сделано, чтобы можно было хранить любые callable: помимо указателей на функции, это может быть например указатель на метод или функциональный объект)


                  1. FoxCanFly
                    12.08.2015 19:11

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


                    1. encyclopedist
                      12.08.2015 19:16

                      Я знаю. Но всегда гарантировать это невозможно.


        1. Gorthauer87
          12.08.2015 19:02

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


          1. FoxCanFly
            12.08.2015 19:06

            Это пример, а не руководство к полной замене во всех случаях. Во всех случаях при помощи структур и объектов можно избежать использования конструкций «указатель на указатель на...»