Выражения и переменные - Язык Паскаль и начала программирования

Программирование: введение в профессию. 1: Азы программирования - 2016 год

Выражения и переменные - Язык Паскаль и начала программирования

2.2.1. Арифметические операции и понятие типа

Операторы write и writeln могут печатать не только строки. Например, если нам срочно потребуется перемножить 175 и 113, мы можем написать для этого программу8:

Откомпилировав и запустив эту программу, мы увидим на экране ответ 19775. Символом “*” в Паскале (и в большинстве других языков программирования) обозначается операция умножения, а конструкция “175*113” представляет собой пример арифметического выражения. Мы могли бы сделать эту программу чуть более “дружественной пользователю”, показав в её выводе, к чему, собственно говоря, относится выдаваемый ответ:

Здесь мы предложили оператору writeln два аргумента, предназначенных к печати: строку и арифметическое выражение. Таких аргументов мы можем задать сколько угодно, перечислив их, как и в этом примере, через запятую. Если теперь откомпилировать и запустить новый вариант программы, выглядеть это будет примерно так:

Обратите внимание на фундаментальную (с точки зрения компилятора) разницу между символами, входящими в строковый литерал, и символами вне его: если выражение 175*113, находящееся вне строкового литерала, было вычислено, то совершенно те же символы, но находящиеся между апострофов и входящие, таким образом, в строковый литерал, компилятор вовсе не пытался рассматривать в качестве выражения и вообще в любом ином качестве, нежели просто как символы. Поэтому в соответствии с нашими указаниями программа сначала напечатала один за другим все символы из заданного строкового литерала (в том числе, как легко видеть, пробелы), а затем следующий аргумент оператора writeln, в роли которого выступает выражение со значением 19775.

Разумеется, умножение — далеко не единственная арифметическая операция, которую поддерживает язык Паскаль. Сложение и вычитание в Паскале обозначаются естественными для этого знаками “+” и “-”, так что, например, выражение 12 + 105 даст в результате 117, а выражение 10 - 25 даст -15. Так же, как и при записи математических формул, в выражениях Паскаля операции имеют разные приоритеты ; например, приоритет умножения в Паскале, как и в математике, выше, чем приоритет сложения и вычитания, поэтому значением выражения 10 + 5 * 7 будет 45, а не 105: при вычислении этого выражения сначала производится умножение, то есть 5 умножают на 7, получается S5, и только после этого полученное число прибавляют к десяти. Точно так же, как и в математических формулах, в выражениях Паскаля мы можем использовать круглые скобки для изменения порядка выполнения операций: (10 + 5) * 7 даст в результате 105.

Отметим, что в Паскале предусмотрены также унарные (то есть имеющие один аргумент) операции “+” и “-”, то есть можно, например, написать -(5 * 7), получится -35: унарная операция -, как и следовало ожидать, меняет знак числа на противоположный. Унарная операция + тоже предусмотрена, но большого смысла в ней нет: её результат всегда равен аргументу.

Несколько сложнее обстоят дела с операцией деления. Обычное математическое деление обозначается косой чертой “/”; нужно только не забывать, что операция деления отличается от умножения, сложения и вычитания: даже если её аргументы целые, результат в общем случае не может быть выражен целым числом. Это приводит к несколько неожиданному для начинающих эффекту. Например, если мы напишем в программе оператор writeln(14/7), в его выдаче мы можем даже не сразу опознать число 2:

Чтобы понять, в чём тут загвоздка и почему writeln не напечатал просто “2”, потребуется довольно пространное объяснение, вводящее понятие типа выражения9. Поскольку это одно из основополагающих понятий в программировании и в дальнейшем мы в любом случае без него не обойдёмся, попытаемся разобраться с ним прямо сейчас.

