Программирование: введение в профессию. 1: Азы программирования - 2016 год
Строки - Язык Паскаль и начала программирования
2.9.1. Строковые литералы и массивы сhаr’ов
Мы уже неоднократно встречались со строками в виде строковых литералов — заключённых в апострофы фрагментов текста; до сей поры они попадались нам только в операторах write и writeln, а писали мы их с одной-единственной целью: немедленно выдать на печать.
Между тем, как мы уже обсуждали во вводной части, текст — это наиболее универсальное представление для едва ли не любой информации; единственным исключением выступают данные, полученные путём оцифровки тех или иных аналоговых процессов — фотографии, звуковые и видеофайлы, которые тоже, в принципе, возможно представить в виде текста, но очень неудобно. Всё остальное, включая, например, картинки, нарисованные человеком с помощью графических редакторов, представлять в виде текста удобно и практично; очевидным следствием этого является острая необходимость уметь писать программы, которые обрабатывают текстовую информацию.
С простейшими случаями обработки текстов мы уже встречались в параграфе, посвящённом программам-фильтрам (см. § 2.7.3), где получали один текст из другого, который анализировали с помощью посимвольного чтения. На практике обработка текстов по одному символу часто оказывается неудобной и хочется рассматривать фрагменты текстов — то есть строки — как единое целое.
Поскольку строка — это последовательность символов, а символы можно представить с помощью значений типа char, логично предположить, что для обработки строк можно использовать массивы элементов типа char. Это действительно возможно; Паскаль даже допускает для таких массивов довольно странные действия, которые ни с какими иными массивами делать нельзя. Например, мы знаем, что массивы можно присваивать друг другу, только если они имеют строго один и тот же тип; но массиву, состоящему из сhаr’ов, Паскаль позволяет присвоить значение, заданное строковым литералом, то есть хорошо знакомой нам последовательностью символов, заключённой в апострофы:
Эта программа успешно пройдёт компиляцию и даже, на первый взгляд, нормально отработает, напечатав сакраментальное “Hello, world!”. Но лишь на первый взгляд; на самом же деле после привычной нам фразы программа “напечатает” ещё 17 символов с кодом 0 и лишь затем выдаст перевод строки и завершится; в этом можно легко убедиться самыми разными способами: перенаправить вывод в файл и посмотреть на его размер, запустить программу конвейером в связке со знакомой нам программой wc, применить hexdump (можно сразу к выводу, можно к получившемуся файлу). Символ с кодом 0, будучи выдан на печать, никак себя на экране не проявляет, даже курсор никуда не двигается, так что этих 17 “лишних” нулей не видно — но они есть и могут быть обнаружены. Что особенно неприятно, так это то, что формально в текстовых данных этот незримый “нулевой символ” встречаться не имеет права, так что выдача нашей программы перестала, формально говоря, быть корректным текстом.
Догадаться, откуда взялись эти паразитные “нолики”, несложно: фраза “Hello, world!” состоит из 13 символов, а массив мы объявили из 30 элементов. Оператор присваивания, скопировав 13 символов из строкового литерала в элементы массива hello[1], hello[2], ..., hello[13], остальные элементы, за неимением лучшего, забил нулями.
Конечно, программу можно исправить и сделать корректной, например, вот так (напомним, что оператор break досрочно прекращает выполнение цикла):
но уж очень это громоздко — мы ведь в очередной раз вынуждены обрабатывать строку по одному символу!
Ситуация, когда нам нужно обрабатывать строку, не зная заранее её длины — совершенно типична. Представьте себе, что вам нужно спросить у пользователя, как его зовут; один ответит лаконичным “Вова”, а другой заявит, что он не менее как Остап-Сулейман-Берта-Мария Бендер-Задунайский. Вопрос, можно ли это предугадать на этапе написания программы, следует считать риторическим. В связи с этим для работы со строками желательно иметь какое-то гибкое средство, учитывающее вот это вот фундаментальное свойство строки — иметь непредсказуемую длину. Кроме того, над строками очень часто выполняется операция конкатенации (присоединения одной строки к другой), и её желательно обозначить как-нибудь так, чтобы её вызов был для программиста необременителен. Между прочим, частный случай конкатенации — добавление к строке одного символа — настолько распространён, что, привыкнув к нему, вы в будущем при работе на Си (где это действие требует куда больших усилий) ещё долго будете эту возможность вспоминать с ностальгией.
Так или иначе, для решения разом большинства проблем, возникающих при работе со строками, в Паскале — точнее, в его поздних диалектах, включая Turbo Pascal и, конечно, наш Free Pascal, предусмотрено специальное семейство типов, которым будет посвящён следующий параграф.
Отметим, что в составе строкового литерала мы можем изобразить не только символы, имеющие печатное представление, но и любые другие — через их коды. В примерах нам уже встречались строки, оканчивающиеся символом перевода строки, такие как ’Неllо’#10; “хитрые” символы можно вставлять не только в начало или конец строкового литерала, но и в произвольное место строки, например, ’one’#9’two’ — здесь два слова разделены символом табуляции. Вообще в составе строкового литерала можно произвольным образом чередовать последовательности символов, заключённых в апострофы, и символы, заданные их кодами; например,
есть валидный строковый литерал, результат выдачи которого на печать (за счёт вставленных в него табуляций и переводов строки) будет выглядеть примерно так:
Символы с кодами от 1 до 26 можно также изображать как ˆА, ˆВ, ..., ˆZ; это оправдывается тем, что при вводе с клавиатуры символы с соответствующими кодами, как уже упоминалось, порождаются комбинациями Ctrl-A, Ctrl-B и т.д. В частности, литерал из нашего примера можно записать и так:
2.9.2. Тип string
Введённый в Паскале специально для работы со строками тип string фактически представляет собой частный случай массива из элементов типа char, но случай довольно нетривиальный. Прежде всего отметим, что при описании переменной типа string можно указать, а можно не указывать предельный размер строки, но “бесконечной” строка от этого не станет: максимальная её длина ограничена 255 символами. Например:
Переменная s1, описанная таким образом, может содержать строку длиной до 15 символов включительно, а переменная s2 — строку длиной до 255 символов. Указывать число больше 255 нельзя, это вызовет ошибку, и этому есть довольно простое объяснение: тип string предполагает храпение длины строки в отдельном байте, ну а байт, как мы помним, число больше 255 хранить не может.
Переменная типа string занимает на один байт больше, чем предельная длина хранимой строки: например, наши переменные s1 и s2 будут занимать соответственно 16 и 256 байт. С переменной типа string можно работать как с простым массивом элементов типа char, при этом индексация элементов, содержащих символы строки, начинается с единицы (для s1 это будут элементы с s1[1] по s1[15], для s2 — элементы s2[1] по s2[255]), но — несколько неожиданно — в этих массивах обнаруживается ещё один элемент, имеющий индекс 0. Это и есть тот самый байт, который используется для хранения длины строки; поскольку элементы массива обязаны быть одного типа, этот байт, если к нему обратиться, имеет тот же тип, что и остальные элементы — то есть char. К примеру, если выполнить присваивание
то выражение s1[1] будет равно ’а’, выражение s1[5] — ’к’; поскольку в слове “abrakadabra” 11 букв, последним осмысленным элементом будет s1[11], он тоже равен ’a’, значения элементов с большими индексами не определены (там может содержаться всё что угодно, и особенно то, что не угодно). Наконец, в элементе s1[0] будет содержаться длина, но поскольку s1[0] — это выражение типа char, было бы неправильно сказать, что оно будет равно 11; на самом деле оно будет равно символу с кодом 11, который обозначается как #11 или ˆК, а длину строки можно получить, вычислив выражение ord(s1[0]), которое и даст искомое 11. Впрочем, есть более общепринятый способ узнать длину строки: воспользоваться встроенной функцией length, которая специально для этого предназначена.
Вне всякой зависимости от предельной длины все переменные и выражения типа string в Паскале совместимы по присваиванию, то есть мы можем заставить компилятор выполнить как s2 := s1, так и s1 := s2, причём в этом втором случае строка при присваивании может оказаться обрезана; в самом деле, возможности s2 несколько шире, никто не мешает этой переменной содержать строку, скажем, в 50 символов длиной, но в s1 больше 15 символов загнать нельзя.
Что особенно приятно, при присваивании стрингов, как и при передаче их по значению в подпрограммы, и при возврате из функций копируется только значащая часть переменной; например, если в переменной, объявленной как string без указания предельной длины, хранится строка из трёх символов, то только эти три символа (плюс байт, содержащий длину) и будут копироваться, несмотря на то, что переменная целиком занимает 256 байт.
Ещё интереснее, что, если ваша подпрограмма принимает var-параметр типа string, то подать в качестве этого параметра можно любую переменную типа string, в том числе такую, для которой при описании ограничена длина. Это исключение из общего правила (о тождественном совпадении типа переменной с типом var-параметра) выглядит некрасиво и небезопасно, но оказывается очень удобным на практике.
Переменные типа string можно “складывать” с помощью символа “+”, который для строк означает соединение их друг с другом. Например, программа
напечатает, как можно догадаться, слово “abrakadabra”.
Строки бывают пустыми, то есть не содержащими ни одного символа. Длина такой строки равна нулю; литерал, обозначающий пустую строку, выглядит как “’’” (два символа апострофа, поставленные рядом); этот литерал не следует путать с “’ ’”, который обозначает символ пробела (или же строку, состоящую из одного символа — пробела).
Выражения типа char практически во всех случаях могут быть неявно преобразованы к типу string — строке, содержащей ровно один символ. Это особенно удобно в сочетании с операцией сложения. Так, программа
напечатает “ABCDEFGHIJKLMNOPQRSTUVWXYZ”.
На самом деле Free Pascal поддерживает целый ряд типов для работы со строками, причём многие из этих типов не имеют ограничений, присущих типу string — то есть могут, например, хранить в себе текст совершенно произвольной длины, работать с многобайтовыми кодировками символов к т. п. Мы не будем их рассматривать, поскольку при освоении материала следующих частей книги всё это великолепие не потребуется; читатель, решивший сделать Free Pascal своим рабочим инструментом (в противоположность учебному пособию), может освоить эти возможности самостоятельно.
2.9.3. Встроенные функции для работы со строками
Без материала этого параграфа можно легко обойтись, ведь со строками можно работать на уровне отдельных символов, а значит, мы можем сделать с ними буквально что угодно без всяких дополнительных средств. Тем не менее, при обработке строк ряд встроенных процедур и функций может существенно облегчить жизнь, поэтому мы всё же приведём некоторые из них.
Одну функцию мы уже знаем: length принимает на вход выражение типа string и возвращает целое число — длину строки. Текущую длину строки можно изменить принудительно процедурой SetLength; например, после выполнения
в переменной s будет содержаться строка “аbrа”. Учтите, что SetLength может также к увеличить длину строки, в результате чего в конце её окажется “мусор” — непонятные символы, которые лежали в этом месте памяти до того, как там разместили строку; поэтому если вы решили увеличить длину строки с помощью SetLength, правильнее всего будет немедленно заполнить чем-нибудь осмысленным все её “новые” элементы. Учтите, что меняется только текущая длина строки, но никоим образом не размер области памяти, которая под эту строку выделена. В частности, если вы опишете строковую переменную на десять символов
а затем попытаетесь установить ей длину, превышающую 10, ничего хорошего у вас не получится: переменная s10 не может содержать строку длиннее десяти символов.
Все процедуры и функции, перечисленные ниже до конца параграфа, выполняют действия, которые необходимо уметь делать самостоятельно; мы крайне не рекомендуем пользоваться этими средствами, пока вы не научитесь свободно обращаться со строками на уровне посимвольной обработки. Каждую из перечисленных ниже функций и процедур можно начинать использовать не раньше, чем вы убедитесь, что можете сделать то же самое “вручную”.
Встроенные функции LowerCase и UpCase принимают на вход выражение типа string и возвращают тоже string — такую же строку, какая содержалась в параметре, за исключением того, что латинские буквы оказываются приведены соответственно к нижнему или к верхнему регистру (иначе говоря, первая функция в строке заменяет заглавные буквы на строчные, а вторая, наоборот, строчные на заглавные).
Функция copy принимает на вход три параметра: строку, начальную позицию и количество символов, и возвращает подстроку заданной строки, начиная с заданной начальной позиции, длиной как заданное количество символов, либо меньше, если символов в исходной строке не хватило. Например, copy(’a2rakadabra’, 3, 4) вернёт строку ’raka’, a copy(’foobar’, 4, 5) — строку ’bar’.
Процедура delete тоже принимает на вход строку (на этот раз через параметр-переменную, потому что строка будет изменена), начальную позицию и количество символов, и удаляет из данной строки (прямо на месте, то есть в той переменной, которую вы передали параметром) заданное количество символов, начиная с заданной позиции (либо до конца строки, если символов не хватило). Например, если в переменной s содержалось всё то же “abrakadabra”, то после выполнения delete(s, 5, 4) в переменной s окажется “abrabra”; а если бы мы применили delete(s, 5, 100), получилось бы просто “abra”.
Встроенная процедура insert вставляет одну строку в другую. Первый параметр задаёт вставляемую строку; вторым параметром задаётся переменная строкового типа, в которую надлежит вставить заданную строку. Наконец, третий параметр (целое число) указывает позицию, начиная с которой следует произвести вставку. Например, если в переменную s занести строку “abcdef”, а потом выполнить insert(’PQR’, s, 4), то после этого в переменной s будет находиться строка “abcPQRdef”.
Функция pos принимает на вход две строки: первая задаёт подстроку для поиска, вторая — строку, в которой следует производить поиск. Возвращается целое число, равное позиции подстроки в строке, если таковая найдена, или 0, если не найдена. Например, pos(’kada’, ’abrakadabra’) вернёт 5, а pos(’aaa’, ’abrakadabra’) вернёт 0.
Может оказаться очень полезной процедура val, которая строит число типа longint, integer или byte по его строковому представлению. Первым параметром в процедуру подаётся строка, в которой должно содержаться текстовое представление числа (возможно, с некоторым количеством пробелов перед ним); вторым параметром должна быть переменная типа longint, integer или byte; третьим параметром указывается ещё одна переменная, всегда имеющая тип word. Если всё прошло успешно, во второй параметр процедура занесёт полученное число, в третий — число 0; если же произошла ошибка (то есть строка не содержала корректного представления числа), то в третий параметр заносится номер позиции в строке, где перевод в число дал сбой, а второй параметр в этом случае остаётся неопределённым.
Паскаль также предоставляет средства для обратного перевода числа в строковое представление. Если представление требуется десятичное, то можно воспользоваться псевдопроцедурой str; здесь можно использовать спецификаторы количества символов аналогично тому, как мы это делали для печати в операторе write. Например, str(12.5:9:3, s) занесёт в переменную s строку “ 12.500” (с тремя пробелами в начале, чтобы получилось ровно девять символов).
Для перевода в двоичную, восьмеричную к шестнадцатеричную систему также имеются встроенные средства, но на этот раз это почти обычные функции, которые называются BinStr, Octstr и HexStr. В отличие от str, они работают только с целыми числами и являются функциями, то есть полученную строку возвращают в качестве своего значения, а не через параметр. Все три зачем-то предусматривают два параметра: первый — целое число произвольного типа, второй — количество символов в результирующей строке.
2.9.4. Обработка параметров командной строки
Запуская разные программы в командной строке ОС Unix, мы часто указывали при запуске, кроме имени программы, ещё параметры командной строки. Например, при запуске редактора текстов и компилятора fpc мы всегда указываем имя файла, в котором содержится или будет содержаться текст нашей программы. Ясно, что и редактор текстов, и компилятор тоже представляют собой программы и, следовательно, как-то могут обращаться к своим параметрам командной строки; может так поступить и программа, которую мы сами пишем на Паскале (да и на любом другом языке тоже).
Пусть наша программа называется “demo”; пользователь может запустить её, например, так:
В этом случае говорят, что командная строка состоит из четырёх параметров: самого имени программы, слова “аbга”, слова “schwabra” и слова “kadabra”.
В программе на Паскале эти параметры доступны с помощью встроенных функций ParamCount и ParamStr; первая, не получая никаких параметров, возвращает целое число, соответствующее количеству параметров без учёта имени программы (в нашем примере это будет число 3); вторая функция принимает на вход целое число и возвращает строку (string), соответствующую параметру командной строки с заданным номером. При этом имя программы считается параметром номер 0 (то есть его можно узнать с помощью выражения ParamStr(0)), а остальные нумеруются с единицы до числа, возвращённого функцией ParamCount.
Напишем для примера программу, которая печатает все элементы своей командной строки, сколько бы их ни оказалось:
После компиляции мы можем попробовать эту программу в действии, например:
Следует обратить внимание на то, что фраза из трёх слов, заключённая в двойные кавычки, оказалась воспринята как один параметр; это не имеет отношения к языку Паскаль и является свойством командного интерпретатора.