MyTetra Share
Делитесь знаниями!
Как научиться понимать синтаксис языка 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 , то происходит две вещи:


  1. В переменную a кладется значение суммы b + c;
  2. Результатом выражения присваивания становится значение присваиваемого выражения.


Сложно? На самом деле все просто. На конкретных цифрах это будет выглядеть так: имеем b = 7 и c = 3. Тогда значение переменной a станет 10. Но и результатом вычисления всего выражения a = b + c тоже будет число 10. Для лучшего понимания можно все заключить в круглые скобочки и сказать, что результатом выражения (a = b + c) является число 10.


И именно поэтому в языке СИ++ появляются такие странные инструкции присваивания:


a = b = x + y;


Для понимания того, что эта инструкция делает, надо в первую очередь понять используемый в ней контекст. Ясно, что в этой инструкции написано выражение, в котором происходит присваивание, а значит нам надо рассматривать выражение с контекстом присваивания. Особенностью контекста присваивания в языке Си++ является то, что присваивание выполняется справа-налево. То есть, сначала вычисляется то, что находится справа, и присваивается тому, что находится слева. Действительно: чтобы задать более левые значения, надо вначале вычислить правые. Поэтому последовательность действий с точки зрения языка можно написать так:


  • Вычисляется выражение x + y. Потому что оно самое правое.
  • Вычисляется выражение b = x + y. При вычислении этого выражения переменной b будет присвоена сумма x + y, а значение всего выражения b = x + y будет эта же сумма.
  • Переменной 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.



Дописать...




Так же в этом разделе:
 
MyTetra Share v.0.59
Яндекс индекс цитирования