Отметим для начала, что все числа, которые мы записывали в приведённых выше примерах, целые; если бы мы написали что-нибудь вроде 2.75, то речь бы пошла о числе другого типа — так называемом числе с плавающей точкой. Мы с вами подробно рассматривали представление чисел обоих видов (см. § 1.6.2 и 1.6.3) и видели, что они хранятся и обрабатываются совершенно по-разному. Заметим, что “2” и “2.0” — с математической точки зрения одно и то же число, но в программировании это совершенно разные вещи, поскольку у них разное представление, и для выполнения операций над ними требуются совершенно разные последовательности машинных команд. Более того, целое число 2 может быть представлено как двухбайтное, однобайтное, четырёхбайтное или даже восьмибайтное целое, знаковое или беззнаковое9 10. Всё это тоже примеры ситуаций, когда речь идёт о разных типах. В Паскале каждый тип имеет своё имя; например, знаковые целые числа могут быть типа shortint, integer, longint и int64 (соответственно однобайтное, двухбайтное, четырёхбайтное и восьмибайтное знаковое целое), соответствующие беззнаковые типы называются byte, word, longword и qword, а числа с плавающей точкой обычно относят к типу real (хотя тот же Free Pascal поддерживает и другие типы чисел с плавающей точкой).

Как это часто бывает в инженерно-технических дисциплинах, понятие типа плохо поддаётся определению: если даже попытаться такое определение дать, скорее всего, оно окажется в чём-то не соответствующим реальности. Чаще всего в литературе можно встретить утверждение, что тип есть множество возможных значений, но, приняв такое определение, мы не сможем объяснить, чем различаются всё те же 2 и 2.0; следовательно, в понятие типа должно входить не только множество значений, но и принятое для данного типа машинное представление этих значений. Кроме того, многие авторы подчёркивают важную роль набора операций, определённых для данного типа, и это, в принципе, разумно. Заявив, что тип выражения фиксирует множество значений, их машинное представление и набор операций, определённых над этими значениями, мы, по-видимому, окажемся близки к истине.

Типы (и выражения) бывают не только числовыми. Например, тип boolean, предназначенный для работы с логическими выражениями, предусматривает всего два значения: true (истина) и false(ложь); набор операций над значениями этого типа включает хорошо знакомые нам конъюнкцию, дизъюнкцию, отрицание и “исключающее или”. Использовавшееся в нашей самой первой программе ’Hello, world!’ есть ничто иное, как константа (а значит, выражение) типа string (строка), и над строками даже есть операция, правда, всего одна — “сложение”, обозначаемое символом “+”, которое на самом деле означает конкатенацию (то есть, попросту говоря, соединение) двух строк в одну. Строки также можно сравнивать, но результатом сравнения, конечно, будет уже не строка, а логическое значение, то есть значение типа boolean. Для работы с одиночными символами используется тип char, и так далее.

Вернёмся к операции деления, с которой весь этот разговор, собственно говоря, и начался. Теперь уже нетрудно догадаться, почему writeln(14/7) повёл себя таким неожиданным образом. Результат операций умножения, сложения и вычитания обычно имеет тот же тип, что и аргументы операции, то есть при сложении или умножении двух целых мы получаем целое; если же мы попытаемся сложить два числа с плавающей точкой, то и результат будет числом с плавающей точкой. С делением всё иначе: его результат всегда имеет тип числа с плавающей точкой, чем и обусловлен полученный эффект.

Говоря подробнее, оператор writeln, если не принять специальных мер, выводит числа с плавающей точкой в так называемой научной нотации — в виде мантиссы к порядка, причём мантисса представляет собой десятичную дробь, удовлетворяющую условию 1 ≤ m < 10, и выдаётся на печать с 16 знаками после запятой; после мантиссы в научной нотации следует буква Е (от слова exponent), отделяющая от мантиссы запись порядка — положительного или отрицательного целого числа р, представляющего собой степень десяти; всё число равно m ∙ 10р. Это поведение оператора writeln можно изменить, если задать в явном виде, сколько знакомест мы хотим выделить на печать числа и сколько из них — на знаки после запятой. Для этого в операторах write и writeln после числового выражения добавляют двоеточие, целое число (сколько знакомест всего), ещё одно двоеточие и ещё одно число (сколько знаков после запятой). Например, если написать writeln(14/7:7:3), то напечатано будет 2.000, причём, поскольку здесь всего пять знаков, перед этим будет напечатано ещё два пробела.

