|
|||||||
Как научиться понимать синтаксис языка C++
Время создания: 14.09.2018 15:32
Раздел: Компьютер - Программирование - Язык C++ (Си++)
Запись: xintrea/mytetra_syncro/master/base/15369283784c43pmorj4/text.html на raw.github.com
|
|||||||
|
|||||||
Я не понимаю язык Си++. Да, я два десятилетия программирую на этом языке, но для меня он и по сей день остается загадкой. Даже если отбросить в сторону все сложности поддерживаемых языком парадигм и структур, то окажется, что язык этот не становится более понятным. Это происходит из-за того, что сложен не только весь язык как таковой, в нем сложна такая простая и детерминированная вещь как синтаксис. Я часто смотрю на код в чужих проектах и говорю себе: "Твою ж мать, что тут написано?". Причем, проблемы не только в понимании структуры чужой программы, проблемы возникают при понимании даже одной строчки кода. Говорят, что писать программы надо с использованием языка, а не на языке. Многие Си++ программисты этого либо не знают, либо не могут себе такой роскоши позволить. Они вязнут в низкоуровневых подробностях, и за собой способны утянуть и других разработчиков и всю структуру проекта. Вместо борьбы со сложностью, чем по своей сути и является программирование, получается игра в "догадайся, что это". Поначалу игра может быть и увлекательной, но быстро начинает утомлять. Здесь я собираю практические рекомендации о том, как ориентироваться в синтаксисе языка Си++. Надеюсь, это поможет начинающим разработчикам не бросить попытки понимания языка и его возможностей. А матерым разработчикам позволит взглянуть на язык с другой стороны, и, может быть, упорядочить и освежить свои представления о нем. Все что здесь написано - сугубо личное видение синтаксиса языка Си++. Здесь могут быть существенные фактологические и логические ошибки, происходящие по одной банальной причине - автор сего текста не знает и не понимает язык Си++, и с трудом продирается сквозь его синтаксис. Контекстная зависимость Почему синтаксис языка Си++ такой сложный? Потому что в нем во главу угла поставлена великая и ужасная Контекстная Зависимость. Что это такое? Говоря простыми словами, это когда один и тот же символ или оператор означает совершенно разные вещи в зависимости от того, в каком месте команды он находится. Поэтому получается, что недостаточно знать операторы языка. В первую очередь надо научиться понимать контекст, в котором применен оператор, и только потом уже разбираться с тем, что оператор на самом деле делает. Хорошая новость заключается в том, что контекстов на самом деле немного. Плохая новость заключается в том, что выражения в языке Си++ можно построить так заковыристо, что понять контекст будет сложно. И, конечно, обычное дело, когда в одной команде языка Си++ для одного и того же оператора в разных частях команды действуют разные контексты. Далее перечислены контексты, которые наиболее часто встречаются в коде Си++:
Это контексты, полученные просто из практических соображений. Можно вычленить и другие контексты, но для упрощения изложения стоит оставиться на них, и при необходимости дополнить данный список. Выражение, инструкция, присваивание Начать понимать синтаксис языка Си++ лучше всего с того, чтобы уложить в голове две базовые вещи, используемые в языке:
Чтобы их различать, можно дать такие определения: Выражение - это то, что выдает (в результате своего вычисления) некое значение. Инструкция - это выражение (зачастую, составное), завершенное точкой с запятой. Выражения, почти всегда, являются составными выражениями. Почему так? Просто потому, что любое имя переменной, константанта или литерал - это уже выражение. Если выражения объединяются оператором, то это тоже выражение, только составное. Например, есть у нас две переменных: x и y. Уже одно только написание имени переменной x будет выдавать значение переменной x. Аналогично и для переменной y. А написание имен переменных, объединенных оператором суммы будет выдавать значение суммы: x + y. В этом и заключается работа с выражениями. Сложность возникает тогда, когда появляется присваивание. Ведь результат вычисления выражения надо чему-то присвоить. Особенностью языка Си++ является то, что присваивание в языке Си++ - это выражение. Другими словами, когда написано a = b + c , то происходит две вещи:
Сложно? На самом деле все просто. На конкретных цифрах это будет выглядеть так: имеем b = 7 и c = 3. Тогда значение переменной a станет 10. Но и результатом вычисления всего выражения a = b + c тоже будет число 10. Для лучшего понимания можно все заключить в круглые скобочки и сказать, что результатом выражения (a = b + c) является число 10. И именно поэтому в языке СИ++ появляются такие странные инструкции присваивания: a = b = x + y; Для понимания того, что эта инструкция делает, надо в первую очередь понять используемый в ней контекст. Ясно, что в этой инструкции написано выражение, в котором происходит присваивание, а значит нам надо рассматривать выражение с контекстом присваивания. Особенностью контекста присваивания в языке Си++ является то, что присваивание выполняется справа-налево. То есть, сначала вычисляется то, что находится справа, и присваивается тому, что находится слева. Действительно: чтобы задать более левые значения, надо вначале вычислить правые. Поэтому последовательность действий с точки зрения языка можно написать так:
Здесь есть один тонкий момент. Я думаю, что переменной a присваивается значение выражения b = x + y. Возможно, правильно было бы сказать, что переменной a присваивается значение выражения b. Согласитесь, выражения b и b = x + y - это все-таки разные выражения, хотя их значения в данном случае одинаковые. Какое же из этих утверждений правильное? Здесь можно рассуждать следующим образом: Мы разбираем данное выражение итеративно, рассматривая блоки операторов присваивания справа-налево. При этом мы должны все время оперировать термином "выражение", и не перепрыгивать на термин "значение (результат) выражения". То, что в момент разбора выражения происходит присвоение значений переменным, роли не играет. Таким образом, правильнее будет утверждать, что переменной a присваивается значение выражения b = x + y. Кстати, в более простых языках, например в таком как Pascal, присваивание является инструкцией, а не выражением. То есть, при выполнении присваивания в Pascal, происходит вычисление только выражения в правой части. А разультата всей конструкции присваивания просто нет как понятия. Поэтому в Паскале нет такой возможности, как множественное присваивание. А если в каких-то диалектах оно и появится, то только в виде синтаксического сахара. Завершая тему присваивания, можно показать еще один "странный" пример. Поскольку присваивание - это выражение, то его можно использовать в качестве аргумента функции: n = function( a = b ); Значение выражения a = b равно тому, что присваивалось, то есть значению b. Поэтому в функцию передастся значение b. Здесь возникает вопрос: а равно ли значение переменной a значению b в момент выполнения функции? Правильный ответ: да, равно. Нетрудно запомнить, что при вычислении выражения с оператором присвоения =, присвоение происходит сразу при вычислении выражения. Перед тем, как передаться в функцию, выражение должно быть вычислено. Именно при этом вычислении переменной a присвоится значение b. И только потом будет вызвана функция. Но тут нужно помнить, что если использовать в выражении операторы постинкремента, например a = b++, то значение такого выражения будет неизменнённое значение b (т.е. значение до инкремента). Функция выполнится, и только после этого значение b увеличится на 1. А значение a все время останется прежним, неувеличенным значением b. Подробнее можно прочитать здесь: Понимание преинкремента и постинкремента в языке C++. Надо ли в своем коде использовать конструкции, подобные n = function( a = b ) ? Мое мнение: нет, не надо. Да, с такой конструкцией код становится несколько короче, но эта краткость идет в ущерб пониманию и читабельности. Гораздо правильнее с точки зрения дальнейшей поддержки исходного кода написать инструкции так, как они будут последовательно выполняться во времени при выполнении вышеозначенного кода. То есть, вот так: a = b; n = function( a ); Выглядит такая запись, по сравнению с предыдущим вариантом, не круто, а прямолинейно и наивно. Но понять ее не составляет никакого труда. Кроме того, современные оптимизирующие компиляторы для обоих вариантов генерируют одинаковый код. Определение указателей и ссылок Чтобы ориентироваться в этих самых звездочках * и амперсандах &, надо, опять таки, понимать, в каких контекстах происходит их использование. Вот пример ниже: int a=0; int &b=a; // Здесь b - ссылка на a int *c=&a; // Здесь c - указатель на a Сначала надо разобраться со строкой 2. Наверняка многие знают, что & - это операция взятия адреса. Тогда почему вторая строка - это создание ссылки на a? И при чем тут адрес? Дело в том, что здесь надо понять в каком контексте происходит использование амперсанда &. Для упрощения, вначале надо рассмотреть короткую форму выражения, то есть, только часть определения, без инициализации. (Пока за скобками оставим тот факт, что ссылка в C++ обязательно должна быть проинициализирована, иначе будет ошибка). Другими словами, надо откинуть часть, начиная с символа равно "=", чтобы узнать, какая часть является определением. А символы после знака равно (включая знак равно, чтобы было более понятно) - явлются инициализацией. Определение: int &b Инициализация: =a Так вот, назначение амперсанда & в контексте определения - говорить компилятору о том, что происходит определение ссылки. А операция взятия адреса тут вообще ни при чем. Оставшаяся часть выражения - =a - это инициализация. Ссылка с именем b связывается с переменной a. Теперь надо понять, что происходит в строке 3. Надо разбить строку на определение (до знака равно) и инициализацию: Определение: int *c Инициализация: =&a Звездочка * в контексте определения говорит о том, что определяется указатель. (А не о том, что происходит операция разыменовывания (блин, какое уродское название)). А амперсанд & в контексте инициализации говорит о том, что происходит операция взятия адреса. Вообще, контекст инициализации фактически эквивалентен контексту любого вычиляемого выражения C++. Потому что инициализация, по факту, и происходит каким-либо выражением. Знак амперсанда & в контексте инициализации, также как и в контексте любого вычисляемого выражения, обозначает взятие адреса. Вот еще один пример: int x; int *y = &x; int z = *y; Во второй строке звездочка * находится в контексте определения, и обозначает создание указателя y, который указывает на тип int. В третьей строке звездочка * находится в контексте инициализации, и обозначает операцию разыменовывания, то есть будет браться значение, которое лежит там, куда указывает указатель y. Дописать... |
|||||||
Так же в этом разделе:
|
|||||||
|
|||||||
|