Программирование: введение в профессию. 1: Азы программирования - 2016 год
Ещё о выражениях - Язык Паскаль и начала программирования
Прежде чем идти дальше, попытаемся завершить обсуждение арифметических выражений; оно осталось бы неполным без побитовых операций, записи целых чисел в системах счисления, отличных от десятичной, и именованных констант.
2.5.1. Побитовые операции
Побитовые операции выполняются над целыми числами одинакового типа, но при этом числа рассматриваются не как собственно числа, а как строки отдельных битов (т. е. двоичных цифр), составляющих машинное представление. Например, если в побитовой операции участвует число 75 типа integer, то имеется в виду битовая строка 000000001001011.
Побитовые операции можно разделить на два вида: логические операции, выполняемые над отдельными битами (причём над всеми одновременно) и сдвиги. Так, операция not, применённая к целому числу (в отличие от уже знакомой нам операции not в применении к значению типа boolean) даёт в результате число, все биты которого противоположны исходному. Например, если переменные х и у имеют тип integer, после выполнения операторов
в переменной у окажется число -76, машинное представление которого 1111111110110100 представляет собой побитовую инверсию приведённого выше представления числа 75. Отметим, что если бы х и у имели тип word, т. е. беззнаковый тип той же разрядности, то результат в переменной у составил бы 65460; машинное представление этого числа в виде 16-битного беззнакового такое же, как у числа -76 в виде 16-битного знакового.
Аналогичным образом над целыми числами работают уже знакомые нам по § 2.3.4 операции and, or и xor. Все эти операции являются бинарными, то есть требуют двух операндов; когда мы применяем их к целым числам, соответствующие логические операции (“и”, “или”, “исключающее или”) выполняются одновременно над первыми битами операндов, над их вторыми битами и так далее; результаты (тоже отдельные биты) соединяются в целое число того же типа и, как следствие, той же разрядности, которое и становится результатом всей операции. Например, восьмибитное беззнаковое представление (то есть представление типа byte) для чисел 42 и 166 будет соответственно 00101010 и 10100110; если у нас есть переменные х, у, р, q и r, имеющие тип byte, то после присваиваний
переменные р, q и r получат соответственно значения 34 (00100010), 174 (10101110) и 140 (10001100).
Операции побитового сдвига, как следует из названия, сдвигают битовое представление на определённое число позиций влево (shl, от слов shift left) или вправо (shr, shift right). Обе операции бинарные, то есть предусматривают два операнда; слева от названия операции ставится исходное целое число, справа — количество позиций, на которое нужно сдвинуть его побитовое машинное представление. При сдвиге влево на k позиций старшие k разрядов машинного представления числа пропадают, а справа (то есть в качестве младших разрядов) дописывается kнулевых битов. Сдвиг влево на k позиций эквивалентен домножению числа на 2k. Например, результатом выражения 1 shl 5 будет число 32, а результатом 21 shl 3 будет 168.
При сдвиге вправо пропадают, наоборот, k младших разрядов, что касается старших битов, то здесь операция работает по-разному для знаковых чисел и для беззнаковых. При сдвиге вправо беззнакового числа слева дописываются нулевые биты; при сдвиге знакового слева дописываются нули или единицы в зависимости от исходного значения самого старшего (“знакового”) бита числа, так что знак числа при сдвиге сохраняется: результатом сдвига вправо положительного числа будет всегда положительное, отрицательного — отрицательное. В частности, 21 shr 3 даст значение 2 (из представления 00010101 получится 00000010), (-64) shr 3 даст -8. Сдвиг вправо числа -1 всегда даёт -1, на сколько бы позиций мы его ни сдвигали.
2.5.2. Именованные константы
Исходно словом “константа” обозначается такое выражение, значение которого всегда одно и то же. Тривиальным примером константы может послужить литерал — например, просто число, написанное в явном виде. К примеру, “37.0” — это литерал, который представляет собой выражение типа real; очевидно, что значение этого выражения всегда будет одно и то же, а именно — 37.0; следовательно, это константа. Можно привести более сложный пример константы: выражение “6*7”. Это уже не литерал, это арифметическое выражение, а литералов тут два — это числа 6 и 7; тем не менее, значение этого выражения тоже всегда одно и то же, так что и это — пример константы.
Среди всех констант выделяют константы времени компиляции — это такие константы, значение которых компилятор определяет во время обработки нашей программы. К таким константам относятся все литералы, что вполне естественно; кроме того, во время компиляции компилятор может вычислять арифметические выражения, не содержащие обращений к переменным и функциям. Поэтому “6*7” — это тоже константа времени компиляции, компилятор сам вычислит, что значение здесь всегда 42, и именно число 42 поместит в машинный код; ни шестёрка, ни семёрка, ни операция умножения в коде фигурировать не будут.
Кроме констант времени компиляции, встречаются также константы времени исполнения — это выражения, которые по идее всегда имеют одно и то же значение, но компилятор это значение во время компиляции вычислить по каким-то причинам не может, так что оно становится известно только во время выполнения программы. Точная граница между этими видами констант зависит от реализации компилятора; например, Free Pascal умеет во время компиляции вычислять синусы, косинусы, квадратные корни, логарифмы и экспоненты, хотя делать всё это он совершенно не обязан, и другие версии Паскаля такого не делают.
Впрочем, можно найти ограничения к для Free Pascal: например, функции обработки строк, даже если их вызвать с константными литералами в качестве параметров, во время компиляции не вычисляются. Так, следующий фрагмент приведёт к ошибке во время компиляции, несмотря на то, что функция сору точно так же встроена в компилятор Паскаля, как и упоминавшиеся выше математические функции вроде синуса и логарифма:
Механизм именованных констант позволяет связать с неким постоянным значением (константой времени компиляции) некое имя, т. е. идентификатор, и во всём тексте программы вместо значения, записанного в явном виде, использовать этот идентификатор. Делается это в секции описания констант, которую можно расположить в любом месте между заголовком и началом главной части программы, но обычно секцию констант программисты располагают как можно ближе к началу файла — например, сразу после заголовка программы. Дело тут в том, что значения некоторых констант могут оказаться (и оказываются) самой часто изменяемой частью программы, и расположение констант в самом начале программы позволяет сэкономить время и интеллектуальные усилия при их редактировании.
Для примера рассмотрим программу hello20_for; она выдаёт “на экран” (в стандартный поток вывода) сообщение “Hello, world!”, причём делает это 20 раз. Эту задачу можно очевидным образом обобщить: программа выдаёт заданное сообщение заданное число раз. Из школьного курса физики нам известно, что практически любую задачу лучше всего решать в общем виде, а конкретные значения подставлять в самом конце, когда уже получено общее решение. Аналогичным образом можно поступать и в программировании. В самом деле, что изменится в программе, если мы захотим изменить выдаваемое сообщение? А если мы захотим выдавать сообщение не 20 раз, а 27? Ответ очевиден: изменятся только соответствующие константы-литералы. В такой короткой программе, как hello_20, конечно, найти эти литералы несложно; а если программа состоит хотя бы из пятисот строк? Из пяти тысяч? И ведь это далеко не предел: в наиболее крупных и сложных компьютерных программах счёт строк идёт на десятки миллионов.
При этом заданные в коде константы, от которых зависит выполнение программы, в достаточной степени произвольны: на это однозначно указывает то обстоятельство, что задача очевидным образом обобщается на произвольные значения. Логично при этом будет ожидать, что нам, возможно, захочется изменить значения констант, не меняя больше ничего в программе; в этом смысле константы подобны настроечным ручкам разнообразных технических устройств. Именованные константы позволяют облегчить такую “настройку”: если без их применения литералы рассыпаны по всему коду, то давая каждой константе собственное имя, мы можем собрать все “параметры настройки” в начале текста программы, при необходимости снабдив их комментариями. Например, вместо программы hello20_for мы можем написать следующую программу:
Как видим, секция описаний констант состоит из ключевого слова const, за которым следует одно или несколько описаний константы; каждое такое описание состоит из имени (идентификатора) новой константы, знака равенства, выражения, задающего значение константы (это выражение само должно быть константой времени компиляции) и точки с запятой. С того момента, как компилятор обработает такое описание, в дальнейшем тексте программы введённый этим описанием идентификатор будет заменяться на связанное с ним константное значение. Само имя константы, что вполне естественно, тоже считается константой времени компиляции; как мы увидим позже (например, когда будем изучать массивы), это обстоятельство достаточно важно.
Облегчением “настройки” программы полезность именованных констант не ограничивается. Например, часто бывает так, что одно и то же константное значение встречается в нескольких разных местах программы, причём по смыслу при изменении его в одном из мест нужно также (синхронно) изменить и все остальные места, где встречается та же самая константа. Например, если мы пишем программу, управляющую камерой хранения из отдельных автоматизированных ячеек, то нам наверняка потребуется знать, сколько ячеек у нас есть. От этого будет зависеть, например, подсчёт числа свободных ячеек, всяческие элементы пользовательского интерфейса, где необходимо выбрать одну ячейку из всех имеющихся, и многое другое. Ясно, что число, означающее общее количество ячеек, будет то и дело встречаться в разных частях программы. Если теперь инженеры вдруг решат спроектировать такую же камеру хранения, но на несколько ячеек больше, нам придётся внимательно просмотреть всю нашу программу в поисках проклятого числа, которое теперь надо везде поменять. Несложно догадаться, что такие вещи представляют собой неиссякаемый источник ошибок: если, скажем, в программе одно и то же число встречается тридцать раз, то можно быть уверенным, что с первого просмотра мы “выловим” от силы двадцать таких вхождений, а остальные упустим.
Ситуация резко осложняется, если в программе есть два разных, не зависящих друг от друга параметра, которые волей случая оказались равны одному и тому же числу; например, у нас имеется 26 ячеек камеры хранения, а ещё у нас есть чековый принтер, в строке которого умещается 26 символов, и оба числа встречаются прямо в тексте программы. Если один из этих параметров придётся изменить, то можно не сомневаться, что мы не только упустим часть вхождений нужного параметра, но разок-другой изменим тот параметр, который менять не требовалось.
Совсем другое дело, если в явном виде количество ячеек нашей камеры хранения встречается в программе лишь один раз — в самом её начале, а далее по тексту везде используется имя константы, например, LockerBoxCount или что-нибудь в этом духе. Изменить значение такого параметра очень просто, поскольку само это значение в программе написано ровно в одном месте; риск изменить что-нибудь не то при этом также исчезает.
Необходимо отметить ещё одно очень важное достоинство именованных констант: в программе, созданной с их использованием, гораздо легче разобраться. Поставьте себя, например, на место человека, который где-нибудь в дебрях длинной (скажем, в несколько тысяч строк) программы натыкается на число 80, написанное вот прямо так, цифрами в явном виде — и, разумеется, без комментариев. Что это за “80”, чему оно соответствует, откуда взялось? Может быть, это возраст дедушки автора программы? Или количество этажей в небоскрёбе на нью-йоркском Бродвее? Или максимально допустимое количество символов в строке текста, выводимой на экран? Или комнатная температура в градусах Фаренгейта?
Потратив изрядное количество времени, читатель такой программы может заметить, что 80 составляет часть сетевого адреса, так называемый порт, при установлении соединения с каким-то удалённым сервером; припомнив, что порт с этим номером обычно используется для веб-серверов, можно будет догадаться, что программа что-то откуда-то пытается получить по протоколу HTTP (и, кстати, ещё не факт, что догадка окажется верна). Сколько времени уйдёт на такой анализ? Минута? Десять минут? Час? Зависит, конечно, от сложности конкретной программы; но если бы вместо числа 80 в программе стоял идентификатор DefaultHttpPortNumber, тратить время не пришлось бы вовсе.
В большинстве случаев используемые правила оформления программного кода попросту запрещают появление в программе (вне секции описания констант) чисел, написанных в явном виде, за исключением чисел 0, 1 и (иногда) -1; всем остальным числам предписывается обязательно давать имена. В некоторых организациях программистам запрещают использовать в глубинах программного кода не только числа, но и строки, то есть все строковые литералы, нужные в программе, требуется вынести в начало и снабдить именами, а в дальнейшем тексте использовать эти имена.
Рассмотренные нами константы называются в Паскале нетипизированными, поскольку при их описании не указывается их тип, он выводится уже при их использовании. Кроме них, Паскаль (во всяком случае, его диалекты, родственные знаменитому Turbo Pascal, в том числе к наш Free Pascal) предусматривает также типизированные константы, для которых тип указывается в явном виде при описании. В отличие от нетипизированных констант, типизированные константы не являются константами времени компиляции; более того, в режиме, в котором компилятор работает по умолчанию, значения таких констант разрешается изменять во время выполнения, что вообще делает сомнительным применение названия “константы”. Историю возникновения этой странной сущности мы оставляем за рамками нашей книги; заинтересованный читатель легко найдёт соответствующие материалы самостоятельно. В нашем учебном курсе типизированные константы не нужны, и рассматривать их мы не будем. Так или иначе, на случай, если вам попадутся примеры программ, использующие типизированные константы, что-то вроде
помните, что это совсем не то же самое, что константы без указания типа, и по своему поведению похоже скорее на инициализированную переменную, чем на константу.
2.5.3. Разные способы записи чисел
До сих пор мы имели дело преимущественно с целыми числами, записанными в десятичной системе счисления, а когда нам приходилось работать с дробными числами, записывали их в простейшей форме — в виде обычной десятичной дроби, в которой роль десятичной запятой играет символ точки.
Современные версии языка Паскаль позволяет записывать целые числа в шестнадцатеричной системе счисления; для этого используется символ “4” и последовательность шестнадцатеричных цифр, причём для обозначения цифр, превосходящих девять, можно использовать как заглавные, так и строчные латинские буквы. Например, $1А7 или $1а7 — это то же самое, что и 423.
Free Pascal поддерживает, кроме этого, ещё литералы в двоичной к восьмеричной системах. Восьмеричные константы начинаются с символа “&”, двоичные — с символа “%”. Например, число 423 можно записать также и в виде 5110100111, и в виде &647. Другие версии Паскаля не поддерживают такую запись чисел; не было её и в Turbo Pascal.
Что касается чисел с плавающей точкой, то они всегда записываются в десятичной системе, но и здесь есть форма записи, отличающаяся от привычной. Мы уже сталкивались с так называемой научной нотацией, когда выводили числа с плавающей точкой на печать; напомним, что при этом печаталась мантисса, то есть число, удовлетворяющее условию 1 ≤ m < 10, затем буква “Е” и целое число, обозначающее порядок (степень 10, на которую нужно умножить мантиссу). Аналогичным образом можно записывать числа с плавающей точкой в тексте программы. Например, 7E3 — это то же самое, что 700.0, а 2.5Е-5 — то же, что 0.000025.