Кроме обычного деления, Паскаль предусматривает целочисленное деление, известное из школьной математики как деление с остатком. Для этого вводятся ещё две операции, обозначаемые словами div и mod, которые означают деление (с отбрасыванием остатка) и остаток от такого деления. Например, если написать

то напечатано будет два числа через пробел: “6 3”.

2.2.2. Переменные, инициализация и присваивание

Все примеры, приведённые в предыдущем параграфе, обладают одним фундаментальным недостатком — они печатают каждый раз одно и то же, невзирая ни на какие обстоятельства, потому что решают не просто одну и ту же задачу (так поступает большинство программ), а одну и ту же задачу для одного и того же частного случая. Любители рассуждать о свойствах алгоритмов, возможно, заявили бы, что алгоритмы, реализованные в наших примерах, не обладают свойством массовости.

Было бы гораздо лучше, если бы программа, умеющая решать некую задачу, пусть даже всего одну и очень простую, всё же решала бы её в общем виде, то есть запрашивала бы у пользователя (или брала бы откуда-то ещё) значения величин и работала бы с ними, а не с теми величинами, которые жестко заданы прямо при написании программы в её исходном тексте. Чтобы этого достичь, нам потребуется одна очень важная возможность: мы должны уметь хранить в памяти11 некую информацию и работать с ней. В Паскале12 для этого используются так называемые переменные.

Переменная в простейшем случае обозначается идентификатором — словом, которое может состоять из латинских букв, цифр и знака подчёркивания, но начинаться должно с буквы13; такой идентификатор называется именем переменной. Позже мы столкнёмся с переменными, которые не имеют имён, но до этого нам пока далеко. Например, мы можем назвать переменную “х”, “counter”, “р12”, “LineNumber” или “grand_total”. Отметим, что Паскаль не различает заглавные буквы и строчные, то есть по правилам Паскаля слова “LineNumber”, “LINENUMBER”, “linenumber” и “LiNeNuMBeR” обозначают одну и ту же переменную; иной вопрос, что использование для одного и того же идентификатора различных вариантов написания считается у программистов крайне дурным тоном.

С переменной в каждый момент времени связано некое значение; говорят, что переменная хранит значение или что значение содержится в переменной. Если имя переменной встречается в арифметическом выражении, производится так называемое обращение к переменной, при котором в выражение вместо имени переменной подставляется её значение.

Паскаль относят к категории строго типизированных языков программирования; это, в частности, означает, что каждая переменная в программе на Паскале имеет строго определённый тип. В предыдущем параграфе мы рассматривали понятие типа выражения; тип переменной можно понимать, с одной стороны, как тип выражения, состоящего из одного только обращения к этой переменной, а с другой стороны — как тип выражения, значение которого может в такой переменной храниться. Например, самый популярный в программах на Паскале тип, который называется integer, подразумевает, что переменные этого типа используются для хранения целочисленных значений, могут содержать числа от -32768 до 32767 (числа типа integer, двубайтные знаковые целые), и обращение к такой переменной тоже будет, естественно, выражением типа integer.

Прежде чем использовать в программе переменную, её необходимо описать, для чего в программу между заголовком и главной частью вставляют секцию описания переменных; эта секция начинается словом “var” (от слова variables — переменные), за которым следуют одно или несколько описаний переменных с указанием их типов. Например, секция описаний переменныхможет выглядеть так:

Здесь переменные х и у имеют тип integer, а переменная flag — тип boolean (напомним, что этот тип, иногда называемый логическим, подразумевает только два возможных значения — true и false, то есть “истина” и “ложь”). Переменные одного типа можно сгруппировать в одно описание, перечислив их через запятую:

В Паскале предусмотрено несколько различных способов для занесения значения в переменную. Например, можно задать начальное значение переменной прямо в секции описаний; это называется инициализацией:

Если этого не сделать, в переменной всё равно будет содержаться какое- то значение, но какое — предсказать невозможно, это может быть произвольный мусор. Такая переменная называется неинициализированной. Использование “мусорного” значения ошибочно по своей сути, так что компилятор, видя такое использование, выдаёт предупреждение; к сожалению, далеко не всегда он может с этим справиться, в некоторых случаях программа может быть написана так “хитро”, что в ней будет производиться обращение к неинициализированной переменной, но компилятор этого просто не заметит. Иногда бывает и наоборот.

