Основные синтаксические конструкции
В этой главе будут рассмотрены основные синтаксические конструкции, которые так или иначе присутствуют в любом современном языке программирования, будь то функциональный или объектно-ориентированный язык. Это констркции условного перехода и циклы. Без подобных конструкций (или их аналогов) невозможно написать программу, сложнее чем "Hello world".
Содержание |
Условный оператор
Условный оператор это основа основ любого языка программирования. На основании условия, то есть значения некоторого выражения принимается решение, куда следует двигаться программе. В основе всех условных операторов лежит булева логика, при которой любое утверждение сводится к двум понятиям: истина и ложь. Если условие истинно, то управление программы движется в одну сторону. Если условие ложно — в другую. Таким образом, в каждый момент времени делается выбор всего из двух направлений. Более сложные условия строятся путем последовательного прохождения элементарных, двоичных условий.
Оператор if
В языке K++, для проверки условия служит оператор if, так же называемый условным оператором. Синтаксис этого оператора знаком наверное любому человеку, который имеет хоть какое-то представление о языках программирования: <source lang="kpp"> if (/* выражение условия */) {
/* тело оператора */
} </source>
Если в коде программы встречается условный оператор, то первым делом происходит вычисление значения его условия — выражения, заключенного в круглые скобки. В качестве такого выражения может быть указано любое выражение, которое возвращает значение. Если значение условного выражения можно трактовать как истину, то управление передается телу оператора, заключенному в фигурные скобки (в случае единственного выражения в теле оператора, фигурные скобки можно опустить). Если же значение условия ложно — управление передается следующему выражению, стоящему после условного оператора (то есть, тело оператора просто пропускается).
Истинным, считается любой существующий объект, а так же логические операторы сравнения и отношения (такие как ==, !=, == и т. д), если их значение истинно (то есть, объекты соответствуют указанному отношению), а так же специальное ключевое слово true, которое считается истинным всегда.
Ложными, соответственно, считаются несозданные объекты, а так же операторы отношения, если их значение ложно (объекты не соответствуют отношению). Ложными так же считаются объекты, соответствующие ключевым словам false и null.
Для ясности приведем несколько выражений, а так же укажем соответствующие им логические значения: <source lang="kpp"> //Выражение булево значение true true false false null false var x; x false var y; y = 1; true var int z; z true 0 true 1 true "hello world" true 0 == 1 false 5 < 6 true 2 > 3 false 5 >= 5 true 'a' != 'b' true </source>
Обратите внимание на следующие моменты:
- Неинициализированная динамическая переменная x считается ложной (равна null)
- Инициализированная динамическая переменная y считается истинной (объект создан)
- Неинициализированная статическая переменная z считается истинной (объект создан)
- 0, так же как любой другой созданный объект считается истиной, в отличие от C++.
- условие 5 >= 5 считается истиной, так как формулируется как "5 больше 5 или равно 5"
По поводу нуля, следует поговорить отдельно. C++ не является в полной мере объектно-ориентированным языком, фактически в нем ОО система является абстракцией, позволяющей оперировать все теми же понятиями стандартных типов данных и памяти, но на более высоком уровне. Все что мы имеем на этапе выполнения программы это кусочки памяти, которые мы считаем объектами. Поскольку числа в нем являются совершенно определенной сущностью, подобные вольности возможны.
В языке К++ все наоборот. Все с чем он оперирует — это объекты. Провести грань между стандартными типами данных и пользовательскими невозможно. Числа, что фигурируют в программе так же являются объектами. Даже числовые константы которые записываются в выражениях — это тоже объекты (это уже было показано в кратком обзоре). За основу логики языка применяется факт существования объекта. Если объекта не существует — значение ложно, существует — истинно (кроме описанных выше исключений). Оператор if помимо чистой логики, служит для определения существования объекта. Получается, что при таком подходе делать для нуля исключение нерационально, и более того — это может привести к ошибкам. Возьмем к примеру код:
<source lang="kpp"> if (x) {
/* код, соответствущий существованию переменной x */
} </source>
Подразумевается, что тело оператора будет выполнено в случае, если передаваемый объект (вероятнее всего динамическая переменная) инициализирован и существует. Но что будет, если мы передадим в качестве объекта динамическую переменную, содержащую объект типа int, равный нулю? Если бы реализация языка обрабатывала значение 0 как ложь, то логика оператора if в частости и всей программы в целом, зависела бы от фактического значения переменной, а соответственно была бы неопределенной и даже непредсказуемой. Получалось бы, что для одних переменных, оператор работает одним образом, а для других — другим. Отладка подобной программы может занять многие часы. А все из-за одной условности.
Таким образом, можно заключить что оператор if следует применять для проверки логического выражения, либо для проверки существования переменной. Если мы хотим проверить равенство переменной нулю, то это необходимо делать явно: <source lang="kpp"> if (x == 0) {
/* что делать, если x = 0 */
} </source>
Оператор if-else
Если нам необходимо организовать ветвление, то есть в случае истинности, выполнить один участок кода, а в случае ложности другой, то применяется расширенная форма оператора if-else: <source lang="kpp"> if (/* выражение условия */) {
/* код, соответствущий истине */
} else {
/* код, соответствущий лжи */
} </source>
Если условие истинно, то будет выполнен первый блок, если ложно то второй.
Условные операторы, подобно многим другим позволяют организовывать вложения, когда в тело одного условного оператора вкладывается один или несколько других условных операторов, например:
<source lang="kpp">
if (/* условие 1 */) {
/* если условие 1 истинно: */ if (/* условие 2 */) { /* если условие 2 истинно */ } if (/* условие 3 */) { /* если условие 3 истинно */ }
} else {
/* если условие 1 ложно: */ if (/* условие 4 */) { /* если условие 4 истинно */ } else { /* код, соответствущий лжи условия 4 */ }
} </source> Таким образом можно строить разветвленные деревья условий, которые и реализуют алгоритмы программ.
Оператор if-elsif-else
Существует так же третья форма условного оператора, которая позволяет проверять несколько условий в единой более эффективной форме. Это операторы if-elsif-else: <source lang="kpp"> if (/* условие 1 */) {
/* код, соответствущий истине условия 1*/
} elsif (/*условие 2*/) {
/* код, соответствущий истине условия 2*/
... } else {
/* код, соответствущий ложи */
} </source> Он работает так: сначала проверяется условие 1. Если оно истинно, то выполняется код в первом блоке, а затем управление выходит за рамки всей связки. Если условие 1 ложно, то проверяется условие 2, и т. д. Таким образом, вышеописанная конструкция функционально эквивалентна следующей конструкции, однако более компактна.
<source lang="kpp"> if (/* условие 1 */) {
/* если условие 1 истинно */
} else {
if (/* условие 2 */) { /* если условие 2 истинно */ } else { if (/* условие 3 */) { /* если условие 3 истинно */ } else { ... } }
} </source>
Тернарный условный оператор
В некоторых случаях может возникнуть ситуация, при которой необходимо присвоить значение некоторой переменной на основании условия. То есть, при истинности условия присвоить одно значение, при ложности — другое. Это можно осуществить двумя способами. Первый способ — использовать обычный условный оператор: <source lang="kpp"> var x = 5; var y = 0; if (x > 3)
y = 3;
else
y = -1;
</source>
Не будем уточнять, зачем кому-то мог понадобиться подобный "гениальный" код, а отметим лишь то, что операция присвоения занимает 4 строки при соблюдении правил форматирования кода. В тех случаях, когда тела условного оператора представляют собой простые выражения "в одну строчку", особенно если, как уже было сказано выше, происходит присвоение, разумно применять специальную форму логического оператора — тернарный оператор ?:. Тогда, тот же код будет выглядеть так: <source lang="kpp"> var x = 5; var y = 0; y = x > 5 ? 3 : -1; </source>
Это единственный в языке К++ тернарный оператор, работающий с тремя выражениями сразу. Первое выражение это условие — оно располагается слева, до знака вопроса. Между знаком вопроса и знаком двоиточия расположено выражение, которое должно подставляться, в случае истины условия. Последним располагается выражениие, подставляющееся в случае лжи.
Данная форма оператора очень удобна для записи непосредственно внутри выражений, фактических параметров в вызовах функций, либо любых других выражениях. Его даже можно применять в операторе подстановки #{ } в строках. Можно его применять и без возврата значения. Например так: <source lang="kpp"> while (true) {
CheckState() != State.OK ? Signal() : Sleep();
} </source>
Оператор множественного выбора (switch)
В тех случаях, когда требуется сравнить значение некоторого выражения с большим количеством возможных значений, применять обычные условные выражения неразумно. Код получится громоздким и трудно читаемым, а самое главное — изменять такой код будет очень сложно. В таких случаях, на помощь программисту приходит оператор множественного выбора, или оператор switch.
Предположим, что перед нами стоит такая задача: Функция получает некоторый числовой код ошибки и на основании него должна выполнять различные действия. В частности выводить соответствующие сообщения. Если от нас требуется только вернуть соответствующую коду ошибки строку, проще всего и эффективнее, разумеется, применять хеши, но мы будем считать что требуется сделать что то еще.
Конструкция множественного выбора реализуется следующим образом: указывается ключевое слово switch, после которого, в круглых скобках указывается выражение-селектор, которое мы будем анализировать. Затем, ставятся фигурные скобки switch-блока, в теле которого могут быть только конструкции, относящиеся непосредственно к оператору. В нашем случае это будет примерно так: <source lang="kpp"> function ProcessError(const int errno) {
switch (errno) { /* switch блок */ }
} </source>
Далее мы должны определить относящиеся к делу случаи, а именно перечислить конкретные значения, с которыми мы будем сравнивать код ошибки и на основании этого сравнения будем выполнять некоторые операции. Это осуществляется с помощью подоператора case, который записывается так: <source lang="kpp"> case /* выражение сравнения */: /* обработчик */ </source>
Выражение сравнения — это любое выражение, которое будет сравниваться с селектором. Если результат сравнения истинен, то управление будет передано обработчику. Интересная особенность этой конструкции заключается в том, что по выполнению обработчика, управление не будет передано за пределы switch-блока, а будет идти последовательно. Если после данного сравнения будет идти еще один оператор case, то будет выполнен его обработчик, вне зависимости от условия, затем следующий и так далее вплоть до конца всей конструкции. Если это поведение нежелательно, то в конце данного обработчика следует указать ключевое слово break. Тогда, смысл конструкции будет такой же, как и у связки if-elsif.
Если требуется рассмотреть отдельный случай, когда ни одно из условий не было удовлетворено (то есть, ни один из операторов case не сработал), применяется действие "по умолчанию". Для этого, необходимо записать ключевое слово default, а затем через двоеточие указать обработчик. Естественно, никакого выражения сравнения тут нет.
Примечание: при использовании обработчиков по умолчанию, следует иметь в виду что:
- на кажый оператор switch может быть указан только один обработчик по умолчанию
- обработчик по умолчанию должен быть записан в самом конце конструкции
Обобщив вышесказанное, напишем наконец, реализацию задуманной нами функции:
<source lang="kpp" line="1">
function ProcessError(const int errno) {
switch (errno) { case AGAIN: Log("требуется повтор операции"); /* запрашиваем у пользователя подтверждение на повтор */ break; case ACCESS_DENIED: Log("ошибка доступа"); break; case REQUEST_TIMEOUT: case TIMEOUT: Log("таймаут"); break; default: Log("неизвестный код ошибки: #{errno}"); }
} </source>
Обратите внимание на строки 10-13. Для случаев REQUEST_TIMEOUT и TIMEOUT использован один обработчик. Подразумевается что коды, соответствующие данным ошибкам разные, но программист решил их объединить в одно целое. Поэтому, после сравнения в строке 8 не идет ни обработчика, ни ключевого слова break.