Закономерно неизбежным был и следующий шаг — переход на Си (подчеркну — в моем случае речь именно о программировании микроконтроллеров), и тут, как говорится, процесс застопорился. Синтаксис Си — та еще штучка, книги по этому языку — отдельная песня. Даже у классиков 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)
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
.
Ничего сложного.
encyclopedist
12.08.2015 12:56+11Ваша статья не имеет смысла, потому что вы ошиблись в самом начале:
const int *var; — указатель на переменную, которая не может менять значение.
— эти 2 объявления означают абсолютно одно и то же. Чтобы получить неконстантный указатель на константные данные, нужно написать:
int const *var; — указатель, который не может менять свое значение, на переменную int, которая может…
int * const z;
Все на самом деле логично: то, что находится до символа*
, относится к тому на что указываем, а то что после*
— к самому указателю. Демоencyclopedist
12.08.2015 13:02+4Ох, вы и меня запутали. z в моём комментарии — это конечно же константный указатель на неконстантные данные.
ServPonomarev
12.08.2015 13:04-11К чему я призываю? Да вот к чему: коллеги-программисты и те, кто хочет ими стать! Будьте проще, не кичитесь своими знаниями глубин Си
Просто не используйте костыль «const» всуе а только там, где он действительно необходим.poxu
12.08.2015 13:21+13А я бы сказал так. Убирайте «const» только тогда, когда это необходимо. А вообще суйте его везде, где только можно.
VioletGiraffe
12.08.2015 21:10+3Как сказал когда-то мой друг: «Лучше переконстить, чем недоконстить» :)
potan
12.08.2015 13:30+1Полюбить очень просто. Есть такой эффект — «Стокгольмский синдром» называется. Очень помогает в подобных случаях.
kolu4iy
12.08.2015 14:27Вот согласен на 100%. После некоторого времени с Си привыкаешь к указателям и прочим радостям. Потом берешь язык уровня повыше (С++ не берём, он другой и инопланетный) — и тут-то и понимаешь как тебе не хватает тех примитивных конструкций, которые можно было сложить в три этажа и обернуть скобками для лучшей читаемости, а не для компилятора…
The_Floyd
12.08.2015 15:19+3Очень сомневаюсь, что нечто трехэтажное обернутое в скобки улучшит читаемость.
saboteur_kiev
12.08.2015 16:06Я верно понял, что вся статья медленно подводила нас к большой разнице между двумя очень похожими утверждениями, которые на проверку оказались идентичными?
Тогда о чем статья?a553
12.08.2015 16:13+1Не совсем так. Посыл статьи правильный — синтаксис упрощать надо, как сделано с указателями, например, в С++. Вот только автор сначала запутался в том, что он упрощать собрался, а потом результат его упрощений оказался крайне сомнительным, да и, опять же, с ошибками.
a553
12.08.2015 16:22+1Насчёт С++ — я имею ввиду, что в С++ можно выражать типы с указателями каким-то таким образом:
Что в сложных случаях будет читабельнее, чем Си вариант. Или с привкусом D:ptr<const ptr<volatile ptr<const int> > >
ptr ! const ptr ! volatile ptr ! const int
FoxCanFly
12.08.2015 17:48В C++ вообще не нужны многоэтажные объявления указателей — там есть инкапсуляция. Если проще —
вместоconst std::string& var
— char* скрыт в объекте std::string. Да еще и ссылка вместо указателя.const char* const* var
a553
12.08.2015 17:54+1Это-то всё фигня. Самое сложное — это функции, и там будет либо многоэтажные объявления, либо куча typedef-ов.
The_Floyd
12.08.2015 18:05+1Откройте для себя std::function и не будет вам никаких многоэтажных объявлений с кучей тайпдефов.
a553
12.08.2015 18:07+1Тот же самый typedef, вид сбоку.
The_Floyd
12.08.2015 18:26Как Вы в таком случае себе представляете сферическое объявление указателя на функцию в вакууме?
FoxCanFly
12.08.2015 18:34Даже в сложных случаях типа
std::function< std::function<int (int)> (std::function<int (int, int)>) >
— «функция, принимающая функцию, принимающую два аргумента типа int и возвращающую int, возвращающая функцию, принимающую int и возвращающую int» — все понятно, читабельно и логично. А как будет выглядеть это на Сишных указателях? Мне например и пробовать сейчас написать это страшно.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, то код выглядит вот так:
Я бы сказал, что никаких плюсов перед typedef в плане выразительности std::function не представляет. Наоборот, я мало того, что указываю, что тип — функция, я ещё и указываю сигнатуру функции рядом используя Си нотацию. Многословненько.std::function< std::function< std::function< std::function< void() >(std::function<void()>) >(std::function<void()>) >(std::function<void()>) > h();
FoxCanFly
12.08.2015 18:55+1В вашем примере для читающего код сигнатуры скрыты за кучей typedef, и без их разбора ему вообще не понятно, что таки туда передавать. Если std::function кажется слишком длинно, можете с помощью using хоть fn ее называть, и будет
fn< fn<int (int)> ( fn<int (int, int)> ) >
a553
12.08.2015 18:57+1Дело не в том, что длинно. Дело в том, что используя std::function я пишу то, что я бы написал, используя typedef или инлайн-нотацию, ПЛЮС описываю непосредственно шаблон.
FoxCanFly
12.08.2015 19:01Как я выше сказал, без описания сигнатуры функции это вообще не нужно. А в данном случае читающий сразу понимает, что куда передается и возвращается. Без хождения по тайпдефам и запоминания каждого, и без парсинга скобок и звездочек.
a553
12.08.2015 19:04+1Видимо, вы просто фанат std::function. :) На мой взгляд, вариант с std::function, в котором почти в два раза больше скобочек и ровно в два раза больше упоминаний функций, выглядит достаточно уродливо. Я бы использовал typedef.
encyclopedist
12.08.2015 19:09+3Ещё лучше использовать using:
using func_t = int(int*); using func2_t = float(func1_t);
encyclopedist
12.08.2015 18:38+1Стоит признать, что
std::function
это не равнозначная замена, у нее есть рантайм издержки.FoxCanFly
12.08.2015 18:48Она не хранит в себе ничего кроме указателя, и издержки такие же. А clang их даже инлайнить умеет.
encyclopedist
12.08.2015 19:07Она type-erased. Хранит указатель на базовый класс с виртуальным operator(). Поэтому один лишний косвенный вызов нужен. (Это сделано, чтобы можно было хранить любые callable: помимо указателей на функции, это может быть например указатель на метод или функциональный объект)
FoxCanFly
12.08.2015 19:11Современные компиляторы убирают использование RTTI, если на этапе компиляции можно вычислить его результат (гарантия конечно не 100%, но в тривиальных случаях да). Clang догадывается даже заинлайнить std::function, проверено.
Gorthauer87
12.08.2015 19:02И получаем аллокацию памяти на ровном месте, где она, быть может, не нужна ещё.
FoxCanFly
12.08.2015 19:06Это пример, а не руководство к полной замене во всех случаях. Во всех случаях при помощи структур и объектов можно избежать использования конструкций «указатель на указатель на...»
sim-dev
Уже после нажатия «опубликовать» увидел кое-какие ошибки — исправил.
sim-dev
Не все ошибки я исправил…
Но, надеюсь, главная мысль «разделяй и властвуй» от читающих не ускользнула. Иначе говоря, главная идея была в том, чтобы используя пользовательские типы сделать из однострочных монстров читаемые определения.
Возможно, неудачно я выбрал примеры (да еще и в спешке налажал) — надо было именно об указателях на функции, как наиболее «страшные» писать.
Ну что ж, век живи — век учись, возможно, следующий блин будет не комом.