Значение переменной можно в любой момент изменить, выполнив так называемый оператор присваивания. В Паскале присваивание обозначается знаком “:=”, слева от которого записывается переменная, которой нужно присвоить новое значение, а справа — выражение, значение которого будет занесено в переменную. Например, оператор

(читается “х положить равным 79”) занесёт в переменную х значение 79, и с того момента, когда этот оператор сработал, в выражениях, содержащих переменную х, будет использоваться именно это значение. Старое значение переменной, каково бы оно ни было, при выполнении присваивания теряется. Работу присваивания можно проиллюстрировать на следующем примере:

В момент выполнения первого из операторов writeln в переменной х содержится начальное значение, заданное при описании, то есть число 25; именно оно и будет напечатано. Затем оператор присваивания, расположенный в следующей строке, изменит значение х; старое значение 25 будет потеряно, и переменная будет теперь содержать значение 36, которое и напечатает второй оператор writeln; после этого в переменную будет занесено значение 49, и последний оператор writeln напечатает как раз его. В целом выполнение этой программы будет выглядеть так:

Несколько более сложен для понимания другой пример присваивания:

Здесь сначала вычисляется значение выражения справа от знака присваивания; поскольку само присваивание пока не произошло, при вычислении используется старое значение х, то, которое было в этой переменной непосредственно перед началом выполнения оператора. Затем вычисленное значение, которое для нашего примера будет на 5 больше, чем значение х, заносится обратно в переменную х, то есть, грубо говоря, в результате выполнения этого оператора значение, содержавшееся в переменной х, становится на пять больше: если там было 17, то станет 22, если было 100, станет 105 и так далее.

2.2.3. Идентификаторы и зарезервированные слова

В предыдущем параграфе нам потребовалось описывать переменные, давая им имена, такие как “х”, “flag”, “LineNumber” и т. п., мы даже ввели понятие идентификатора: слово, которое может состоять из латинских букв, арабских цифр и знака подчёркивания, причём начинаться должно обязательно с буквы. Если вспомнить самое начало разговора о Паскале, то аналогичное имя-идентификатор нам потребовалось в заголовке программы в качестве имени для всей программы целиком. Кроме программы и переменных, в Паскале существует целый ряд сущностей, описываемых пользователем (то есть программистом), которым тоже нужны имена; постепенно мы обсудим большую часть этих сущностей и научимся их использовать. Общее правило тут одно: в качестве имени для чего бы то ни было в Паскале может выступать идентификатор, и правила для создания идентификаторов используются одни и те же, независимо от того, для чего очередной идентификатор понадобился.

При этом нам встречались слова, которые предоставляет нам сам Паскаль, то есть такие, которые нам не нужно было описывать: это, с одной стороны, слова program, var, begin, end, div, mod, а с другой — слова write и writeln, integer, boolean.

Несмотря на то, что все эти слова в той или иной степени являются частью языка Паскаль, они относятся к разным категориям. Слова program, var, begin, end, div и mod (а также многие другие, часть из которых мы изучим в будущем) по правилам Паскаля считаются зарезервированными словами (иногда их называют также ключевыми словами); это значит, что мы не можем использовать их в качестве имён для переменных или чего-то другого; формально они вообще не считаются идентификаторами, отсюда название “зарезервированные”: свойство зарезервированности как раз и проявляется в том, что эти слова не могут служить идентификаторами.

С другой стороны, слова write, writeln и даже слово integer, хотя и введены Паскалем, тем не менее представляют собой обыкновенные идентификаторы (на самом деле write и writeln — не совсем обыкновенные, а вот integer и boolean — действительно обыкновенные), то есть мы можем описать переменную с именем integer (какого-нибудь другого типа), но при этом потеряем возможность использовать тип integer, ведь его имя будет занято под переменную. Конечно же, так делать не следует; но стоит иметь в виду, что такая возможность существует, хотя бы из соображений общей эрудиции.

Стоит отметить, что в делении слов, предоставляемых языком Паскаль, на зарезервированные слова к “встроенные идентификаторы” наблюдается определённая степень произвола. Например, слова true и false, используемые для обозначения логической истины и логической лжи, в классических вариантах Паскаля, а также и в знаменитом Turbo Pascal считались простыми идентификаторами (о чём большинство программистов даже не подозревало, поскольку никогда и никому не приходило в голову использовать эти слова для чего-то другого). Создатели FreePascal сочли это неудобным, так что компилятор Free Pascal рассматривает эти два слова (а также new, dispose и exit) как зарезервированные.

2.2.4. Ввод информации для её последующей обработки

Кроме присваивания, существуют и другие ситуации, в которых переменная изменяет своё текущее значение; один из самых типичных примеров — выполнение так называемых операций ввода. В ходе такой операции программа получает информацию из внешнего источника — с клавиатуры, из файла на диске, из канала связи по компьютерной сети и т. п.; естественно, полученную информацию нужно где-то сохранить, и для этого используются переменные.

В языке Паскаль наиболее популярным средством для операций ввода служит оператор ввода, обозначаемый словом read, и его вариация readln. Рассмотрим для начала простенький пример — программу, которая вводит с клавиатуры целое число, возводит его в квадрат и печатает полученный результат:

Как видим, в программе используется одна переменная типа integer, которая называется х. Главная часть программы при этом включает в себя три оператора. Первый из них, read, предписывает выполнить чтение целого числа с клавиатуры, после чего занести прочитанное число в переменную х. Выполнение программы при этом остановится до тех пор, пока пользователь не введёт число, причём в связи с определёнными особенностями режима работы терминала (точнее, в нашем случае — его программного эмулятора, который повторяет особенности работы настоящих терминалов) программа “увидит” введённое число не раньше, чем пользователь нажмёт клавишу Enter.

Второй оператор в нашем примере — это оператор присваивания; выражение, стоящее в его правой части, берёт текущее значение переменной х (то есть значение, которое только что было прочитано с клавиатуры оператором read), умножает его само на себя, т. е. возводит в квадрат, а результат заносит обратно в переменную х. Третий оператор — уже хорошо знакомый нам writeln — печатает получившийся результат. Выполнение этой программы может выглядеть так:

Сразу после запуска программа “замирает”, так что неопытный пользователь может подумать, что она вообще зависла, но на самом деле программа просто ждёт, когда пользователь введёт требуемое число. В примере выше число 25 ввёл пользователь, а число 625 выдала программа.

Кстати, сейчас самое время показать, почему выражение “ввод с клавиатуры” не вполне соответствует действительности и правильнее будет говорить о “вводе из стандартного потока ввода”. Для начала создадим текстовый файл, содержащий одну строку, а в этой строке — число, пусть это для разнообразия будет S7. Файл мы назовём num.txt. Для его создания можно воспользоваться тем же редактором текстов, который вы применяете для ввода текстов программ, но можно поступить и проще — например, так:

Теперь запустим нашу программу square, перенаправив ей ввод из файла num.txt:

Число 1369 — это квадрат числа 37; его напечатала наша программа. При этом исходное число она, как мы видим, с клавиатуры не вводила — она в соответствии с нашими указаниями прочитала его из файла num.txt. В этом несложно убедиться: отредактируйте файл num.txt, заменив число 37 на какое-нибудь другое, и снова запустите программу quad с перенаправлением из файла, как показано в примере; в этот раз программа напечатает квадрат того числа, которое вы занесли в файл.

В нашем примере мы сначала использовали системную команду echo с перенаправлением вывода, чтобы сформировать файл, содержащий число, а затем запускали нашу программу с перенаправлением ввода из этого файла. Примерно тех же результатов можно добиться без всяких промежуточных файлов, запустив команду echo одновременно с нашей программой так называемым конвейером (см. § 1.4.9); при этом вывод команды echo пойдёт непосредственно на ввод нашей программе. Делается это так:

Перенаправим вывод в файл result.txt:

В этот раз на экран вообще ничего не вывелось, зато результат оказался записан в файл, в чём легко убедиться:

Теперь мы знаем на собственном опыте: вывод “на экран” далеко не всегда идёт на экран, а ввод “с клавиатуры” отнюдь не всегда осуществляется с клавиатуры. Именно поэтому правильнее говорить о выводе в стандартный поток вывода и о вводе из стандартного потока ввода. Подчеркнём, что сама программа вообще не знает, откуда идёт её ввод и куда направлен её вывод, все перенаправления осуществляет интерпретатор командной строки непосредственно перед тем, как запустить нашу программу.

2.2.5. Берегись нехватки разрядности!

Приехал как-то Илья Муромец рубить голову Змею Горынычу.

Приехал — и отрубил Змею голову.

А у Змея две головы выросли.

Отрубил Илья Муромец Змею две головы — а у того четыре выросли.

Отрубил он ему четыре — а у того восемь голов выросло.

Рубил Муромец головы, рубил, рубил-рубил,

в конце концов нарубил в общей сложности 65535 голов —

вот тут-то и помер Змей Горыныч.

Потому что был он шестнадцатиразрядный.

Продолжим эксперименты с программой square, начатые в предыдущем параграфе, но на этот раз возьмём числа побольше.

Если с первыми двумя запусками всё было вроде бы в порядке, то на третьем что-то явно пошло не так. Чтобы понять, что происходит, вспомним, что переменные типа integer в Паскале могут принимать значения от -32768 до 32767; но ведь квадрат числа 200 равен 40000, то есть в переменную типа integer он попросту не помещается! Отсюда нелепый результат, ко всему ещё и отрицательный.

Результат, несмотря на его нелепость, очень просто объяснить. Мы уже знаем, что имеем дело со знаковым целым числом к у нас произошло переполнение (см. § 1.6.2). Разрядность наших чисел составляет 16 бит, так что при переполнении итоговое число получается на 216 = 65536 меньше, чем должно быть. Правильный результат должен был составлять 2002 = 40000, но в результате переполнения из него вычлось 65536, так что результат составил 40000 — 65536 = -25536; ровно это мы к наблюдаем в примере.

Наибольшее число, которое наша программа обрабатывает корректно — 181, его квадрат составляет 32761; квадрат числа 182, составляющий 33124, в разрядность числа типа integer уже “не лезет”. Но всё не так страшно, просто нужно применить другой тип переменной. Наиболее очевидным кандидатом на роль такого типа оказывается longint, имеющий разрядность 32 бита; переменные этого типа могут принимать значения от -214748S648 до 214748S647 (т. е. от —231 до 231 — 1). В программе достаточно изменить одно слово — просто заменить integer на longint:

и возможности нашей программы (после её перекомпиляции) резко возрастут:

Конечно, радоваться рано, свой предел есть и здесь:

но это всё же лучше, чем то, что было.

Можно расширить разрядность числа ещё сильнее, применив тип int64, использующий знаковые 64-битные числа14. После замены longint на int64 и перекомпиляции наша программа сможет возвести в квадрат прямо-таки “огромные” числа:

хотя, конечно, кто ищет — тот всегда найдёт; разумеется, максимально возможное число есть и для int64:

Последнее, что мы можем сделать для расширения диапазона чисел — это заменить знаковые числа беззнаковыми. Много это не даст, мы выгадаем всего один бит, но чисел разрядности, превышающей 64, в Паскале (во всяком случае, во Free Pascal) нет. Итак, меняем int64 на qword (от слов quadro word, то есть “учетверённое слово”; под “словом” на архитектурах семейства х86 традиционно понимается 16 бит) и пробуем:

Поскольку максимально возможное значение нашей переменной теперь составляет 264 — 1 = 18446744073709551615, мы можем предсказать, на каком числе программа даст сбой. Квадрат числа 232 составляет 264, что на единицу больше допустимого. Следовательно, наибольшее число, которое наша программа ещё сможет возвести в квадрат — это 232 — 1 = 4294967295. Проверяем:

Вот такой вот несколько неожиданный эффект от переполнения, или, говоря более строго, от переноса в несуществующий разряд, ведь мы на сей раз имеем дело с беззнаковыми. Помните анекдот про Змея Горыныча?






Для любых предложений по сайту: [email protected]