Справочник от Автор24
Поделись лекцией за скидку на Автор24

Основы программирования

  • ⌛ 2014 год
  • 👀 744 просмотра
  • 📌 699 загрузок
Выбери формат для чтения
Загружаем конспект в формате doc
Это займет всего пару минут! А пока ты можешь прочитать работу в формате Word 👇
Конспект лекции по дисциплине «Основы программирования» doc
КУРС ЛЕКЦИЙ по дисциплине «Основы программирования» Воронеж 2014 Лекция №1 Тема: Этапы решения задач. Понятие алгоритма и языка программирования. Вопросы: 1. Использование ЭВМ для решения практических задач. 2. Семь этапов решения задач, понятие алгоритма и языка программирования. 1. Беседа по теме «Использование ЭВМ для решения практических задач» 2. Семь этапов решения задач, понятие алгоритма и языков программирования. Выделим главные этапы методики программирования задач. 1. Постановка задачи. Основное требование к постановке задачи – достаточное количество информации для решения задачи. Очень часто постановка задачи выполняется не программистом, а некоторым Заказчиком. Программист является Исполнителем заказа. От него требуется добиться от Заказчика полной информации о решаемой задаче. 2. Моделирование и формализация задачи. Цели этого этапа уже обсуждались выше в разделе методики разработки алгоритма. При моделировании важно иметь опыт программирования, знать возможности компьютера и языка программирования и выдвигать гипотезы с учетом этих возможностей. К разработке алгоритма следует приступать только после принятия гипотезы решения задачи.  Помимо идеи решения задачи, результатами этого этапа должны быть формализованная постановка задачи типа "дано-найти" и достаточное количество контрольных примеров для последующего тестирования программы. К категории "Дано:" обычно относятся данные, вводимые в начале работы программы и обеспечивающие массовость алгоритма. К категории "Найти:" относятся данные, получаемые в результате работы программы. 3. Разработка алгоритма. Этот этап представляет собой реализацию идеи решения задачи. 4. Тестирование алгоритма. Этап предполагает проверку алгоритма вручную с использованием подготовленных ранее контрольных примеров. Для сложных задач этот этап может оказаться весьма трудоемким, поэтому опытные программисты пропускают его и тестируют программу.  5. Программирование алгоритма. Программирование является формальной записью алгоритма средствами языка программирования. 6. Тестирование программы. Тестирование выполняется путем вывода промежуточных результатов работы программы и сравнения их с контрольным примером. Для этого либо используют специальные средства отладки программ, имеющиеся в интегрированной среде языка программирования, либо временно добавляют в программу команды вывода промежуточных значений. Уменьшить трудоемкость поиска ошибок в программе можно более тщательным проектированием алгоритма и планированием процесса тестирования на ранних стадиях разработки программы. 7. Эксплуатация программы и интерпретация результатов. В сложных программах может быть недостаточно тестирования для устранения всех ошибок. Очень часто они обнаруживаются на стадии эксплуатации.  Пример 1. Рассмотрим задачу с достаточно сложным алгоритмом решения для того, чтобы, во-первых, продемонстрировать этапы 1 - 3 рассмотренной методики и, во-вторых - принцип поэтапной детализации алгоритма.    Постановка задачи. Существует способ обойти шахматным конем доску, побывав на каждом поле по одному разу. Построить алгоритм обхода доски. Идея решения задачи. Очередной ход следует делать на то поле, с которого на другие поля меньше всего ходов. Формализация задачи. Назовем термином "потенциал поля" количество допустимых ходов коня. Введем следующие обозначения: • С — матрица 8*8, содержащая потенциалы полей (фрагмент C показан на рис. 13); • R — матрица 8*8, содержащая решение задачи в виде номеров ходов коня; • Sx, Sy — массивы из 8 элементов, содержащие смещения коня относительно текущей координаты, необходимые для реализации правила буквы "Г": Sx = ( 1, 2, 2, 1,-1,-2,-2,-1); Sy = (-2,-1, 1, 2, 2, 1,-1,-2). • x, y — текушие координаты коня; • x1,y1 — координаты поля с минимальным потенциалом для текущих (x, y); • m — значение минимального потенциала допустимого поля. Будем учитывать пройденные поля путем задания соответствующим элементам матрицы C значения 9, т.е. значения вне множества допустимых потенциалов. Разработка алгоритма решения задачи. На рис. 7 показан укрупненный алгоритм решения поставленной задачи. На рис. 8 - 11 показаны основные шаги поэтапной детализации основного алгоритма.     Следует обратить внимание на нумерацию блоков в детализирующих блок-схемах. Число до первой точки является номером детализируемого блока в основной схеме. Число после первой точки является номером блока в схеме детализации первого уровня и т.д. Входы в детализирующие блок-схемы и выходы из них показаны окружностями с номерами блоков — источников информации и получателей результатов. Значком & на рис. 11 обозначена логическая операция И. Пример 3. Поскольку тестирование вручную алгоритма решения задачи о шахматном коне было бы достаточно громоздким, рассмотрим технологию тестирования  на примере алгоритма Евклида (рис. 13). Для тестирования вручную следует оставить достаточно свободного места справа от блок-схемы. Контрольный пример не должен быть слишком сложным - это затрудняет тестирование, но и не быть тривиальным - это может привести к случайному совпадению с правильным решением. В первом столбце таблицы справа от блок-схемы, записываются переменные или условия, значения которых могут изменяться. Начиная со второго столбца сверху-вниз записываются результаты выполнения алгоритма. Начало нового цикла соответствует добавлению нового столбца таблицы. Исполнитель (разработчик алгоритма) должен выполнять команды формально, строго придерживаясь предписаний в блок-схеме.  Лекция №2 Тема: Понятие алгоритма и языка программирования. Вопросы: 1. Понятие алгоритма. Свойства и способы представления алгоритмов. 2. Основные алгоритмические структуры. 3. Понятие и описание языков программирования. Эволюция языков программирования, их классификация. Алгоритм - конечный набор правил, расположенных в определенном логическом порядке, позволяющий исполнителю решать любую конкретную задачу из некоторого класса однотипных задач. Алгоритм от обычного предписания отличают следующие свойства: + однозначность — наличие единственного толкования пра­вил выполнения действий и порядка их выполнения; + конечность — обязательное завершение каждого из дей­ствий, составляющих алгоритм, а также завершение вы­полнения алгоритма в целом; + результативность — получение при выполнении алгоритма определенного результата; + массовость — возможность применения алгоритма дл решения целого класса задач (предполагается его пра­вильная работа при меняющихся в заданных пределах значениях исходных данных); + правильность — способность алгоритма давать правильные результаты при решении поставленных задач. Процессор ЭВМ умеет выполнять лишь простейшие команды. Для решения этих задач программист должен составить подробное описание последовательности действий, которые необходимо выполнить центральному процессору компьютера. Составление такого пошагового описания процесса решения задачи называется алгоритмизацией, а алгоритмом называется конечный набор правил, расположенных в определенном логическом порядке, позволяющий исполнителю решать любую конкретную задачу из некоторого класса однотипных задач. Алгоритм – заранее заданное понятное и точное предписание возможному исполнителю совершит определенную последовательность действий для получения решения задачи за конечное число шагов. В программировании алгоритм является фундаментом программы, а основным исполнителем — компьютер. На стадии тестирования алгоритма исполнителем может быть сам программист. Алгоритм должен удовлетворять определенным требованиям. Принято выделять следующие семь: 1. наличие ввода исходных данных. 2. наличие вывода результата выполнения. 3. однозначность (компьютер «понимает» только однозначные инструкции). 4. общность – алгоритм предназначен для решения некоторого класса задач. 5. корректность – алгоритм должен давать правильное решение задачи. 6. конечность – решение задачи должно быть получено за конечное число шагов. 7. эффективность – для решения задачи должны использоваться ограниченные ресурсы компьютера (процессорное время, объем оперативной памяти и т.д.). На практике наиболее распространены следующие формы представления алгоритмов: ◦ Словесная (запись на естественном языке); ◦ Графическая (изображения в виде графических символов); ◦ Псевдокоды (полуформализованные описания алгоритмов на условном алгоритмическом языке, включающие как элементы языка программирования, так и фразы естественного языка, общепринятые математические обозначения и т.д.); ◦ Программная (тексты на языках программирования). Словесный способ. Представляет собой описание последовательных этапов обработки данных. Алгоритм задается в произвольном изложении на естественном языке. Например, алгоритм нахождения наибольшего общего делителя двух натуральных чисел может быть следующим: 1. задать два числа; 2. если два числа равны, то взять любое из них в качестве ответа и остановиться, в противном случае продолжить выполнение алгоритма; 3. определить большее из чисел; 4. заменить большее из чисел разностью большего и меньшего из чисел; 5. повторить алгоритм с шага 2. Графический способ является более компактным и наглядным по сравнению со словесным. При графическом представлении алгоритм изображается в виде последовательности связанных между собой функциональных блоков, каждый из которых соответствует выполнению одного или нескольких действий. Такое графическое представление принято представлять в виде блок-схемы. Для изображения основных алгоритмических структур и блоков на блок-схемах используют специальные графические символы, показанные в таблице 1. Таблица 1. Основные блочные символы. Обозначение Функция Начало, конец. Процесс (выполнение операции или группы операций, в результате которых изменяются значение, форма представления или расположение данных). Ввод, вывод. Условие (выбор направления выполнения алгоритма или программы в зависимости от некоторых переменных условий). Цикл с параметром. Соединитель. Процедуры и функции (использование ранее созданных и отдельно описанных алгоритмов или программ). В теории алгоритмов доказано, что любой, сколь угодно сложный алгоритм может быть составлен из трех основных алгоритмических структур: линейной, ветвления и цикла, показанных, соответственно на рис. 1, 2, 3. Линейная структура предполагает последовательное выполнение действий, без их повторения или пропуска некоторых действий. Обычно программисты стремятся к тому, чтобы алгоритм имел линейную структуру. Структура "ветвление" предполагает выполнение одной из двух групп действий в зависимости от выполнения условия в блоке ветвления. На рис. 2 знаком "+" показано выполнение условия, а знаком "-" — его невыполнение. Часто используется неполная команда ветвления, когда один из блоков действия отсутствует. Структура "цикл" имеет несколько разновидностей. На рис. 3 показан цикл типа "пока" с предусловием. Действия внутри этого цикла повторяются пока выполняется условие в блоке ветвления, причем сначала проверяется условие, а затем выполняется действие. Достаточно часто используются другие типы цикла, показанные на рис. 4 и 5. В цикле с постусловием проверка условия выхода из цикла выполняется после очередного действия. Цикл "для" является модификацией цикла "пока" для ситуации, когда заранее известно количество повторений некоторых действий. Запись в блоке заголовка цикла на рис.5 показывает пример описания заголовка цикла, в котором действия повторяются столько раз, сколько целых значений приобретает параметр цикла i от своего начального значения 1 до конечного N с шагом 1. Обычно шаг не указывается, если он равен 1. В языках программирования имеются команды, реализующие показанные выше структуры. При разработке блок-схемы допускается делать любые записи внутри блоков, однако эти записи должны содержать достаточно информации для выполнения очередных действий. Пример 1. Разработать блок-схему алгоритма Евклида, определяющего наибольший общий делитель (НОД) двух натуральных чисел A и B. В основе алгоритма Евклида лежит правило: НОД(A,B)= НОД(min(A,B), |A-B|), где НОД(A,B) — наибольший общий делитель двух натуральных чисел A и B. Основной идеей решения задачи является многократное применение указанного выше правила, после которого большее из чисел очередной пары уменьшается. Решение получено, когда числа оказываются равны друг другу. Поскольку количество повторений заранее неизвестно, в алгоритме следует применить цикл "пока" с предусловием (рис. 6). Методика разработки алгоритмов. Разработке алгоритма предшествуют такие этапы, как формализация и моделирование задачи. Формализация предполагает замену словесной формулировки решаемой задачи краткими символьными обозначениями, близкими к обозначениям в языках программирования или к математическим. Моделирование задачи является важнейшим этапом, целью которого является поиск общей концепции решения. Обычно моделирование выполняется путем выдвижения гипотез решения задачи и их проверке любым рациональным способом (прикидочные расчеты, физическое моделирование и т.д.). Результатом каждой проверки является либо принятие гипотезы, либо отказ от нее и разработка новой. При разработке алгоритма используют следующие основные принципы. 1. Принцип поэтапной детализации алгоритма (другое название — "проектирование сверху-вниз"). Этот принцип предполагает первоначальную разработку алгоритма в виде укрупненных блоков (разбиение задачи на подзадачи) и их постепенную детализацию. 2. Принцип "от главного к второстепенному", предполагающий составление алгоритма, начиная с главной конструкции. При этом, часто, приходится "достраивать" алгоритм в обратную сторону, например, от середины к началу. 3. Принцип структурирования, т.е. использования только типовых алгоритмических структур при построении алгоритма. Нетиповой структурой считается, например, циклическая конструкция, содержащая в теле цикла дополнительные выходы из цикла. В программировании нетиповые структуры появляются в результате злоупотребления командой безусловного перехода (GoTo). При этом программа хуже читается и труднее отлаживается. Говоря о блок-схемах, как о средстве записи алгоритма, можно дать еще один совет по их разработке. Рекомендуется после внесения исправлений в блок-схему аккуратно перерисовывать ее с учетом этих исправлений. Аккуратность записи есть аккуратность мысли программиста. Аккуратно записанный и детализованный алгоритм упрощает его программирование и отладку. Программа — это детальное и законченное описание алгоритма средствами языка программирования. Исполнителем программы является компьютер. Для выполнения компьютером программа должна быть представлена в машинном коде — последовательности чисел, понимаемых процессором. Написать программу в машинных кодах вручную достаточно сложно. Поэтому сегодня практически все программы создаются с помощью языков программирования, которые по своему синтаксису и семантике приближены к естественному человеческому языку. Это снижает трудоемкость программирования. Однако, текст программы, записанный с помощью языка программирования, должен быть преобразован в машинный код. Эта операция выполняется автоматически с помощью специальной служебной программы, называемой транслятором. Основными свойствами программ для ЭВМ как одной из форм описания и разновидностей машинных алгоритмов является их выполнимость, мобильность, эффективность и правильность. Выполнимость программ - возможность их выполнения на дан­ном типе компьютеров. Возможность выполнения зависит от типа ЭВМ, наличия внешних устройств, надлежащего объема оперативной и внешней памяти, операционной системы и системы программиро­вания. Мобильность программ - возможность переноса программы на другой тип ЭВМ. Примером мобильности является возможность выполнения в системе структурного программирования Qbasic про­грамм, записанных на традиционном Бейсике. Эффективность программ - обычно это минимальность времени их выполнения на ЭВМ. Однако, если созданные программы содержат ошибки, то утверждения об их эффективности не имеют никакого смысла. Правильность программ - правильность результатов, получаемых с их помощью. Правильность результатов определяется соответствием докумен­тации или другими описаниями программ. Программы содержат ошибки, если их выполнение на ЭВМ при­водит к возникновению отказов, сбоев или неправильных резуль­татов. От использования программ, содержащих ошибки, следует отказываться. Правильность диалоговых алгоритмов и программ можно оценить сопоставлением их со сценарием диалога. Любое отклонение резуль­татов выполнения алгоритмов и программ от сценария диалога - это ошибка. Диалоговый алгоритм - правильный, если результаты их выполнения строго соответствуют сценарию. Сравнение текста программы с описанием алгоритма, а затем ал­горитма со сценарием диалога подтверждает полное соответствие программы заданному сценарию «выбор по меню». Таким образом, правильность программ может проверяться через правильность реализованных в них алгоритмах. Средства создания программ. В общем случае для создания программ нужно иметь следующие компоненты • текстовый редактор — для набора исходного текста программы; • компилятор — для перевода текста программы в машинный код; • редактор связей — для сборки нескольких откомпилированных модулей в одну программу; • библиотеки функций — для подключения стандартных функций к программе. Современные системы программирования включают в себя все указанные компоненты и называются интегрированными системами. Исходный текст программы можно получить без записи его вручную в текстовом редакторе. Существуют системы визуального программирования — RAD-среды (Rapid Application Development), которые, не исключая возможности записи программы вручную, позволяют создавать текст программы автоматически, путем манипуляций со стандартными элементами управления, включенными в RAD-среду. Поэтому для RAD-среды понятие «программирование» часто заменяют понятием «проектирование». По способу разработки программ можно выделить два подхода: • процедурное программирование — это программирование, при котором выполнение команд программы определяется их последовательностью, командами перехода, цикла или обращениями к процедурам; • объектно-ориентированное программирование – программирование, при котором формируются программные объекты, имеющие набор свойств, обладающие набором методов и способные реагировать на события, возникающие как во внешней среде, так и в самом объекте (нажатие мыши, срабатывание таймера, превышение числовой границы и т.д.). Таким образом, выполнение той или иной части программы зависит от событий в программной системе. Объектно-ориентированное программирование (ООП) не исключает, а охватывает технологию процедурного программирования. Основные системы программирования Из универсальных языков программирования наиболее популярны следующие: Basic; Pascal; C++; Java. Язык Pascal является компилируемым и широко используется как среда для обучения программированию в ВУЗах. RAD-средой, наследующей его основные свойства, является среда Borland Delphi. Для языка C++ RAD-средой является Borland C++ Builder. Этот компилируемый язык часто используется для разработки программных приложений, в которых необходимо обеспечить быстродействие и экономичность программы. Язык Java — интерпретируемый язык — позволяет создавать платформно-независимые программные модули, способные работать в компьютерных сетях с различными операционными системами. RAD-средой для него является Symantec Cafe. Основные этапы развития языков программирования Языки программирования развивались одновременно с развитием ЭВМ. С начала 50-х годов это были низкоуровневые языки (машинные и ассемблеры). В 1956 году появился язык Фортран, а в 1960 — Алгол-60. Это языки компилирующего типа, существенно уменьшившие трудоемкость программирования. Языки ориентированы на выполнение математических вычислений. В дальнейшем возникло большое количество различных языков, претендовавших на универсальность (PL/1) или для решения конкретных задач (COBOL — для деловых задач, ЛОГО — для обучения, Пролог — для разработки систем искусственного интеллекта). С середины 60-х до начала 80-х разработаны и получили распространение языки Pascal, Basic, Си, Ада и другие. Принципиально новым этапом в развитии языков программирования стало появление методологии непроцедурного (ООП) программирования. Основные достоинства ООП — быстрота разработки интерфейса программного приложения, возможность наследования свойств программных объектов. Языки программирования. Чтобы компьютер выполнил решение какой-либо задачи, ему необходимо получить от человека инструкции, как ее решать. Набор таких инструкций для компьютера, направленный на решение конкретной задачи, называется компьютерной программой. Современные компьютеры не настолько совершенны, чтобы понимать программы, записанные на каком-либо употребляемом человеком языке – русском, японском… команды, предназначенные для ЭВМ, необходимо записывать в понятной ей форме. С этой целью применяются языки программирования – искусственные языки, алфавит, словарный запас и структура которых удобны человеку и понятны компьютеру. В самом общем смысле языком программирования называется фиксированная система обозначений и правил для описания алгоритмов и структур данных. Языки программирования имеют как бы два лица. Одно из них обращено к человеку, использующему язык для записи своих программ, а другое адресовано ЭВМ, которая должна понимать команды. Исходя из этого все языки программирования делятся на языки низкого, высокого и сверхвысокого уровня. Язык низкого уровня – это средство записи инструкций компьютеру простыми приказами-командами на аппаратном уровне. Такой язык отражает структуру данного класса ЭВМ и поэтому иногда называется машинно-ориентированным языком. Пользуясь системой команд, понятной компьютеру, можно описать алгоритм любой сложности. Более многочисленную группу составляют языки программирования высокого уровня, средства которых допускаю описание задачи в наглядном, легко воспринимаемом виде. Отличительной особенностью этих языков является их ориентация на систему команд той или иной ЭВМ, на систему операторов, характерных для записи определенного класса алгоритмов. К языкам программирования этого типа относятся: Бейсик, Фортран, Турбо Паскаль, Си, Алгол. Программа на языках программирования высокого уровня записывается системой обозначений, близкой человеку. Программу на языке высокого уровня проще понять и значительно легче отладить. К языкам сверхвысокого уровня можно отнести Алгол – 68, при разработке которого сделана попытка формализовать описание языка, приведшая к появлению абстрактной и конкретной программ. Абстрактная программа создается программистом, конкретная – выводится из первой. Предполагается, что при таком подходе принципиально невозможно породить неверную синтаксически (а в идеале и семантически) конкретную программу. Язык программирования Турбо Паскаль. Язык программирования Турбо Паскаль назван в честь выдающегося французского математика и философа Блеза Паскаля, разработан в 1968-1971 гг. Никлаусом Виртом, профессором, директором Института информатики Швейцарской высшей политехнической школы. Язык Турбо Паскаль, созданный первоначально для обучения программированию как систематической дисциплине, скоро стал широко использоваться для разработки программных средств в профессиональном программировании. Трансляторы языка Turbo Pascal. Так как тест записанной на Паскале программы не понятен компьютеру, то требуется перевести его на машинный язык. Такой перевод программы с языка программирования на язык машинных кодов называется трансляцией (перевод), а выполняется он специальными программами – трансляторами. Существует три вида трансляторов: интерпретаторы, компиляторы и ассемблеры. Интерпретатором называется транслятор, производящий пооператорную (покомандную) обработку и выполнение исходной программы. Если команда повторяется, то интерпретатор рассматривает ее как встреченную впервые. Компилятор преобразует (транслирует) всю программу в модуль на машинном языке, после этого программа записывается в память компьютера и лишь потом исполняется. Поэтому достоинство компиляторов — быстродействие и автономность получаемых программ. Достоинство интерпретаторов — их компактность, возможность остановить в любой момент выполнение программы, выполнить различные преобразования данных и продолжить работу программы. Примерами служебных программ — интерпретаторов являются GW Basic, Лого, школьный алгоритмический язык, многие языки программирования баз данных. Компиляторами являются Turbo Pascal, С++, Delphi. Ассемблеры переводят программу, записанную на языке ассемблера (автокода), в программу на машинном языке. Любой транслятор решает следующие основные задачи: 2. анализирует транслируемую программу, в частности определяет, содержит ли она синтаксические ошибки; 2. генерирует выходную программу (ее часто называют объектной или рабочей) на языке команд ЭВМ (в некоторых случаях транслятор генерирует выходную программу на промежуточном языке, например, на языке ассемблера); 2. распределяет память для выходной программы (в простейшем случае это заключается в назначении каждому фрагменту программы, переменным, константам, массивам и другим объектам своих адресов участков памяти). Вопросы для самоконтроля: 1. Назовите семь этапов решения задач. 2. Дайте определения понятиям алгоритм и программирование. 3. Перечислите основные свойства алгоритмов. 4. Назовите и изобразите основные алгоритмические конструкции. 5. Назовите способы представления алгоритмов. 6. Изобразите основные блоки, используемые в блок-схемах. 7. Что такое языки низкого и высокого уровней и чем они отличаются? 8. Дайте определение транслятора. 9. В чем заключается отличие компиляторов и интерпретаторов? Лекция №3 Тема: Основные понятия языка Turbo Pascal. Вопросы: 1. Алфавит языка Turbo Pascal. 2. Определение и описание структуры программы. 1. Алфавит языка Turbo Pascal. Как и каждый язык, Паскаль имеет свой алфавит. В него вхо­дят: 1. латинские буквы, 2. цифры от 0 до 9, 3. специальные символы (+ , - ,* , / , = , ’ , . , : , ; , < , > , ^ , @ , $ , #), 4. парные символы (<>, <=, >=, :=, [], (), {}, (* *), (. .)), 5. пробелы и зарезервированные слова. Зарезервированные слова не могут использоваться в качестве идентификаторов. Идентификаторы в Турбо Паскале — это имена констант, переменных, типов, программ и т.д. Длина идентификатора может быть любой, но значение имеют первые три символа. Идентификатор начинается буквой, за которой могут следовать буквы, цифры и знаки подчеркивания. Текст программы на Турбо Паскале может содержать комментарии. Символом начала комментария является (* или {, символом окончания комментария — *) или }. Текст, заключенный в скобки комментариев, игнорируется компилятором, поэтому в нем можно употреблять буквы русского алфавита. Строка, начинающаяся символами {$ или (*$, является директивой компилятору. За этими символами следует команда компилятору. Программа представляет собой последовательность операторов и других элементов языка, построенную в соответствии с определенными правилами и предназначенную для решения определенной задачи. Программа состоит из заголовка программы и тела программы, за которым следует точка – признак конца программы. В свою очередь, тело программы содержит разделы описаний и раздел операторов. Исходя из этого можно записать структуру программы следующим образом: Program имя программы; {заголовок программы} Uses <раздел модулей>; label <раздел меток>; const <раздел констант>; type <раздел типов>: var <раздел переменных>; procedure или function <раздел подпрограмм> Begin раздел операторов End. Рис. 1. Структура программы на языке Паскаль в общем виде. На практике при написании программы разделы const, type, var, label могут следовать друг за другом в любом порядке и встречаться в разделе описаний сколько угодно раз, а также могут отсутствовать вообще. Раздел операторов должен присутствовать в любой программе и является основным. 2. Константы, переменные, типизированные константы. Константами называются элементы данных, значения которых установлены в разделе описания и в процессе выполнения программы не изменяются. Раздел описания констант начинается с зарезервированного слова const (constant –константа). Формат: Const <идентификатор>=<значение константы>; Например: Const X=250; Y=9.16; C=x/y; Name=’Иванов Иван’; Переменными называются величины, которые могут менять свои значения в процессе выполнения программы. Раздел описания переменных начинается с зарезервированного слова var (variable –переменная) и имеет формат: Var <идентификатор>:<тип>; Например: Var A,b:integer; S:string; Типизированные константы – промежуточные объекты между переменными и константами. Они описываются в разделе описания констант, но для них указывается тип. Const <идентификатор>:<тип>=<значение>; Например: Const Q: integer=0; Типизированным константам можно присваивать другие значения в ходе выполнения программы, поэтому фактически они представляют собой переменные с начальными значениями. Поскольку типизированные константы фактически не отличаются от переменных, их нельзя использовать в качестве значения при объявлении других констант или границ типа-диапазона. Вопросы для самоконтроля: 1. Из чего состоит алфавит языка Turbo Pascal?. 2. По каким правилам строятся идентификаторы? 3. Определите структуру программы в общем виде. 4. Дайте определения понятиям константа, переменная, типизированная константа. 5. В чем состоит отличие констант от типизированных констант? Лекция №4 Тема: Пользовательские и стандартные типы данных. Вопросы: 1. Пользовательские типы данных. 2. Структура типов данных. Простые и вещественные типы. 3. Символьный и логический типы данных. 4. Интервальный и перечислимый типы данных. 1. Пользовательские типы данных. Данные делятся на константы и переменные. В программе они определяются именами, по которым к ним можно обращаться для получения значений. Константы, переменные, значения функций или выражения характеризуются своими типами. Тип определяет множество допустимых значений, которое может иметь объект, а также множество допустимых операций, которые применимы к нему. Кроме того, тип определяет также и формат внутреннего представления данных в памяти ПК. Каждый тип данных имеет свой диапазон значений и специальное зарезервированное слово для описания. В паскале для описания типов используется зарезервированное слово type. Формат объявления пользовательского типа: Type <имя типа>=<значение типа>; Например, Type My_type=1..10; 2. Структура типов данных. Простые и вещественные типы. Рис. 2. Структура типов данных в языке Turbo Pascal. Порядковые типы отличаются тем, что каждый из них имеет конечное число возможных значений. Эти значения можно определенным образом упорядочить и, следовательно, с каждым из них можно сопоставить некоторое целое число – порядковый номер значения. Вещественные типы тоже имеют конечное число значений, которое определяется форматом внутреннего представления вещественного числа. Однако количество возможных значений вещественных типов настолько велико, что сопоставить с каждым из них целое число не представляется возможным. В таблице 2 показаны целые типы данных языка Turbo Pascal. Таблица 2. Целые типы данных. Название Длина, байт Диапазон значений Byte 1 0...255 ShortInt 1 -128...+128 Word 2 0…65535 Integer 2 -32768...+32767 LongInt 4 -2147483648...+2147483647 Диапазон возможных значений целых типов зависит от их внутреннего представления. Так integer хранится в памяти последовательностью 2-х байтов, при этом старший разряд старшего байта является знаковым («-» - 1, «+» - 0), а оставшиеся 15 разрядов используются для двоичной записи значения числа. Максимальное двоичное число, записанное 15 двоичными разрядами – (215-1)=32 767. Короткое целое shortint представлено одним байтом, в котором также один старший разряд – знаковый, а оставшиеся – значения. 27-1=127. Word и byte знака не имеют, т.е. для word все 16 разрядов отводятся под значение, поэтому диапазон 0..216-1=65335, аналогично для byte 0..28-1=255. Над целочисленными значениями определены следующие операции: «+»; «-»; «*» ; «/»; «div» - взятие целой части при делении; «mod» - взятие остатка при делении; арифметикологические операции (not, and, or, xor). Результат выполнения всех операций, кроме деления, является целым числом. Результат от деления (/) всегда будет вещественного типа. Для получения целочисленного результат от деления применяются операции div и mod: 10 mod 3=3; 10 div3=1. В таблице 3 показаны вещественные типы данных языка Turbo Pascal. Таблица 3. Вещественные типы данных. Длина, байт Название Количество цифр мантиссы Диапазон 4 Single 7-8 1,5*10-45..3,4*1038 6 Real 11-12 2,9*10-39..1,7*1038 8 Double 15-16 5*10-324..1,7*10308 10 Extended 19-20 3,4*10-4951..1,1*104932 8 Сomp - -2*1063..2*1063-1 Вещественные числа представляются в памяти ЭВМ знаком, порядком со знаком и мантиссой (без знака) ±m*10±p, где m – мантисса, p - порядок. Для представления числа в таком виде, оно предварительно нормализуется. Классически нормализованное число должно иметь мантиссу в диапазоне 0≤m<1. 25,375=0,25375*102=0,25375Е2 -376,25=-0,37625*103=-0,37625Е3 0,0017=0,17*10-2=0,17Е-2 В памяти компьютера мантисса хранится целым числом. Порядок также хранится как целое число. Десятичная точка (запятая) подразумевается перед левым (старшим) разрядом мантиссы, но при действиях с числом ее положение сдвигается влево или вправо в соответствии с двоичным порядком числа, хранящимся в экспоненциальной части, поэтому действия над вещественными числами называют арифметикой с плавающей точкой (запятой). Результат выполнения арифметических операций получается также вещественного типа. Выражение, составленное из переменных целого и вещественного типа, имеет вещественный тип. 3. Символьный и логический типы данных. Для описания символьных переменных используется зарезервированное слово char. Множество значений, принимаемый переменной такого типа, это 256 символов таблицы кодов ASCII. Каждому символу приписывается целое число в диапазоне 0...255. Это число служит кодом внутреннего представления символа, его возвращает функция ORD. В памяти компьютера переменная символьного типа занимает один байт. Символьная константа задается указанием символа, который записывается между апострофами. Значение переменной символьного типа можно задать в операторе присваивания с помощью символьной константы или функции Chr. Функция Chr устанавливает соответствие между однобайтовыми целыми значениями кода и символами. Var r1,r2: char; r1:=’A’; r2:=’a’; X1:=ord(r1); (65) Х2:=ord(r2); (97) Символы кодов ASCII упорядочены, поэтому к переменным такого типа могут быть применимы операции сравнения <, >, ≤, ≥, =, <>. Для определения переменных логического типа используется зарезервированное слово Boolean. Переменная этого типа может принимать только два значения: ложь – false и истина – true. Эти значения считаются упорядоченными, так что False True. Под переменную этого типа отводится один байт памяти. Значению false соответствует 0 в этом байте, значению true – 1. Над переменными булевского типа можно выполнять логические операции: not, and, or, xor. 4. Интервальный и перечислимый типы данных. Интервальный тип – подмножество своего базового типа, в качестве которого может выступать любой порядковый тип, кроме типа-диапазона. Путем сужения диапазона главного типа задается наименьшее и наибольшее значения, между которыми ставится (..). Обе константы должны иметь одинаковый тип, и значение слева всегда должно быть меньше значения справа. Пример: 1) type digit =’0’..’9’; digit2=48..57; 2) var date : 1..31; month : 1..12; chars : ‘A’..’Z’; Переменная такого типа обладает всеми свойствами базового типа, но ее значения не выходят за границы указанного диапазона. Перечислимый тип задается посредством перечисления всех своих значений, которые может принимать переменная этого типа. Значения указываются в скобках через запятую, например 1) var colors: (red, white, blue); 2) type TypeMonth= (jan, feb, mar, may, jun, jul, aug, sep, oct, nov, dec); Var Month: TypeMonth; Begin ….. if month=aug then writeln(‘Хорошо бы поехать к морю’); ….. end. Порядковый номер перечислимой константы определяется ее позицией в списке при описании и может быть получен по функции ord. Первая перечислимая константа в списке имеет порядковый номер ноль (red-0, white – 1, blue – 2). Поскольку значениями перечислимого типа являются константы, то к перечислимому типу не применимы ни арифметические операции, ни операции ввода-вывода. Вопросы для самоконтроля: 1. Что определяет тип переменной? 2. Приведите пример описания пользовательского типа. 3. Какова структура типов данных в языке Turbo Pascal? 4. Какие целые и вещественные типы данных существуют в языке Turbo Pascal? 5. Как хранятся в памяти переменные целого и вещественного типов? 6. Какие операции применяются к переменным целого и вещественного типов? 7. Приведите примеры описания символьных и логических переменных. 8. Приведите примеры использования символьных и логических переменных. 9. Приведите примеры описания и использования переменных интервального и перечислимого типов данных. Лекция №5 Тема: Выражения, операции, операторы. Вопросы: 1. Выражения, операции, операнды. Приоритет операций. 2. Простые операторы. Стандартные процедуры и функции. 3. Процедуры ввода-вывода. 4. Структурные операторы 5. Составной оператор. 6. Условный оператор. 7. Оператор выбора. 8. Оператор цикла с параметром. 9. Операторы цикла с условием. 10. Вложенные циклы. 1. Выражения, операции, операнды. Приоритет операций. Выражение – это некоторое формальное правило для вычисления нового значения. Выражение состоит из операндов (величин и выражений, над которыми производится операция); круглых скобок и знаков операций. Операнд – это имя переменной, имя функции, константы, элемент массива и т.п. Например 1)a*b-c , где a, b, c – операнды; 2) Ln(alpha*sin(x)-ln(y)); 3) D*c[1,1]; 4) (a+b)/c-e*f/g. Операции определяют действия, которые надо выполнить над операндами. В языке Паскаль операции обозначаются специальными символами, парой символов или специальными словами. Операции делятся на арифметические, логические, строковые, отношения, отношения над множествами и т.д. Все операции делятся на две большие группы: унарные – применяемые к одному операнду, и бинарные – применяемые к двум операндам. Унарная операция всегда ставится перед операндом, а бинарная - между операндами. Арифметическим называется выражение, составленное из операндов арифметического типа и использующие только знаки арифметических операций и круглые скобки. Результат арифметического выражение целое и вещественное значение. Выражением отношения называется словосочетание языка, в котором два выражения связаны знаком операции отношения. Операции отношения выполняют сравнение двух операндов и определяют, истинно значение или ложно. Результатом выполнения логического (булевского) выражения является логическое значение True или False. Операндами служат данные только булевского типа. Например, 1) a, b: integer; a and b not a 2) a, b: integer; (a<3) and (b>=0) При вычислении выражения существует определенный порядок выполнения операций, этот порядок определяется приоритетом операций в соответствии с соответствующей таблицей 4. Таблица 4. Приоритет операций. Операция Приоритет Вид операции @, not Первый (высший) Унарная операция *, /, div, mod, and, shr, shl Второй Операция типа умножения +, -, or, xor Третий Операция типа сложения =, , , , , , in Четвертый (низший) Операции отношения При вычислении выражений используется естественный порядок вычисления, при котором сначала выполняются операции с более высоким приоритетом, а из двух операций с равным приоритетом сначала выполняется та, которая левее. Порядок выполнения операций можно изменить при помощи скобок. Выражение, заключенное в скобки считается операндом и вычисляется в первоочередном порядке. Shr, shl – сдвиговые операции, которые применяются к целым числам. При выполнении логических операций: not, and, or, xor следует пользоваться таблицей истинности 5. Таблица 5. Логические операции. a b not a a and b a or b a xor b 1 1 1 1 1 1 1 1 1 1 1 1 Логические операции можно применять и к переменным целого типа. Переменные целого типа хранятся в памяти компьютера в двоичном виде и логические операции применяются по разряду. 2. Простые операторы. Стандартные процедуры и функции. Оператором называется предложение языка программирования, задающее описание некоторого действия. Основная часть программы на языке Турбо Паскаль представляет собой последовательность операторов. Разделителем операторов служит точка с запятой. Операторы делятся на простые и структурные. Простыми называются операторы, которые не содержат в себе других операторов. К простым относятся: операторы присваивания, оператор безусловного перехода и оператор процедуры. Оператор присваивания (:=) предписывает выполнить выражение, заданное в его правой части, и присвоить результат переменной, идентификатор которой расположен в левой части. Переменная и выражение должны быть совместимы по типу. Например: А:=В+С; S:=’Hello’; Y:=ln(x)*sin(alpha); F:=true; Оператор безусловного перехода (goto) означает «перейти к» и применяется в случаях, когда после выполнения некоторого оператора надо выполнить не следующий по порядку, а какой-либо другой, отмеченный меткой оператор. goto 25; goto metka1; Безусловный переход можно осуществлять далеко не из каждого места программы и далеко не в любое место программы. Так, нельзя с помощью этого оператора перейти из основной программы в подпрограмму или выйти из подпрограммы, не рекомендуется осуществлять переход внутрь структурированного оператора, т. к. он может дать неправильный результат, и т.д. Оператор вызова процедуры служит для активизации предварительно определенной пользователем, или стандартной процедуры. Например: Write(x,y,z); Writeln; Read(x); My_proc(m,n,k); В языке Паскаль имеется библиотека стандартных процедур и функций, к которым можно обращаться при написании программы без каких-либо специальных дополнительных описаний. К ним относятся процедуры и функции, показанные в таблице 6. Таблица 6. Стандартные процедуры и функции языка Паскаль. Обращение Тип результата Действие Pi - π = 3.141592653... sqr (x) Тип аргумента Квадрат аргумента sqrt (x) Real Корень квадратный sin(x) Real Синус, угол в радианах cos (х) Real Косинус, угол в радианах АrсТаn (х) Real Арктангенс (значение в радианах) ехр (х) Real Экспонента ln(x) Real Логарифм натуральный abs(x) Тип аргумента Возвращает модуль х chr(x) Char Возвращает символ по его коду ord(x) integer Возвращает код символа х Round(x) Integer вещественный аргумент х преобразуется в целый путем округления до ближайшего целого Trunc(x) Integer целая часть числа х, дробная отбрасывается int(x) Integer Целая часть числа frас (х) Real Дробная часть числа odd(x) Boolean Возвращает True, если аргумент - нечетное число Random Real Псевдослучайное число, равномерно распределенное в диапазоне 0...[1] Random(x) Integer Псевдослучайное целое число, равномерно распределенное в диапазоне 0...(х-1) Randomize - Инициация генератора псевдослучайных чисел dec (x[, i] ) - Уменьшает значение x на i, а при отсутствии i -на 1 inc(x[, i] ) - Увеличивает значение x на i, а при отсутствии i - на 1 3. Процедуры ввода-вывода. Ввод в языке Паскаль выполняется процедурами: Read, Readln. Процедура чтения Read обеспечивает ввод числовых данных, символов, строк и т.д. для последующей их обработки программой. Формат записи имеет вид: Read (X1, X2,…,Xn); где X1, X2,…,Xn – переменные допустимых типов данных (список параметров ввода). Допускается использование процедуры readln без параметров для приостановки выполнения программы до нажатия клавиши Enter. Порядок выполнения процедур read и readln. Когда в программе встречается обращение к процедуре read (readln), то выполнение программы приостанавливается, и она ожидает ввода данных с клавиатуры. Значения X1, X2,…,Xn набираются минимум через один пробел на клавиатуре и высвечиваются на экране. После набора данных для одной процедуры Read нажимается клавиша ввода Enter. Значения переменных должны вводится в строгом соответствии с синтаксисом языка Паскаль. Если соответствие нарушено (например, Х1 имеет тип integer, а при вводе набирается значение типа char), то возникают ошибки ввода-вывода. После нажатия клавиши enter, когда все данные уже введены, происходит обработка введенных данных, перевод их в машинное представление, запись соответствующей переменной и продолжение выполнения программы. При вводе данные разделяются между собой пробелом или нажатием клавиши enter. При вводе десятичных чисел используется десятичная точка (.). Отличие процедур read и readln: при вводе с клавиатуры все нажатые символы помещаются в специальный буфер ввода. В этот буфер в том числе помещаются и символы CR, LF. Процедура read выбирает из этого буфера все символы до последних CR, LF, а readln считывает и два последних символа из буфера. Оставленный после процедуры read символы CR, LF могут быть в дальнейшем прочитаны процедурами readln и восприняты как введенные пользователем. При этом не произойдет ожидаемой остановки программы, а она будет продолжена дальше. Пример: Var A : integer; В : real; Ch : char; Begin … Read(A,B,Ch); … end. 1_3.5_А – верно А_В_1 – неверно Вывод в языке Паскаль выполнения процедурами: write, writeln. Процедура записи Write производит вывод числовых данных, символов, строк и булевских значений. Формат записи имеет вид: Write(X1, X2,…,Xn); где X1, X2,…,Xn – выражения типа integer, byte, real, char, boolean т.д. – список параметров вывода. Отличие процедур состоит в том, что после выполнения процедуры write курсор остается в позиции, следующей за последним выведенным символом, а после выполнения процедуры writeln – перемещается в первую позицию следующей строки. Процедура Writeln может быть использована без параметров для перевода курсора на новую строку. Параметры в списке перечисляются через запятую, текстовые сообщения заключаются в апострофы. Пример: Write(x, y, z, ‘Text’, f); Вывод параметров происходит без каких-либо разделителей, т.е. следующий печатается сразу же за предыдущим a:=1; b:=2; c:=4; Write(a,b,c); Результат на экране {124} Чтобы разделить значения можно использовать пробел Write(a,’_’,b,’_’,c); Результат на экране {1_2_4}, либо воспользоваться спецификацией формата. Спецификация формата определяет ширину поля, резервируемую под выводимый результат, т.е. количества печатаемых символов. Спецификация формата задается через двоеточие после имени выводимой переменной или константы целым числом. Write(a: 3); Результат на экране {_ _ _1} Дополнение позиций осуществляется пробелами. Write (a:2, b:3, c:2); Результат на экране {_ 1 _ _ 2 _ 4} Имеются особенности при выводе вещественных чисел. При выводе и задании ширины поля число будет выведено в нормализованном виде. Для представления числа в привычном виде с фиксированной десятичной точкой в спецификации формата дополнительно указывается количество цифр в дробной части выводимого результата. Это значение указывается в виде целого числа через двоеточие после ширины поля. x:=7.375; write(x:8:3); Результат на экране {_ _ _ 7.375} Write(x:5:2); Результат на экране {_ 7.38} 4. Структурные операторы. Составной оператор. Структурные операторы представляют собой конструкции, построенные из других операторов по строго определенным правилам. К ним относятся: составные операторы, условные операторы, операторы цикла и операторы работы с записями. Составной оператор используется там, где по синтаксису разрешено использование только одного оператора, а требуется выполнить несколько. Составной оператор представляет собой последовательность операторов, разделенных точкой с запятой, и заключенных между ключевыми словами begin и end (рис.3.). Слова begin и end называют операторными скобками. Рис.3. Синтаксическая диаграмма составного оператора. Пример составного оператора: If a<>b then Begin C:=a; a:=b; b:=c; End; Составной оператор воспринимается как единое целое и может находится в любом месте программы, где синтаксис языка допускает наличие оператора. При использовании составных операторов следует учитывать, что: a) внутри этих операторов могут быть любые операторы, содер­жащие в свою очередь простые и составные операторы; б) передача управления извне внутрь составного оператора запрещена. Фактически, весь раздел операторов, обрамленный словами begin . . . end, представляет собой один составной оператор. Поскольку зарезервированное слово end является закрывающей операторной скобкой, оно одновременно указывает и конец предыдущего оператора, поэтому ставить перед ним символ «;» необязательно. Наличие точки с запятой перед end в предыдущих примерах означало, что между последним оператором и операторной скобкой end располагается пустой оператор. Пустой оператор не содержит никаких действий, просто в программу добавляется лишняя точка с запятой. В основном пустой оператор используется для передачи управления в конец составного оператора. 5. Условный оператор. Условные операторы предназначены для выбора на исполнение одного из возможных действий (операторов) в зависимости от некоторого условия. В качестве условий выбора используется значение логического выражения. В Паскале есть два условных оператора: if и case. Оператор условия if используется в том случае, когда существует не более двух альтернатив (рис.4.). Он может быть задан в сокращенном или полном виде. Рис.4. Синтаксическая диаграмма условного оператора. В сокращенном виде ветка else(иначе) отсутствует. Перед else (;) не ставится! Действие оператора: проверяется значение логического выражения, стоящего после ключевого слова if. Если значение этого выражения истинно, то выполняется оператор, стоящий после ключевого слова then, если значение ложно, то для краткой формы происходит переход на следующий оператор, а для полной выполняется оператор, стоящий после ключевого слова else. Пример: нахождение максимума из двух чисел. m=max{a,b}. 1) if a>b then m:= a else m:=b; 2) m:=a; if b>m then m:=b; В операторе if непосредственно за ключевыми словами then и else могут следовать любые операторы, в том числе и операторы if. В этом случае они называются вложенными. Пример: вычислить значение функции var x,y,a,b: real; begin writeln(‘введите х, а и b’); readln(x, a, b); if a=b then y:= sqr(sqr(x)) else y:= x*sqr(x); writwln(y:10:3); end. 6. Оператор выбора. В тех случаях, когда альтернатив боле двух, используется условный оператор case. Формат записи оператора имеет вид, показанный на рисунке 5. Рис.5. Синтаксическая диаграмма оператора case. Рис.6. Синтаксическая диаграмма альтернативы. Где селектор – переменная или выражение скалярного типа (кроме real, string, longint), Константа1, константа2, константа3 – метки выбора, оператор – простые или составные операторы. Операторы и предшествующие им метки называются альтернативами (рис.6.). Выбор альтернативы определяется значением селектора. Выполнение оператора case начинается с вычисления значения селектора. Затем полученное значение сравнивается с метками. Если это значение совпадает с какой-либо меткой, выполняется тот опе­ратор, перед которым она стоит, остальные операторы игнорируются. Если такого совпадения не обнаружено, выполнятся ветвь else, состоящая из одного или нескольких операторов. И в том и в другом случае следующим оператором, подлежащим выполнению, будет оператор программы, расположенный после ключе­вого слова end. Ветвь else не является обязательной и в операторе может отсутствовать. Использование оператора case предполагает соблюдение следую­щих правил: 1) перед каждым оператором альтернативы может располагаться несколько меток, либо в качестве метки может быть использован диапазон значений (все метки и диапазоны отделяются друг от друга запятыми); 2) метки, предшествующие альтернативам, должны иметь тип одинаковый с типом селектора; 3) повторение меток в альтернативах недопустимо; 4) метки не описываются в разделе label программы и на них нельзя передавать управление с помощью оператора Goto. Пример: Вычислить значение функции , где n – целое положительное число. Case n of 1: f:=sin(n*x); 2: f:= cos(n*x); 3: f:= sin(n*x)/cos(n*x) else f:= ln(n*x); end; 7. Оператор цикла с параметром. Если в программе возникает необходимость неоднократно выполнить некоторые операторы, то используются операторы повтора (цикла). В языке Турбо Паскаль различают три вида операторов цикла: while, repeat, for. Они используются для организации циклов различных типов. Оператор for используется, если число повторений заранее известно. Этот оператор часто называют цикл с параметром, т.к. число повторений задается переменной, называемой параметром цикла. Оператор for состоит из заголовка и тела цикла: Оператор имеет вид, показанный на рисунке 7. Рис. 7. Синтаксическая диаграмма оператора цикла с параметром. Оператор - простой или составной оператор (тело цикла); переменная - параметр цикла; выражение1, выражение2 - соответствен­но начальное и конечное значения параметра. На параметр цикла в операторе for накладываются следующие ограничения: 1) в качестве параметра может использоваться только переменная дискретного типа (например, целого); 2) начальное и конечное значения параметра могут быть конс­тантами, переменными или выражениями, но должны иметь одинаковый с ним тип; 3) параметр цикла, а также его начальное и конечное значения не могут быть изменены никаким оператором в теле цикла; 4) после завершения цикла значение его параметра становится неопределенным, если только цикл не был прерван оператором пере­хода. Выполнение оператора for начинается с присваивания его па­раметру цикла начального значения m1. Затем значение параметра (назовём его текущим и обозначим i ) сравнивается с конечным значением m2. Если в операторе цикла используется ключевое слово to и i  m2, то выполня­ется тело цикла, после чего параметр i увеличивается на 1; когда i становится строго больше m2, цикл перестает выполняться и следует переход на оператор, расположенный за циклом. При использовании в операторе for ключевого слова downto i после каждого выполнения тела цикла уменьшается на 1. Выполнение цикла продолжается до тех пор, пока i не станет строго меньше m2. Пример: Вычислить сумму ряда , S= x + x2/2! + x3/3! + .. + xk/k! ak+1=ak * x/(k+1) k>1 var x,s,a:real; i:integer; begin read(x); s:=0; a:=x; for i:=1 to n do begin s:=s+a; a:=a*x/(i+1); end; write(‘Сумма=’,s:5:5); end. 8. Операторы цикла с условием. Операторы цикла с условием используются, когда количество повторений цикла заранее неизвестно. Операторы while и repeat позволяют организовать такие циклы. Оператор цикла с предусловием while имеет формат, показанный на рисунке 8. Рис. 8. Синтаксическая диаграмма оператора цикла с предусловием. Выражение - логическое выражение; оператор - простой или состав­ной оператор, который является телом цикла. Оператор while выполняется так: 1) вычисляется логическое выражение L, результат вычисления анализируется; 2) если результат вычислений true, то выполняется тело цикла, после чего осуществляется возврат к оператору while; если результат false, то тело цикла не выполняется, а управление пе­редаётся оператору программы, расположенному непосредственно пос­ле тела цикла. Если первая проверка условия окончания цикла дала результат false, то тело цикла не выполнится ни разу. Логику действия оператора while можно сформулировать так: «выполнять тело цикла, пока условие истинно». Пример: вычислить сумму ряда при ak+1=ak * x/(k+1) k>1 var x,a,s,eps:real; k:integer; begin read(x); Eps:=0.001; S:=0; a:=x; k:=1; while abs(a)>=Eps do begin s:=s+a; k:=k+1; a:=a*x/k; end; writeln(‘S= ‘, s:10:5); end. Оператор цикла с постусловием repeat имеет вид, показанный на рисунке 9. repeat oператор1; oператор2; …………… oператорN until L; где L - логическое выражение; опе­ратор1, оператор2,..., операторN - операторы, представляющие в совокупности тело цикла. Рис.9. Синтаксическая диаграмма оператора цикла с постусловием. Последовательность выполнения оператора repeat: 1) исполняются операторы, составляющие тело цикла; 2) вычисляется логическое выражение L, результат вычисления анализируется; 3) если результатом вычислений является false, тело цикла выполняется снова, в противном случае (результат вычислений - true) очередного повторения тела не происходит, а осуществляется выход из цикла на следующий по порядку оператор программы. Условие выполнения цикла проверяется в конце, поэтому тело цикла будет выполнено, по крайней мере, один раз. Логику действия оператора repeat можно выра­зить инструкцией «выполнять тело цикла до тех пор, пока условие не станет истинным». Пример: ak+1=ak * x/(k+1) k>1 var x, a, s, eps: real; k: integer; Begin Eps:=0.001; S:=0; a:=x; k:=1; repeat s:=s+a; k:=k+1; a:=a*x/k until abs(a)< Eps; writeln(‘S= ‘, s:10:5); end. Применяя операторы while и repeat, необходимо позаботиться о том, чтобы значения переменных, входящих в условие окончания цик­ла, менялись в теле цикла, иначе циклический процесс будет продолжаться бесконечно («зацикливание» программы). Подчеркнем отличия между операторами repeat и while. 1. В операторе repeat проверка условия выхода из цикла про­изводится в конце, а не в начале цикла, поэтому тело цикла выпол­няется хотя бы один раз. 2. В операторе repeat условие выхода из цикла удовлетворяет­ся, если логическое выражение L истинно, а в операторе while - если ложно. 3. Тело цикла, организованного с помощью оператора while, может содержать только один оператор (в том числе и составной), в то время как между словами repeat и until их можно разместить несколько. Начиная с версии ТР7.0 допускается досрочный выход из цикла процедурой break. Действие этой процедуры заключается в передаче управления оператору, стоящему сразу за циклическим оператором. Процедура continue – обеспечивает досрочное завершение очередного прохода цикла; эквивалент передачи управления в самый конец циклического оператора. В ряде случаев бывает удобно организовать бесконечный цикл типа while true do …или repeat … until false, выход из которых может быть осуществлен только при помощи процедуры break. 9. Вложенные циклы. Если телом цикла является циклическая структура, то такие циклы называют вложенными. Цикл, содержащий другой цикл, называется внешним, а цикл, содержащийся в теле другого цикла, называют внутренним. Внешний и внутренний циклы могут быть трех типов: циклы с параметром, циклы с предусловием и циклы с постусловием. При реализации вложенных циклов выполняются следующие условия: 1. все операторы внутреннего цикла полностью располагаются в теле внешнего цикла; 2. очередное выполнение внешнего цикла реализуется только после того как внутренний цикл будет полностью выполнен. Рис.10. Структура сложного цикла: а) детализированный вариант; б) упрощенный вариант. Рассмотрим реализацию вложенных циклов на примере вывода таблицы умножения. Для решения этой задачи будем использовать циклы с параметром. Program Table; Var i, j: byte; begin for i:= 1 to 10 do for j:=1 to 10 do writeln(i,’*’,j, ‘ = ‘, i*j); end. Выполнение программы начинается с внешнего цикла. При первом обращении внешнего цикла переменной i присваивается значение 1. Затем циклически выполняются следующие действия: 1. проверяется условие i<=10; 2. если условие истинно, то выполняется оператор в теле цикла (внутренний цикл); 3. вычисляется значение переменной j. Циклически выполняются следующие действия: 1. проверяется условие j<=10; 2. если условие истинно, то выполняется оператор внутреннего цикла (вывод на экран строки таблицы умножения в соответствии с текущими значениями переменных i и j); 3. значение параметра j увеличивается на 1и снова выполняется внутренний цикл, до тех тор пока j<=10. Когда это условие станет ложным внутренний цикл полностью выполнится и управление передается на оператор внешнего цикла. И так до тех пор, пока i<=10. Когда это условие станет ложным внешний цикл будет полностью выполнен и работа программы будет закончена. Вопросы для самоконтроля: 1. Дайте определение выражению, операции, операнду, оператору. 2. Какие простые операторы вы знаете? 3. Приведите примеры использования простых операторов? 4. Объясните назначение и принципы использования процедур read, readln, write, writeln? 5. Что такое составной оператор и для чего он используется? 6. Приведите пример уловного оператора и объясните принцип его работы? 7. Приведите пример оператора выбора и объясните принцип его работы? 8. Перечислите циклические операторы и поясните различие в их использовании? 9. Какие процедуры позволяют досрочно завершить работу циклического оператора? 10. Приведите пример организации бесконечного цикла. 11. Поясните принципы выполнения вложенных циклов. Лекция №6. Тема: Структурное программирование. Вопросы: 1. Структурное программирование, управляющие конструкции, пошаговая детализация. 2. Теорема о структурировании. 1. Структурное программирование, управляющие конструкции, пошаговая детализация. Структурное программирование сосредоточено на логике программы и включает 3 главные составляющие: 1. Проектирование сверху вниз. 2. Модульное программирование. 3. Структурное кодирование. 1. Проектирование сверху вниз. Этот метод предусматривает сначала определение задачи в общих чертах, а затем постепенное уточнение структуры, путем внесения более мелких деталей. На каждом шаге такого уточнения необходимо выявить основные функции, которые нужно выполнить. Таким образом данная задача разбивается на ряд подзадач, пока эти подзадачи не станут на столько простыми, что каждой из них будет соответствовать один модуль. 2. Модульное программирование - это процесс разделения программы на логические части (модули) и последовательное программирование каждой из них. Каждый модуль должен иметь свое назначение, быть замкнутым, вход и выход должны быть точно определены. Воздействие изменения в одном модуле на другую часть программы называется "волновым эффектом". Избегать использования глобальных переменных. Использование модулей приводит к уменьшению сложности. 3. Структурное кодирование - это метод написания хорошо структурированных программ, который позволяет получать программы более удобные для тестирования, модификации и использования. Логическая структура базируется на строго доказанной теореме о структурировании. Эта теорема утверждает, что любую правильную программу можно написать с использованием только следующих структур: • последовательности (линейности) • выбора (ветвления) • повторения (цикла) На протяжении 60-х годов попытки создания многих больших программных систем наталкивались на ряд трудностей. Графики создания программного обеспечения обычно не выполнялись, а конечные продукты отличались ненадежностью. Люди начали понимать, что создание программного обеспечения более сложная задача, чем они представляли. Исследовательские работы 60-х годов привели к развитию структурного программирования – дисциплинированного подхода к написанию программ, отличающихся от неструктурированных программ ясностью, простотой тестирования и отладки и легкостью модификации. С помощью указанных управляющих конструкций можно добиться сколь угодно высокой сложности. Обычно операторы программы выполняются друг за другом в той последовательности, в которой они написаны. Это называется последовательным выполнением. Однако некоторые операторы позволяют программисту указать, что следующим должен выполнятся не очередной оператор, а какой-то другой. Это называется передачей управления. 2. Теорема о структурировании. В 60-е годы стало ясно, что неограниченное использование передач управления является источником множества неприятностей при групповой разработке программного обеспечения. Вина была возложена на оператор goto, который позволяет передавать управление в очень широких пределах. Исследование Бома и Джопини показало, что программы могут быть написаны без использования оператора goto. Также в этом исследовании была доказана следующая теорема: Теорема о структурировании: любая простая программа может быть преобразована в функционально ей эквивалентную программу, построенную на основе 3 следующих структур: 1. Следования. 2. Если-то-иначе. 3. Цикл с предусловием. и состоящая из тех же предикатов и функциональных узлов, а также функции присваивания значений некоторому счетчику предикатов проверяющих значения счетчика. К неструктурным операторам в языке Pascal относятся: 2. Goto 3. break 4. continue 5. exit (Выход из выполняемого блока в окружающую среду. Если текущий блок – процедура или функция, выход производится во внешний блок. Если exit указан в программе, то управление передается системе программирования.) 6. halt (прекращение выполнения программы и передача управления системе программирования.) Цели структурного программирования: 1. Обеспечить дисциплину программирования 2. Улучшить читабельность программы 3. Повысить эффективность программы 4. Повысить надежность программы Вопросы для самоконтроля: 1. Поясните основные принципы структурного программирования. 2. Перечислите структурные и неструктурные операторы. 3. В чем заключается теорема о структурировании? Лекция №7. Тема: Массивы. Вопросы: 1. Определение и описание массивов. 2. Основные свойства матриц. 3. Действия над массивами и элементами массивов. 4. Организация циклов с использованием массивов. 5. Сортировка массивов. 6. Транспонирование матриц. 1. Определение и описание массивов. Рассмотренные выше простые типы данных позволяют использовать в программе одиночные объекты - числа, символы, строки и т.п. В Турбо Паскале могут использоваться также объекты, содержащие множество однотипных элементов. Это массивы - формальное объединение нескольких однотипных объектов (чисел, символов, строк и т.п.), рассматриваемое как единое целое. К необходимости применения массивов мы приходим всякий раз, когда требуется связать и использовать целый ряд родственных величин. Структурированные типы данных определяют упорядоченную совокупность скалярных переменных и характеризуются типом своих компонентов. Массивы – это структурированный тип данных, состоящий из фиксированного числа элементов, имеющих один тип данных. Элементы массива упорядочены определенным способом. Каждый элемент массива определяется именем этого массива и своим порядковым номером в массиве (индексом). Массивы бывают одномерные и многомерные. Все элементы массива в Паскале имеют одинаковый тип, который называется базовым. Характеристиками каждого элемента массива являются имя, размерность и длина (количества элементов массива). Одномерный массив – это упорядоченная последовательность элементов, расположенных в памяти друг за другом, каждый элемент имеет свой порядковый номер. Аналогом одномерного массива в математике является вектор: Х=(х1, х2, х3, х4) - индекс пишется справа внизу. На языке Паскаль: x[1] x[2] x[3] x[4] - индекс пишется в квадратных скобках. При использовании многомерных массивов в математической записи индексы пишутся рядом друг за другом, а в языке Паскаль – в [] друг за другом через запятую. Примером двумерного массива в математике является матрица: a[1,1] a[1,2] a[1,3] a[2,1] a[2,2] a[2,3] a[3,1] a[3,2] a[3,3] Массив – это регулярный тип в языке Паскаль. Его описание имеет вид, показанный на рисунке 9. Рис. 9. Синтаксическая схема объявления массива. Идентификатор – имя массива; Тип1 – задает диапазон изменения индексов. Как правило, это интервальный тип. Индексы могут принимать только целочисленные значения. Тип2 – тип элементов массива. В качестве элементов массива могут выступать переменные любого типа, допустимого в языке Паскаль. Тип индекса задает количество элементов в массиве, т.е. его длину. Это количество определяется числом возможных значений типа, указанного в описании массива. В программе каждый массив должен быть описан. Его описание может быть сделано либо в разделе описания переменных, либо в разделе описания типов. Примеры: 1) var x: array[1..10] of real; {одномерный массив из 10 элементов вещественного типа} a: array[1..3,1..3] of integer; {двумерный массив 3х3 из элементов целого типа} c: array[-5..5] of boolean; {одномерный массив из 10 элементов логического типа} Массив х состоит из 10 элементов, массив а – из 9, массив с – из 10 элементов. Во втором случае задается пользовательский тип, в разделе type, обязательно до того, как он будет использоваться. 2) В программах, обрабатывающих массивы неизвестной длины, в разделе описания констант предварительно определяют возможное максимальное значение размера массива, а затем в программе запрашивают текущее значение размера и используют это значение далее при заполнении и обработке массива. Const n=10; m=20; type massiv=array[1..n,1..m] of integer; var a,b,c: massiv; Так как тип <тип>, идущий за словом OF, - любой тип Турбо Паскаля, то он может быть, в частности, и другим массивом, например: type mat = array [0..5] of array [-2..2] of array [Char] of Byte; Такую запись можно заменить более компактной: type mat = array [0..5,-2..2,Char] of Byte; Глубина вложенности структурированных типов вообще, а следовательно, и массивов - произвольная, поэтому количество элементов в списке индексных типов (размерность массива) не ограничено, однако суммарная длина внутреннего представления любого массива не может быть больше 65520 байт. В памяти ПК элементы массива следуют друг за другом так, что при переходе от младших адресов к старшим наиболее быстро меняется самый правый индекс массива. Если, например, var а : array[1. .2,1. .2] of Byte; begin a [1,1]:=1;  a [2,1]:=2;  a [l, 2]:=3;  a [2,2]:=4;  end. то в памяти последовательно друг за другом будут расположены байты со значениями 1,3,2, 4 . Контроль правильности значений индексов массива может проводиться с помощью директивы компилятора R. По умолчанию директива R находится в пассивном состоянии {$R-}. Перевод в активное состояние вызывает проверку всех индексных выражений на соответствие их значений диапазону типа индекса. 2. Основные свойства матриц. Матрицы, у которых число строк равно числу столбцов называются квадратными. А(n,n) – квадратная матрица. Основные свойства квадратных матриц: 1) квадратная матрица имеет главную и побочную диагонали. Например, для матрицы А: на главной диагонали лежат элементы 1, 5, 9, на побочной – 3, 5, 7. Для элемента a[i,j]: Если i=j – элементы расположены на главной диагонали; Если i>j – элементы расположены ниже главной диагонали; Если in+1 – элементы расположены под побочной диагональю. 2) Квадратная матрица, у которой все элементы, исключая элементы главной диагонали, равны нулю, называется диагональной матрицей. 3) Диагональная матрица, у которой все элементы, стоящие на главной диагонали, равны 1, называется единичной матрицей. 4) Если в матрице A(n,m) поменять местами строки и столбцы, то получится матрица AT(n,m), которая называется транспонированной. 2. Действия над массивами и элементами массивов. Доступ к каждому элементу массива в программе осуществляется с помощью индекса - целого числа (выражения порядкового типа), служащего своеобразным именем элемента в массиве (если левая граница типа-диапазона равна 1, индекс элемента совпадает с его порядковым номером). При упоминании в программе любого элемента массива сразу за именем массива должен следовать индекс элемента в квадратных скобках, например: a[1] := 10.5; c[-2] := a[1] > [2] ; for k : = 1 to 10 do x[k] := 0; Здесь приведены обращения к элементам массивов с именами а, с и х. Значение переменной k должно быть заранее определено. В качестве индекса можно использовать константу, переменную или выражение, соответствующее типу индексов, объявленному в описании массива. Индексированные элементы массива называются индексированными переменными и могут быть использованы так же как и простые переменные. Например, они могут находится в выражениях в качестве операндов, использоваться в операторах for, While, Repeat, входить в качестве параметров в операторыread, readln, write, writeln; им можно присваивать любые значения, соответствующие их типу. В правильно составленной программе индекс не должен выходить за пределы, определенные типом-диапазоном. Например, можно использовать элементы А[1], X[10], С[-5], но нельзя A[0] или X[11]. Турбо Паскаль может контролировать использование индексов в программе на этапе компиляции и на этапе счета программы. К элементам массива применимы все операции, которые применимы к переменным базового типа. Для работы с массивом как единым целым используется идентификатор массива без указания индекса в квадратных скобках. Массив может участвовать только в операциях отношения «равно», «не равно» и в операторе присваивания. Массивы, участвующие в этих действиях, должны быть идентичны по структуре, т.е. иметь одинаковые типы индексов и одинаковые типы компонентов. Например, если массивы А и В описаны как Var A, B : array[1..20] of real; то применение к ним допустимых операций даст следующий результат: выражение результат А=В True, если значение каждого элемента массива А равно соответствующему значению элемента массива В А<>В True, если хотя бы одно значение элемента массива А не равно значению соответствующего элемента массива В А:=В Все значения элементов массива В присваиваются соответствующим элементам массива А. Значения элементов массива В остаются неизменны Над массивами не определены операции отношения. Нельзя, например, записать if a = b then ...  Сравнить два массива можно только поэлементно.   3. Организация циклов с использованием массивов. Инициализация (присваивание начальных значений) массива заключается в присваивании каждому элементу массива одного и того же значения, соответствующего базовому типу. Наиболее эффективно эта операция выполняется с помощью оператора for, например: for I:=1 to 4 do A[I]:=0; В этом случае всем элементам массива будут присвоены нулевые значения. Для инициализации двумерного массива обычно используется вложенный оператор for, например: For i:=1 to 10 do For j:=1 to 15 do B[i,j]:=0; Паскаль не имеет средств ввода-вывода элементов массива сразу, поэтому ввод и вывод значений производится поэлементно. Значения элементам массива можно присвоить с помощью оператора присваивания, как показано в примере инициализации, однако чаще всего они вводятся с экрана с помощью оператора read или readln с использованием оператора организации цикла for. For i:=1 to 4 do readln(А[i]); Аналогично значения двумерного массива вводятся с помощью вложенного оператора for. For i:=1 to 10 do For j:=1 to 15 do Readln(B[i,j]); В связи с тем, что использовался оператор Readln, каждое значение будет вводится с новой строки. Можно ввести и значения отдельных элементов, а не всего массива. Так оператором Read(A[3]); Read(B[6,9]); вводится значение третьего элемента вектора А и значение элемента, расположенного в шестой строке девятого столбца матрицы В. Оба значения набираются на одной строке экрана, начиная с текущей позиции расположения курсора. Вывод значений элементов массива выполняется аналогичным образом, но используются операторы Write, Writeln: For i:=1 to 4 do writeln(А[i]); или For i:=1 to 10 do For j:=1 to 15 do writeln(B[i,j]); Копированием массивов называется присваивание значений всех элементов одного массива всем соответствующим элементам другого массива. Копирование можно выполнить одним оператором присваивания, например: A:=D; или с помощью оператора for. For i:=1 to 4 do A[i]:=D[i]; В обоих случаях значение элементов массива D не изменяется, а значения элементов массива А становятся равными значениям соответствующих элементов массива D. Очевидно, что оба массива должны быть идентичны по структуре. Иногда требуется осуществить поиск в массиве каких либо элементов, удовлетворяющих некоторым известным условиям. Пусть, например, надо выяснить сколько элементов массива А имеют нулевое значение. Для ответа на этот вопрос введем дополнительную переменную К и воспользуемся операторами for и if: К:=0; for i:=1 to 4 do if A[i]=0 then k:=k+1; После выполнения цикла переменная К будет содержать количество элементов массива А с нулевым значением. Перестановка значений элементов массива осуществляется с помощью дополнительной переменной того же типа, что и базовый тип массива. Например, так запишется фрагмент программы, обменивающий значения первого и пятого элементов массива А: Vs:= A[5]; A[5]:=A[1]; A[1]:=Vs; 4. Сортировка массивов. Сортировка- один из наиболее распространенных процессов современной обработки данных. Сортировкой называется распределение элементов множества по группам в соответствии с определенными правилами. Например, сортировка элементов массива, в результате который получается массив, каждый элемент которого, начиная со второго, не больше стоящего от него слева, называется сортировкой по невозрастанию. Рассмотрим три метода сортировки одного и того же массива М из 20 целых чисел. Описание массива выполним следующим образом: Const Count=20; M : array[1..Count] of byte=(9,11,12,3,19,1,5,17,10,18,3,19,17,9,12,20,20,19,2,5); Для сравнения эффективности разных способов сортировки введем целую переменную А, значение которой будет равно числу итераций (повторов просмотра массива). Для наблюдения текущего состояния массива после каждой перестановки элементов будем выводить его на экран. Линейная сортировка (сортировка отбором). Идея линейной сортировки по невозрастанию заключается в том, чтобы, последовательно просматривая весь массив, отыскать наибольшее число и поместить его на первую позицию, обменяв его с элементом, который ранее занимал первую позицию. Затем, просматриваются все остальные элементы массива и выполняется аналогичная операция по отбору из рассматриваемой части массива максимального элемента и обмену местами этого элемента и первого в рассматриваемой части и т.д. Введем в разделе описания следующие целые переменные: I – для указания позиции первого элемента рассматриваемой части массива; J - для указания позиции очередного сравниваемого с ним элемента; N – для временного хранения значения первого элемента для обмена значениями с максимальным из рассматриваемой части массива; L – параметр цикла при выводе текущего значения элементов массива в процессе сортировки для наблюдения происходящих в массиве наблюдений; A – переменная, значение которой будет равно числу перестановок элементов. В начале запишем вывод исходного массива на экран: Writeln (‘Исходный массив:’); For I:=1 to Count do Write (‘ ’, M[I]); Перед началом сортировки установим значение счетчика итераций А, равное 0. Для сортировки организуем два цикла For. Внешний цикл с параметром I, указывающим позицию первого элемента в несортированной части массива, и внутренний цикл с параметром J, указывающим позицию очередного, сравниваемого с первым, элемента неотсортированной части массива. Сравнение элементов запишем оператором: If M[I]X do I:=I+1; While X>M[J] do J:=J+1; A:=A+1; {увеличить число итераций обмена элементов} If I<=J then Begin {обменять местами элементы} W:=M[I]; M[I]:=M[J]; M[J]:=W; I:=I+1; J:=J-1; {печатать текущее состояние массива после каждой перестановки} For L:=1 to Count do write (‘’, M[L]); writeln(‘Число итераций=’, A); End; Until I>J; If FirstX, затем при просмотре правой части справа налево отыскивается такой элемент, что M[I]X, не будут обменены с элементами, расположенными справа от середины и удовлетворяющими условию M[I]=set of<элемент1,…,элементn>; var <идентификатор,…>:<имя типа>; или var <идентификатор>:set of <элемент1,…>; Например: type Simply=set of ‘a’..’h’; Number=set of 1..31; Var Pr : Simply; N : Number; Letter : set of char; {определение множества без предварительного описания в разделе типов} В данном примере переменная Pr может принимать значения символов латинского алфавита от ‘a’ до ’h’; N – любое значение в диапазоне 1..31; Letter – любой символ. Попытка присвоить другие значения вызовет программное прерывание. Количество элементов множества не должно превышать 256, соответственно номера значений базового типа должны находится в диапазоне 0..255. Контроль диапазонов осуществляется включением директивы {$R+}. Объем памяти, занимаемый одним элементом множества, составляет 1 бит. Объем памяти для переменной типа множество вычисляется по формуле: , где Max и Min – верхняя и нижняя границы базового типа. 2. Операции над множествами. При работе с множествами допускается использование операций отношения “=”, ”<>”, ”>=”, ”<=”, объединения, пересечения, разности множеств и операции in. Результатом выражений с применением этих операций является значение True или False. Операция «равно» (=). Два множества А и В считаются равными, если они состоят из одних и тех же элементов. Порядок следования элементов в сравниваемых множествах значений не имеет. Например: Значение А Значение В Выражение Результат [1,2,3,4] [‘a’,’b’,’c’] [‘a’..’z’] [1,2,3,4] [‘c’,’a’] [‘z’..’a’] A=B A=B A=B True False True Операция «не равно» (<>). Два множества А и В считаются не равными, если они отличаются по мощности или по значению хотя бы одного элемента. Например: Значение А Значение В Выражение Результат [1,2,3] [‘a’..’z’] [‘c’..’t’] [3,1,2,4] [‘b’..’z’] [‘t’..’c’] A<>B A<>B A<>B True True False Операция «больше или равно» (>=). Эта операция используется для определения принадлежности множеств. Результат операции А>=В равен True,если все элементы множества В содержатся в множестве А. В противном случае результат равен False. Например: Значение А Значение В Выражение Результат [1,2,3,4] [‘a’..’z’] [‘z’,’x’,’c’] [2,3,4] [‘b’..’t’] [‘c’,’x’] A>=B A>=B A>=B True True True Операция «меньше или равно» (<=). Эта операция используется аналогично предыдущей операции, но результат выражения АВ равен True, если все элементы множества А содержатся в множестве В. в противном случае результат равен False. Например: Значение А Значение В Выражение Результат [1,2,3] [‘d’..’h’] [‘a’,’v’] [1,2,3,4] [‘z’,’a’] [‘v’,’n’,’a’] A<=B A<=B A<=B True True True Операция in используется для проверки принадлежности какого либо значения указанному множеству. Обычно применяется в условных операторах. Например: Значение А Значение В Результат 2 ‘v’ X1 If A in [1,2,3] then… If A in [‘a’..’n’] then… If A in [X0,X1,X2,X3] then… True False True При использовании операции in проверяемое на принадлежность значение и множество в квадратных скобках не обязательно предварительно описывать в разделе описаний. Операция in позволяет эффективно и наглядно проводить сложные проверки условий, заменяя иногда десятки других операций. Например, выражение If (a=1) or (a=2) or (a=3) or (a=4) or (a=5) or (a=6) then… можно заменить более коротким выражением if a in [1..6] then… Часто операцию in пытаются записать с отрицанием: X not in M Такая запись является ошибочной, так как две операции следуют подряд; правильная инструкция имеет вид Not (x in M). Объединение множеств (+). Объединением двух множеств является третье множество, содержащее элементы обоих множеств. Например: Значение А Значение В Выражение Результат [1,2,3] [‘A’..’D’] [] [1,4,5] [‘E’..’Z’] [] A+B A+B A+B [1,2,3,4,5] [‘A’..’Z’] [] Пересечение множеств (*). Пересечением двух множеств является третье множество, которое содержит элементы, входящие одновременно в оба множества. Например: Значение А Значение В Выражение Результат [1,2,3] [‘A’..’Z’] [] [1,4,2,5] [‘B’..’R’] [] A*B A*B A*B [1,2] [‘B’..’R’] [] Разность множеств (-). Разностью двух множеств является третье множество, которое содержит элементы первого множества, не входящие во второе множество. Например: Значение А Значение В Выражение Результат [1,2,3,4] [‘A’..’Z’] [X1,X2,X3,X4] [1,3,4] [‘D’..’Z’] [X4,X1] A-B A-B A-B [2] [‘A’..’C’] [X2,X3] Использование в программе данных типа set дает ряд преимуществ: значительно упрощаются сложные операторы if, увеличивается степень наглядности программы и понимания алгоритма решения задачи, экономятся память, время компиляции и выполнения. Имеются и отрицательные моменты, основной из них – отсутствие в языке Паскаль средств ввода-вывода элементов множества, поэтому программист сам должен писать соответствующие процедуры. Дополнительно к этим операциям можно использовать две процедуры. INCLUDE - включает новый элемент во множество. Обращение к процедуре: INCLUDE (S,I) Здесь S - множество, состоящее из элементов базового типа TSetBase;           I - элемент типа TSetBase, который необходимо включить во множество. EXCLUDE - исключает элемент из множества. Обращение: EXCLUDE(S,I) Параметры обращения - такие же, как у процедуры INCLUDE. В отличие от операций + и -, реализующих аналогичные действия над двумя множествами, процедуры оптимизированы для работы с одиночными элементами множества и поэтому отличаются высокой скоростью выполнения. В примере, иллюстрирующем приемы работы с множествами, реализуется алгоритм выделения из первой сотни натуральных чисел всех простых чисел. В его основе лежит прием, известный под названием «решето Эратосфена». В соответствии с этим алгоритмом вначале формируется множество BEGINSET, состоящее из всех целых чисел в диапазоне от 2 до N. В множество PRIMERSET (оно будет содержать искомые простые числа) помещается 1. Затем циклически повторяются следующие действия: • взять из BEGINSET первое входящее в него число NEXT и поместить его в PRIMERSET; • удалить из BEGINSET число NEXT и все другие числа, кратные ему, т.е.2*NEXT, 3*NEXT и т.д.  Цикл повторяется до тех пор, пока множество BEGINSET не станет пустым. Эту программу нельзя использовать для произвольного N, так как в любом множестве не может быть больше 256 элементов. Program Primer_numbers_detect; {Выделение всех простых чисел из первых N целых} const N = 255; {Количество элементов исходного множества} type SetOfNumber = set of 1..N; var n1,next,i : Word; {Вспомогательные переменные}  BeginSet, {Исходное множество}  PrimerSet : SetOfNumber; {Множество простых чисел} . begin BeginSet := [2. .N] ; {Создаем исходное множество} PrimerSet:= [1]; {Первое простое число}  next:= 2; {Следующее простое число} while BeginSet <> [] do {Начало основного цикла}  begin n1 := next;{n1-число,кратное очередному простому (next)}  {Цикл удаления из исходного множества непростых чисел:}  while n1 <= N do  begin Exclude(BeginSet,nl); n1 := n1+next {Следующее кратное}  end; {Конец цикла удаления}  Include(PrimerSet,next); {Получаем следующее простое, которое есть первое невычеркнутое из исходного множества}  repeat inc(next) until (next in BeginSet) or (next > N)  end; {Конец основного цикла}  {Выводим результат:}  for i := 1 to N do if i in PrimerSet then Write(i:8);  WriteLn  END. Вопросы для самоконтроля: 1. Что такое множество? 2. Приведите пример описания типа «множества». 3. Что такое мощность множества? 4. Какой объем в памяти занимает множество? 5. Приведите примеры операций над множествами. 6. Чем отличаются процедуры INCLUDE и EXCLUDE от операций сложения и вычитания? Лекция №9. Тема: Строки. Вопросы: 1. Описание строкового типа. 2. Строковые выражения. 3. Строковые процедуры и функции. 1. Описание строкового типа. Строка в Турбо Паскале – последовательность символов произвольной длины до 255 символов. для определения данных строкового типа используется идентификатор String, за которым следует заключенное в квадратные скобки значение максимально допустимой длины строки данного типа. Если это значение не указывается, то по умолчанию длина строки равна 255 байт. Строку можно рассматривать как массив символов, однако в связи с широким использованием строк и некоторыми особенностями по сравнению со стандартными массивами они выделены в отдельный тип данных. Переменную строкового типа можно определить через описание типа в разделе определения типов или непосредственно в разделе описания переменных. Строковые данные могут использоваться в программе также в качестве констант. Недопустимо применение строковых переменных в качестве селектора в операторе Case. Определение строкового типа устанавливает максимальное количество символов, которое может содержать строка. Формат: Type <имя типа> = string [максимальная длина строки]; var <идентификатор,…> : <имя типа>; Переменную типа string можно задать и без описания типа: var <идентификатор,…> : string [максимальная длина строки]; Пример: const Address=’ул. Ленина, 75а’; {строковая константа} Type Flot = string[125]; Var Fstr : Flot; {описание с задание типа} St1 : String; {по умолчанию длина строки = 255} St2, St3 : String[50]; Nazv : String[280]; {ошибка, длина превышает 255} Строка в языке Турбо Паскаль трактуется как цепочка символов. (Для строки из N символов отводится N+1 байт; N байт – для хранения символов строки, а один байт – для значения текущей длины строки.) К любому символу в строке можно обратиться, указав его номер. В самом начале строки (по нулевым номером) расположен байт, содержащий значение текущей длины строки. 2. Строковые выражения. Выражения, в которых операндами служат строковые данные, называются строковыми. Они состоят из строковых констант, переменных, указателей функций и знаков операций. Над строковыми данными допустимы операция конкатенации и операции сравнения. Конкатенация – применяется для объединения нескольких строк в одну результирующую строку. Например: Выражение Результат ’A’+’T’+’’+’386’ ’AT 386’ ’Турбо’+’Паскаль’+’7.0’ ’Турбо Паскаль 7.0’ следует учитывать, что в операциях конкатенации длина результирующей строки не должна превышать 255. Операции сравнения (=, <>, >, <, >=,<=) проводят сравнение двух строковых операндов и имеют приоритет более низкий, чем операция конкатенации, т.е. вначале всегда выполняются все операция конкатенации, если они присутствуют, и лишь потом реализуются операции сравнения. Сравнение строк производится слева направо до первого несовпадающего символа, и та строка считается больше, в которой первый несовпадающий символ имеет больший номер в стандартной таблице обмена информацией. Результат выполнения операций отношения над строковыми операндами всегда имеет булевский тип и принимает значение True, если выражение истинно, и False, если выражение ложно. Для присваивания строковой переменной результата строкового выражения используется оператор присваивания (:=). Пример. Str1 :=’Группа учащихся’; Str2 :=Str1+’Школы-лицея’; Fio :=’Бочаров А.А.’; Если значение переменной после выполнения оператора присваивания превышает по длине максимально допустимую при описании величину, все лишние символы справа отбрасываются, например: Описание А Выражение Значение А A: string[6] А:=’Группа 1’ ’Группа’ A: string[8] А:=’Группа 1’ ’Группа 1’ A: string[2] А:=’Группа 1’ ’Гр’ Допускается смешение в одном выражении операндов строкового и литерного типа. Если при этом литерной переменной присваивается значение строкового типа, длина строки должна быть равна единице, иначе возникает ошибка выполнения. К отдельным символам строки можно обратиться по номеру (индексу) данного символа в строке. Индекс определяется выражением целочисленного типа, которое записывается в квадратных скобках сразу за идентификатором строковой переменной или константы. Запись подобная Str[0] дает доступ к нулевому байту, содержащему значение текущей длины строки. Значение нулевого байта не должно превышать 255, но нарушение этого правила не вызывает программного прерывания, так как директива компилятора R по умолчанию находится в пассивном состоянии {$R-}. Для обеспечения строкового контроля за диапазоном допустимых значений индекса следует перевести директиву R в активное состояние {$R+}. В этом случае компилятор активизирует дополнительные команды для проверки правильности диапазона. Обычно активный режим R устанавливается на стадии отладки программ. Над длиной строки можно осуществлять необходимые действия и таким способом изменять длину. Например, удалить из строки все ведомые пробелы можно следующим образом: var st : String;  i : Byte; begin i := ord(st [0] ) ; {i - текущая длина строки}  while (i <> 0) and (st[i] = ' ') do  begin  dec(i); st[0] := chr(i)  end; ..... end. 3. Строковые процедуры и функции. Delete (St,Poz,N) –удаление N символов строки St, начиная с позиции Poz. Если значение Poz>255, возникает программное прерывание. Insert (Str1, Str2, Poz) – вставка строки Str1 в Str2, начиная с позиции Poz. Например: Var S1, S2 : string[11]; … S1 := ‘EC’; S2 := ‘ЭВМ1841’; Insert(S1, S2, 4); В результате выполнения последнего выражения значение строки S2 станет равным ‘ЭВМ ЕС 1841’. Str (IBR, St) – преобразование числового значения величины IBR и помещение результата в строку St. После IBR может записываться формат, аналогичный формату вывода. Если в формате указано недостаточное для вывода количество разрядов, поле вывода расширяется автоматически до нужной длины. Значение IBR Выражение Результат 1500 Str (IBR:6, St) ‘__1500’ 4.8Е+03 Str (IBR:10, St) ‘______4800’ 76854 Str (--IBR:3, St) ‘--76854’ Val (St, IBR, Code) – преобразует значение St в величину целочисленного или вещественного типа и помещает результат в IBR. Значение St не должно содержать незначащих пробелов в начале и конце. Code – целочисленная переменная. Если во время операции преобразования ошибки не обнаружено, значение Code равно нулю, если же ошибка обнаружена (литерное значение переводится в цифровое), Code будет содержать номер позиции первого ошибочного символа, а значение IBR не определено. Значение St Выражение Результат ‘1450’ Val (St, IBR, Code) Code=0 ’14.2Е+02’ Val (St, IBR, Code) Code=0 ’14.2А+02’ Val (St, IBR, Code) Code=5 Copy (St, Poz, N) – выделяет из St подстроку длиной N символов, начиная с позиции Poz. Если Poz>Lengh(St), то результатом будет пробел; если Poz>255, возникнет ошибка при выполнении. Poz, N – целочисленные значения. Concat (Str1, Str2,…,StrN) – выполняет сцепление строк Str1, Str2,…,StrN в том порядке, в каком они указаны в списке параметров. Сумма символов всех сцепленных строк не должна превышать 255, например: Выражение Результат Concat(‘AA’,’XX’,’Y’) ‘AAXXY’ Concat(‘Индекс’,’394063’) ’Индекс 394063’ Length (St) – вычисляет текущую длину в символах строки St. Результат имеет целочисленный тип, например: Значение St Выражение Результат ‘123456789’ Length (St) 9 ‘System 370’ Length (St) 10 Pos (Str1, Str2) – обнаруживает первое появление в строке Str2 подстроки Str1. Результат имеет целочисленный тип и равен номеру той позиции, где находится первый символ подстроки Str1. Если в Str2 подстроки Str1 не найдено, результат равен 0. Значение St1 Выражение Результат ‘abcdef’ Pos (‘de’,Str1) 4 ‘abcdef’ Pos (‘r’,Str1) 0 UpCase (Ch) – преобразует строчную букву в прописную. Параметр и результат имеют литерный тип. Обрабатывает буквы только латинского алфавита, например: Значение Выражение Результат ‘d’ UpCase (Ch) ‘D’ ‘w’ UpCase (Ch) ‘W’ Пример работы со строками: var х : Real;  у : Integer;  st,st1: String;  begin st := concat('12','345'); {строка st содержит 12345}  st1 := copy(st,3,Length(st)-2); {st1 содержит 345} insert('-',st1,2); {строка st1 содержит 3-45}  delete(st,pos('2',at),3); {строка st содержит 15}  str(pi:6:2,st); {строка st содержит 3.14}  val(''3,1415' ,x,y) ; {у содержит 2, х остался без изменения} end. Вопросы для самоконтроля: 1. Что такое строковый тип? 2. Как объявляются переменные строкового типа? 3. Как нумеруются элементы строки? 4. Что такое операция конкатенации? 5. Как сравниваются строки? 6. Перечислите и приведите примеры строковых процедур и функций. Лекция №10. Тема: Подпрограммы. Вопросы: 1. Подпрограммы в языке Turbo Pascal. 2. Процедуры и функции пользователя. 3. Локальные и глобальные имена. 4. Процедуры. 5. Функции. 6. Параметры-константы. 7. Параметры без типа. 8. Массивы и строки открытого типа. 9. Параметры-процедуры и параметры функции. 1. Подпрограммы в языке Турбо Паскаль. За наличие подпрограмм как средства структурирования программ язык программирования Турбо Паскаль называется процедурно-ориентированным. Программа, написанная на языке Turbo Pascal, как правило, не является монолитной, а состоит из нескольких логически связанных между собой программных единиц. Каждая программная единица в свою очередь может состоять из единиц более низкого уровня. В таких случаях говорят, что программа имеет иерархическую (соподчинённую) структуру. Среди программных единиц, составляющих программу, одна является основной, ведущей единицей, с которой начинается выполнение всей программы. Она называется главной программой. Остальные единицы носят название подпрограмм (рис.10). Рис. 10. Иерархическая структура программы: А - главная программа; В - подпрограмма первого уровня; C и D - подпрограммы второго уровня Подпрограмма представляет собой самостоятельный фрагмент программы, снабженный собственным именем, оформленный по определенным правилам и предназначенный для реализации некоторой части общего алгоритма задачи. Одну и ту же подпрограмму можно использовать в одной или нескольких точках главной программы или другой подпрограммы, поместив в эти точки операторы вызова подпрограммы, которые обеспечивают ее выполнение (активизацию). Каждый такой оператор должен содержать имя вызываемой подпрограммы и исходные данные. Данные эти передаются подпрограмме в момент её вызова, затем выполняются действия, заданные операторами подпрограммы, после чего управление передается вместе с результатами счёта в место вызова подпрограммы (рис.11). Рис.11. Взаимодействие вызывающей программы и подпрограммы Подпрограммы в основном используются в трёх случаях. Во-первых, когда решаемая задача является такой сложной и большой по объёму, что разработка программы в целом вызывает серьёзные затруднения. Поэтому приходится программировать задачу по частям, оформляя каждую в виде отдельной программной единицы и поручая их разработку разным исполнителям. Во-вторых, если одну и ту же совокупность математических операций, предназначенную, например, для вычисления определённого интеграла, решения системы линейных уравнений и т.п., нужно выполнить в нескольких точках программы с разными исходными данными. И, в-третьих, когда возникает необходимость в универсализации программных единиц с целью использования их при решении других задач. Представление программы в виде совокупности относительно самостоятельных единиц делают её более ясной и легко проверяемой, что позволяет выполнить её отладку, а в случае необходимости и модификацию с наименьшими затратами времени и средств. Это, в конечном счете, приводит к повышению качества и эффективности программ. Подпрограммы в Паскале реализованы посредством процедур и функций. Имея один и тот же смысл и аналогичную структуру, процедуры и функции различаются назначением и способом их использования. Процедура – это независимая именованная часть программы, которую можно вызвать по имени для выполнения определенных действий. Структура процедуры повторяет структуру программы. Процедура не может выступать как операнд в выражении. Упоминание имени процедуры в тексте программы приводит к активизации процедуры и называется ее вызовом. Функция аналогична процедуре, но имеются два отличия: функция передает в точку вызова скалярное значение; имя функции может входить в выражение как операнд. Итак, отличие подпрограмм – процедур от подпрограмм – функций состоит в том, что процедуры служат для задания совокупности действий, направленных на изменение внешней по отношению к ним программной обстановки, а функции, являясь частным случаем процедур, отличаются от них тем, что они обязательно возвращаются в точку вызова основной программы единственный результат как значение имени этой функции. Все процедуры и функции языка Турбо Паскаль делятся на две группы: встроенные (стандартные) и определенные пользователем. Первые входят в состав языка и вызываются для выполнения по строго фиксированному имени. Вторые разрабатываются и именуются самим пользователем. Все стандартные средства расположены в специализированных библиотечных модулях, которые имеют системные имена. Скалярные процедуры и функции: Dec(X{,n}) – процедура уменьшает значение целочисленной переменной Х на величину n. При отсутствии необязательного параметра n значение Х уменьшается на единицу. Inc(X{,n}) - процедура увеличивает значение целочисленной переменной Х на величину n. При отсутствии необязательного параметра n значение Х увеличивается на единицу. Pred(S) – функция возвращает элемент, предшествующий S в списке значений типа. Тип результата совпадает сипом размера. Если предшествующего S элемента не существует, возникает программное прерывание. Succ(S) – функция возвращает значение, следующее за S в списке значений типа. Тип результата совпадает сипом размера. Если следующее за S значение отсутствует, возникает программное прерывание. ODD(I:integer):Boolean – возвращает True, если I нечетное, и False, если I четное. Функции преобразования типов: Chr (I:byte):char Ord (S):longint Round (X:real):longint Trunc (X:real):longint Процедуры управления программой: Delay(I:word) Exit Halt(N:word) RunError (ErrorCode: word) 2. Процедуры и функции пользователя. Если в программе возникает необходимость частого обращения к некоторой группе операторов, выполняющих действия или вычисляющих значение какого-либо выражения, то рационально сгруппировать такую группу операторов в самостоятельный блок, к которому можно обращаться, указав его имя. Такие разработанные программистом самостоятельные программные блоки называются подпрограммами пользователя. Они являются основой модульного программирования. При вызове подпрограммы (процедуры или функции), определенной программистом, работа главной программы на некоторое время приостанавливается и начинает выполняться вызванная подпрограмма. Она обрабатывает данные, переданные ей из главной программы. По завершении выполнения подпрограмма-функция возвращает главной программе результат (подпрограмма-процедура не возвращает явно результирующего значения). Передача данных из главной программы в подпрограмму и возврат результата выполнения функции осуществляется с помощью параметров. Параметром называется переменная, которой присваивается некоторое значение в рамках указанного применения. Различают формальные параметры – параметры, определенные в заголовке подпрограммы, и фактические параметры – выражения, задающие конкретные значения при обращении к подпрограмме. При обращении к подпрограмме ее формальные параметры замещаются фактическими, переданными из главной программы. Так же, как описание программы в целом, описание подпрограммы состоит из заголовка, раздела описания объектов подпрограммы и раздела операторов, который заканчивается символом ’ ; ’ (рис.12): Рис.12. Структура описания подпрограммы Описание подпрограмм размещают в разделе описаний вызывающей программной единицы вместе с описанием меток, констант, типов и переменных. 3. Локальные и глобальные имена. Каждая программная единица, будь то главная программа или подпрограмма, оперирует с различными объектами: константами, типами, переменными, имеющими свои имена. Желание придать подпрограмме относительную независимость в использовании имён привело к разделению их на глобальные (общие) и локальные (местные). Принципы, которые положены в основу деления имён на глобальные и локальные, можно сформулировать так. 1. Имена объектов, описанных в разделах const, type и var главной программы, являются глобальными, т.е. доступными для использования как в ней самой, так и во всех вложенных в неё программных единицах. 2. Имена, описанные в любой вложенной единице, для единицы, объемлющей ее, считаются локальными, а для вложенных в нее - глобальными. Они доступны как в пределах этой единицы, так и для всех вложенных в эту единицу, но недоступны для объемлющей программной единицы. 3. Если в двух программных единицах - объемлющей и вложенной совпадают имена различных объектов (что вполне допустимо), то во вложенной единице может быть использовано только имя локального объекта. Глобальное же имя для этой единицы становится недоступным. Говорят, что оно экранируется (закрывается) локальным именем. Обратимся вновь к иерархической структуре программы (рис 9.1.). Имена объектов, описанных в программной единице А (главная программа), являются глобальными для всей программы и доступны не только для самой единицы А, но и для единиц В, С и D. Имена объектов единицы В для объемлющей единицы А являются локальными и для этой единицы не доступны, но доступны в пределах единиц С и D, для которых они являются глобальными. Сами же единицы С и D имеют каждая свои локальные имена, недоступные для единиц В и А. Например: Program Exampl; Procedure P; Procedure A; Var j : integer; {локальная переменная j, является глобальной по отношению к процедуре В} Procedure B; Var j : integer; { локальная переменная j, экранирует глобальную переменную j, описанную в вызывающей процедуре А} Begin Writeln (j); End; Begin J:=1; B; {вызов процедуры В} End; Begin A; {вызов процедуры А} End. 4. Процедуры. Как отмечено выше, при вызове подпрограммы между ней и главной программой происходит обмен информацией. В момент вызова подпрограмма получает данные, необходимые для её выполнения (входные параметры), а после завершения их обработки может в случае необходимости передать главной программе результаты счёта (выходные параметры). Обмен данными между главной программой и процедурой может осуществляться либо с помощью глобальных переменных, либо с использованием фактических и формальных параметров. Фактические параметры - это те исходные данные, которые передаются в процедуру с целью их обработки. Фактическими параметрами, в общем случае, могут быть константы, выражения, имена переменных. Они должны присутствовать в операторах вызова процедуры, которые размещаются в тех точках главной программы, где этого требует алгоритм задачи. Оператор вызова процедуры состоит только из обращения к процедуре (аналог - обращение к стандартной функции) и имеет вид: имя процедуры (список фактических параметров) Допустим, что у нас имеется процедура treug, которая вычисляет площадь треугольника по трём его сторонам a, b и c. Тогда вызывающий оператор может выглядеть так treug (a, b, c); или treug(1.2, 2.3, 1.5); В первом случае фактическими параметрами, задающими длины сторон треугольника, являются имена переменных, а во втором - константы. Формальные параметры в процедуре являются как бы «двойниками» фактических параметров и выполняют две функции: 1) на этапе разработки процедуры с их помощью показывают как процедура обрабатывает исходные данные, т.е. они наряду с другими переменными процедуры используются для описания её алгоритма; 2) во время обращения к процедуре каждый её формальный параметр «принимает» значение фактического; именно поэтому списки формальных и фактических параметров должны быть согласованы по количеству элементов, порядку их следования и типу. Это значит, что формальных и фактических параметров должно быть одинаковое количество, порядок их следования в своих списках в направлении слева направо должен быть одним и тем же и, наконец, тип каждого фактического параметра должен совпадать с типом соответствующего ему формального. Формальными параметрами могут быть только имена переменных. Они располагаются в заголовке процедуры, вид которого: procedure имя_процедуры (список формальных параметров); Формальные параметры процедуры делятся на параметры-значения, параметры-переменные и бестиповые параметры. Это значит, что формальных и фактических параметров должно быть одинаковое количество, порядок их следования в своих списках в направлении слева направо должен быть одним и тем же и, наконец, тип каждого фактического параметра должен совпадать с типом соответствующего ему формального. Формальными параметрами могут быть только имена переменных. Они располагаются в заголовке процедуры, вид которого procedure имя_процедуры (список формальных параметров); Формальные параметры процедуры делятся на параметры-значения, параметры-переменные и бестиповые параметры. Параметры-значения способны выполнять роль только входных параметров процедуры. Они могут принимать значения фактических параметров, изменять их в ходе выполнения процедуры, но вернуть значения изменённых параметров в главную программу с их помощью нельзя. Описание параметров-значений в заголовке процедуры выглядит так: имя1, имя2, ... : t1; имя3, имя4, ... : t2; ... где имя1, имя2, имя3, имя4, ... - имена параметров; t1, t2 - типы параметров. Для каждого формального параметра-значения резервируется память ЭВМ, в которую при вызове процедуры копируется значение соответствующего фактического параметра, после чего всякая связь между фактическими и формальными параметрами обрывается. Если фактическим параметром является выражение, то оно предварительно вычисляется. Далее эти копии обрабатываются процедурой, причем фактические параметры будут существовать только в течение времени ее выполнения. Пример. Используя процедуру, вычислить площадь треугольника со сторонами a, b и c. Для решения задачи воспользоваться формулой Герона s = (p*(p‑a)*(p-b)*(p-c))1/2, где p - полупериметр треугольника. Рис. 13. Алгоритм главной программы. Рис. 14. Алгоритм подпрограммы. program example; var a, b, c : real ; procedure tr (ab, bc, ca : real ); var p, s : real; begin p := (ab + bc + ca) / 2; s := sqrt (p * (p - ab) * (p - bc) * (p - ca)); writeln ('s= ', s:6:2) end; begin readln (a, b, c); tr (a, b, c); { оператор вызова процедуры } end. Глобальные переменные a, b и c, описанные в главной программе, используются в операторе вызова процедуры в качестве входных фактических параметров. Они согласуются с формальными параметрами ab, bc и ca процедуры по числу, типу и взаиморасположению в своих списках. Следовательно, при вызове процедуры её формальный параметр ab примет значение фактического параметра a, параметр bc значение параметра b, а параметр ca значение параметра c. Как формальные параметры-значения, так и переменные p и s, описанные в разделе var процедуры, являются локальными переменными и главной программе недоступны. Поэтому вывод результата счёта-переменной s предусмотрен не в главной программе, а в процедуре. Значения глобальных переменных a, b и c после выполнения процедуры останутся неизменными. Для того, чтобы процедура могла не только изменить значения передаваемых ей параметров, но и возвратить их новые значения в главную программу, в заголовке процедуры нужно объявить соответствующие им формальные параметры переменными, поставив перед ними ключевое слово var: var имя1, имя2, ... : t; где имя1, имя2, ... - имена параметров; t - тип параметров. Используя формальные параметры-переменные, процедура получает доступ непосредственно к тем ячейкам памяти, где находятся значения фактических параметров. Достигается это тем, что в процессе обращения к процедуре в ячейки памяти, соответствующие формальным параметрам-переменным, записываются не копии значений фактических параметров, а адреса ячеек памяти, в которых эти фактические параметры находятся. При использовании формального параметра-переменной процедура сначала определяет адрес, по которому нужно обратиться, а затем считывает данное, содержащееся в ячейке с этим адресом. Таким образом, и главная программа и процедура используют одни и те же ячейки памяти, что и делает возможной передачу результатов счета из процедуры в главную программу. Описанный способ передачи параметров, называемый передачей по ссылке, делает невозможным использование констант и выражений в качестве фактических параметров. Ими могут быть только переменные. Используя процедуру tr, вычислить поверхность треугольной пирамиды, если известны длины ее ребер a, b, c, d, e и f. program example; var a, b, c, d, e, f, s1, s2, s3, s4 : real ; procedure tr (ab, bc, cd : real ; var s : real ); var p : real ; begin p:=(ab + bc + cd ) / 2; s:=sqrt (p * (p - ab) * (p - bc) * (p - cd)) end; begin readln (a, b, c, d, e, f); tr (a, b, c, s1); tr (a, d, e, s2); tr (b, e, f, s3); tr (c, f, d, s4); write ('поверхность равна ', s1 + s2 + s3 + s4) end. В главной программе предусмотрены четыре обращения к процедуре tr (по числу граней пирамиды). Список её формальных параметров состоит из трёх локальных переменных (параметры-значения ab, bc, cd) и одной глобальной (параметр-переменная s). При первом обращении к процедуре в ячейку s будет записан адрес переменной s1, а в ячейки ab, bc и cd значения переменных a, b и c. Результат выполнения процедуры (площадь треугольника со сторонами a, b и c) будет записан в ячейку s1, адрес которой процедура найдёт в ячейке s. Таким образом, главная программа получит доступ к результату, вычисленному в процедуре, и использует его в вычислении площади поверхности пирамиды. После выполнения процедуры значения переменных ab, bc и cd будут «забыты». Остальные обращения к процедуре tr будут выполнены аналогично. 4. Функции. Функция, будучи аналогичной процедуре, отличается от нее тем, что результатом ее выполнения является только одно значение, которое возвращается в точку вызова. Второе отличие состоит в том, что имя функции может использоваться в выражении в качестве операнда. Для возврата в главную программу вычисленного функцией значения в теле функции необходимо предусмотреть хотя бы один оператор присваивания, в котором слева от символа ’:=’ ставится имя функции, а справа - выражение, вычисляющее её значение. Таких операторов в теле функции может быть и несколько, но при каждом обращении к ней должен выполняться только один. Тип значения, вычисляемого функцией, задаётся в её заголовке, имеющем вид: function имя_функции (список формальных параметров):t; где t - тип вычисляемого значения. Вычисление площади треугольника оформить в виде функции. function tr1(ab, bc, ca : real ) : real ; var p : real ; begin p:=(ab + bc + ca) / 2; tr1:=sqrt(p * (p - ab) * (p - bc) * (p - ca)) end; Обращение к функции аналогично обращению к процедуре, но самостоятельным оператором не является. В качестве операторов вызова функции используют операторы, содержащие выражения, например, оператор присваивания, условный оператор или процедуру вывода. При этом обращение к функции должно входить в выражение в качестве его операнда. Используя функцию tr1, решить задачу, вычисляющую площадь поверхности треугольной трапеции, можно следующим образом: program example; var a, b, c, d, e, f, s : real; function tr1(ab, bc, ca : real ):real; var p : real; begin p:=(ab + bc + ca) / 2; tr1:=sqrt (p * (p - ab) * (p - bc) * (p - ca)) end; begin readln (a, b, c, d, e, f); s:=tr1 (a, b, c) + tr1 (a, d, e) + tr1(b, e, f) + tr1(c, f, d); writeln ('s= ', s:10:2) end. Оператором вызова служит оператор присваивания. Операндами его арифметического выражения являются четыре обращения к функции. И процедуры и функции в качестве формальных параметров могут использовать как параметры-значения, так и параметры-переменные. Однако, использование параметров-переменных зачастую приводит к нежелательным побочным эффектам, связанным с их изменением в процессе выполнения подпрограммы. В такой ситуации возрастает вероятность появления трудноуловимых ошибок. Поэтому опытные программисты рекомендуют всюду, где это возможно, в качестве формальных параметров использовать параметры-значения. 5. Параметры-константы. Часто в качестве параметра в подпрограмму следует передать ту или иную переменную, но изменять ее подпрограмма не должна. В этом случае нежелательно передавать этот параметр как параметр-переменную. Можно его передать как параметр-значение, однако если эта переменная имеет большой размер (массив, запись и т.д.), то копия такого параметра займет большую часть стека и даже может его переполнить. Это же приводит к уменьшению быстродействия программы. В этой ситуации параметр лучше передавать как параметр-константу. Такой параметр, если он структурированного типа, передается своим адресом, но предусматривается защита от его изменения. Параметр-константа указывается в заголовке подпрограммы аналогично параметру-значению, но перед именем параметра записывается зарезервированное слово const. Действие слова const распространяется до ближайшей точки с запятой, т.е. в пределах одной группы. Пример: Function NewString ( const S: string): string; Тип параметра-значения может быть любым за исключением файлового. При вызове подпрограммы на месте параметра-переменной в качестве фактического параметра можно использовать любое выражение совместимого для присваивания типа, не содержащего файловую компоненту. Параметр-константу нельзя передавать в другую подпрограмму в качестве фактического параметра. Пример: Function max (const Mas: tArr; N: byte): integer; Var Ma : integer; I : byte; Begin Ma := Mas[1]; For I:= 2 to N do If Ma < Mas[i] then Ma:= Mas[I]; Max:=Ma End; 6. Параметры без типа. В Турбо Паскале можно использовать параметры-переменные и параметры-константы без указания типа. В этом случае фактический параметр может быть переменной любого типа, а ответственность за правильность использования того или иного параметра возлагается на программиста. Пример: Function Equal (var Param1, Param2; Len: Word) : Boolean; Здесь Param1, Param2 – параметры-переменные без типа (вместо них можно использовать, например, любые переменные простого типа, типа-массив, типа-запись и т.д.);Len – параметр-значение. Следует иметь ввиду. Что параметр без типа внутри подпрограммы типа не имеет и его перед использованием следует преобразовать к конкретному типу, применяя иднетификатор соответствующего типа, при этом полученный результат может быть любого размера. Пример: Function max (var Mas; N: byte): integer; Type tArray = array[1..MaxInt] of integer; {тип массива максимального размера} Var Ma : integer; I : byte; Begin Ma := tArray(Mas)[1]; For I:= 2 to N do If Ma < tArray(Mas)[i] then Ma:= tArray(Mas)[i]; Max:=Ma End; В этом случае в качестве первого передаваемого параметра можно использовать любой массив (и не только массив), так что подпрограмма становится более универсальной. Тем не менее здесь необходимо передавать в качестве второго параметра фактический размер информации, что не очень удобно. 7. Массивы и строки открытого типа. В версии 7.0 можно в качестве параметров-переменных использовать массивы и строки открытого типа, у которых не задаются размеры. В качестве фактического параметра в этом случае можно использовать массив или строку любого размера, однако массив должен состоять из тех же компонент, что и компоненты открытого массива. Такие параметры введены для того, чтобы подпрограмма могла обрабатывать массив или строку любого размера. Фактический размер массива в этом случае может быть определен с помощью функции High. Открытый массив задается как и обычный массив, но только без указания типа индекса. Следует иметь ввиду, что индексация элементов открытого массива всегда начинается с нуля, а максимальный индекс элемента равен значению функции High. Пример: Function max (var Mas: array of integer): integer; Var Ma : integer; I : byte; Begin Ma :=Mas[0]; For I:=1 to High(Mas) do {цикл до наибольшего индекса} If Ma < Mas[i] then Ma:=Mas[i]; Max:=Ma End; В этом примере в подпрограмму передается только один параметр и она может работать с любым одномерным массивом целых чисел. Однако следует иметь ввиду, что при работе подпрограммы для открытого массива в стеке опять-таки создается его копия, что может его переполнить. Разновидность открытого массива – открытая строка, которая может задаваться либо с помощью стандартного типа OpenString, либо с помощью типа String и использования ключа компилятора {$P+}, например заголовок процедуры, заполняющей каким-либо символом строку, может иметь вид: Prucedure FillChar (var Str : OpenString; Ch : Char); или {$P+} Prucedure FillChar (var Str : String; Ch : Char); 8. Параметры-процедуры и параметры-функции. Передаваемым параметром может быть также параметр-процедура или параметр-функция, т.е. параметр процедурного типа. Фактически этот параметр является параметром-значением, т.к. записывается без зарезервированного слова var. В качестве фактического параметра в этом случае используется соответствующая процедура или функция, имеющая необходимое количество параметров требуемых типов. Для параметров-процедур и параметров-функций существуют те же правила, что и для других переменных процедурного типа: подпрограммы должны компилироваться с ключом {$F+} или иметь директиву far должны быть стандартными подпрограммами, не должны объявляться внутри других подпрограмм, не иметь директив inline или interrupt. Пример: программа, печатающая таблицы сложения и умножения двух целых чисел в заданном диапазоне. Program Example; Type Func= function (X,Y : integer): integer; {$F+} function Add(X,Y : integer): integer; begin Add:=X+Y; End; function Multiply(X,Y : integer): integer; begin Multiply:=X*Y; End; {$F-} procedure PrintTable (A,B : integer; Operation : Func); {процедура печати таблицы} var i,j : integer; begin for i:= 1 to A do begin for j:= 1 to B do Wrire(Operation(i,j):5); Writeln; End; Writeln End; Begin {начало основной программы} PrintTable (10, 10, Add); PrintTable (10, 10, Multiply); End. Вопросы для самоконтроля: 1. Что такое подпрограммы? 2. Для чего используют подпрограммы? 3. Какие виды подпрограмм бывают и чем они отличаются? 4. Что такое глобальные и локальные объекты? 5. Что такое процедура? 6. Что такое функция? 7. Что такое параметры-переменные? 8. Что такое параметры-значения? 9. Что такое формальные параметры? 10. Что такое фактические параметры? 11. Что такое параметры-константы? 12. Что такое параметры без типа? Лекция №11. Тема: Рекурсивные определения и алгоритмы. Вопросы: 1. Определение рекурсии. 2. Пример рекурсивного алгоритма. 1. Определение рекурсии. Под рекурсией (от лат. recursio - возвращение) понимают способ организации вычислительного процесса, который позволяет находить n-ный член какой-либо последовательности (чаще всего числовой), используя для этого один или несколько её предыдущих членов. Например, широко известный ряд чисел Фибоначчи 1, 1, 2, 3, 5, 8, ... для n > 2 вычисляется по рекуррентной формуле: F(n) = F(n - 1) + F(n - 2) Подпрограмма называется рекурсивной, если она вызывает саму себя (прямая ре­курсия). Рекурсивной также будет процедура, вызывающая другую процедуру, которая, в свою очередь, обращается к первой процедуре (косвенная рекурсия). Возможны и более сложные конструкции. При написании программ, в которых та или иная процедура рекурсивно вызывает саму себя или другую процедуру, следует соблюдать определенные «правила пред­осторожности». Рекомендуется компилировать программу с директивой {$S+}. Эта директива включает проверку переполнения стека (области памяти, в кото­рой хранится информация о состоянии вызывающей подпрограммы). Если в про­цессе выполнения программы происходит переполнение стека, вызов процедуры или функции, откомпилированной с директивой {$S+}, приводит к завершению работы программы, а на дисплей выдается сообщение об ошибке. Количество рекурсивных вызовов называется глубиной рекурсии. Глубина рекурсии должна быть конечной. Позаботиться об этом должен программист, выбирающий или разрабатывающий рекурсивный алгоритм. 2. Пример рекурсивного алгоритма. Примером использования рекурсии является вычисление факториала Для решения задачи, в которой используется рекурсия, необходимо, чтобы процедура или функция могла бы вызвать саму себя. TurboPascal такую возможность предоставляет. program example96; var n, f : integer; function factorial (m : integer) : integer ; begin if m=0 then factorial := 1 else factorial := factorial (m - 1) * m end; begin readln (n); f:=fac (n); write ('факториал ',n:2, '=',f:2) end. Замечание. Поскольку n! растет очень быстро, то при n > 7 полученные значения превышают максимально возможное значение типа integer (32767), поэтому рекомендуется n! считать как longint. Программа содержит два обращения к функции factorial - одно в операторе вызова в главной программе, другое - в операторе присваивания в теле самой функции. Из главной программы функция вызывается один раз. При этом ей передаётся значение фактического параметра n. Для определённости допустим, что n = 3. Итак, функция начинает выполняться со значением формального параметра m = 3. Поскольку m  0, должна выполниться ветвь else оператора if, т.е. оператор присваивания factorial := factorial(2)*3. Но правая часть этого оператора содержит обращение к функции factorial, поэтому функция обратится сама к себе с параметром равным 2. Это в свою очередь приведёт к новому обращению к функции с фактическим параметром равным 1. Таким образом, однократный вызов функции из главной программы приводит к цепочке последовательных незавершенных вызовов этой же функции самой себя. При этом в каждом последующем вызове используется значение формального параметра на 1 меньшее того, что использовалось в предыдущем. Все эти значения запоминаются в разных ячейках памяти. Так будет продолжаться до тех пор, пока значение функции не станет полностью определённым, что произойдёт при значении параметра m = 0. В этом случае будет выполнен оператор factorial := 1, после чего процесс вычисления факториала начнёт «раскручиваться» в обратную сторону, последовательно выполняя операторы: factorial(1):=factorial (0)*1 factorial (2):=factorial (1)*2 factorial (3):=factorial (2)*3 Другой пример использования рекурсии приводится в программе Cantor, кото­рая строит двумерное множество Кантора. Алгоритм его построе­ния следующий. 1. Построить квадрат размером L. 2. Вырезать расположенный в центре квадрат размером 1/2. 3. Разделить исходный квадрат на четыре равные части размером 1/2. 4. Для каждого из четырех квадратов повторить шаги 2 и 3. В результате получается самоподобное множество — фрактал. В программе Cantor сохраняется изображение границы вырезанного квадрата, а построение множества идет не от большего квадрата к меньшим, а наоборот. Листинг. Программа «Двумерное множество Кантора» program Cantor; uses Crt, Graph, graphs; var ch : Char; const min_size =1; procedure draw(x, у : Integer: size : Word); var s : Word; begin if size > min_size then begin s := size div 2: draw(x - size, у + size, s); draw(x - size, у - size, s): draw(x + size, у + size, s); draw(x + size, у - size, s): end; Rectangle(x - size, у - size, x + size, у + size); Bar(x - size + 1. у - size + 1. x + size - 1, у + size - 1); end; begin open_graph; SetFillStyle(SolidFill. Black): SetColor(White); draw(GetMaxX div 2, GetMaxY div 2, GetMaxY div 4); OutTextXY(265. 235, 'Press '); ch := ReadKey: SetColor(Black); SetWriteModetXorPut); draw(GetMaxX div 2. getmaxy div 2. GetMaxY div 4): SetColor(White); OutTextXY(265, 235, 'Press '): ch := ReadKey; close_graph; end. Процедура SetWriteMode модуля Graph устанавливает режим вывода линии. Режим задается с помощью логической операции. В нашем примере используется опера­ция исключающего ИЛИ (ей соответствует константа XorPut), поэтому изображе­ние линии комбинируется с изображением, уже выведенным на экран. Вопросы для самоконтроля: 1. Что такое рекурсия? 2. Что называется прямой рекурсией? 3. Что называется косвенной рекурсией? 4. Что называется глубиной рекурсии? 5. Приведите примеры задач, для решения которых удобно применять рекурсию. Лекция №12. Тема: Структурированный тип данных записи. Вопросы: 1. Определение и описание типа запись. 2. Обращение к полям записи. 1. Определение и описание типа запись. Иногда для решения задач, в которых возникает необходимость хранить и обрабатывать совокупность данных различного типа, используются отдельные массивы для каждого типа данных, а для установления соответствия между ними вводятся соответствующие индексы. Итак, реальные данные об объектах часто описываются величинами разных типов. Например, товар на складе описывается следующими величинами: наименование, количество, цена, наличие сертификата качества и т.д. В этом примере наименование – величина типа string, количество –integer, цена – real, наличие сертификата качества можно описать величиной типа Boolean. Для записи комбинации объектов разных типов в Паскале применяется комбинированный типа данных – запись. Запись представляет собой наиболее общий и гибкий структурированный тип данных, так как она может быть образована из неоднотипных компонентов и в ней явным образом выражена связь между элементами данных, характеризующими реальный объект. Запись – это структурированный тип данных, состоящий из фиксированного числа компонентов одного или нескольких типов. Определение типа записи начинается идентификатором record и заканчивается зарезервированным словом end. Между ними заключен список компонентов, называемых полями, с указанием идентификаторов полей и типа каждого поля. Формат записи имеет вид: Type <ИМЯ ТИПА>= record <идентификатор поля> : <тип компонента>; … <идентификатор поля> : <тип компонента>; end; var <идентификатор,…> : <ИМЯ ТИПА>; Пример: Type Car=record Number : integer; {номер} Marca : string[20]; {марка автомобиля} FIO : string[40]; {фамилия, инициалы владельца} Address : string[60] {Адрес владельца} End; Var M,V : Car; В данном примере запись Car содержит четыре компонента: номер, название марки машины, фамилию владельца и его адрес. Доступ к полям записи осуществляется через переменную типа «запись». В нашем случае это переменные M и V типа Car. Идентификатор поля должен быть уникален только в пределах записи, однако во избежание ошибок лучше делать его уникальным в пределах всей программы. Объем памяти, необходимый для записи, складывается из длин полей. 2. Обращение к полям записи. Значения полей записи могут быть использованы и в выражениях. Имена отдельных полей не применяются по аналогии с идентификаторами переменных, поскольку может быть несколько записей одинакового типа. Обращение к значению поля осуществляется с помощью идентификатора переменной и идентификатора поля, разделенных точкой. Такая комбинация называется составным именем. Например, чтобы получить доступ к полям записи Car, надо записать: M.Number, M.Marka, M.FIO, M.Address Составное имя можно использовать везде, где допустимо применение типа поля. Для присваивания полям значений используется оператор присваивания. Пример: M.Number := 1678; M.Marka := ‘ГАЗ - 24’; M.FIO := ‘Демьяшкин В.А.’; M.Address := ‘ул. Пушкинская 12 - 31’; Составные имена можно использовать, в частности, в оператора ввода-вывода: Read(M.Number, M.Marka, M.FIO, M.Address); Write(M.Number:4, M.Marka:7, M.FIO:12, M.Address:25); Допускается применение оператора присваивания и к записям в целом, если они имеют один и тот же тип. Например, V:=M; После выполнения этого оператора значения полей записи V станут равными значениям соответствующих полей записи М. В ряде задач удобно пользоваться массивами записей. Их можно описать следующим образом: Type Person=record FIO : string[60]; Age: 1..99; Prof : string[30] End; Var List : array[1..50] of Person; Обращение к полям записи имеет несколько громоздкий вид, что особенно неудобно при использовании мнемонических идентификаторов длиной более пяти символов. Для решения этой проблемы в языке Паскаль предназначен оператор With, который имеет следующий формат: With <переменная типа запись> do <оператор>; Один раз указав переменную типа запись в операторе With, можно работать с именами полей как с обычными переменными, т.е. без указания перед идентификатором поля имени переменной, определяющей запись. Пример: Присвоить значения полям записи Car с помощью оператора With. With M do begin Number := 1678; Marka := ‘ГАЗ - 24’; FIO := ‘Демьяшкин В.А.’; Address := ‘ул. Пушкинская 12 - 31’; End; Паскаль допускает вложение записей друг в друга (т.е. поле записи может быть в свою очередь тоже записью). Для вложенных полей приходится продолжать уточнения: Пример: Type TypeAddress=record town: string[20]; street: string[20]; Nhouse: integer; Nflat: integer End; Var Car: record Number : integer; {номер} Marca : string[20]; {марка автомобиля} FIO : string[40]; {фамилия, инициалы владельца} Address : TypeAddress {Адрес владельца} End; Var M,V : Car; Соответственно оператор With тоже может быть вложенным: With V do With Address do town:=’Voronezh’; Это эквивалентно With V do with Address do town:=’Voronezh’; Или With V, Address do town:=’Voronezh’; Или V. Address. town:=’Voronezh’; Уровень вложения не должен превышать 9. Записи используются обычно при работе с динамическими структурами и для организации файлов на магнитных дисках. Записи могут служить также для описания комплексных чисел, так как в языке Паскаль нет для этого специальных средств. В этом случае действительная и мнимая части комплексного числа являются полями записи. Вопросы для самоконтроля: 1. Объясните правила использования типа запись. 2. Поясните, что называется полем записи. 3. Какие требования предъявляются к идентификаторам полей записи? 4. Что такое составное имя поля записи и из каких частей оно состоит? 5. Для чего используется оператор with? Лекция №13. Тема: Записи с вариантной частью. Вопросы: 1. Определение и описание записи с вариантной частью. 2. Правила использования записи с вариантной частью. 1. Определение и описание записи с вариантной частью. Записи, представленные выше, имеют строго определенную структуру. В некоторых случаях это резко ограничивает возможности их применения. Поэтому в языке Паскаль имеется возможность задать тип записи, содержащий произвольное число вариантов структуры. Такие записи называют записями с вариантами. Записи с вариантами обеспечивают средства объединения записей, которые похожи, но не идентичны по форме. Они состоят из фиксированной и вариантной частей. Использование фиксированной части не отличается от описанного ранее. Вариантная часть формируется с помощью оператора case. Он задает особое поле записи – поле признака, которое определяет, какой из вариантов в данный момент будет активизирован. Значением признака в каждый текущий момент выполнения программы должна быть одна из расположенных далее констант. Константа, служащая признаком, задает вариант записи и называется константой выбора. Формат: Type Rec = record <поле1> : <тип>; . . . <поле> : <тип>; Case <поле признака> : <имя типа> of <константа выбора1> : <поле,…: тип>; . . . <константа выбораN> : <поле,…:тип>; end; Компоненты каждого варианта (идентификаторы полей и их типы) заключаются в круглые скобки. У части Case нет отдельного end, как этого следовало бы ожидать по аналогии с оператором Case. Одно слово end заканчивает всю конструкцию записи с вариантами. Необходимо отметить, что количество полей каждого из вариантов не ограничено. Объем памяти, необходимый для записи с вариантами, складывается из объемов полей фиксированной части и максимального по объему поля переменной части. Пример: Type Rec = record Number : byte; Code : integer; Case Flag : Boolean of True : (Price1 : integer); False : (Price2 : real); End; Var PRec : Rec; Поля Number и Code расположены в фиксированной части записи, они доступны в программе в любой текущий момент независимо от значения поля признака. Поле Price1 может использоваться только в том случае, если значение поля признака Flag равно True. Поле Price2 доступно в противоположном случае, т.е. если значение Flag равно False. 2. Правила использования записи с вариантной частью. При использовании записей с вариантами необходимо придерживаться следующих правил: • Все имена полей должны отличаться друг от друга по крайней мере одним символом, даже если они встречаются в разных вариантах; • Запись может иметь только одну вариантную часть, причем вариантная часть должна размещаться в конце записи; • Если поле, соответствующее какой-либо метке, является пустым, то оно записывается следующим образом: <метка> : (); Вопросы для самоконтроля: 1. Поясните зачем применяются записи с вариантами. 2. Из каких частей состоит запись с вариантами? 3. Что называется полем признака? 4. Как записываются компоненты каждого варианта записи? 5. Какие правила следует соблюдать при использовании записей с вариантной частью? Лекция №14. Тема: Файловый тип данных. Вопросы: 1. Целесообразность применения и особенности файлового типа данных. Виды файлов. 2. Объявление файлов. 3. Доступ к файлам. 4. Имена файлов. 5. Общая схема работы с файлами. 6. Инициализация файлов. 7. Процедуры и функции для работы с файлами. 8. Текстовые файлы. 9. Типизированные файлы. 10. Нетипизированные файлы. 1. Целесообразность применения и особенности файлового типа данных. Виды файлов. В системах обработки больших объемов информации данные хранятся не в оперативной памяти, а во внешней памяти, на внешних запоминающих устройствах, например на жестком магнитном диске. На внешних носителях данные хранятся в виде физических файлов. Под физическим файлом понимается именованная область внешней памяти на магнитном диске для хранения данных или логическое устройство – источник или приемник информации. Целесообразность применения файлов диктуется следующими причинами: 1. ввод больших объемов данных, подлежащих обработке, утомителен и требует большого времени. Гораздо удобнее создать отдельный файл данных, который может быть подготовлен заранее и самое главное, применяться неоднократно; 2. файл данных может быть подготовлен другой программой, становясь, таким образом, связующим звеном между двумя разными задачами. А также средством связи программы с внешней средой; 3. программа, использующая данные из файла, не требует присутствия пользователя в момент фактического исполнения. Любой файл имеет три характерных особенности: 1. файл или устройство имеет имя; 2. файл содержит компоненты (элементы, записи) одного типа; 3. размер (длина) создаваемого файла не оговаривается и ограничивается только емкостью доступной памяти или устройства. Для работы с файлом в общем случае надо объявить переменную файлового типа, связать ее с физическим файлом, открыть, обработать и закрыть. В программе объявляется переменная файлового типа (логический файл). Связь логического с физическим устанавливается динамически, т.е. в процессе выполнения программы, с помощью процедуры Assign. Это позволяет посредством одного логического файла обработать ряд физических файлов того же типа, подключая логический файл поочередно к различным физическим файлам процедурой Assign. Создание и обработка данных, содержащихся в файлах, производится с помощью процедур обмена данными между оперативной памятью и магнитным диском: read и write. Процедура read копирует данные из внешней памяти или внешнего устройства в область оперативной памяти, выделенную переменным. Процедура write копирует данные из области оперативной памяти, выделенной заданным переменным, на магнитный диск или внешнее устройство (например, экран или принтер). Обработка, т.е. изменение значений каких-либо данных (переменных), может производится только в оперативной памяти. Внешняя память на магнитных дисках предназначена только для хранения данных. Для изменения данных, расположенных на МД, они должны быть выведены (скопированы) в ОП с помощью процедуры read, скорректированы, а затем возвращены на МД процедурой write. В Паскале можно использовать три типа файлов: 2. текстовые (объявляются словом text); 3. типизированные (объявляются словом file of); 4. нетипизированные (бестиповые, объявляются словом file). Нетипизированные файлы отличаются тем, что для них не указывается тип компонентов файла. Эти файлы используют для высокоскоростного копирования файлов из внешней памяти в оперативную и обратно. 2. Объявление файлов. Объявление файлов можно производить в разделе Type или Var. Файловый тип или переменную файлового типа можно задать одним из трех способов: <имя>=FILE OF <тип>; <имя>=TEXT; <имя>=FILE; Здесь <имя> - имя файлового типа (правильный идентификатор); FILE OF – зарезервированные слова (файл, из); ТЕХТ – имя стандартного типа текстовых файлов; <тип> - любой тип Паскаля, кроме файлов. Пример объявления текстовых файлов: Var F1, F2: text; Где F1, F2 – файловые переменные, имена логических файлов. При объявлении типизированных файлов определяется тип компонентов файла. Пример объявления типизированного файла: Type product= Record name : string[20]; code: word; cost: comp; End; Var F1 : file of product; F2 : file of real; Пример объявления нетипизированного файла: Var F3:file; 3. Доступ к файлам. Любой программе на Паскале по умолчанию доступны текстовые файлы INPUT и OUTPUT, определенные по умолчанию. Все остальные файлы и логические устройства становятся доступными программе только после выполнения процедуры Assign – связывания файловой переменной (логического файла) с именем физического файла или устройства. Форма вызова процедуры Assign: Assign (F, FN); где F – файловая переменная, (правильный идентификатор, объявленный в программе как переменная файлового типа); FN – текстовое выражение, определяющее имя физического файла или логического устройства. Если FN задано в виде пустой строки, например, ASSIGN(f,’’), то в зависимости от направления обмена данными файловая переменная связывается со стандартным файлом INPUT или OUTPUT. Примеры связи логических файлов с физическими: Assign (F, ‘LR1.DAT’); Assign (INPUT, ‘LR.DAT’); Assign (OUTPUT, ‘LR.RES’); 4. Имена файлов. Имя файла – это любое выражение строкового типа, которое строится по правилам определения имен в MS-DOS: • имя содержит до восьми разрешенных символов; разрешенные символы – это прописные и строчные латинские буквы, цифры и символы: ! @ # $ % ^ ( ) ‘ ~ - _ • имя начинается с любого разрешенного символа; • за именем может следовать расширение – последовательность до трех разрешенных символов; расширение, если оно есть отделяется от имени точкой. • Перед именем может указываться путь к файлу: имя диска и/или имя текущего каталога и имена каталогов вышестоящих уровней. Например, C:\ST\LAB\work.pas, где C - имя диска, ST - имя директории на диске, LAB - имя поддиректории директории ST, work.pas - имя программы на Паскале. 5. Общая схема работы с файлами. Вне зависимости от файлового типа любая программа работы с файлом должна выполнить следующие действия. 1. Определить переменные файлового типа (логические файлы). 2. Каждому из используемых физических файлов поставить в соответствие переменную файлового типа. 3. Открыть файл — т. е. сделать существующий файл доступным для ввода и/или вывода. В случае отсутствия файл должен быть создан. Для того чтобы ускорить обмен информацией с внешними устройствами, в файловой системе используется механизм буферизации. Все операции обмена данными между оперативной памятью и диском выполняются через файловый буфер. При записи в файл вся информация сначала направляется в буфер и там накапливается до тех пор, пока он весь не заполнится. Только после этого (или при использовании специальной команды сброса) происходит передача данных на внешнее устройство. Аналогично при чтении из файла данные вначале считываются в буфер. 4. Обработать файл. Все предыдущие этапы носили подготовительный характер. Принцип обработки файлов любых типов состоит в следующем: данные из файла сначала считываются в оперативную память компьютера, для чего в программе назначаются переменные подходящих типов. Вся дальнейшая обработка ведется над этими переменными. В случае необходимости результаты записываются в новый файл или дописываются в уже существующий. Чтение (ввод) данных или их запись (вывод) выполняются при помощи стандартных инструкций ввода/вывода. 5. Закрыть файловую переменную, т.е. сохранить данные на диске после окончания работы с файлом. 6. Инициализация файла. Файл можно представить как потенциально бесконечный список значений одного и того же (базового) типа. Все элементы файла считаются пронумерованными. Начальный элемент имеет нулевой номер. В любой момент времени программе доступен только один элемент файла, на который ссылается текущий указатель (указатель обработки). Часто позицию размещения доступного элемента называют текущей позицией. Как правило, все действия с файлом производятся поэлементно, причем в этих действиях участвует тот элемент файла, который обозначается текущим показателем. В результате совершения операций текущий указатель может перемещаться, настраиваясь на тот или иной элемент файла. Инициировать файл - значит указать для него направление передачи данных. В Турбо Паскале можно открыть файл для чтения, для записи информации, для чтения и записи одновременно. Для чтения файл инициируется с помощью стандартной процедуры RESET: RESET (<ф. п.>); Здесь <ф. п.> - файловая переменная, связанная ранее процедурой ASSIGN с уже существующим файлом или логическим устройством - приемником информации. При выполнении этой процедуры дисковый файл или логическое устройство подготавливается к чтению информации. В результате специальная переменная – указатель, связанная с этим файлом, будет указывать на начало файла, т.е. на компонент с порядковым номером 0. Инициализация файла для записи: REWRITE (<ф. п.>). Здесь REWRITE - стандартная процедура Паскаля, которая инициирует запись информации в файл или в логическое устройство, связанное ранее с файловой переменной <ф. п.>. Процедурой REWRITE нельзя инициировать запись в ранее существовавший дисковый файл: при выполнении этой процедуры старый файл уничтожается и никаких сообщений об этом в программу не передается. Новый файл подготавливается к приему информации и его указатель принимает значение 0. Процедура APPEND(<ф.п.>) Инициирует запись в ранее существовавший текстовый (только) файл для его расширения, при этом указатель файла устанавливается в его конец. Если текстовый файл ранее уже был открыт с помощью RESET или REWRITE, использование процедуры APPEND приведет к закрытию этого файла и открытию его вновь, но уже для добавления записей. 7. Файловые процедуры и функции. Следующие процедуры и функции можно использовать с файлами любого вида. Процедура CLOSE(<ф. п.>) закрывает файл, однако связь файловой переменной с именем файла, установленная ранее процедурой ASSIGN, сохраняется. При создании нового или расширении старого файла процедура обеспечивает сохранение в файле всех новых записей и регистрацию файла в каталоге. Процедура RENAME(<ф. п.>, <новое_имя>) переименовывает файл. Здесь <новое_имя> - строковое выражение, содержащее новое имя файла. Процедура ERASE(<ф. п.>) уничтожает файл. Перед выполнением процедур RENAME и ERASE необходимо закрыть файл, если он ранее был открыт процедурами RESET, REWRITE или APPEND. Процедура FLUSH(<ф. п.>) очищает внутренний буфер файла, таким образом, гарантирует сохранность всех последних изменений файла на диске. В ходе выполнения процедуры FLUSH все новые записи будут действительно записаны на диск. Процедура игнорируется, если файл был инициирован для чтения процедурой RESET. Функция EOF (<ф. п.>) - логическая функция, тестирующая конец файла. Принимает значение TRUE, если файловый указатель стоит в конце файла. При записи это означает, что очередной компонент будет добавлен в конец файла, при чтении - что файл исчерпан. Процедура CHDIR(<путь>) изменение текущего каталога. Здесь <путь> - строковое выражение, содержащее путь к устанавливаемому по умолчанию каталогу Процедура GETDIR (<устройство>, <каталог>) позволяет определить имя текущего каталога (каталога по умолчанию). Здесь <устройство> - выражение типа WORD, содержащее номер устройства: 0 - устройство по умолчанию, 1 - диск А, 2 - диск В и т.д.; <каталог> - переменная типа STRING, в которой возвращается путь к текущему каталогу на указанном диске. Процедура MKDIR(<каталог>) создает новый каталог на указанном диске. Здесь <каталог> - выражение типа STRING, задающее путь к каталогу. Последним именем в пути, т. е. именем вновь создаваемого не может быть имя уже существующего каталога. Процедура RMDIR(<каталог>) удаляет каталог. Удаляемый каталог должен быть пустым, т. е. не содержать файлов или имен каталогов нижнего уровня. Функция IORESULT : word возвращает условный признак последней операции ввода-вывода. Если операция завершилась успешно, функция возвращает ноль. В противном случае - код ошибочной операции. Функция FSEARCH: PATHSTR ищет файл в списке каталогов. Формат вызова: FSEARCH(<имя> < список каталогов>) Здесь <имя> - имя отыскиваемого файла (строковое выражение или переменная типа PATHSTR; имени может предшествовать путь); <список каталогов> - список каталогов, в которых отыскивается файл (строковое выражение или переменная типа STRING); имена каталогов разделяются точкой с запятой. 8. Текстовые файлы. Текстовые файлы предназначены для хранения текстовой информации, например исходных данных. Он может содержать данные любых типов. Текстовый файл может быть создан с помощью текстового редактора или с помощью операторов Write и Writeln программы. Текстовые файлы представляют собой последовательность строк переменной длины, а строки - последовательность символов. Доступ к каждой строке возможен лишь последовательно, начиная с первой. При создании текстового файла в конце каждой строки ставится специальный признак EOLN (End Of LiNe – конец строки) – последовательность кодов ASCII #13(CR) и #10(LF), а в конце всего файла – признак EOF(End Of File – конец файла) – код #26. Функция EOLn (<ф.п.>) – логическая функция, принимает значение TRUE, если достигнут маркер конца строки. Процедуры Read и Readln обеспечивают ввод (копирование) чисел, символов и строк в ОП переменных, определенных в списке данных оператора. Ввод данных можно производить из текстового файла, созданного с помощью текстового редактора, или с клавиатуры. Форма обращения к процедуре Read: Read (<ф. п.>, <список ввода>), где <список ввода> - последовательность имен переменных, разделенных запятыми: скалярных, элементов массива или элементов записи. С помощью Read можно вводить символы, строки, целые и вещественные значения в область ОП переменных соответствующего типа. Последовательность данных, вводимых из файла, должна соответствовать последовательности списка данных. Переход на следующую строку при чтении данных из файла осуществляется с помощью процедуры Readln. Список данных в ней не обязателен; (например, Readln (FID) ; - для файла FID) если он содержит список данных, то переход на следующую строку осуществляется после ввода всех значений списка данных. Процедура Readln идентична процедуре Read. За исключением того, что после считывания значения в последнюю переменную списка данных процедуры Readln происходит переход на следующую строку файла (экрана), т.е. оставшаяся часть строки до EOLN пропускается. Так что следующая процедура Read или Readln начнет ввод данных с первого символа следующей строки. Например: Read (Fid, А, В, С); Read (С, D, Е); Readln (I, G); При использовании процедуры Readln (FID), производится переход в начало следующей строки файла FID, т. е. пропуск всех символов текущей строки, вплоть до EOLN. При вводе значения переменной типа CHAR очередной символ считывается из файла и присваивается переменной. При вводе из файла значений переменных типа String [n] считываются очередные n символов и помещаются в строку символов; количество считанных символов равно n, если не встретились символы EOLN или EOF. Если они встретились, то в считанную строку вводится столько символов, сколько их оказалось до появления EOLN или EOF; сами символы EOLN и EOF во введенную строку не помещаются. Попытка с помощью Read считывать из файла значение строки после достижения EOLN или EOF возвращает в качестве считанных значений "пустое" значение (пустую строку). Для перехода на новую строку после чтения очередной строки надо использовать процедуру Readln. Процедура Read выполняет ввод числовых значений по следующим правилам: 1) пропускаются все ведущие (предшествующие числу) пробелы, признаки табуляции и признаки конца строк (EOLN); Поэтому при вводе из файла ряда числовых значений, расположенных в нескольких строках, процедура Readln не требуется, можно использовать Read; если при пропуске ведущих пробелов встретится символ EOF, переменная получит значение 0. 2) выделяется значение очередного числа: от первого символа, отличного от пробела, до очередного пробела или признака EOLN или EOF. 3) выделенная подстрока контролируется на правильность арифметической константы; 4) полученное значение преобразуется в форму хранения числа, после чего копируется в область ОП переменной. Если выделенное значение ошибочно (например, делается попытка ввести 6укву О вместо цифры 0), возникает ошибка ввода-вывода и выдается сообщение; например: 1 Invalid numeric format – ошибочен числовой формат, т.е. ошибочно числовое значение. В этом случае надо скорректировать в файле арифметическое значение или программу и только после этого повторить ввод данных. Для работы с текстовыми файлами введена расширенная форма операторов ввода и вывода. Оператор Read(T,X1,X2,...XK) эквивалентен группе операторов begin Read(T,X1); Read(T,X2); ........... Read(T,XK) end; Здесь Т - текстовый файл, а переменные Х1, Х2,...ХК могут быть либо переменными целого, действительного или символьного типа, либо строкой. Вывод данных в текстовый файл осуществляется в основном для просмотра подготовки к печати результатов работы программы. Вывод данных производится с помощью процедур Write и Writeln. С их помощью можно выводить (копировать) числа, символы, строки и логические значения из ОП в файл или устройство (например, на экран или принтер). Форма обращения к процедуре Write: Write ( <ф.п.>,<список вывода>) ; <список вывода> — последовательность из одного и более выражений типа CHAR, STRING, BOOLEAN, а также любого целого или вещественного типа. Например: Write (F, А, В, С) ; - в файл F выводятся значения переменных А, В, С; Write ( А, В, С ) ; - для вывода А, В, С в стандартный файл Output. При выводе в текстовый файл более 248 символов с помощью процедуры Write (длина его строки равна 248 символам) курсор переходит в начало следующей строки, а ранее сформированные строки поднимаются вверх на одну строку. При выполнении процедуры Writeln без списка данных в файл передается только признак EOLN. При чтении значений переменных из файла они преобразуются из текстового представления в машинное. Оператор Write(T,X1,X2,...XK) эквивалентен группе операторов begin Write(T,X1); Write(T,X2); ........... Write(T,XK) end; Здесь Т - также текстовый файл, но переменные Х1,Х2,...ХК могут быть целого, действительного, символьного, логического типа или строкой. При записи значений переменных в файл они преобразуются из внутреннего представления в текстовый. TURBO PASCAL вводит дополнительные процедуры и функции, применимые только к текстовым файлам, это Append, Flush, SeekEOLn, SeekEOF. Функция SeekEOLn( var f: Text ): Boolean возвращает значение True, если до конца строки остались только пробелы. Функция SeekEOF( var f: Text ): Boolean возвращает значение True до конца файла остались строки, заполненные пробелами. 9. Типизированные файлы.     Типизированный (или компонентный) файл - это файл с объявленным типом его компонент. Типизированные файлы используются для хранения данных типа компонентов файла, определенных при его объявлении. Все компоненты типизированного файла имеют одинаковую длину. Это дает возможность организовать прямой доступ к каждому компоненту файла. Ввод-вывод данных типизированных файлов производится с помощью процедур Read и Write. Переменные в списках ввода-вывода должны иметь тот же тип, что и компоненты файла. Описание величин файлового типа имеет вид:                 type M= File Of T; где М - имя файлового типа, Т - тип компоненты. Например: type FIO= String[20]; SPISOK=File of FIO; var STUD, PREP: SPISOK; Здесь STUD, PREP - имена файлов, компонентами которых являются строки. Описание файлов можно задавать в разделе описания переменных: var fsimv: File of Char; fr: File of Real; Компонентами файла могут быть все скалярные типы, а из структурированных - массивы, множества, записи. Все операции над компонентными файлами производятся с помощью стандартных процедур: Reset, Rewrite, Read, Write, Close. Для ввода - вывода используются процедуры:     Read(f,X);                 Write(f,X); где f - имя логического файла, Х - либо переменная, либо массив, либо строка, либо множество, либо запись с таким же описанием, какое имеет компонента файла. Выполнение процедуры Read(f,X) состоит в чтении с внешнего устройства одной компоненты файла и запись ее в X. Повторное применение процедуры Read(f,X) обеспечит чтение следующей компоненты файла и запись ее в X. Выполнение процедуры Write(f,X) состоит в записи X на внешнее устройство как одной компоненты. Повторное применение этой процедуры обеспечит запись X как следующей компоненты файла. Для работы с компонентными файлами введена расширенная форма операторов ввода и вывода:     Read(f,X1,X2,...XK)                 Write(f,X1,X2,...XK)     Здесь f - компонентный файл, а переменные Х1, Х2,...ХК должны иметь тот же тип, что и объявленный тип компонент файла f. 10. Нетипизированные файлы. Нетипизированные (или бестиповые) файлы позволяют записывать на диск произвольные участки памяти ЭВМ и считывать их с диска в память. Операции обмена с бестиповыми файлами осуществляется с помощью процедур BloсkRead и BlockWrite. Кроме того, вводится расширенная форма процедур Reset и Rewrite. В остальном принципы работы остаются такими же, как и с компонентными файлами. При открытии файла длина буфера устанавливается по умолчанию в 128 байт. TURBO PASCAL позволяет изменить размер буфера ввода - вывода, для чего следует открывать файл расширенной записью процедур:             Reset(var f: File; BufSize: Word) или Rewrite(var f: File; BufSize: Word). Параметр BufSize задает число байтов, считываемых из файла или записываемых в него за одно обращение. Минимальное значение BufSize - 1 байт, максимальное - 64 К байт. Чтение данных из нетипизированного файла осуществляется процедурой: BlockRead( var f: File; var X; Count: Word; var QuantBlock: Word ). Эта процедура осуществляет за одно обращение чтение в переменную X количества блоков, заданное параметром Count, при этом длина блока равна длине буфера. Значение Count не может быть меньше 1. За одно обращение нельзя прочесть больше, чем 64 К байтов. Необязательный параметр QuantBlock возвращает число блоков (буферов), прочитанных текущей операцией BlockRead. В случае успешного завершения операции чтения QuantBlock = Count, в случае аварийной ситуации параметр QuantBlock будет содержать число удачно прочитанных блоков. Отсюда следует, что с помощью параметра QuantBlock можно контролировать правильность выполнения операции чтения.     Запись данных в нетипизированный файл выполняется процедурой: BlockWrite( var f: File; var X; Count: Word; var QuantBlock: Word), которая осуществляет за одно обращение запись из переменной X количества блоков, заданное параметром Count, при этом длина блока равна длине буфера. Необязательный параметр QuantBlock возвращает число блоков (буферов), записанных успешно текущей операцией BlockWrite. Пример Организация простого ввода массивов данных. Const N= 1000; {Максимальная длина ввода} Var f: text; {объявляем переменную файлового типа} m: array [1..N] of real; {объявляем массив вещественных чисел, размерностью N} i: integer; {переменная-счетчик} begin {начало тела программы} assign (f,'prog.dat'); {связываем логический файл с физическим файлом prog.dat} reset(f); {инициируем файл для чтения} i:= 1; {инициируем счетчик} while not EOF(f)and(i<= N) do {пока не достигли конца файла и последнего элемента массива выполняем следующее:} begin {начало подпрограммы} read (f, m[i]); {ввод элементов массива из файла} inc (i); {увеличиваем счетчик} end; {конец подпрограммы} close (f); {закрываем файл f} ........ end. {завершение программы} Вопросы для самоконтроля: 1. В чем заключается целесообразность применения файлов? 2. Какие типы файлов используются в языке Pascal? 3. Для чего используется файловая переменная? 4. Как устанавливается связь между файловой переменной и файлом? 5. Как объявляются файловые переменные? 6. Какова схема работы с файлами? 7. С помощью каких процедур инициализируются файлы? 8. Зачем применяется процедура Close? 9. Для чего применяется функция Ioresult? Что она возвращает после корректного выполнения операции ввода-вывода? 10. Объясните назначение функций Eof, Eoln, SeekEof, Eoln? 11. Какие файлы называются текстовыми и в чем заключается их специфика? 12. Какие файлы называются типизированными и каким образом в них представляется информация? 13. Какие файлы называются нетипизированными и какие процедуры используются для работы ними? Лекция №15 Тема: Составление библиотек подпрограмм. Вопросы: 1. Определение модулей и целесообразность их использования. 2. Структура модулей. 3. Заголовок модуля и связь модулей друг с другом 4. Интерфейсная часть. 5. Исполняемая часть. 6. Инициирующая часть. 7. Компиляция модулей 8. Стандартные модули 1. Определение модулей и целесообразность их использования. Модуль  это автономно компилируемая программная единица, включающая в себя различные компоненты раздела описаний (типы, константы, переменные, процедуры и функции) и, возможно, некоторые исполняемые операторы инициирующей части. Наличие модулей в Turbo Pascal позволяет программировать и отлаживать программу по частям, создавать библиотеки подпрограмм и данных, воспользоваться возможностями стандартных модулей, практически неограниченно увеличивать кодовую (содержащую коды команд) часть программы. Модуль содержит описания типов данных, переменных и других объектов, а также подпрограммы, которые используются в различных программах. Подпрограмму имеет смысл включать в состав модуля в том случае, когда она реализует действие, которое приходится выполнять достаточно часто. Подпрограммы, входящие в модуль, можно написать, отладить и откомпилировать один раз, а использовать многократно. Модули представляют собой прекрасный инструмент для разработки библиотек прикладных программ и мощное средство модульного программирования. Важная особенность модулей заключается в том, что компилятор Турбо Паскаля размещает их программный код в отдельном сегменте памяти. Максимальная длина сегмента не может превышать 64 Кбайта, однако количество одновременно используемых модулей ограничивается лишь доступной памятью, что дает возможность создавать весьма крупные программы. 2. Структура модулей. Модуль имеет следующую структуру: Unit module_name Interface Интерфейсная секция Implementation Секция реализации Секция инициализации Рис. 1. Структура модуля Здесь Unit  зарезервированное слово (единица), начинает заголовок модуля; name  имя модуля (правильный идентификатор). Interface – интерфейсная секция – содержит те описания типов, переменных и других объектов данных, которые можно использовать в других программах или модулях. Секция реализации начинается с зарезервированного слова implementation. Все описания, содержащиеся в секции реализации, являются локальными, их область действия – данный модуль. Здесь же содержаться полные описания функций и процедур модуля. Последняя часть модуля – секция инициализации. Она может быть пустой и содержать только зарезервированное слово end или включать в себя исполняемые операторы, выполняющие необходимые действия по инициализации модуля (например, по присваиванию начальных значений переменным). 3. Заголовок модуля и связь модулей друг с другом. Заголовок модуля состоит из зарезервированного слова Unit  следующего за ним имени модуля. Это имя должно совпадать с именем дискового файла, в который помещается исходный текст модуля. Если, например, имеем заголовок Unit Global; то исходный текст соответствующего модуля должен размещаться в дисковом файле GLOBAL.PAS. Имя модуля служит для его связи с другими модулями и основной программой, поэтому заголовок модуля опускать нельзя (в отличие от заголовка программы). Эта связь устанавливается специальным предложением Uses <сп.модулей>  Здесь Uses - зарезервированное слово (использует); <сп.модулей> - список модулей, с которыми устанавливается связь; элементами списка являются имена модулей, отделяемые друг от друга запятыми, например:  Uses CRT, Graph, Global; Если объявление Uses... используется, оно должно открывать раздел описаний основной программы. Модули могут использовать другие - модули. Предложение Uses в модулях может следовать либо сразу за зарезервированным словом Interface, либо сразу за словом Implementation. 4. Интерфейсная часть. Через интерфейс осуществляется взаимодействие основной программы с модулем (модуля с модулем). Интерфейсная часть открывается зарезервированным словом Interface. В этой части содержатся объявления всех глобальных объектов модуля (типов, констант, переменных и подпрограмм), которые должны стать доступными основной программе и/или другим модулям. При объявлении глобальных подпрограмм в интерфейсной части указывается только их заголовок, например: Unit Cmplx;  Interface     type      complex = record    re, Im : real    end;  Procedure AddC (x, у : complex; var z : complex); Procedure MulC (x, у : complex; var z : complex);  Если теперь в основной программе написать предложение Uses Cmplx; то в программе станут доступными тип Complex и две процедуры  AddC и МulC из модуля Cmplx. Отметим, что объявление подпрограмм в интерфейсной части автоматически сопровождается их компиляцией. Таким образом, обеспечивается доступ к подпрограммам из основной программы и других модулей. Следует учесть, что все константы и переменные, объявленные в интерфейсной части модуля, равно как и глобальные константы и переменные основной программы, помещаются компилятором Турбо Паскаля в общий сегмент данных (максимальная длина сегмента 65536 байт). Порядок появления различных разделов объявлений и их количество может быть произвольным. В интерфейсной части модулей нельзя использовать опережающее описание. В интерфейсах различных модулей недопустимо циклическое обращение друг к другу, т.к. компилятор в этом случае не может установить связей. 5. Исполняемая часть. Исполняемая часть начинается зарезервированным словом Implementation и содержит описания подпрограмм, объявленных в интерфейсной части. В ней могут объявляться локальные для модуля объекты - вспомогательные типы, константы, переменные и метки, если они используются в инициирующей части. Описанию подпрограммы, объявленной в интерфейсной части модуля, в исполняемой части должен предшествовать заголовок, в котором можно опускать список формальных переменных (и тип результата для функции), так как они уже описаны в интерфейсной части. Но если заголовок подпрограммы приводится в полном виде, т.е. со списком формальных параметров и объявлением результата, он должен совпадать с заголовком, объявленным в интерфейсной части, например: Implementation Procedure AddC; begin z.re := x.re + y.re; z,im := x.Im * y.im; end; Все вспомогательные программные элементы, объявленные в исполняемой части, называются скрытыми, т. к. они доступны для использования только в данном модуле и невидимы для программы, использующей модуль. Локальные переменные и константы, а также все программные коды, порожденные при компиляции модуля, помещаются в общий сегмент памяти. В отличие от интерфейсов модулей в исполнительных частях модулей допустимо циклическое обращение друг к другу, т.к. все равно взаимодействие осуществляется через интерфейсы, и здесь не возникает проблемы с установлением необходимых связей. 6. Инициирующая часть. В некоторых случаях перед обращением к модулю следует провести его инициализацию (например, установить связь с теми или иными файлами с помощью процедуры Assign, инициализировать какие-то переменные и т.д.). Необходимые действия можно выполнить в секции инициализации модуля. Эта секция начинается словом begin, после которого идут исполняемые операторы, а затем помещается слово end. (с точкой), например: begin Assign (F1, ‘ FILE1.DAT ‘); end. В инициирующей части размещаются исполняемые операторы, содержащие некоторый фрагмент программы. Эти операторы исполняются до передачи управления основной программе и обычно используются для подготовки ее работы. Например, в них могут инициироваться переменные, открываться нужные файлы, устанавливаться связи с другими ПК по коммуникационным каналам и т.п. Следует иметь в виду, что операторы секции инициализации выполняются единственный раз в момент запуска программы. Если инициализация модуля не нужна, то в секции помещается лишь слово end. 7. Компиляция модулей В среде Турбо Паскаля имеются средства, управляющие способом компиляции модулей и облегчающие разработку крупных программных проектов. В частности, определены три режима компиляции: COMPILE, МАКЕ и BUILD. Режимы отличаются только способом связи, компилируемого модуля или основной программы с другими модулями, объявленными в предложении USES. При компиляции модуля или основной программы в режиме COMPILE все упоминающиеся в предложении USES модули должны быть предварительно откомпилированы, и результаты компиляции помещены в одноименные файлы с расширением .TPU. Например, если в программе (модуле) имеется предложение Uses Global; то на диске в каталоге, объявленном опцией UNIT DIRECTORIES, уже должен находиться файл GLOBAL.TPU. Файл с расширением TPU (от англ. Turbo Pascal Unit) создается в результате компиляции модуля. В режиме МАКЕ компилятор проверяет наличие TPU-файлов для каждого объявленного модуля. Если какой-либо из файлов не обнаружен, система пытается отыскать одноименный файл с расширением .PAS, т.е. файл с исходным текстом модуля, и, если .PAS-файл найден, приступает к его компиляции. Кроме того, в этом режиме система следит за возможными изменениями исходного текста любого используемого модуля. Если в PAS-файл (исходный текст модуля) внесены какие-либо изменения, то независимо от того, есть ли уже в каталоге соответствующий TPU-файл или нет, система осуществляет его компиляцию перед компиляцией основной программы. Более того, если изменения внесены в интерфейсную часть модуля, то будут перекомпилированы также и все другие модули, обращающиеся к нему. Режим МАКЕ, таким образом, существенно облегчает процесс разработки крупных программ с множеством модулей: программист избавляется от необходимости следить за соответствием существующих TPU-файлов их исходному тексту, так как система делает это автоматически. В режиме BUILD существующие TPU-файлы игнорируются, и система пытается отыскать (и компилировать) соответствующий PAS-файл для каждого объявленного в предложении USES модуля. После компиляции в режиме BUILD программист может быть уверен в том, что учтены все сделанные им изменения в любом из модулей. Подключение модулей к основной программе и их возможная компиляция осуществляются в порядке их объявления в предложении USES. При переходе к очередному модулю система предварительно отыскивает все модули, на которые он ссылается. Ссылки модулей друг на друга могут образовывать древовидную структуру любой сложности, однако запрещается явное или косвенное обращение модуля к самому себе. Например, недопустимы следующие объявления: Unit A; Unit B; Interface Interface Uses B; Uses A; ………. ………… Implementation Implementation ……….. …………. end. end. Это ограничение можно обойти, если «спрятать» предложение USES в исполняемые части зависимых модулей: Unit A; Unit B; Interface Interface ………. ………. Implementation Implementation Uses B; Uses A; ………. ………. end. end. Дело в том, что Турбо Паскаль разрешает ссылки на частично откомпилированные модули, что приблизительно соответствует опережающему описанию подпрограммы. Если интерфейсные части любых двух модулей независимы (это непременное условие!), Турбо Паскаль сможет идентифицировать все глобальные идентификаторы в каждом из модулей, после чего откомпилирует тела модулей обычным способом. 8. Стандартные модули В Турбо Паскале имеется восемь стандартных модулей, в которых содержится большое число разнообразных типов, констант, процедур и функций. Этими модулями являются SYSTEM, DOS, CRT, PRINTER, GRAPH, OVERLAY, TURBO3 и GRAPHS. Модули GRAPH, TURBO3 и GRAPHS содержатся в одноименных ТPU-файлах, остальные входят в состав библиотечного файла TURBO.TPL. Лишь один модуль SYSTEM подключается к любой программе автоматически, все остальные становятся доступны только после указания их имен в списке, следующем за словом USES. Модуль SYSTEM  в него входят все процедуры и функции стандартного Паскаля, а также встроенные процедуры и функции Турбо Паскаля, которые не вошли в другие стандартные модули (например, INC, DEC, GETDIR и т.п.). Модуль PRINTER делает доступным вывод текстов на матричный принтер. В нем определяется файловая переменная LST типа TEXT, которая связывается с логическим устройством PRN. Модуль CRT содержит процедуры и функции, обеспечивающие управление текстовым режимом работы экрана. Модуль GRAPH содержит обширный набор типов, констант, процедур и функций для управления графическим режимом работы экрана. Модуль DOS содержит процедуры и функции, открывающие доступ программам к средствам дисковой операционной системы MS DOS. Модуль OVERLAY необходим при разработке громоздких программ с перекрытиями. Два библиотечных модуля TURBO3 и GRAPH3 введены для совместимости с ранней версией 3.0 системы Турбо Паскаль. Вопросы для самоконтроля: 1. Дайте определение модуля. 2. Из каких частей состоит модуль? 3. Для чего используется заголовок модуля? 4. Каким образом осуществляется связь модулей с программами и другими модулями? 5. Для чего служит интерфейсная часть модуля? 6. Для чего служит исполняемая часть модуля? 7. Для чего служит инициирующая часть модуля? 8. Чем отличаются переменные, описанные в интерфейсной части модуля, от переменных, описанных в исполняемой части модуля? 9. Какие существуют способы компиляции модулей и чем они отличаются? 10. Какие стандартные модули имеются в Turbo Pascal? Лекция №16. Тема: Работа с экраном в текстовом режиме Вопросы: 1. Процедуры ввода символов. 2. Процедуры управления курсором, цветом и звуком. 1. Процедуры ввода символов. Стандартная процедура read (readln) выполняет обработку введенных параметров только после завершающего нажатия клавиши Enter. Вводимые в диалоге символы после нажатия на клавиатуре заносятся в специальный буфер ввода и хранятся там до наступления момента их обработки. Однако, при организации диалога программы с пользователем часто бывает необходимо выполнить какие-либо действия немедленно после нажатия клавиши. Выполнение таких действий позволяют организовать функции KeyPressed и ReadKey. Функция ReadKey (читать клавишу) возвращает символ (типа char), соответствующий нажатой на клавиатуре клавише. Если ни одна клавиша не нажата, то функция ждет до тех пор, пока какая-либо клавиша будет нажата. При использовании функции ReadKey символ, соответствующий нажатой клавише, на экран не выводится. Функция KeyPressed проверяет, пуст или нет буфер ввода, и возвращает значение True, если в буфере ввода есть хотя бы один символ. Если буфер пуст, возвращается значение False. При такой проверке сами символы из буфера не извлекаются. Выполнение этой функции в отличие от процедуры Read не задерживает выполнение программы. Выбор символа из буфера выполняет функция ReadKey. Причем заодно обращение извлекается только один символ (байт). Следует иметь в виду, что нажатие функциональных клавиш, а также клавиш управления курсором, вызывает запись в буфер ввода последовательности из двух байт. Первый байт всегда содержит ноль, а второй – дополнительный код нажатой клавиши. 2. Процедуры управления курсором, цветом и звуком. Положение курсора на экране определяется номером строки и номером позиции в строке, которые можно рассматривать как координаты курсора. В стандартном режиме на экране могут быть отражены до 25 строк текста длиной до 80 символов. За начало координат принят левый верхний угол экрана. Процедура GoToXY(Х, Y:byte); Переводит курсор в нужное место экрана. Например: GoToXY(34,13); Writeln(‘Hello”); выводит сообщение примерно на середине экрана. Процедура TextColor(Color:byte); определяет цвет выводимых символов. Процедура TextBackGround(Color:byte); определяет цвет фона. В качестве параметра цвет для процедуры TextColor можно использовать код цвета или именованную константу в диапазоне от 0 до 15, а для процедуры TextBackGround – в диапазоне от 0 до 7. Например, TextBackGround (Blue); TextColor (15); Стандартно используется 16-тицветовая палитра, т.е. для кодирования кода цвета достаточно 4-х бит. Цвет вона в стандартном варианте может иметь 8 цветов(3 бита), в расширенном – 16 (4 бита). Т.о. для всей цифровой информации о цвете достаточно одного байта. Эта вся информация хранится в системной переменной TextAttr: byte в следующем виде Bl R G B I R G B Цвет фона Цвет текста Стандартно используется три цвета: красный, зеленый и синий (RGB –палитра), I - интенсивность цвета. Значение нижней половины байта дает код цвета в соответствии со следующей таблицей. Цвет Код Константа Черный Black Синий 1 Blue Зеленый 2 Green Бирюзовый 3 Cyan Красный 4 Red Сиреневый 5 Magenta Коричневый 6 Brown Белый (светло серый) 7 LightGray Серый 8 DarkGray Голубой 9 LightBlue Светло-зеленый 10 LightGreen Светло-бирюзовый 11 LightCyan Светло-красный 12 LightRed Светло-сиреневый 13 LightMagenta Желтый 14 Yellow Белый 15 White Blink – мерцание. Стандартно самый старший разряд байта цифровых атрибутов отвечает за мерцание, т.е., если он будет установлен, то символы на экране будут мигать. Однако, если выполнить отключение режима мерцания, то этот разряд можно использовать также для задания интенсивности цвета фона, увеличив тем самым цветовую палитру в два раза. TextColor (Red+Blink); Процедура ClrScr удаляет все символы с экрана (очищает экран), при этом экран закрашивается текущим цветом фона. Заданным процедурой TextBackGround (если цвет фона в программе не задавался, то экран закрашивается черным цветом). Кроме того, курсор устанавливается в точку экрана с координатами (1,1), т.е. в начало первой строки. Процедура Sound (F: word) заставляет динамик звучать с нужной частотой. F – выражение, определяющее частоту звука в герцах. Динамик будет звучать впредь до вызова процедуры NoSound, которая выключает динамик. Если он к этому моменту не был включен, вызов процедуры игнорируется. Процедура Delay(T: word) обеспечивает задержку работы программы на интервал времени Т, заданный в миллисекундах. Вопросы для самоконтроля: 1. В чем состоит отличие процедуры ввода Readkey от процедуры read? 2. Для чего применяется функция Keypressed? 3. Какая процедура позволяет перемещать курсор в нужное место экрана? 4. Какие процедуры позволяют изменять цвет фона и цвет текста? 5. Какая процедура производит очистку экрана? Лекция №17. Тема: Работа с экраном в графическом режиме Вопросы: 1. Характеристика графического режима работы. 2. Процедуры и функции для работы с экраном в графическом режиме. 1. Характеристика графического режима работы. Кроме текстового режима мониторы могут работать в цветных графических режимах. При этом разрешающая способность и цветовая гамма определяется характеристиками видеоадаптера и монитора. Рассмотрим стандартный режим SVGA (Super Video Graphic Array) – 604-480-16. В графическом режиме все изображение на экране состоит из отдельных светящихся точек – pixel (picture element). Количество пикселей в стандартном режиме SVGA по горизонтали – 640, по вертикали – 480. Каждая точка может светится своим цветом. Все буквы и цифры выводимые на экран также представляют собой некоторую комбинацию светящихся точек. При этом для удобства работы используются специальные таблицы, содержащие начертания символов для таблицы кодов ASCII, поэтому при выводе на экран из программы какого-либо сообщения соответствующее изображение символов выбирается из этой таблицы. 2. Процедуры и функции для работы с экраном в графическом режиме. Для работы в графическом режиме используются процедуры и функции, находящиеся в модуле Graph. Рассмотрим основные из них. Открытие графической системы InitGraph (var Driver, Mode: integer; Path: string); Эта процедура всегда должна быть выполнена первой до начала работы в графическом режиме. Параметры Driver и Mode позволяют указать явно тип видеоадаптера и режим его работы. Если до обращения к процедуре параметру Driver присвоить значение 0, то произойдет автоопределение адаптера и автоматически будет выбран оптимальный его тип и режим работы. Параметр Path задает путь нахождения драйвера графического интерфейса. Это файл, имеющий расширение bgi (Borland graphic interface). Для стандартного SVGA режима нужен драйвер egavga.bgi. Если такой драйвер находится в текущей директории, то строку Path можно задать пустой. При открытии графической системы могут возникнуть ошибки, связанные, например, с отсутствием графического драйвера, неправильным заданием типа адаптера или режима его работы и т.д. Поэтому после выполнения процедуры InitGraph следует проверить корректность открытия графического режима функцией GraphResult. Если возвращаемое значение равно 0, то ошибок не было и графическая система успешно открыта. Все возвращаемые ошибки имеют отрицательный код. If GraphResult<>0 then Begin Writeln(‘’); Halt; End; Процедура ClearDevice – очищает графический экран. После обращения к процедуре указатель устанавливается в левый верхний угол экрана, а сам экран заполняется цветом фона, заданным процедурой SetBkColor. Процедура SetColor (Color: word) – устанавливает цвет выводимых линий и символов. Процедура SetBkColor (Color: word) – устанавливает цвет фона. В графическом режиме не выполняются процедуры write, writeln, read, readln. Потому что они настроены на текстовый режим работы. Вместо read в графическом режиме следует использовать для ввода символов функцию ReadKey. Следует иметь в виду, что в отличие от read, ReadKey не обеспечивает эхо печати введенного символа. Вывод на экран в графическом режиме обеспечивается процедурами OutText и OutTextXY. В графическом режиме отсутствует курсор, однако, имеется невидимый указатель, аналогичный курсору, который характеризуется своими координатами (X, Y). Процедура OutText (Text: string) выводит строку, начиная с текущего нахождения этого указателя, а OutTextXY(X, Y: integer, S: string) выводит строку, начиная с заданного положения, которое определяется координатами X, Y. Эта процедура не меняет положение указателя. Позиционирование указателя можно выполнять процедурой MoveTo(x,y: integer). Вывод отдельной светящийся точки выполняется процедурой PutPixel (x, y: integer; c: word), где x, y - координаты точки, с – координаты ее цвета. Функции GetMaxX и GetMaxY возвращают значения типа Word, содержащие максимальные координаты экрана в текущем режиме работы соответственно по горизонтали и вертикали. Функции GetX и GetY возвращают значения типа Integer, содержащие текущие координаты указателя соответственно по горизонтали и вертикали. Функция GetPixel(x,y: integer):word определяет цвет точки с заданными координатами. Изображение геометрических фигур. Процедура Line(xн,yн,xк,yк: integer) вычерчивает линию с указанными координатами начала (x н, yн) и конца(xк, yк). Процедура LineTo(x, y: integer) вычерчивает линию от текущего положения указателя до положения, заданного координатами x, y. Процедура SetLineStyle(Type, Pattern, Trick: word) устанавливает новый стиль вычерчивания линий (Type, Pattern, Thick – соответственно тип, образец и толщина линии). Тип линии может быть задан с помощью одной из следующих констант: 0-4 Процедура Rectangle (x1, y1, x2, y2: integer) вычерчивает прямоугольник с указанными координатами левого верхнего (x1, y1) и правого нижнего (x2, y2) углов прямоугольника. Процедура Circle(x,y: integer; r: word) вычерчивает окружность радиуса r и координатами центра x и y. Процедура Arc(x, y: integer; BegA, EndA, R: word) чертит дугу окружности радиуса r с координатами центра x и y. BegA, EndA – начальный и конечный углы дуги. Процедура Arc(X,Y: Integer; BegA,EndA,R: Word); чертит дугу окружности. Здесь X, Y - координаты центра; BegA, EndA - соответственно начальный и конечный углы дуги; R - радиус. Углы отсчитываются против часовой стрелки и указываются в градусах. Нулевой угол соответствует горизонтальному направлению вектора слева направо. Если задать значения начального угла 0 и конечного - 359, то будет выведена полная окружность. При вычерчивании дуги окружности используются те же соглашения относительно линий и радиуса, что и в процедуре Circle. Процедура Ellipse(X,Y: Integer; BegA,EndA,RX,RY: Word); вычерчивает эллипсную дугу. Здесь X, Y - координаты центра; BegA, EndA - соответственно начальный и конечный углы дуги; RX, RY- горизонтальный и вертикальный радиусы эллипса в пикселях. Процедура SetTextStyle(Font,Direct,Size: Word); устанавливает стиль текстового вывода на графический экран. Здесь Font - код (номер) шрифта; Direct - код направления; Size - код размера шрифта. Для указания кода шрифта можно использовать следующие предварительно определенные константы: 0-4 Для заливки указанных областей экрана можно задавать не только цвет заливки, но и тип орнамента, который может быть как стандартным, так и пользовательским. Такие установки выполняются при помощи процедур: Процедура SetFillStyle(Fill,Color: Word); устанавливает стиль (тип и цвет) заполнения. Здесь Fill - тип заполнения; Color - цвет заполнения. С помощью заполнения можно покрывать какие-либо фрагменты изображения периодически повторяющимся узором. Для указания типа заполнения используются следующие предварительно определенные константы: 0-12 Процедура SetFillPattern(Pattern: FillPatternType;Color: Word); устанавливает образец рисунка и цвет штриховки. Здесь Pattern - выражение типа FillPatternType; устанавливает образец рисунка для Fill - UserFill в процедуре SetFillStyle; Color - цвет заполнения. Образец рисунка задается в виде матрицы из 8x8 пикселей и может быть представлен массивом из 8 байт следующего типа: type FillPatternType = array [1..8] of Byte; каждый разряд любого из этих байтов управляет светимостью пикселя, причем первый байт определяет 8 пикселей первой строки на экране, второй байт - 8 пикселей второй строки и т.д. При завершении работы с графическим режимом он должен быть закрыт процедурой CloseGraph. Вопросы для самоконтроля: 1. Каковы характеристики работы экрана в графическом режиме работы. 2. Какой драйвер используется для работы с экраном в графическом режиме? 3. Какая процедура инициирует графический режим? 4. Какая процедура проверяет корректность открытия графического режима? 5. Какие процедуры устанавливают цвет фона и цвет выводимых объектов? 6. Какие процедуры позволяют начертить на экране линию, прямоугольник, окружность, эллипс и дугу? 7. С помощью какой процедуры выводится текст на экран в графическом режиме? Лекция №18. Тема: Указатели и динамическая память. Вопросы: 1. Динамическая память. 2. Адреса и указатели. 3. Состояние указателей. 4. Выделение и освобождение динамической памяти. 5. Действия над указателями и динамическими переменными. 1. Динамическая память. Все переменные, объявленные в программе, размещаются в одной непрерывной области оперативной памяти, которая называется сегментом данных. Длина сегмента данных определяется архитектурой микропроцессоров 80x86 и составляет 65536 байт, что может вызвать известные затруднения при обработке больших массивов данных. С другой стороны, объем памяти ПК (обычно не менее 640 Кбайт) достаточен для успешного решения задач с большой размерностью данных. Выходом из положения может служить использование так называемой динамической памяти. Динамическая память - это оперативная память ПК, предоставляемая программе при ее работе, за вычетом сегмента данных (64 Кбайт), стека (обычно 16 Кбайт) и собственно тела программы. Размер динамической памяти можно варьировать в широких пределах (см. прил.1). По умолчанию этот размер определяется всей доступней памятью ПК и, как правило, составляет не менее 200...300 Кбайт. Динамическая память - это фактически единственная возможность обработки массивов данных большой размерности. Многие практические задачи трудно или невозможно решить без использования динамической памяти. Динамическое размещение данных означает использование динамической памяти непосредственно при работе программы. В отличие от этого статическое размещение осуществляется компилятором Турбо Паскаля в процессе компиляции программы. При динамическом размещении заранее не известны ни тип, ни количество размещаемых данных, к ним нельзя обращаться по именам, как к статическим переменным. 2. Адреса и указатели. Оперативная память ПК представляет собой совокупность элементарных ячеек для хранения информации - байтов, каждый из которых имеет собственный номер. Эти номера называются адресами, они позволяют обращаться к любому байту памяти. Турбо Паскаль предоставляет в распоряжение программиста гибкое средство управления динамической памятью - так называемые указатели. Указатель - это переменная, которая в качестве своего значения содержит адрес байта памяти. В ПК адреса задаются совокупностью двух шестнадцатиразрядных слов, которые называются сегментом и смещением. Сегмент - это участок памяти, имеющий длину 65536 байт (64 Кбайт) и начинающийся с физического адреса, кратного 16 (т.е. О, 16, 32, 48 и т.д.). Смещение указывает, сколько байт от начала сегмента необходимо пропустить, чтобы обратиться к нужному адресу. Адресное пространство ПК составляет 1 Мбайт (речь идет о так называемой стандартной памяти ПК; на современных компьютерах с процессорами 80386 и выше адресное пространство составляет 4 Гбайт, однако в Турбо Паскале нет средств, поддерживающих работу с дополнительной памятью; при использовании среды Borland Pascal with Objects 7.0 такая возможность имеется). Для адресации в пределах 1 Мбайта нужно 20 двоичных разрядов, которые получаются из двух шестнадцатиразрядных слов (сегмента и смещения) следующим образом (рис.17.1): содержимое сегмента смещается влево на 4 разряда, освободившиеся правые разряды заполняются нулями, результат складывается с содержимым смещения. Puc.17.1. Схема формирования адреса в ПК. Фрагмент памяти в 16 байт называется параграфом, поэтому можно сказать, что сегмент адресует память с точностью до параграфа, а смещение - с точностью до байта. Каждому сегменту соответствует непрерывная и отдельно адресуемая область памяти. Сегменты могут следовать в памяти один за другим без промежутков или с некоторым интервалом, или, наконец, перекрывать друг друга. Таким образом, по своей внутренней структуре любой указатель представляет собой совокупность двух слов (данных типа WORD), трактуемых как сегмент и смещение. С помощью указателей можно размещать в динамической памяти любой из известных в Турбо Паскале типов данных. Лишь некоторые из них (BYTE, CHAR, SHORTINT, BOOLEAN) занимают во внутреннем представлении один байт, остальные - несколько смежных. Поэтому на самом деле указатель адресует лишь первый байт данных. Рассматриваемые до сих пор параметры программы обладали тем свойством, что под них выделяется, вполне определенный размер памяти и между отдельными объектами устанавливаются связи еще на этапе компиляции. Во время работы программы вносить изменения в выделенный размер памяти или установление связей не удается. Например, программа работает с различным количеством целых чисел. Естественно разместить их в каком-то массиве. Размер массива должен быть определен заранее, и если программа должна быть универсальной, при определении массива необходимо учитывать случай максимального количества таких чисел. Однако это приведет к неэффективному использованию оперативной памяти. Для этого в Паскале предусмотрено возможность динамического выделения участков памяти. Для этих целей были придуманы указатели. Указатели позволяют создавать переменные во время выполнения программы, то есть динамически. При необходимости можно размещать в памяти новые переменные и освобождать память, когда необходимость в них отпадает. Таким образом, программист получает возможность более гибко использовать память компьютера, но платой за это является потеря наглядности программы и увеличение её сложности. Указатель является физическим носителем адреса величины базового типа. Она занимает 4 байта памяти (2 слова). Первое слово дает смещение адреса, а второе – адрес сегмента. А данные, на которые он указывает, могут простираться в памяти на десятки килобайт. Переменной, на которую указывает указатель, не обязательно присваивать какое-либо имя. К ней можно обращаться через имя указателя. Указатели, используемые в Паскале, бывают типизированные и нетипизированные. Указатели, содержащие адрес, по которому записана переменная заранее определенного типа, называют типизированными. Для объявления типизированного указателя используется знак ^, который помещается перед соответствующим типом. Их описание выглядит следующим образом: Var Px: ^char; Py: ^integer; В этом примере описаны два типизированных указателя: Px, Py. Значения этих переменных представляют собой адреса в оперативной памяти, по которым содержаться данные типа char и integer соответственно. Описание типов указателей – единственное исключение из общего правила, согласно которому все идентификаторы должны быть описаны перед использованием. Однако если базовый тип является еще не объявленным идентификатором, то он должен быть описан в той же самой части объявления, что и тип «указатель». Например: RecPtr = ^RecordType; RecordType = record Name : string; Number : integer; end; Необходимо заметить, что применительно к типизированным указателям операция присваивания допустима только для указателей, ссылающихся на данные одного типа. Предположим, в программе объявлены такие указатели: Var px,py : ^char; pz : ^integer; В этом случае операция присваивания допустима для указателей px и py. px:=py; однако совершенно не допустимы следующие операторы: px:=pz; или pz;=py; В Паскале можно объявлять указатель и не связывать его при этом с каким-либо конкретным типом данных. Для этого служит стандартный тип pointer. Он обозначает нетипизированный указатель, т.е. указатель который не указывает ни на какой определенный тип. С помощью нетипизированных указателей удобно динамически размещать данные, структура и тип которых меняются в ходе программы. Нетипизированные указатели объявляются следующим образом: Var Pp : pointer; Нетипизированный указатель может присутствовать в операторе присваивания вместе с любым типизированным указателем. Например: Var px : ^char; py : ^integer; pz : pointer; Для этих переменных допустимы операторы присваивания: px : pz; py : pz; pz : py; pz : px; 3. Состояния указателя Для указателя, после того как он объявлен в разделе описания переменных, возможны три состояния. Указатель может содержать адрес некоторой переменной, «пустой» адрес Nil или иметь неопределенное состояние. Первый случай в объяснениях не нуждается. Во втором случае, когда требуется, чтобы указатель ни на что не указывал, ему присваивается специальное значение Nil. Что же касается неопределенного состояния, то оно имеет место сразу после начала работы программы (до того как указателю будет присвоен какой-нибудь адрес в памяти или значение Nil) либо после освобождения памяти, на которую данный указатель ссылается. Может возникнуть вопрос, в чем разница между неопределенным состоянием указателя и случаем, когда его значение равно Nil? Поскольку Nil – значение конкретное, хотя и ни на что не указывающее, можно сказать, что два указателя, содержащие Nil, имеют равные значения. В то же время значения двух указателей в неопределенном состоянии равными признать нельзя. 4. Выделение и освобождение динамической памяти. В соответствии с двумя типами указателей существуют и две разные процедуры создания динамических переменных: new(p); — для типизированных указателей; getmem(p, size); — для нетипизированных указателей. Параметр size задает размер памяти в байтах, которую необходимо выделить. В 16-разрядных компиляторах размер size не может превышать 64 Кбайт. Это существенное ограничение, которое не позволяет, например, сохранить в одной переменной образ всего графического экрана. Например: new(pl); new(p2); new(pmas); getmem(p3,200); При выполнении процедуры new в динамической памяти выделяется столько байтов, сколько требуется для хранения переменной заданного типа. При этом указателю, который является параметром-переменной, присваивается адрес первого байта выделенной памяти. Для нетипизированных указателей в памяти выделяется ровно столько байтов, сколько указано во втором параметре процедуры getmem (в нашем примере — 200 байт), а указатель получает значение адреса первого байта выделенной области. Таким образом, в приведенном примере указатели pi, р2, рЗ, pmas получают свои значения только после выполнения процедур выделения памяти. Обратите внимание — динамические переменные не имеют собственного имени, и в процедурах выделения памяти задается не имя переменной, а имя указателя. Обращаться к динамическим переменным нужно через их указатели, используя знак ^. Эта операция называется разыменованием указателя. До использования процедур new или getmem значения указателей считаются неопределенными, и работать с ними нельзя. Для типизированных указателей. Для динамически размещаемых переменных, на которые ссылаются типизированные указатели, память выделяется и освобождается с помощью процедур New и Dispose соответственно. Процедура New создает новую динамическую переменную, на которую ссылается типизированный указатель. Procedure New (var p : pointer); Процедура Dispose освобождает место, занятое в памяти динамической переменной, на которую ссылается динамический указатель. Procedure Dispose (var p : pointer); Например: Var px, py: ^char; pz : ^integer; begin New(px); New(py); New(pz); px^ := ‘A’; py^ := ‘7’; pz^ := 888; … Dispose (px); Dispose (py); Dispose (pz); … end. Процедуры mark(p) и release (р) — эти две процедуры могут использоваться только вместе и позволяют очистить сразу целую область памяти. При этом про­цедура mark(p) запоминает в указателе р адрес начала области динамиче­ской памяти, а процедура release (р) очищает всю память, начиная с ад­реса р. Процедура mark обычно помещается в начало программы, а release, наоборот, в конец. Для нетипизированных указателей. Для динамически размещаемых переменных, на которые ссылаются нетипизированные указатели, память выделяется и освобождается с помощью процедур GetMem и FreeMem соответственно. Процедура GetMem создает динамическую переменную определенного размера, на которую ссылается нетипизированный указатель. Procedure GetMem (var p : pointer; size : word); Самый большой блок, который может быть зарезервирован за один раз, равен 65520 байт. Если в куче для размещения динамической переменной недостаточно свободного пространства, имеет место ошибка времени выполнения. Процедура FreeMem освобождает память, занятую динамической переменной данного размера, зарезервированную за нетипизированным указателем (с помощью процедуры GetMem). Procedure FreeMem (var p : pointer; size : word); В обеих процедурах: p – нетипизированный указатель, для которого раньше была выделена память процедурой GetMem; size – выражение, определяющее размер освобождаемой динамической переменной в байтах. Это значение должно совпадать с числом байтов, выделенных для этой переменной процедурой GetMem. Часто для определения второго параметра процедуры GetMem используется функция SizeOf, возвращающая длину в байтах внутренноего представления указанного объекта. Function SizeOf (x) : integer; Например: Var px, py : pointer; begin GetMem (px, SizeOf (char); GetMem (py, SizeOf (integer); px^ := ‘A’; py^ := ‘7’; … FreeMem (px, SizeOf (char); FreeMem (py, SizeOf (integer); … end. 5. Действия над указателями и динамическими переменными. После того как указатель объявлен и под динамическую переменную, на которую он ссылается, выделена память, над указателем и динамической переменной обычно производят какие-то манипуляции. К динамическим переменным применимы все действия применимые для данных соответствующего типа. Иными словами, динамической переменной можно манипулировать так же, как и аналогичной статической переменной. Что же касается указателей (а не данных, на которые они указывают), то для них допустимо только следующее. 1. Проверка равенства. if px = py then… где рх и ру – указатели. Этот оператор проверяет, ссылаются ли указатели на один и тот же адрес памяти. (Однако если бы вместо = присутствовало < или >, то была бы зафиксирована ошибка) 2. Значение одного указателя можно присвоить другому, если оба указателя ссылаются на динамическую переменную одного типа. px:=py; Это ограничение распространяется только на типизированные указатели. Нетипизированному указателю можно присвоить значение любого указателя. Точно так же любому указателю можно присвоить значение нетипизированного указателя. Указателям значения присваиваются процедурой New или GetMem при выделении памяти для динамических переменных. Поэтому присваивать с помощью оператора присваивания указателям какие-либо явно заданные значения нельзя, за исключением Nil. Вопросы для самоконтроля: 1. Что такое динамическая память? 2. Что означает динамическое размещение данных? 3. Что такое указатель? 4. Что такое адрес и как он определяется? 5. Дайте определение типизированного указателя. 6. Дайте определение нетипизированного указателя. 7. Как объявляются указатели? 8. С помощью каких процедур выделяется и освобождается динамическая память? 9. Как получить адрес переменной и значение переменной по ее адресу? Лекция №19. Тема: Линейные списки: основные виды и способы реализации. Вопросы: 1. Определение линейного списка. 2. Простейшие операции над списками. 3. Стеки. 4. Очереди. 5. Деревья. 1. Определение линейного списка. Рассмотрим структуры данных, которые можно создавать с помощью указателей. Начнем со связных списков. Список принадлежит к числу абстрактных типов данных и представляет собой упорядоченную последовательность элементов. Упорядочен­ность означает, что для каждого элемента можно указать предшествующий ему и следующий за ним элементы. Принято выделять следующие типы связанный списков: • односвязные линейные списки; • односвязные циклические списки; • двусвязные линейные списки; • двусвязные циклические списки. Рисунок 18.1. Связный однонаправленный список. Линейный связанный однонаправленный список образует последовательность, представленную на рис. 18.1. Первый элемент на­зывается головой списка. Поле-указатель каждого элемента списка (кроме последнего) именует, т.е. идентифицирует следующий элемент, "привязывая" его к предыдущему. За последним элементом списка следующего нет, поэтому его указатель равен nil, т.е. установлен на "ничто". Если разорвать связь с предыдущим элементом, изменив значение указателя в нем, теряется ссылка на следующий элемент списка. Он и все элементы за ним превращаются в "мусор" в свободной памяти. Они занимают память, но в программе их "не видно". Для идентификации первого элемента (и списка в целом) нужен указатель на голову списка, определенный в программе и расположенный в ее статической или автоматической памяти. 2. Простейшие операции над списками. Рассмотрим определения и операции, необходимые для создания и обработки списка. Элементы списка являются структурами, у которых одно из полей — указатель на структуры этого же типа. Что определить сначала — тип структур или тип указателей на них? Указатели любого типа имеют размер 4 байт, поэтому сначала определяется тип указателей на структуры, а затем тип структур. Тип элементов списка строк обозначим именем Lstr, а тип указателей на них — Pstr. type Pstr = ^Lstr; Lstr = record v: string; next : Pstr; end; Элемент списка, на который установлен указатель р типа Pstr, идентифицируется выражением р^.v Выражения p^.v и p^.next обозначают поля этого элемента — строку типа string и адрес следующего элемента типа Pstr. Основными операциями, которые можно выполнять над списками, являются опера­ции включения записи в список и ее удаления из списка. Чтобы добавить в связный список новый узел, достаточно изменить один указатель, сами узлы при этом переме­щаться не должны. Удаление узла также осуществляется изменением соответствующе­го указателя так, чтобы он ссылался на узел, следующий за удаляемым. Связанный список, в котором последний элемент указывает на первый, называет­ся циклическим. Обычно такой список идентифицируется указателем на последний элемент. Он используется, когда в процессе выполнения программы нужен доступ и к первому, и к последнему элементу, например, при работе с очередью — элемент добавляется в ее конец, а удаляется из начала. В элементе списка можно хранить указатель как на следующий элемент, так и на предыдущий. Такой список называется двусвязным. Он используется, когда по связям списка нужно двигаться не только вперед, но и назад. В циклическом списке последний узел содержит указатель на первый его элемент. Таким образом получается замкнутая структура данных. Один из узлов в этом слу­чае условно считается корневым, а пустого указателя в циклическом списке нет. В двусвязных списках каждый узел содержит два указателя — на предыдущий и последующий узлы. 6. Стеки. Стек представляет собой частный случай списка, доступ к которому возможен только в корневой точке. Добавление или удаление нового элемента производится в начале списка (рис. 18.2). Иногда стек обозначают английской аббревиатурой LIFO — Last In First Out, что можно перевести как «последний вошел — первый вышел». Это сокращение правильно передает механизм работы стека. Рис. 18.2. Стек. Для стека определены операции занесения элемента в стек и извлечения элемента из стека. Операция занесения элемента в стек определяется только значением эле­мента. Извлечение элемента заключается в присвоении переменной значения пер­вого элемента стека и удалении этого элемента. 4. Очереди. Очередь, как и стек, является частным случаем списка. Доступ возможен только к первому и к последнему элементам очереди. Данные могут быть добавлены в конец очереди и удалены из ее начала (рис. 18.3). Элемент, который был добавлен в очередь первым, первым достигнет ее начала. Английское обозначение такой структуры данных — FIFO, то есть First In First Out — «первый вошел — первый вышел». Рис. 18.3. Очередь. Как и для стека, для очереди определены операции занесения элемента в очередь и его извлечения из очереди. 5. Деревья. Дерево представляет собой совокупность элементов с иерархической структурой. Дерево состоит из узлов, причем один из узлов (начальный) играет особую роль. Он называется корнем, или корневым узлом. Узлы дерева связаны между собой отношениями. У каждого узла есть узел-предок и некоторое число узлов-потом­ков. Корень представляет собой особый случай. Такие структуры данных встреча­ются в повседневной жизни. Это, например, генеалогические деревья. Использу­ются деревья и в компьютерной науке. При компиляции программы, например, строится дерево синтаксического анализа. Существуют разные способы реализации деревьев. Рассмотрим пример реализации дерева. В этой реализации каждый узел содержит несколько указателей на несколько узлов. Если указателей два («пра­вый» и «левый»), такое дерево называется бинарным (рис. 18.4). Один из указате­лей может быть равен nil. Рис. 18.4. Бинарное дерево. У корневого узла нет входящих в него ветвей, есть только исходящие. Вершина, на которую имеется указатель из другой вершины, называется потомком этой вер­шины. Последняя, соответственно, называется предком. Если вершина не имеет потомков, она называется терминальной вершиной. В бинарном дереве целочисленных значений часто придерживаются соглашения о том, что во всех левых вершинах должны находиться меньшие числа, а в пра­вых — большие. Основные операции над деревьями — это занесение элемента в дерево, удаление элемента из дерева и обход дерева. Вопросы для самоконтроля: 1. Что такое список? 2. Что такое стек? 3. Что такое очередь? 4. Что такое дерево? 5. Какие виды списков существуют? 6. Перечислите операции со списками и деревьями. Лекция №20. Тема: Объектно-ориентированная модель программирования. Вопросы: 1. Объектно-ориентированный подход. 2. Подходы в области ООП. 3. История возникновения объектно-ориентированного подхода. 4. Структурный подход к программированию. 5. Проблемы программного обеспечения. 6. Основания и история объектно-ориентированного подхода к программированию. 7. Реальные системы как системы взаимодействия объектов. 8. Описание структуры объектов. 9. Понятие класса. 10. Наследование классов. 11. Полиморфизм. 12. Отношения между классами. 13. Метаклассы и метаданные. 14. Объектно-ориентированное программирование в Turbo Pascal. 15. Объектно-ориентированное программирование в Delphi. 1. Объектно-ориентированный подход. В настоящее время «объектно-ориентированное программирование» – термин настолько распространенный, что ни одна статья, ни одна публикация без него не обходится. Объектно-ориентированный подход, конечно, не панацея, однако в настоящее время именно с ним связаны наибольшие достижения в организации программирования. Будем рассматривать объектно-ориентированное программирование, по возможности не вдаваясь в детали реализа­ции конкретного языка программирования. Объектно-ориентированный подход позволяет существенно улучшить организацию разработки программных систем. Особенно важно, это улучшение между всеми этапами разработки программного обеспечения. Взаимосвязь обусловлена как разработанными в последнее время технологиями программирования, так и используемым мета подходом – проектированием по образцам. На рус­ском языке литература в основном посвящена конкретным объектно-ориентированным языкам программирования, таким как Си++, Java, Смолток и т. д. Публикации по общим вопросам объектно-ориентированного подхода к программированию не дают пол­ной картины взаимосвязи разных технологий. Объектно-ориентированный анализ (ООА), объектно-ориентированное программирование (ООП), объектно-ориентированное проектирование (ООПР) в настоящее время находятся в стадии развития. Терминология в этих технологиях еще не устоялась, и между ними много общего. Для изучения общей идеологии примем теорию объектно-ориентированного анализа (ООА). Метод ООА, понимаемый в контексте программной или системной инженерии, представляется как последовательность построения цепочки моделей: • информационная модель, в которой центральным моментом явля­ется абстрагирование концептуальных сущностей какой-либо задачи в терминах объектов и атрибутов. Отношения между сущностями фор­мализуются в связях, которые базируются на линиях поведения, пра­вилах и физических законах, «превалирующих» в реальном мире; • модели состояний, описывающие поведение объектов и связей во времени на основе концепции «жизненного цикла», относящейся и к тому, и к другому. Модели состояний, выражающиеся в переходных диаграммах и таблицах, «взаимодействуют между собой посредством событий, их организовывают в уровни, чтобы сделать систему взаимодействия упорядоченной и понятной»; • модели процессов, описывающие взаимодействия моделей состояний в терминах «действий», расчленяющихся на фундаментальные процессы и многократно используемые в форме так называемых ДПД (диаграмм потоков данных и действий). Представленные таким образом процессы могут быть преобразованы непосредственно в операторы объектно-ориентированного проектирования. 2. Подходы в области ООП. Два подхода объективно сложились в настоящее время – «структурный» и «объектно-ориентированный». Каж­дый из этих подходов поддерживается приблизительно равными по мощности языками. Выбор того или другого подчас сложен, отсюда – серия «мифов», представляющих собой на самом деле типичные заб­луждения в отношении: • противопоставления структурного и объектно-ориентированного анализа; • дилеммы первичности функциональной или информационной моделей; • противопоставления диаграмм потоков данных и SADT (Structured Analysis and Design Technique) – диаграмм. Общие корни этих заблуждений лежат в недо­статочно ясном понимании тонкостей технологии этих двух разных подходов. Здесь наблюдается заметное неравенство в возможностях изучения этих самых тонкостей. Классический структурный анализ, предусматривающий расчленение процесса на функциональные модели, легко воспринимается участниками всех уровней любого бизнес процесса – от охранника до руководителя предприятия. Что касается ООА, то с этим и возникают осложнения. В самом деле, попробуем поговорить с руководством предприятия или экспертом предметной области (менеджером или бухгал­тером), употребляя такие термины, как наследование, инкапсуляция, полиморфизм и т. д. В этом случае можно встретить непонимание. Это в лучшем случае. И все же очевидно, что объектно-ориентированная модель наиболее адекватно отражает реальный мир, представляющий собой в целом совокупность взаимодействующих посредством обмена сообще­ниями объектов. В настоящее время число программных продуктов, поддерживаю­щих объектно-ориентированный подход, невелико по сравнению с поддерживающими структурный анализ. Но быстро растет, и надо быть готовым всем участникам моделирования к восприятию технологий на основе ООА, в ко­нечном счете – «реинжиниринга», бизнес процессов. 1. Программирование. Здесь ООА как элемент техноло­гии уже завоевал себе имя и место. Осмысленное использование объектно-ориентированных языков программирования и методов построения программ невозможно без глубокого понимания основ ООА. В частности, необходимыми представляются фундаментальные понятия объектно-ориентированного метода программирования, такие, как объект, класс, наследование, полиморфизм и т. д. Эти понятия необходи­мы и для изучения конкретных методологий, например, языка UML. Необходимость в распространении знаний основ ООА, продиктованной не толь­ко нуждами повышения эффективности программирования. Речь идет о действительно новых областях человеческой деятельности, объединенных общим термином CASE (Computer Aided System Engineering) – средства, о ко­торых без преувеличения можно сказать, что за ними будущее. 2. Создание CASE-средств, а также базирующихся на их применении видов деятельности, таких, как бизнес-консалтинг, системный анализ, проектирование информационных систем и сопутствующих услуг, таких, как обучение и сопровождение разработок. Этот вид профессиональной деятельности подразумевает, как известно, создание не только комплексных технологических конвейеров для производства программных средств, но и инструментов для реше­ния исследовательских и проектных задач, связанных с разработкой информационных систем. Важными частями этих разработок являются такие компоненты, как анализ предметной области, создание проектных спецификаций, выпуск проектной документации, планирование и контроль разработок, моделирование деловых приложений. 3. История возникновения объектно-ориентированного подхода С момента появления первых электронно-вычислительных машин разработка программного обеспечения прошла большой путь от вос­хищения фактом возможности написать хоть какую-нибудь програм­му до осознания того, что именно технология разработки програм­много обеспечения определяет прогресс в вычислительной технике. Ранее развитие вычислительной техники было сосредоточено на решении технических проблем. Предметом забот была аппаратура, вычислительная машина как таковая. Казалось вполне естественным, что программы для таких машин должны были разрабатываться в двоичных кодах. Программирование было уделом энтузиастов. По мере того как вычислительные машины становились мощнее и надежнее, значение программного обеспечения осознавалось все большим количеством ученых и практиков. Важнейший про­рыв произошел в конце 1950-х гг., когда появились языки программирования высокого уровня: Фортран, Алгол и др. Появление язы­ков программирования высокого уровня было обусловлено тем, что написание программ без них становилось все более сложной задачей. Решив текущие проблемы, эти языки создали но­вые. Ускорив и упростив программирование, языки программирования существенно расширили круг задач, которые стали решаться с помощью ЭВМ. Новые задачи, более сложные, чем решавшиеся ранее, привели к тому, что имеющихся средств снова стало недостаточно для успешного их решения. Такое развитие характерно для всей истории применения компьютеров вплоть до настоящего времени. Вычислительная техника все время используется на пределе своих возможностей. Каждое новое достижение в аппаратном либо в программном обеспечении приводит к попыткам расширить сферу применения ЭВМ. Тем самым ставит новые задачи, для решения которых нужны новые возможности. Создаются новые языков программирования. Появился язык Кобол, который и должен был решать задачи обработки экономической информации. Он создавался таким образом, чтобы программа на нем выглядела почти как текст на английском языке. Появился язык PL/I, призван­ный объединить все возможности, которые только можно представить себе в языке программирования. Языки, которые мы уже упоминали (Фортран, Алгол, PL/I) поддерживают процедурный стиль программирования. Программа разрабатывается в терминах тех действий, которые она выполняет. Основной единицей программы является процедура. Процедуры вызывают другие процедуры, все вместе они работают по опреде­ленному алгоритму, который ведет к решению задачи. Алгоритм – это точная последовательность действий для получения необходи­мого результата. Слово «алгоритм» (algorithmi, algorismus) произошло от латинской транслитерации имени среднеазиатского математика аль-Хорезми, который описал точную последовательность действий для вычисления наибольшего общего делителя. Появление таких языков вытекало из круга задач, которые решались с помощью вычислительной техники. Это – вычислительные задачи. Основными пользователями ЭВМ были физики и инженеры, которые интересовались возможностью быстро вычислить необходимый результат. Применение ЭВМ для решения задач искусственного интеллек­та и обработки текстов привело к созданию функциональных язы­ков, в частности, языка Лисп. Эти языки имеют также хорошо про­работанное математическое основание: лямбда-исчисление. В от­личие от языков типа Алгола, в которых действия в основном вы­ражаются в виде итерации – повторения какого-либо фрагмента программы несколько раз, в языке Лисп вычисления производят­ся с помощью рекурсии – вызова функцией самой себя, а основная структура данных – это список. В 1960-е г. многими теоретиками и практиками было осозна­но, что только лишь создание новых, более совершенных языков программирования не может решить все проблемы разработки про­грамм. Начались интенсивные исследования в области тестирова­ния программ, организации процесса разработки программного обеспечения и др. Первой, пожалуй, была осознана проблема ошибок. Сравни­вая программирование с другими инженерными дисциплинами, многие задавались вопросом: почему можно добиться безошибоч­ной работы радиоприемника или даже процессора ЭВМ, но чрез­вычайно сложно добиться безошибочной работы программы. Можно ли так организовать тестирование и проверку программ, чтобы добиться 100-процентной уверенности в ее правильности. Очевидно, что, лишь подавая некоторые данные на вход про­граммы и наблюдая результат, это сделать невозможно хотя бы в силу слишком большого количества вариантов исходных данных. Первое, что нужно сделать, – это попытаться упорядочить процесс тестирования. Работы по организации процесса тестирования начали появ­ляться в конце 1960-х г. Примерно к середине 1970-х г. были заложены основы организации тестирования, которыми в основ­ном пользуются в настоящее время. В монографии Майерса, исследуются как сами методы тестирования, такие как модульное тестирование, внешнее тестирова­ние, нисходящее и восходящее тестирование, анализ передачи уп­равления в программе при тестировании, так и принципы проек­тирования и разработки надежных программных систем. В 1970 – 80-х г. бурно развивалась теория доказательства пра­вильности программ. Она основывалась на том, что текст програм­мы задает все, что она делает. Утверждалось, что, имея формальное описание семантики всех конструкций языка, можно на основе ана­лиза текста строго математически вывести заключение о правиль­ности или неправильности программы. Возникла необходимость создать строгое описание не только синтаксиса языка, но и смысла всех его конструкций. Если с синтаксисом принципиально проблема была решена еще в конце 50-х годов созданием формальной теории грамматики языка Хомского и способа записи грамматики Бэкуса – Наура, то с семантикой оказалось сложнее. Было разработано несколько мето­дов описания семантик: W-грамматики, аксиоматический подход, денотационный метод, атрибутные грамматики, Венский метод описания и ряд других. С помощью этих методов было описано несколько реальных языков программирования: PL/1, Алгол-68 и Паскаль и др. Методы доказательства развивались, и с их помощью можно было сделать выводы о корректности небольших учебных примеров. Однако дальше встали две проблемы, которые фактически похоронили возможность широкого практического применения доказательства программ: определение, что такое корректная программа, и сложность реальных программ. Формально определить, каков правильный результат программы, можно только для небольшого круга ма­тематически сформулированных проблем. Для большинства реальных программ строгое описание того, что программа должна делать, существенно больше по объему самой программы и требует очень высокой математической квалификации программиста. Для большого количества программ описание в принципе не формализуемо. Для примера посмотрите на объем руководства, например, текстового редактора (которое довольно не строго). Именно поэтому результаты теории доказательств нашли при­менение в очень ограниченных областях программирования (например, в разработке компиляторов). Для массового программирования они оказались неприменимы. Попытка Э. Дейкстры соеди­нить структурное программирование с методами доказательства программ осталась изящным упражнением, образцом для вос­хищения, но не для подражания. Работы по развитию формальных методов доказа­тельства программ продолжаются. Одним из недавних достиже­ний в этой области стала разработка системы управления париж­ским метрополитеном, полностью доказанная с помощью В-метода. Этот пример подтверждает как мощность математи­ческого доказательства программ, так и то, что трудоемкость проекта оказывается до сих пор слишком высокой для широко­го применения формальных методов – только лемм было дока­зано около тридцати тысяч. Усилия преобразовать программирование из эмпирического процесса в более упорядоченный, придать ему более «инженерный» характер вылились в создание методик программирования. Это был очень существенный шаг. Разработка методов построения программ, с одной стороны, создавала основу для массового промышленного программирования, а с другой стороны, обобщая и анализируя текущее состояние программного обеспечения, давала мощный им­пульс созданию новых языков программирования, операционных систем, сред программирования. Одной из наиболее широко применяемых методик программи­рования стало структурное программирование. Следствием интенсивных исследований в области методов про­граммирования явилось появление большого числа средств авто­матизации разработки программ (CASE-средств, Computer Aided Software Engineering). Предполагалось, что после записи задачи на каком-либо высокоуровневом языке эти системы автоматически (или хотя бы с минимальным участием человека) сгенерируют го­товую, правильно работающую программу, которая адекватно ре­шает поставленную задачу. В целом CASE-средства не смогли достичь обещанного: либо описание задачи по сложности оказывалось сравнимым с резуль­тирующей программой, либо решался крайне ограниченный круг сравнительно простых задач, либо качество генерируемой програм­мы оказывалось слишком низким. Несмотря на провал при достижении основной цели, опыт разработки CASE-средств дал очень много для развития методов программирования. Небольшая часть систем используется напрямую, часть идей была использована в современных средствах быстрой разработки программ (Rapid Application Development), таких, как Builder, Delphi, Powerbuilder и др. Кроме того, методы анализа и проекти­рования программ очень много унас­ледовали от методов, использовавшихся при разработке средств ав­томатизированного программирования. По мере увеличения сложности решаемых задач и соответствен­но увеличения размеров программных систем все большее значение приобретали вопросы организации процесса разработки програм­много обеспечения. Широко известна (и не потеряла своего значе­ния до сих пор) книга Брукса. Автор по­казал значение организации работы программистских коллективов для успеха (или провала) проекта. Ценность этой книги даже не в том, что приведены конкретные организационные структуры (напри­мер, метод хирургических бригад не нашел широкого применения), а в том, что она развеяла миф о человеко-месяце. Механическое уве­личение количества людей, работающих над программой, не уско­рит процесса ее разработки. В 1970-е гг. была сформулирована модель процесса разработки. Эта модель выделяла несколько фаз процесса: анализ, дизайн, кодирование, тестирование, внедрение. Каждая последующая ста­дия «вытекает» из предыдущей стадии, поэтому и модель получила назва­ние каскадной. Последующее развитие привело к модификации этой модели – к форме спирали, при которой стадии разработки итеративно повто­рялись, но каждый раз на новом уровне, с учетом предыдущей разра­ботки. Осознание исключительной важности организации разра­ботки привело к построению всеохватывающих моделей и даже меж­дународных стандартов. Приведенный исторический экскурс развития методологии про­граммирования, совершенно не претендующий на полноту, иллю­стрирует то, что все время программирование «боролось» за воз­можность решать все более сложные задачи, создавать все более сложные программные системы и делать это как можно быстрее. Периодически то или иное достижение объявлялось панацеей (будь то структурное программирование, CASE-средства или что-либо иное). С той же закономерностью оказывалось, что широко разрек­ламированное средство не способно решить все проблемы. Очередное увеличение сложности решаемых задач и размеров создаваемых программных систем привело к широкому распрост­ранению объектного программирования. 4. Структурный подход к программированию Создателем структурного подхода к программированию счи­тается Э. Дейкстра. Фактически структурное программирова­ние – это первая законченная методология программирования. «Законченная» – потому, что структурное программиро­вание предлагает путь от задачи до ее воплощения в программе. Структурное программирование оказало огромное влияние на раз­витие программирования. Этот метод, который применялся очень широко в практическом программировании и по сей день, не поте­рял своего значения для определенного класса задач. Структурный подход базируется на двух основополагающих принципах: 1) использование процедурного стиля про­граммирования; 2) последовательная декомпозиция ал­горитма решения задачи сверху вниз. Задача решается применением последовательности действий. Первоначально она формулируется в терминах входа-выхода. Это означает, что на вход программы подаются некоторые данные, программа работает и выдает ответ. После этого начинается последовательное разложение всей за­дачи на более простые действия. Например, если нам необходимо написать программу проверки правильности адреса, то мы запишем ее следующим образом: Прочитать адрес. Сверить адрес с базой имеющихся адресов. Если результат проверки положителен, напечатать «Да», в противном случае напечатать «Нет». Очевидно, что такая запись один к одному отображается в про­грамме на языке высокого уровня, например, на Паскале. program check_address (input, output); var an_address : Address; begin read_address(an_address); if check_database (an_address)); then writeln(«Да») else writeln(«Нет»); end Эта программа использует процедуру read_address для чте­ния адреса и процедуру check_database для сверки прочитанно­го адреса с информацией в базе данных. Теперь мы можем продолжить процесс составления программы для процедур следующего уровня: чтение адреса и сверки с базой данных. Что чрезвычайно важно, на любом этапе программу можно проверить, достаточно лишь написать заглушки – процедуры, имитирующие вход и выход процедур нижнего уровня. В приведенной выше программе можно использовать процедуру чтения адреса, которая вместо ввода с терминала просто подставляет какой-нибудь фиксированный адрес, и процедуру сверки с базой данных, которая ничего не делает, а просто всегда возвращает истину. Программа компонуется с заглушками и может работать. Ины­ми словами, заглушки позволяют проверить логику верхнего уровня до реализации следующего уровня. Последовательно применяя метод заглушек, можно на каждом шаге разработки про­граммы иметь работающий скелет, который постепенно обраста­ет деталями. Структурное программирование поддерживалось языками про­граммирования, появившимися в конце 60-х г. (Паскаль, Алгол-68, Симула). Именно к тому времени было осознано значение программного обеспечения при решении задач с помощью вычислительной техники. Эти языки поддерживали разнообразные вло­женные процедуры, разные способы передачи параметров. Несмотря на то, что структурное программирование ясно опре­делило значение модульного построения программ при разработке больших проектов, языки программирования еще слабо поддержи­вали модульность. Единственным способом структуризации про­грамм являлось составление ее из подпрограмм или функций. Кон­троль за правильностью вызова функций, в том числе соответствия количества и типов фактических аргументов ожидаемым формаль­ным параметрам, осуществлялся только на стадии выполнения (по­нятие прототипа функции появилось позже). 5. Проблемы программного обеспечения Объектно-ориентированное программирование родилось и по­лучило широкое распространение ввиду осознания трех важнейших проблем программирования. Во-первых, раз­витие языков и методов программирования, хотя и существенно облегчило и ускорило разработку программных систем, не успева­ло за растущими с еще большей скоростью потребностями в про­граммах. Единственным реальным способом удовлетворить необ­ходимость в резком ускорении разработки остался метод многократ­ного использования разработанного программного обеспечения. Иными словами, требовалось не строить каждый раз систему с нуля, а использовать разработанные ранее модули, которые уже прошли цикл отладки и тестирования и успешно работают. Разумеется, процедурное программирование предоставляло спо­соб разработки функций, которые могли служить блоками для по­строения программ. Однако ни гибкость этого метода, ни масшта­бы использования не позволяли существенно ускорить массовое программирование. Второй, фактически связанной с первой, проблемой являлась необходимость упрощения сопровождения и модификации разра­ботанных систем. Факт постоянного изменения требований к системе был осознан как нормальное условие развития системы, а не как неумение или недостаточно четкая организация работы создателей. Сопровождение и модификация систем требуют не мень­ших усилий, чем собственно разработка. Требовалось радикально изменить способ построения программных систем, с тем чтобы, во-первых, локальные модификации не могли нарушить работоспоcобность всей системы и, во-вторых, было легче производить изме­нения поведения системы. Третья проблема, возможно, наиболее существенная, которую требовалось решить, – это облегчение проектирования систем. Да­леко не все задачи поддаются алгоритмическому описанию и тем более алгоритмической декомпозиции, как того требует структур­ное программирование. Требовалось приблизить структуру про­грамм к структуре решаемых задач и сократить так называемый семантический разрыв между структурой решаемой задачи и струк­турой программы. О семантическом разрыве говорят в том случае, когда понятия, лежащие в основе языка задачи и средств ее реше­ния, различны. Поэтому наряду с необходимостью записи самого решения требуется еще перевести одни понятия в другие. Сравните это с переводом с одного естественного языка на другой. Именно потому, что таких понятий в русском языке раньше не было, появляются слова типа брокер, оффшор или инвестор. К сожалению, в программировании заимствование слов невозможно. Итак, упрощение проектирования, ускорение разработки за счет многократного использования готовых модулей и легкость модифика­ции – вот три основных достоинства объектно-ориентированного программирования, которые пропагандировались его сторонниками. 6. Основания и история объектно-ориентированного подхода к программированию Объектно-ориентированный подход к программированию связывают прежде всего с языками программирования, такими как Смолток, Си++, Java и т. д. Поскольку языки являются главными инструмента­ми объектно-ориентированного программирования, именно при их разработке появилось большинство тех идей, которые и со­ставляют основу объектно-ориентированного метода в настоя­щее время. Накопление новых идей шло в течение примерно десятилетия, начиная с конца 1960-х гг. К концу 1970-х гг. переход на новый уровень абстракции стал свершившимся фактом в академических кругах. Потребовалось еще около десяти лет, чтобы новые идеи про­никли в промышленность. Первым шагом на пути создания собственно объек­тной модели следует считать появление абстрактных типов дан­ных. Первым на необходимость структуризации систем по уровням абстракции указал Дейкстра. Идея инкапсуляции (скрытия инфор­мации) была высказана Парнасом. Позднее был разработан механизм абстрактных типов данных, который был дополнен Хоором в его теории типов и подтипов. Считается, что первой полной реализацией абстрактных ти­пов данных в языках программирования является язык Симула, который в свою очередь опирается на языки Модула, CLU, Euclid и др. Первым «настоящим» объектно-ориентированным языком про­граммирования принято считать Смолток, разработанный в лабо­ратории компании Ксерокс в Паоло-Альто. (Многие по-прежнему считают его единственным настоящим объектным языком програм­мирования). Затем появились (и продолжают появляться) другие объектно-ориентированные языки, которые определяют современное состоя­ние программирования. Наиболее распространенными из них ста­ли Си++, CLOS, Эйффель, Java. Однако только языками программирования объектно-ориен­тированный подход не исчерпывается. Языки лишь предоставля­ют инструментарий, которым можно воспользоваться или не вос­пользоваться. На языке Си++ можно писать и в процедурном сти­ле. Объектно-ориентированное программирование предполагает единый подход к проектированию, построению и развитию сис­темы. Появление объектно-ориентированного метода произошло на основе всего предыдущего развития методов разработки про­граммного обеспечения, а также многих других отраслей науки. Помимо языков программирования можно отметить следующие достижения технологии, которые способствовали возникновению объектно-ориентированного подхода к проектированию систем: 1. Развитие вычислительной техники, в частности, аппаратной поддержки основных концепций операционных систем и построе­ние функционально-ориентированных систем. Характерными достижениями являются: – проектирова­ние вычислительных машин с использованием понятия объекта на основании исследований по не-фон-Неймановской архитектуре; – сокращение семантического разрыва между низкоуровневой архитектурой традиционных процессоров и высокоуровневыми понятиями операционных систем; – разработка объектно-ориентированных операционных си­стем. 2. Достижения в методологии программирования, в частности, модульное построение систем и инкапсуляция информации. 3. Теория построения и моделирования систем управления ба­зами данных внесла в объектное программирование идеи построе­ния отношений между объектами. Моделирование данных с помо­щью отношений между объектами было впервые предложено Ченом в методе ER-моделирования. В этом методе модель данных строится в виде объектов (сущностей), их атрибутов и отношений между ними. 4. Исследования в области искусственного интеллекта позволи­ли лучше осознать механизмы абстракции. Теория фреймов, пред­ложенная Минским для представления реальных объектов в си­стемах распознавания образов, дала мощный импульс не только системам искусственного интеллекта, но и механизмам абстракции в языках программирования. 5. Развитие философии и теории познания. Объект­но-ориентированное построение систем – это определенный взгляд на моделируемый реальный мир. Еще древние греки рассматривали мир в виде объектов или процессов. Декарт выдвинул предположение, что для человека ес­тественным представляется объектно-ориентированное рассмотре­ние окружающего мира. 7. Реальные системы как системы взаимодействия объектов Программные системы призваны решать задачи, которые воз­никают в разнообразных областях деятельности человека. Про­граммные системы создаются с помощью средств разработки про­грамм – языков программирования, систем управления базами дан­ных, операционных систем. Все эти средства имеют математическое основание, логику, теорию конечных авто­матов, алгебру и т. д. Соответственно удобнее всего с их помощью решаются математические, вычислительные задачи: именно с них и начиналось программирование. Применение вычислительной техники для решения задач в дру­гих, иногда очень далеких от математики, областях привело к воз­никновению проблемы семантического разрыва между выразитель­ными средствами языков программирования и языком, на котором сформулированы задачи. Программные системы предназначены для моделирования ре­альных систем, поэтому очень важно, в каких терминах мы пыта­емся описывать эти реальные системы. Описание в виде последова­тельности действий (процедурный подход к программированию) оказалось слишком сложным. Объектно-ориентированный подход предлагает описывать системы в виде взаимодействия объектов. Предположим, что мы должны разработать систему автомати­зации банка. Для примера рассмотрим, как могла бы осуществлять­ся операция снятия денег через банкомат (рис. 19.1.). В данной опера­ции задействованы три объекта: «клиент Иванов», «банкомат на Тверской», «счет№ 66579801», который открыт в данном банке Ивановым. Рис. 19.1. Схема взаимодействия объектов Подойдя к банкомату и введя свою карточку, объект «клиент Иванов» посылает банкомату сообщение «Начать рабо­тать». Получив такое сообщение, банкомат высвечивает какое-ни­будь сообщение на экране и запрашивает код доступа. «Банкомат на Тверской» посылает сообщение «клиенту Иванову»: «Сообщи­те идентификационный номер». Если идентификация прошла ус­пешно, «клиент Иванов» просит выдать ему 100 рублей. Он посы­лает сообщение банкомату, а тот в свою очередь посылает сообще­ние объекту «счет № 66579801». Приняв это сообщение, объект «счет № 66579801» проверяет, есть ли у него 100 рублей, и, если есть, пересылает разрешение на снятие денег, одновременно уменьшая свой баланс на соответствующую сумму. Банкомат передает день­ги, и на этом процедура заканчивается. Данное упрощенное описание близкое к реальности, составлено целиком в терминах взаимодействия объектов. Объекты выполняют необходимые дей­ствия, передавая друг другу сообщения. Описание в терминах объектов достаточно близко к языку пред­метной области. Составленное описание работы банкомата не тре­бует никаких знаний в области программирования и может быть сделано и понято экспертом в предметной области (в данном слу­чае – банковское дело). С другой стороны, описание в терминах объектов достаточно стро­гое. Наличие в объектно-ориентированных языках программирова­ния средств для реализации понятия объекта позволяет сравнительно просто перевести описание работы банкомата в работающую про­грамму. Таким образом, объектно-ориентированный подход вы­полняет свою первую задачу – сокращение семантического разры­ва между языком предметной области и языком программирования. Описание в виде объектов позволяет вычленить компоненты системы. Те же самые объекты – «счет № 66579801» и «клиент Иванов» – будут участвовать и в другой операции, когда клиент приходит в отделение банка для снятия или зачисления денег на счет. С другой стороны, в приведенном описании операции участву­ют только существенные в данном случае объекты. Банкомат со­стоит из множества частей, и для того, чтобы выдать наличные, он должен осуществить достаточно сложные манипуляции. Однако нас интересует не механическое устройство банкомата, а его возмож­ности по выдаче денег. Все внутреннее устройство оказывается скрытым, спрятанным внутри объекта «банкомат». Подобный принцип построения систем, называемый инкапсуляцией, был при­менен при построении абстрактных типов данных. Подробно­сти реализации той или иной операции, внутренняя структура объекта невидимы для остальных участников системы. В приведенном примере даже те операции, которые существен­ны при построении всей системы, могут быть скрыты от конкретных объектов. Объект «клиент Иванов» взаимодействует с объектом «банкомат на Тверской». Способ реализации метода «выдать деньги» не важен для него, и он его не знает. Обновляет ли банко­мат счет в момент транзакции или же оставляет лишь запись о тран­закции, а обновление счета произойдет в конце дня – вопрос реали­зации. Механизм взаимодействия этих двух объектов определен лишь теми сообщениями, которые они друг другу посылают. Конкретные действия, в том числе и посылка сообщений другим объектам системы, могут изменяться, не нарушая работоспособности си­стемы. При взаимодействии объектов часто используется модель кли­ент – сервер. Клиент – это объект, который запрашивает какие-либо действия, выполняемые другим объектом – сервером. Клиент выступает инициатором операции. Объект-сервер обеспечивает своими ресурсами выполнение запросов одного или нескольких клиентов. Посылка запросов осуществляется с помощью сообщений. Сервер выполняет операции в соответствии с некоторым контрактом, заключенным с ним. Контракт обусловливает как набор операций, которые сервер «подрядился» выполнять (интерфейс объекта), так и «качество» этих операций. Качество операций часто оценивается в терминах инвариантов (логических выражений, всегда сохраняющих свое значение). Для операций можно определить предусловие – выражение, имеющее одно и то же значение вся­кий раз непосредственно перед выполнением операции, и постус­ловие – инвариант, вычисляемый сразу после выполнения операции. Если предусловие не выполнено, операция не должна осуществляться. Если постусловие не выполнено, это значит, что контракт нарушен. Приведем еще один пример. При разработке модели автомоби­ля нас может интересовать как внешнее поведение машины, так и ее структура. Внешнее поведение автомобиля описывается стиму­лами или сообщениями, ему посылаемыми, и реакцией автомобиля на эти сообщения. Поворот руля вызывает изменение направления движения, нажатие на тормоз – замедление движения, нажатие на педаль газа – ускорение. Для внешнего мира (для человека, управ­ляющего автомобилем) автомобиль представляется объектом, у которого есть несколько методов: «повернуть руль», «нажать на тор­моз», «прибавить газ» и т. д., и состоянием: направлением движе­ния и скоростью. Автомобиль в свою очередь состоит из узлов: двигателя, колес, коробки передач, руля и т. д. В ответ на внешнее сообщение автомо­биль, рассматриваемый как система, передает необходимые сооб­щения внутренним узлам. Эти крупные узлы в свою очередь состо­ят из более мелких узлов и деталей. Взаимозаменяемость узлов и деталей определяется тем, что но­вый узел обладает тем же самым интерфейсом, что и старый. Таким образом, определив интерфейс объекта на определенном уровне, мы добиваемся независимости реализации поведения объекта. При нажатии на газ любая машина должна разгоняться, а при нажатии на тормоз – замедляться, хотя, быть может, и с разной скоростью. Если модель автомобиля используется в тренажере, замена од­ной модели другой не представляет труда до тех пор, пока обе они поддерживают один и тот же интерфейс. В свою очередь, объект «ав­томобиль» является композицией других объектов, также взаимо­заменяемых при сохранении интерфейса. Из приведенных примеров интуитивно ясно, что такое объект. Дать его четкое и приемлемое для всех определение доста­точно сложно. Объект – это слишком общее понятие. Града Буч дает следующее определение: «что-то, с чем можно оперировать. У объекта есть состояние, поведение и возможность отличить его от других объектов». Немного другое определение объекта приводит Ивар Якобсон: «Объект – это сущность, способная сохранять свое состояние (ин­формацию) и обеспечивающая набор операций (поведение) для проверки и изменения этого состояния». Объект – это модель или абстракция реальной сущности в про­граммной системе. Предмет моделирования при построении объек­та может быть различным. Сейдевич и Старк говорят о разных типах абстракции, используемой при построении объекта: • абстракция понятия: объект – это модель какого-то понятия предметной области; • абстракция действия: объект объединяет набор операций для выполнения какой-либо функции; • абстракция виртуальной машины: объект объединяет операции, которые используются другими, более высокими уровнями абстракции; • случайная абстракция: объект объединяет не связанные между собой операции. В толковом словаре русского языка С. И. Ожегова сказано: «Объект. 1. То, что существует вне нас и независимо от нашего сознания, явления внешнего мира, материальной действительности. 2. Явление, предмет, на который направлена какая-нибудь деятельность…». По-видимому, примерами «необъектов» могут служить мир, время, материя, смысл и т.п., хотя можно представить ИС, в которой хранятся сведения об этих категориях. Часто слово «объект» считается близким по смыслу слову «предмет». Однако слово «предмет» удобнее использовать в том случае, когда объект, существующий вне нас, становится носителем определенной совокупности свойств и входит в различные взаимоотношения, которые представляют интерес для потребителей информации, хранящейся в АИС. Другими словами, предмет – это объект, ставший объектом рассмотрения, наблюдения, оказавшийся носителем определенных свойств. Один и тот же объект воспринимается разными системами как разные предметы. Предмет является результатом абстракции реального объекта, результатом огрубления действительности, при котором игнорируется бесконечное многообразие свойств и взаимодействий объекта. Таким образом, предмет – это модель реального объекта: если объект имеет онтологический статус, то предмет – гносеологический (табл. 19.1.). Таблица 19.1 Слово Понятие Проявление понятия Объект Онтологическое – первичное неопределяемое понятие В качестве объектов могут быть не только физические объекты, но и объекты мышления Предмет Гносеологическое – познавательное понятие, модель объекта В результате абстракции реального объекта, результатом огрубления действительности, при котором игнорируется бесконечное многообразие свойств и взаимодействий объекта Каждый объект характеризуется своим состоянием. Состояние банковского счета – это сумма лежащих на нем денег. Состояние банкомата включает в себя следующие состояния: – «включен» или «выключен»; – готов или не готов к принятию запроса; – наличие денег в банкомате. Состояние объекта характеризуется текущим значением его атрибутов. В нашем примере у счета есть атрибут – баланс. В про­стейшем случае он выражается числом – количеством рублей и ко­пеек. Операции снятия со счета и зачисления на счет изменяют ба­ланс и состояние объекта. У объекта «банкомат» имеется несколь­ко атрибутов. Количество денег в банкомате может характеризо­ваться атрибутом – числом. Состояние «включен» или «выключен» может выражаться логическим значением, для которого истина со­ответствует состоянию «включен», а ложь – состоянию «выключен». Также логическим значением «истина» или «ложь» можно выра­зить готовность банкомата к принятию запроса. Атрибутами могут быть не только простейшие величины – числа, логические значения и т. п., но и сложные величины, объекты. Предположим, для целей контроля счет будет хранить историю всех транзакций. Транзакция – это объект, который состоит из нескольких характеристик: • типа транзакции (положить или снять), • суммы переведенных денег, • места, откуда была совершена операция (банкомат, отделение банка), • получателя (источника) денег. У объекта-счета атрибут «история транзакций» будет состо­ять из набора объектов-транзакций. Все перечисленные атрибуты объектов могут изменяться. Баланс уменьшается или увеличивается, банкомат включается или выключается и т. д. Однако не все атрибуты объекта могут изменяться. У объекта «счет» есть номер, по которому он отличается от всех остальных счетов. В отличие от баланса атрибут «номер» не может изменять­ся. До тех пор, пока счет существует, он сохраняет свой номер. То же самое можно сказать об адресе банкомата, идентификационном коде клиента и т. д. Метод идентификации объекта должен отвечать на вопрос, как отличить один объект от другого. Иными словами, если имеются два объекта, как можно определить, что эти объекты разные. На самом деле существуют два вопроса: равны ли два объекта и тождественны ли они. Предположим, в мешке лежат шары бело­го и черного цвета. Пусть каждый из них – объект. Мы вынимаем один шар, рассматриваем его и кладем обратно в мешок. Вытащив два раза шар из мешка, мы хотим знать, вытащили ли мы одинако­вые шары или один и тот же шар. Если все шары одного размера и веса и отличаются только цве­том, вытащив подряд два черных шара, мы можем определенно утверждать, что шары равны. Ответить на вопрос, вытащили ли мы один и тот же шар, невозможно. Ответить на этот вопрос мож­но, если, например, все шары пронумерованы. Точно так же, если программа обращается к двум объектам, она может сравнить все известные атрибуты объектов и определить, равны ли они. Для того чтобы сравнивать объекты на тождество, необходимо, чтобы у любого объекта существовала некоторая уни­кальная характеристика. Во многих случаях один из атрибутов объекта по определению является уникальным. Например, для всех счетов в банковской си­стеме атрибут «номер счета» должен быть уникальным, т.е. не мо­жет существовать двух счетов с одинаковым номером. Кроме того, как мы уже отмечали, номер является неизменяемым атрибутом объекта. Сравнив номера двух счетов, можно однозначно сде­лать вывод о тождественности этих объектов. Разумеется, чтобы такое сравнение работало, необходимо принять определенные меры. Создавая новый счет, необходимо присваивать ему номер, отличный от номеров всех существующих в настоящий момент счетов. Необходимо обеспечить неизменяе­мость номера счета при всех преобразованиях состояния объекта. В таком случае ответственность за правильность идентификации объекта лежит на программисте, разрабатывающем систему. Перекладывая ответственность на разработчика, среда программирования создает возможности для ошибок. Многие объектные модели предлагают встроенные методы идентификации объектов. При создании любого нового объекта ему присваивается уникальный идентификатор, отличающийся от идентификаторов всех остальных имеющихся в системе объектов. Форма этого идентификатора может быть различной. Это может быть число с достаточно большим диапазоном значений, чтобы хватило на все объекты, или адрес в универсальной системе адресации, например, URL для идентификации страниц в Интернете или специальным образом генерируемое имя. Суть его не меняется. Зная идентификаторы двух объектов, всегда можно сделать вывод об их тождественности или не тождественности. Правильность идентификации объектов при­обретает особое значение в распределенных системах, в которых программы, рабо­тающие на разных компьютерах независимо друг от друга, в про­извольное время создают и уничтожают объекты. Транзакция – последовательность логически связанных действий, переводящих информационную систему из одного состояния в другое. Транзакция либо должна завершаться полностью, либо система должна быть возвращена в исходное состояние. 8. Описание структуры объектов Важнейшей характеристикой объекта является описание того, как он может взаимодействовать с окружающим миром. Это описание называется интерфейсом объекта. Объекты взаимодействуют между собой с помощью сообщений. Принимая сообщение, объект выполняет соответствующее действие. Эти действия обычно называются методами. В нашем примере работы банкомата у объекта «счет № 66579801» имеются методы: – «снять деньги со счета»; – «положить деньги на счет». Эти два метода и составляют интерфейс объекта «счет». У объекта «клиент Иванов» имеется метод: – «сообщить свой код». У объекта «банкомат на Тверской» есть методы: – «начать работу»; – «принять деньги»; – «выдать деньги». У объекта «счет» есть еще и атрибут – «баланс». Является ли атрибут частью интерфейса? Интерфейс – это внешнее описание объекта. При разработке банковской системы и, в частности, объекта «счет» мы отвечаем на вопрос: является ли баланс информацией, необходимой другим объектам? Очевидно, что является, а именно: другим объектам нужно знать остаток денег на счете. В таком случае к объекту «счет» необходимо добавить еще один метод – «сообщить остаток денег на счете»; его интерфейс теперь будет состоять из трех методов. Таким образом, атрибут «баланс» не является непосредственно частью интерфейса. Другие объекты могут обратиться к этому ат­рибуту только опосредованно, с помощью метода «сообщить оста­ток денег на счете». Они не могут умножить этот атрибут на два или разделить пополам. Вполне возможно, что для некоторых объектов разрешены лю­бые операции с некоторыми атрибутами. Если объект описывает фигуру, которую требуется нарисовать на экране монитора, у него есть атрибуты – координаты этой фигуры. В зависимости от того, как мы проектируем этот объект, мы можем поместить эти атрибу­ты во внешнюю часть объекта, в его интерфейс. Тогда любые другие объекты могут непосредственно изменять координаты, перемещая объект по экрану. Фактически в таком случае интерфейс объекта состоит из методов «сообщить значение координаты X», «сообщить значение координаты Y», «установить значение координаты X» и «установить значение координаты Y». Наряду с методами и атрибутами, входящими в интерфейс и доступными другим объектам, у объекта могут быть методы или атрибуты, предназначенные для «внутреннего употребления», к которым может обращаться только сам объект. Например, у банкомата имеется довольно сложная внутренняя структура и, соответственно, методы, заставляющие эти составные части работать вместе. Однако для банковской системы они не важ­ны, и ни клиент, ни объект «счет» не могут к ним обращаться. Они не входят в интерфейс объекта «банкомат». Подводя итог, можно сказать, что объект известен другим объек­там только по своему интерфейсу. Внутренняя структура его скры­та. Важным следствием является возможность изменения внутрен­ней структуры объекта независимо от других взаимодействующих с ним объектов. Если какая-либо часть банкомата будет заменена, в работе всех остальных участников банковской системы никаких изменений не произойдет. Применительно к программированию это означает, что намного легче и безопаснее производить модификации системы. Они хорошо локализованы, и их эффект предсказуем (в случае изменений реализации никакие другие объекты гарантированно не должны изменяться). Для объекта, описывающего трехмерное изображение на экра­не терминала, существует метод «вращать». Если мы придумали более эффективную реализацию этого метода и ее осуществили, то тестирование всей системы фактически сводится к тестированию только этого нового метода. Поскольку интерфейс объекта не изме­нился, работоспособность всей графической системы, частью кото­рой является этот объект, не нарушилась. В любой системе объекты создаются, функционируют и, в конце концов, уничтожаются. Рассмотрим вначале традиционную модель выполнения программы. Для того чтобы программа начала выполняться, ее необходимо загрузить в оперативную память компьютера и запустить. По за­вершении выполнения оперативная память освобождается, а ее со­стояние теряется. То же самое происходит при выключении ком­пьютера – состояние памяти теряется. Соответственно, все объекты, созданные программой и находящиеся в памяти, уничтожаются. При следующем запуске той же самой программы объекты созда­ются заново, быть может, с другими исходными данными, но без всякой связи с объектами, существовавшими при предыдущем за­пуске программы. Время жизни объектов при такой модели выпол­нения ограничено временем выполнения про­граммы. Создание объектов, естественно, выполняется явно. При этом задавать, когда и какие объекты создаются, можно либо на стадии разработки, либо на стадии выполнения. При написании программы в ней объявляются переменные, ко­торые обозначают объекты. Компилятор обеспечивает создание этих объектов при запуске программы. Динамическое создание объектов на стадии выполнения озна­чает, что программа в ходе своей работы может обращаться к не­ким особым фабрикам объектов для создания новых объектов. Фаб­рика объектов – это концептуальное понятие, которое озна­чает механизм, создающий другие объекты. Она может быть реали­зована в разных средах по-разному. Это может быть особый объект, отдельная функция или отдельная подсистема языка. Удаление объектов зависит от способа их создания. Фактичес­ки существуют два подхода: объекты должны уничтожаться явно, с помощью специальных вызовов; объекты уничтожаются тог­да, когда они больше никому не нужны. Определить, нужны или не нужны объекты, можно по тому, используются ли эти объекты и возможно ли обращение к этим объектам. Если все пути обращения к объектам уничтожены (уничтожены все ссылки на данный объект), то объект становится недоступным, и он уничтожается. Такое унич­тожение иногда называется уничтожением по достижимости. Возможно ли существование объектов после завершения про­граммы? Да, возможно, если они создаются не в оперативной па­мяти, по определению сохраняющейся только на время выполнения программы, а на постоянном носителе информации, например, на диске. Средой жизни объектов в таком случае может являться, на­пример, объектно-ориентированная база данных. Создание постоянных объектов принципиально отличается от сохранения и восстановления состояния объектов. Во время работы программы состояние объекта можно сохра­нить на постоянном носителе информации, например, на диске. При следующем запуске программы она восстановит сохраненное состо­яние для соответствующего объекта. Одним из часто применяемых методов является так называемая сериализация. Значения всех атрибутов объекта записываются в виде строк символов, конкатенируются вместе, и результирующая строка записывается в файл на диске. При восстановлении объекта читается строка из файла, разбивается на подстроки, соответствующие индивидуальным атрибутам объекта, и затем атрибутам присваиваются значения, преоб­разованные из строк. Последовательность событий при этом про­цессе можно изобразить в виде временной диаграммы. При сохранении и восстановлении состояния объекта, факти­чески, при новом запуске программы, создается новый объект. Зна­чения его атрибутов устанавливаются таким образом, что они со­ответствуют значениям атрибутов объекта при предыдущем запус­ке программ. Однако если у объекта есть уникальный идентифи­катор, то он совершенно не обязательно должен совпадать с иденти­фикатором старого объекта (рис. 19.2). При создании постоянного объекта речь идет именно о сохра­нении объекта как такового. Иными словами, постоянный объект уничтожается при завершении программы и не создается заново при новом запуске программы. Программа и при первом, и при втором запуске обращается к одному и тому же объекту, храняще­муся в постоянной памяти (рис. 19.3). Рис. 19.2. Временная диаграмма сохранения и восстановления состояния объекта Рис. 19.3. Временная диаграмма создания объекта в базе данных Со временем жизни и идентификацией объектов тесно связано еще одно понятие – понятие объектов первого и второго сорта или равноправия объектов в системе. Как мы уже отмечали, объекты могут состоять из других объек­тов, могут ссылаться на другие объекты. Существует ли атрибут объекта как самостоятельный объект или же атрибут объекта – это объект второго сорта, существующий лишь как часть содержащего его объекта? Прежде чем ответить на этот вопрос, надо сформули­ровать, что такое самостоятельный объект. Мы считаем объект са­мостоятельным или первого сорта в том случае, если он обладает всеми признаками идентификации объектов, принятыми в данной объектной среде, и время его жизни не связано со временем жизни породившего объекта. Если объект «эллипс» описывает фигуру, изображенную на эк­ране монитора, то у него есть атрибут «цвет». Логично предполо­жить, что цвет – это тоже объект, у которого есть свои внутренние атрибуты и методы (например, кодировка в системе «красный, си­ний, зеленый», метод «изменить цвет» и т. д.). Существует ли объект «цвет эллипса» независимо от самого эллипса? Скорее всего нет. При удалении эллипса и его цвет исчезнет из системы. Можно ли обратиться к объекту «цвет эллипса»? Это зависит от реализации, но вполне возможны обе ситуации – у цвета может быть (а может и не быть) уникальный идентификатор объекта. В любом случае «цвет эллипса» является объектом второго сорта. Сортность связана именно с тем, как объект создавался. В той же графической системе может существовать параметр «стандартный цвет эллипса». Этот параметр, естественно, также представ­ляется объектом, который уже существует независимо от конк­ретного эллипса. При изображении фигуры на экране в зависи­мости от каких-либо условий система может использовать стан­дартный цвет, а может – и цвет, ассоциированный с этой фигу­рой. Объект «стандартный цвет эллипса» является объектом пер­вого сорта. На данном этапе приведенные рассуждения достаточно приблизитель­ны и дают лишь качественное понятие о различных характерис­тиках объектов. Эти понятия реализуются и конкретизируются в объектной модели конкретной среды разработки систем. В примерах мы уже затрагивали вопросы структуры объектов. Поскольку определение структуры объектов является одним из важнейших решений, принимаемых при проектировании системы, данный параграф вкратце суммирует возможные типы структуры объектов. Прежде всего, если передача сообщений между объектами – это динамика поведения системы, то структура объектов, связь между разными объектами – это статическая характеристика системы (хотя сами связи могут устанавливаться и динамически). Объект может состоять из других объектов, которые выступают в качестве его атрибутов. Автомобиль или банкомат состоит из узлов. В данном случае мы говорим об отношении включения одних объектов в другие. Эти составные части могут быть доступны или Недоступны извне включающего их объекта в зависимости от кон­кретных проектных решений. С другой стороны, в качестве атрибутов объект может исполь­зовать ссылки на другие объекты, как в примере цвета эллипса по умолчанию (рис. 19.4). Рис. 19.4. Включение объектов и ссылка на объект Главное отличие состоит в том, что, если объект может быть включен в текущий момент времени лишь в один объект, ссылаться на него может множество объектов. Но здесь встает другой вопрос, является ли включенный объект объектом первого или второго сорта. В модели сборочного производства все агрегаты являются объектами перво­го сорта (у всех двигателей есть уникальный номер, сборка и раз­борка двигателя производится, как отдельная операция), но физи­чески двигатель может быть поставлен только на один автомобиль. 9. Понятие класса В системе обычно функционирует множество объектов. Некоторые из них «похожи» или однотипны. Например, в банковской системе имеется много объектов-счетов, много объектов-клиентов. Однотипные объекты объединяются в классы. Понятие класса позволяет существенно упростить разработку и реализацию системы. Конечно, можно каждый объект разрабатывать отдельно. Первоначально мы напишем программу для объекта «клиент Иванов» и объекта «счет № 66579801». При добавлении нового клиента или счета придется разрабатывать программы для них. Поведение этих объектов совпадает с уже имеющимися клиентами и счетами, поэтому и имеет смысл ввести понятие, которое описывает общие черты объектов. Все объекты одного и того же класса обладают одинаковым интерфейсом и реализуют этот интерфейс одним и тем же способом. Два объекта одного класса могут отличаться только текущим со­стоянием, причем всегда теоретически возможно так изменить со­стояние одного объекта, чтобы он стал равным другому объекту. У всех объектов-счетов, принадлежащих классу «Счет», име­ется номер и баланс, все они реагируют на сообщение «проверить наличие денег и снять сумму со счета». Важно, что реагируют они на это сообщение одинаково, т. е. реализация метода у всех объектов одного класса одинакова. Индивидуальные объекты называются экземплярами класса, а класс – это шаблон, по которому строятся объекты. Термины «объект» и «экземпляр» эквивалентны. Таким образом, наша банковская система состоит из экземпляров трех классов: класса счетов, класса банкоматов и класса клиентов (рис.19.4). В дальнейшем мы будем названия классов писать с большой буквы, а объектов – с маленькой. Графически изображая классы и отношения между ними, мы будем использовать обозначения, принятые в Унифицированном языке моделирования UML. Счет Клиент Банкомат Баланс Имя Идентифицированный код Адрес Снять со счета Начать работу Выдать деньги Сообщить идентифицированный код Рис. 19.4. Классы, используемые в банковской системе На самом деле счета могут быть различны. Срочный вклад отличается от расчетного счета и от вклада до востребования. У них имеются разные характеристики, и они по-разному реализуют одни и те же операции. Поэтому для их описания необходимы разные классы (рис. 19.5). Счет Депозит Баланс Баланс Срок Процент Снять со счета Снять со счета Рис. 19.5. Разделение счетов на разные классы На данный момент при разделении счетов на различные типы мы повторили ряд атрибутов и методов в обоих классах. При этом правомерно ставится вопрос, чем отличается понятие класса от таких понятий, как «интер­фейс» и «тип». Интерфейс – это внешняя часть класса. Интерфейс определяет, как объекты данного класса могут взаимодействовать с другими объектами этого или других классов. Однако, если у двух объектов совпадают интерфейсы, это еще не означает, что они принадлежат к одному и тому же классу. Кроме совпадения интерфейсов необходимо совпадение реализации этих интерфейсов, совпадение поведения объектов. Никакими преобразованиями состояния экзем­пляра класса «Расчетный счет» невозможно привести его к экземпляру класса «Срочный вклад», поскольку у него отсутствуют атрибуты «процент» и «срок» и другой алгоритм выполнения метода «снять со счета». Тем не менее, интерфейсы этих двух классов оди­наковы. Понятия типа и класса часто употребляются в одном и том же смысле. Можно привести следующее отличие класса от типа: «Тип определяется тем, какие с ним можно производить манипуляции. Класс – это больше, чем просто набор операций. Можно заглянуть внутрь класса, например, для того, чтобы узнать его информационную структуру». Класс рассматривается как одна из нескольких возможных реализаций типа. Мы будем использовать термин «тип», прежде всего в контексте языков программирования. Тип – это область определения некой величины, т.е. множество ее возможных значений и набор применимых операций. Тип может задаваться классом. Класс «Расчетный счет» определяет множество возможных значений как множество допустимых значений атрибута «баланс». Набор допустимых операций состоит из одной операции «снять со счета». Тип может определяться и не классом. Например, во многих объектно-ориентированных языках программирования существуют простейшие типы данных, не являющиеся классами: целые числа, символы и т. д. 10. Наследование классов Важнейшим свойством классов и их принципиальным отличием от абстрактных типов данных является наследование. Наследование – это отношение между классами, при котором один класс разделяет структуру или поведение одного или нескольких других классов. Прежде всего, механизм наследования позволяет выделить общие части разных классов. Допустим были выделены разные типы счетов в банковской системе. Однако они имеют много общего. Выделив общую часть, создадим класс «Счет» (рис. 19.6). Счет Баланс Снять Положить Проверить баланс Рис.19.6. Базовый класс «Счет» Классы «Расчетный счет» и «Депозит» сохраняют все свойства (как методы, так и атрибуты) класса «Счет», дополняя и уточняя его поведение. Говорят, что класс «Депозит» наследует класс «Счет». Графически это изображается в виде иерархии (рис. 19.7): Счет Баланс Снять Положить Проверить баланс Расчетный счет Депозит Срок Процент Снять Положить Снять Положить Истек ли срок Рис. 19.7. Схема наследования классов-счетов У класса «Счет» есть атрибут «баланс» и три метода: «положить», «снять» и «проверить баланс». Класс «Расчетный счет» ис­пользует (наследует) атрибут своего базового класса и изменяет (уточняет) два метода: «снять» и «положить». Метод «проверить баланс» наследуется без изменений. Класс «Депозит» не только уточ­няет эти методы, но и добавляет новые атрибуты и методы. Таким образом, у класса «Расчетный счет» имеется атрибут «баланс» и три метода: «снять», «положить» и «проверить баланс». У класса «Депозит» имеются три атрибута: «баланс», «срок», «процент» и четыре метода: «снять», «положить», «проверить баланс», «истек ли срок». Экземпляр класса «Расчетный счет» по определению принадлежит к этому классу и реализует его интерфейс. В силу того что класс «Расчетный счет» выведен из класса «Счет», этот же экземпляр реализует интерфейс класса «Счет». Уточнение свойств классов может происходить в несколько ша­гов. Счета могут быть рублевые и валютные. Соответственно и те и другие могут быть расчетными или депозитами. Депозиты, в свою очередь, могут быть нескольких видов. Отображая подобное деление в структуре классов, получим иерархию, представленную на рис. 19.8. Очевидно, что можно построить иерархию классов по-разному. Фактически иерархия классов играет роль классификатора объектов. В данном случае при; построении системы классов разработчик пытается принять во внимание следующие соображения: «Столь ли существенна разница между рублевыми и валютными вкладами, что их следует разделить на разные классы?», «Разные виды депозитов – это разные характеристики одного и того же класса или же разные классы?» и т. п. Как видно из рисунка, было решено, что разница между руб­левыми и валютными счетами настолько существенна, что они представлены в виде отдельных классов. Разные виды депозитов также представлены в виде отдельных классов. Если бы мы реши­ли, что денежная единица, в которой выражается сумма на счете, лишь дополнительный атрибут счета и разные типы депозитов» различаются дополнительной характеристикой класса «Депозит», то иерархия классов преобразовалась бы к виду, изображен­ному на рис. 19.7. Легко заметить, что мы вернулись к структуре классов, пока­занной на рис. 19.5, с дополнительными атрибутами. При создании иерархий классов, подобных приведенным в качестве примеров, возникает вопрос, существуют ли объекты, относящиеся к классу «Счет». Ответ, скорее всего отрицательный. Любой реальный счет является либо расчетным счетом, либо депозитом, либо еще каким-то типом счета. Счета вообще не существует. Класс «Счет» введен для того, чтобы описать общие характеристики всех счетов, а не для того, чтобы создавать объекты этого класса. Классы, для которых не существует экземпляров, называются абстрактными. Конкретными классами в отличие от них называются классы, экземпляры которых могут существовать в системе. Рис. 19.8. Схема многоуровневого наследования классов Механизм абстрактных классов является чрезвычайно мощным понятием, которое широко используется. Назначением абстрактных классов является определение общих, наиболее характерных методов и атрибутов наследуемых из них классов. Чаще всего абстрактные классы используются для задания общего интерфейса иерархии конкретных классов, хотя и атрибуты, и реализация каких-либо методов могут присутствовать в абстрактных классах. Рис. 19.9. Упрощенная иерархия валютных и рублевых счетов Совершенно не обязательно, что все базовые классы – классы, из которых выводятся другие классы, – должны быть абстрактными. Можно представить класс «Газеты», экземпляры которого представляют все ежедневные газеты, и использовать его в качестве базового для класса «Газеты с приложением», куда будут отнесены ежедневные газеты с воскресным приложением. В данном случае наследуемый класс расширяет функциональность базового, однако экземпляры и базового класса и наследуемого класса имеют право на существование. Пример показывает, что одну и ту же информацию можно представить по-разному. Разумеется, упрощение иерархии классов не дается даром. Если при выделении рублевых счетов в отдельный класс методы «снять» и «положить» имели дело только с одной денежной единицей, то теперь они должны уметь работать со множеством денежных единиц. Аналогично методы класса «Депозит» должны учитывать тип депозитного счета. Таким образом, платой за упрощение иерархии классов является усложнение логики индивидуальных методов. И наоборот, чем более подробно классификация отражена в структуре наследования классов, тем проще каждый индивидуальный метод, хотя общее количество методов возрастает. Рассмотренные примеры показали, как класс может унаследовать методы и атрибуты одного базового класса. Такое наследование носит название одинарного, или простого наследования. Наряду с ним существует и множественное наследование, при котором у одного класса имеется несколько базовых. Множественное наследование позволяет объединять характеристики разных классов в одном. С другой стороны, при реализации этой банковской системы многие объекты, в том числе и счета, должны храниться в базе данных. Система классов, обеспечивающая хранение объектов в базе данных, может состоять из базового класса «Постоянный объект», у которого есть методы «сохранить» и «извлечь» для реализации операций записи и чтения из базы данных и атрибуты «имя таблицы» и «номер строки» для описания местоположения объекта. Для того чтобы стало возможным хранить в базе данных какой-либо конкретный счет, он должен быть выведен из класса «Постоянный объект» (рис. 19.10). Рис. 19.10. Множественное наследование Класс «Валютный депозит» наследует атрибуты и методы обоих своих родителей. От класса «Валютный счет» наследуются атрибут «баланс» и методы «снять», «положить» и «проверить баланс» (методы определены в классе «Счет»). От класса «Постоянный объект» наследуются атрибуты «имя таблицы» и «номер строки» и методы «сохранить» и «извлечь». Методы «снять», «положить», «сохранить» и «извлечь» класс «Валютный депозит» переопределяет в соответствии со своей спецификой. Кроме того, этот класс добавляет несколько атрибутов и методов: «срок», «процент» и «истек ли срок». 11. Полиморфизм Когда какой-либо объект, например банкомат, посылает сообщение объекту-счету о необходимости снятия денег, конкретный вид счета вообще не важен. Важно, что объект может правильно обработать сообщение «снять». Полиморфизмом называется возможность взаимодействия с объектом без определения к какому конкретному классу он относится. Посылая сообщение любому счету, мы используем полиморфизм всех счетов. В данном случае полиморфизм является ограниченным. Если сообщение «снять» будет послано, например, клиенту, он не смо­жет его обработать, т. е. нам не важно, какой конкретно счет обрабатывает сообщение, но тем не менее, это должен быть счет, объект одного из классов, выведенных из базового класса «Счет». В большинстве случаев используется именно ограниченный полиморфизм. Тем не менее, иногда любой объект системы может обработать некоторое сообщение. Например, в языке Java у всех объектов имеется метод toString, приводящий значение объекта к строковому виду. Соответственно любому объекту независимо от его класса можно послать сообщение toString. При использовании полиморфизма используется знание интерфейса объекта, однако поведение конкретного объекта в ответ на полученное сообщение может быть различно в зависимости от конкретного класса этого объекта. Соответственно посылающий объект точно не знает, что произойдет при вызове метода. Мы уже нео­днократно говорили, что разные типы счетов реализуют одни и те же операции по-разному. Поэтому, например, вполне возможно, что депозитный счет откажется выдавать деньги, несмотря на то что на балансе они имеются. Наличие механизмов наследования и полиморфизма в объектной модели позволяет эффективно решать многие задачи разработки систем. Например, наследование как средство специализации. Специализация, пожалуй, наиболее очевидное применение наследования. Сосредоточив в классах, призванных служить базовыми, общие свойства группы связанных, но тем не менее различных объектов, мы получаем возможность сравнительно быстро создавать конкретные классы, уточняя характеристики базового. Основной выигрыш при разработке системы происходит от того, что конкретные классы используют уже готовые решения для общих свойств, изменяя лишь то, что действительно требует специализации. Специализация может заключаться как в добавлении новых методов и атрибутов (например, газеты и газеты с приложением), так и в переопределении уже имеющихся методов (например, перераспределение классов-счетов). Наследование как способ задания интерфейса. Механизм абстрактных классов и полиморфизма позволяет решать многие задачи в терминах базовых классов, не заботясь о том, какие конкретно классы участвуют в той или иной операции. Представим себе несколько фигур, которые можно изображать на экране терминала в графическом редакторе. Из этого класса выведены конкретные: класс квадрата, эллипса, треугольника, трехмерного шара и т. д. Базовый класс задает те действия, которые можно производить с фигурами: сдвинуть, повернуть, масштабировать, закрасить и т. д. Большинство действий графического редактора можно запрограммировать в терминах действий над базовым классом, (например, при нажатии определенной клавиши сдвинуть фигуру к левому краю страницы). Полиморфизм обеспечит правильное выполнение конкретных действий, а добавление новых типов фигур путем выведения нового конкретного класса из класса «Фигура» не нарушит алгоритмов работы редактора. 12. Отношения между классами Включение. Класс может включать в себя другой класс, если он определяет атрибуты, являющиеся объектами другого класса. Такое отноше­ние соответствует отношению включения между объектами. Класс автомобилей определяет, что у автомо­биля есть атрибут – мотор, принадлежащий к классу «Моторы». В свою очередь класс «Моторы» задает один из атрибутов – карбюратор, являющийся экземпляром класса «Карбюраторы». В данном случае используется включение по значению, т. е. у каждого автомобиля есть свой мотор, а у каждого мотора есть свой карбюратор. Суть включения не изменяется, все равно у автомобиля имеется мотор. И в том и в другом случае класс – автомобиль включает атри­бут – мотор. С учетом объема информации, хранящейся в объекте класса, положение не изменяется – информация, хранящаяся в классе, включает информацию включаемого класса. Также не изменяется доступность информации – и в том и в другом случае доступность информации определяется интерфейсом включаемого класса. Ассоциация. Отношение ассоциации устанавливает двухстороннюю связь между объектами разных классов. Предположим, имеется класс, описывающий преподавателей в университете, и класс, описывающий студентов. Студент может выбрать руководителя для своего курсового проекта среди преподавателей. Между студентами и преподавателями устанавливается ассоциация «руководит – имеет руководителя курсового проекта», иными словами, преподаватель может руководить несколькими студентами, а у студента есть руководитель. Если преподаватель отказывается от руководства каким-либо студентом (например, из-за хронического невыполнения заданий), он разрывает ассоциацию. При этом происходит следую­щее. Во-первых, данный студент удаляется из списка студентов, которыми руководит преподаватель (изменяется атрибут объекта «преподаватель»), а во-вторых, студент лишается руководителя (изменяется атрибут объекта «студент»). Ситуация пояснена на рис. 19.11. Ассоциация – это двухсторонняя ссылка, при каждом изменении ассоциации изменяются атрибуты обоих объектов. Говорят о множественности ассоциации, т. е. о количестве объектов, участвующих в ней. В примере преподаватели и студенты множественность ассоциации 1-п (один преподаватель может руководить несколькими студентами, у студента может быть только один руководитель). Легко представить себе примеры ассоциаций с множественностью 1 -1, n – m и т. д. Наследование. Отношение наследования между классами мы рассмотрели выше. Иногда об отношении наследования говорят как об отношении «является». Объект класса-наследника «является» объектом наследуемого класса, поскольку он обладает всеми атрибутами свое­го родителя и всеми методами. Однако поведение объекта может быть изменено при наследовании путем использования полиморфизма. Рис. 19.11. Ассоциация «руководитель – руководит» преподавателя и студентов Наследование выражает соотношение «частное – общее» между классами. Базовый класс – это более общее понятие, которое уточняется в наследуемых классах. Базовый класс можно рассматривать как более высокий уровень абстракции по сравнению с выведен­ным из него классом. Кроме того, построение иерархий классов с помощью наследования является способом классификации понятий и объектов. При классификации различные явления и предметы объединяются в группы согласно каким-либо признакам. Хорошо спроектированная система классов отличается от плохо спроектированной тем же, чем отличается продуманная научная классификация от случайного объединения предметов в группы. Вспомним систему классификации животного мира Линея и сравним ее с группировкой животных, например по цвету, в которой, вероятно, тигр и бабочка-ма­хаон попадут в одну группу. Имеется более точное определение наследования как классифи­кации. Вспомним, что мы говорили о контрактах для методов класса. Для метода определяется предусловие (условие, при котором он может выполняться) и постусловие (условие, истинное при выполнении контракта). Для того чтобы считать наследование классификаци­ей (или, как иногда говорят, строгим наследованием), необходимо учитывать, что: • для каждого метода порожденного класса его предусловие не сильнее, чем предусловие соответствующего метода родительского класса; • для каждого метода порожденного класса его постусловие не слабее, чем постусловие соответствующего метода родительского класса; • инварианты базовых классов – это подмножества инварианта порожденного класса. Инвариантом класса мы называем логическое выражение, значение которого истинно для любого экземпляра класса. Использование. Отношение использования представляет собой наиболее изменчивое отношение между классами. Один класс использует другой, если при выполнении действий он опирается на свойства объектов другого класса. Простейший пример – использование объектов другого класса в качестве аргументов методов. Если в качестве аргумента метода перерисовки фигуры задается объект, описывающий экран монитора, класс «Фигура» использует класс «Монитор». Вызов методов другого класса также является отношением ис­пользования. Фактически при этом отношении один класс «знает» о существовании второго и «знает» по крайней мере его интерфейс. 13. Метаклассы и метаданные Сказав, что классы задают шаблон для создания объектов, мы не сказали, как же именно новые объекты создаются. Если мы рассмотрим класс как объект, у которого есть един­ственный метод – создать новый объект, получится завершенная картина. Объединив все объекты-классы в особый класс, получим понятие метакласса, т. е. класса, описывающего поведение всех остальных классов. В таком случае экземпляры метакласса можно рас­сматривать как метаобъекты, т. е. объекты, описывающие другие объекты. Понятие метакласса, как и метаобъекта, имеет практический смысл. По меньшей мере метаобъекты ответственны за создание новых объектов и за информацию о структуре классов и объектов. В языке Си++ понятие метаклассов и метаобъектов отсутствует, однако фактически конструкторы классов и статические элементы классов относятся к функциональности метаклассов. Метаклассы реализованы в таких языках программирования, как Смолток, CLOS и др. Имея метаклассы, можно динамически, т. е. в ходе вы­полнения программ, создавать новые классы, изменять существую­щие, добавляя или модифицируя элементы классов. Метаданные – это данные о данных. Обычно этот термин в язы­ках программирования применяют к данным, которые описывают структуру программных данных, в случае объектно-ориентированного языка программирования – структуру классов и объектов. Наличие метаданных позволяет динамически опрашивать объект, какие у него есть методы и атрибуты. Если объект во время работы системы может сообщить, к какому классу он принадлежит, эта информация исполь­зуется другими частями системы для определения того, какие опера­ции данный объект может выполнить. Механизм динамичес­кого вызова интерфейсов в объектной модели CORBA, используемой для взаимодействия распределенных объектов и систем основан на опросе классов во время выполнения программы. 14. Объектно-ориентированное программирование в Turbo Pascal Рассмотрим абстракцию применительно к программам: «программа есть алгоритмы и структуры данных». При разработке программ, представляющих собой, вообще говоря, модели некоторых сущностей – реальных или умозрительных, естественной является локализация информации (данных) об этих сущностях – объединение переменных, хранящих эту информацию, в некоторые структурированные совокупности (списки для описания результатов эксперимента, матрицы для описания линейных преобразований, структуры для объединения разнородной информации, etc.). Выгода от такого объединения очевидна – для многих элементарных и не совсем элементарных структур в ЯП высокого уровня существуют встроенные механизмы обработки. То есть за счет представления некоторых конкретных данных в некотором общем виде (абстрагируясь от их семантики) мы можем применять к ним общие методы обработки. Кроме того, очень часто до полной реализации всех модулей программы трудно определить состав и наиболее эффективное представление данных в памяти. К примеру, довольно сложно определить, следует ли использовать массив или связный список для хранения однотипных данных. И если список, то какой – одно– или двусвязный. В то же время вполне понятно, что сам по себе список понадобится. В такой ситуации было бы полезным абстрагироваться от представления данных, ограничившись определением действий (операций, операторов и процедур/функций) над данным представлением. Оба из приведенных примеров есть ничто иное, как применение абстракций через спецификацию и параметры, с которыми мы уже знакомы. В самом деле, при таком подходе существует договоренность о способе обработки какого-то элемента (группы элементов) той или иной процедурой или операцией при выполнении для некоторого (любого) объекта – элемента данных – определенных условий. Единственное, что остается – определиться с условиями, которым должны отвечать обрабатываемые объекты. Можно сказать, что общим свойством для всех объектов подобного рода будет принадлежность некоторому классу объектов. Классом мы здесь назовем совокупность всех объектов, обладающих некоторым общим свойством класса (или несколькими его свойствами). То есть класс является абстракцией всех свойств объектов, принадлежащих данному классу, за исключением свойств самого класса. При таком подходе к данным и алгоритмам их обработки процесс разработки программ становится как более гибким – у программиста появляется гораздо больше свободы для маневра на всех этапах разработки программы, так и более прозрачным – абстракция данных делает необходимым применение абстракций к алгоритмам и естественной – декомпозицию. Пример. Допустим, что приходится писать программу обработки результатов какого–либо эксперимента, представленные в виде матриц. Допустим также, что результаты – количественные, то есть могут быть представлены в виде чисел (0, 1, 2, .., N), и большинство из них равны 0. Алгоритмы обработки будем считать настолько сложными, что для их программирования a priori потребуется отдельный специалист, который знаком с программированием постольку поскольку. Размеры задач – большие, тестовые данные – есть, их размерность невелика. Для того, чтобы специалист мог программировать свои алгоритмы, методы программирования должны быть довольно простыми, желательно не выходящими за рамки матричного исчисления. С другой стороны, необходимая экономия памяти ЭВМ делает операции с теми же матрицами довольно сложными – необходимо либо оперировать блочными матрицами, либо списками, представляющими разреженные матрицы, причем на начальном этапе способ обработки матриц не очевиден. Помочь здесь может декомпозиция и, соответственно, абстрагирование. Рассмотрим некую абстракцию – класс «матрица». Над элементом этого класса определим операции «обращение к элементу», «умножение на число», «умножение матриц», «сложение», т. е произведем абстракцию алгоритмов через спецификацию. Наконец, определим, каким образом и для каких объектов будут вызываться данные операции – абстракция через параметры. Результатом всех этих абстракций является обоснование следующей декомпозиции: специалист разрабатывает алгоритмы обработки результатов эксперимента в терминах введенных операций и матриц; программист разрабатывает вспомогательные алгоритмы (умножение, деление и тому подобные) в терминах матриц и операций доступа к отдельным элементам; программист два занимается проблемами представления матриц. Выигрыш от этого следующий: программист два за два часа программирует реализацию простейшего объекта класса «матрица» – обычного двумерного массива – в соответствии со спецификацией. С этого момента программист раз становится независимым и может программировать элементарные вспомогательные алгоритмы (тоже довольно простая работа, выполнимая за короткое время). Почти одновременно с ним в работу включается специалист, который разрабатывает сложные алгоритмы в терминах матриц и одновременно тестирует их на тестовых данных. Это возможно, потому как тестовые данные могут быть представлены существующими объектами класса «матрица». Параллельно с этим программист два занимается реализацией экономного представления других объектов класса «матрица», и производит выбор подходящего варианта. Результат следующий: программа пишется и одновременно отлаживается примерно в два с половиной раза быстрее за счет параллельности разработки, еще в некоторое количество раз быстрее за счет того, что каждый исполнитель работает с небольшим по объему и сложности модулем. В частности, специалист не занимается проблемами представления, в которых он, возможно, и не сможет разобраться, но в то же время свободно работает с некими абстрактными объектами. Заметно, что экономится общее время разработки, а затраты на разработку в принципе не меняются. Что характерно, параллельно с указанными программистами могут трудиться, скажем, разработчики интерфейса или автоматического сбора данных. Из всего вышесказанного можно сделать вывод о том, что применение абстракций по отдельности к алгоритмам и данным менее эффективно, чем к их совокупности. Результатом такого одновременного абстрагирования является то, что программист начинает представлять программу целиком, не отделяя данные от алгоритмов, их обрабатывающих. Кроме того, абстрагирование от конкретных свойств (подробностей) объектов определенных классов и оперирование только свойствами класса приводит к тому, что человек при должной тренировке и образовании может охватить мысленным взором весь проект и/или любой его модуль, а реализация может производиться множеством программистов, причем частично параллельно. Таким образом, имеет смысл говорить о том, что объединение алгоритмов с данными является естественным развитием концепции модульного программирования, как, впрочем, и объединение такого рода конгломератов в группы (классы) по некоторым общим признакам; такого рода объединения позволяют программисту (группе программистов) создавать большие проекты. Естественно, что все предыдущие рассуждения не имели бы особого смысла, если бы оставались только рассуждениями. Однако это не так. Концепция объединения по общим признакам, а также объединения данных с алгоритмами, их обрабатывающими, была осознана, а затем строго формализована: были выявлены принципы, необходимые и достаточные для реализации такого подхода. Рассмотрим, что представляют объединения данных и алгоритмов. Любое такого рода объединение характеризуется состоянием – совокупностью текущих значений элементов данных (1). Состояния же меняются под воздействиями, не принадлежащими объединению (или не являются стабильными). Процесс смены состояний, как правило, сопровождается выдачей «наружу» других воздействий (на другие объединения). Для людей хотя бы поверхностно знакомых с теориями моделирования такого рода ситуации являются хорошо знакомыми. Но сейчас речь о другом. Если ввести стандарт на взаимодействия объектов друг с другом, то можно рассматривать их как элементы одного и того же класса и легко ими управлять. Такой подход называется программированием, управляемым сообщениями, и получил широкое распространение. Преимущества объектно-ориентированного программирования: – естественное для программирования рассмотрение алгоритмов в совокупности с данными и наоборот; – наличие более или менее стандартного подхода к абстрагированию и, следовательно, декомпозиции программ; – однотипность представления объектов, позволяющая создать эффективные системы управления ими. Итак, мы выяснили, что объединение данных с алгоритмами является естественным для образа мышления человека. Кроме того, объединение такого рода конгломератов по некоторым признакам в классы является не только следствием привычного образа мышления, но и удобным средством для декомпозиции и упрощения структуры программ. Мы выясним, какие принципы необходимы (и достаточны) для практического применения таких объединений в программировании. Довольно давно, практически сразу после появления языков третьего поколения (в 1967 году), возникла идея несколько преобразовать постулат фон Неймана о том, что данные и программы неразличимы в памяти машины. Программисты решили: пусть данные и программы если не станут одним и тем же, то сильно к этому приблизятся. Правда, делали они все это не от хорошей жизни и за деньги – разрабатывали сложную систему моделирования сложной системы (т. е. столкнулись с задачей, решение которой без декомпозиции оказалось невозможно). Попытки обосновать декомпозицию и привели к уже полученным нами выводам. Недостатком являлась чрезмерная размытость подхода, его объяснение скорее на уровне понимания и интуиции, чем на уровне правил. Усилия многих программистов и системных аналитиков, направленные на формализацию подхода, увенчались успехом. Были разработаны три основополагающих принципа того, что потом стало называться объектно-ориентированным программированием (ООП): наследование, инкапсуляция, полиморфизм. Результатом их первого применения стал язык Симула-1 (Simula-1), в котором был выведен новый тип – объект. В описании этого типа одновременно указывались данные (поля) и процедуры, их обрабатывающие – методы. Родственные объекты объединялись в классы, описания которых оформлялись в виде блоков программы. При этом класс можно было использовать в качестве префикса к другим классам, которые становились в этом случае подклассами первого. Впоследствии Симула-1 был обобщен и появился первый универсальный ООП ориентированный ЯП – Симула-67 (67 – по году создания). Пределом объектной ориентации принято считать Смолток (SmallTalk), в котором доступ к полям объектов возможен только через их методы. Как выяснилось, ООП оказалось пригодным не только для моделирования (Simula) и графических приложений (SmallTalk), но и для большинства других приложений, а его приближенность к человеческому мышлению и возможность многократного использования кода сделали его одной из наиболее бурно используемых концепций в программировании. Разберем три принципа, которые стали почти достаточными для реализации концепции ООП. Предварительно введем определения слов «объект» и «класс». Объект совокупность (разнотипных) данных (полей объекта), физически находящихся в памяти ЦВМ, и алгоритмов, имеющих доступ к ним. Каждый объект может обладать именем (идентификатором), используемым для доступа ко всей совокупности полей, его составляющих. В предельных случаях объект может не содержать полей или методов. Класс – тип (описание структуры данных и операций над ними), предназначенный для описания множества объектов. Каждый класс может иметь подклассы – классы, обладающие всеми или частью его свойств, а так же собственными свойствами. Класс, не имеющий ни одного представителя (объекта) обычно называют абстрактным. Инкапсуляция. Несмотря на непривычность слова – это просто связывание полей и методов в одну структуру (складывание их в одну «капсулу»). Это удобно, хотя и без остальных двух принципов никакого нового качества программирования не дает. Действительно, если объединить данные хотя бы с алгоритмами доступа к ним, то программист окажется независимым от представления данных в объекте: объект становится абстракцией представления своих собственных данных. В общем случае объекту можно приписать свойства (методы), абстрагирующие не только представление, но и придающие объекту другие свойства, к примеру, способность отображаться. Теоретически принцип инкапсуляции применим как к отдельным объектам, так и к классам. В случае классов с методами объединяются не сами данные, а структуры данных, и объединение с конкретными данными происходит в момент создания объектов данного класса. На практике же большинство ОО языков просто не позволяют создавать объекты, не создав предварительно класса. Наследование. Этот принцип относится только к классам объектов. Наследование означает, что каждый объект может иметь наследников, каждый из которых будет обладать всеми полями и методами своего предка. Кроме того, как правило, классы-наследники совместимы по типу со своими предками (к сожалению, это справедливо не для всех ОО языков). Наследование бывает двух видов: • одиночное, при которой каждый класс имеет одного и только одного предка; • множественное, при которой каждый класс может иметь любое количество предков. Множественное наследование обладает более мощными возможностями: в одном классе-наследнике объединяются свойства (поля и методы) множества различных классов. К примеру, один из предков может рисовать себя, а другой – производить вычисления. представитель их наследника сможет делать и то, и другое. Полиморфизм. Этот принцип неразрывно связан с наследованием и гласит, что каждый класс – наследник может обладать не только свойствами, унаследованными от предка, но и своими собственными. В частности, свойства предка могут быть перекрыты наследником – на место свойств предка могут быть подставлены свойства наследника. Существование принципа полиморфизма является естественным следствием существования принципа наследования: наследование без изменения набора свойств не имеет смысла. Кроме того, без полиморфизма невозможно реализовать объединение различных объектов (классов) по некоторому набору свойств (невозможно абстрагироваться от части свойств объектов), а без этого теряется весь смысл подхода. Позднее связывание. Несмотря на то, что три перечисленных принципа называют «тремя китами ООП», сами по себе они не имеют смысла без наличия особого механизма, названного поздним (динамическим) связыванием. Приведем пример. Допустим, что у нас создана программа, обрабатывающая объекты определенного класса (для определенности возьмем уже упоминавшийся класс «матрица»). Естественно, что программа разрабатывалась (и тестировалась) с использованием одного представителя данного (кстати говоря абстрактного) класса, а точнее – его класса-наследника «двумерный массив». Естественно, что все обращения к элементам данных производились через соответствующие методы, абстрагирующие представление. Однако при этом идейно правильном подходе мы не сможем написать программу, которая могла бы обрабатывать других представителей класса «матрица», если у нас нет ничего, кроме реализации трех принципов ООП. Действительно, в момент компиляции нашей программы, мы жестко определяем, что при вызове метода доступа к элементу объекта класса «матрица» вызывается метод объекта класса-наследника «двумерный массив». Даже если мы разместим по тем же адресам (или передадим в качестве параметров) объекты других классов-наследников («блочная матрица», например), все равно будет вызываться метод класса «двумерный массив». Что и приведет к краху программы. Следовательно, необходим механизм, который в процессе выполнения программы определял бы принадлежность объекта конкретному классу и производил бы вызов методов, относящихся к данному классу. Этот механизм и получил название механизма позднего связывания. Однако позднее связывание должно быть применено не ко всем методам, а только к специфическим для каждого класса-наследника. К примеру, вызов метода обращения должен быть одинаков для всех объектов класса «матрица» – алгоритм одинаков для всех матриц. То есть все методы класса по способу вызова делятся на две группы: • те, для которых механизм позднего связывания не применяется; они, насколько известно автору, не получили названия (за исключением «обычные»); • те, для которых этот механизм применяется; они получили название «виртуальные методы» (среди них еще выделяют «динамические виртуальные методы», но они по принципам вызова не отличаются). Концепция ООП является естественным развитием концепции модульного программирования, направленным на увеличение производительности программиста, приближение процесса программирования к процессу человеческого мышления и стандартизацию как программ, так и подхода к ним. Объектно-ориентированное программирование базируется на трех принципах и одном механизме: инкапсуляция, наследование, полиморфизм и позднее связывание. Объектно-ориентированный подход к программированию подразумевает выделение общих свойств у различных объектов и широчайшее применение абстракций всех уровней. Язык Pascal был создан задолго до того, как выяснилось, что ООП становится de facto стандарной концепцией разработки программного обеспечения. Соответственно появившиеся реализации ООП подхода на Pascal'е несут в себе отпечаток дообъектного прошлого этого языка. Лидер разработок компиляторов Паскаля в Borland Андрес Хейлсберг (Andres Heilsberg) решил ввести элементы ООП лишь в версию (5.5), а следующие версии сделать полностью ООП-ориентированными. К сожалению, последнее ему не удалось осуществить. 1. Синтаксис. Для того, чтобы объявить класс на Pascal'е, необходимо воспользоваться ключевым словом Object. Так как класс всегда является типом, делать это можно лишь в Type части программы: Type Class1 = Object {список полей} A: Byte; V: Real; {список методов} Procedure Nothing(Var K: Byte); End; Легко заметить, что поля и методы (общее для них название – члены класса) объявляются очень похоже на поля записи и обычные процедуры/функции. Объекты класса объявляются так же, как и обычные переменные: Var Object1, Object2: Class1; Соотвенно, доступ к полям объекта некоторого класса производится аналогично доступу к полям записи: Object1.V:= Object2.A; Обращение к методам класса производится аналогичным образом: Object1.Nothing(Object1.A); 2. Наследование. Pascal не поддерживает множественного наследования, то есть каждый класс может иметь не более одного наследника. Для того, чтобы объявить класс наследником какого-то другого класса достаточно указать имя класса-предка при объявлении наследника: Type Class2 = Object(Class1) M: Integer; End; Var Object3: Class2; При таком объявлении объект Object3 обладает тремя полями – A, V, M – и одним методом – Nothing(Var Byte). 3. Методы. Методы объектов обладают единственным отличием от обычных процедур/функций: они, собственно, принадлежат объектам. Следовательно, они обладают доступом к полям именно «своего» объекта. Так как объектов в программе может быть множество, то, во избежание дублирования кода, каждый метод получает в качестве неявного параметра указатель на объект, для которого он вызван. Данный указатель доступен в теле метода как @Self. Естественно, что объявленный (декларированный) при определении класса метод должен быть определен (дефинирован) в программе. Делается это примерно следующим образом: Procedure Class1.Nothing(Var K: Byte); Begin {body of method} V:= K; @Self.V:= K; End; Третья и четвертая строки абсолютно идентичны. В сущности, третья является лишь удобным сокращением четвертой. Известно, что все методы класса делятся на обычные (статические) и виртуальные. Любой метод считается статическим, если не указано обратное. Указать же, что метод является виртуальным, можно, указав после его декларации в дефиниции класса ключевое слово Virtual: Type Class3 = Object Procedure Nothing(Var K: Byte); Virtual; End; Это ключевое слово должно быть указано для всех методов, для которых необходимо применять механизм позднего связывания. Порядок чередования виртуальных и невиртуальных методов в дефиниции класса не регламентируется. 4. Конструкторы и деструкторы. Среди всех методов класса выделяют две особые группы, имеющие особое значение при создании и удалении объектов этого класса: • конструкторы; • деструкторы. Конструкторы предназначены для инициализации полей объектов в момент их создания. Объявляются они следующим образом: Type Class4 = Object B: Byte; Constructor Init (CB: Byte); Destructor Done; Virtual; End; Constructor Class3.Init(CB: Byte); Begin B:= VB; End; Destructor Class3.Done; Begin End; Заметим, что в вышеприведенном примере определен также и виртуальный деструктор. Назначение деструкторов обратно назначению конструкторов – выполнять некоторые действия при удалении объектов. Конструкторы, в отличие от деструкторов, не могут быть виртуальными. Несмотря на то, что как конструктор, так и деструктор могут быть вызваны непосредственно, их специфическое назначение привело к появлению возможности вызова их параллельно с созданием/удалением объектов. Так как создание/удаление объектов в процессе выполнения программы на Pascal'е возможны только при использовании ДРП, то функции создания/удаления типизированных переменных имеют дополнительный синтаксис: { . . . } Type PClass4 = ^Class4; Var P: PClass4; { . . . } Begin P:= New(PClass4, Init(4)); { . . . } Dispose(P, Done); End; Именно здесь мы впервые встретились с возможностью получить доступ к объектам различных классов с помощью одной и той же переменной. Действительно, в зависимости от хода алгоритма указателю P может быть присвоено значение адреса как объекта класса Class4, так и адреса объекта класса-наследника от Class4. 5. Перекрытие методов. Перекрыть метод предка в классе наследнике очень просто: продекларировать метод с тем же именем. Type Class5 = Object(Class1) Procedure Nothing; End; При этом, если перекрывается метод виртуальный, то перекрывающий метод тоже обязан быть виртуальным и, кроме того, иметь тот же список параметров. Если требование объявлять перекрывающий метод виртуальным введено, вероятно, для того, чтобы программист не забывал о его унаследованной виртуальности (компилятор вполне в состоянии самостоятельно определить, является ли перекрываемый метод виртуальным), то второе – требование одинаковости списка параметров – необходимо: при вызове виртуального метода для любого объекта любого класса-наследника компилятор может генерировать код передачи одинакового списка параметров – ведь реальный тип объекта заранее неизвестен. 15. Объектно-ориентированное программирование в Delphi Программа, решающая некоторую задачу, заключает в себе описание части мира, относящейся к этой задаче. Описание действительности в форме системы взаимодействующих объектов, естественнее, чем в форме иерархии подпрограмм, поэтому ООП экономит мышление программиста больше, чем модульное программирование. Среди предпосылок ООП можно назвать модульное программирование, абстрактные типы данных, ситуационное моделирование, фреймы. Первым алгоритмическим языком, где появились классы и объекты, был Simula-67. Окончательно принципы ООП оформились в языке Smalltaik-80. Рассмотрим домашний аквариум. Он населен рыбами, моллюсками, водорослями – все это объекты. Отдельные рыбки, улитки, водоросли – это экземпляры объектов. Рассмотрим объект «рыба». Она имеет размер, цвет, пол, местоположение и скорость. Кроме того, рыба способна выполнять определенные действия – плавать, обозревать пространство перед собой, поедать корм или других рыб. Обобщая, можно сказать, что объект имеет состояние (значения цвета, размера и т. д.) и методы (действия, на которые способен объект). Программируя объект, состояние можно хранить в наборе переменных, а методы реализовать в форме процедур. Объект представляет собой единство состояния и методов. В Турбо Паскале объект – это особый тип данных, а экземпляры объекта – переменные этого типа. Состояние характеризуется значениями полей объекта. Методами объекта являются ассоциированные с ним функции и процедуры, которым доступны поля. Передача сообщений объекту происходит в виде вызовов его методов с заданными параметрами. Чтобы познакомиться с описанием объекта в Турбо Паскале, предста­вим в форме объекта динамический список из целых чисел. Элементами списка являются записи типа PEIement = TElement; TElement = record R: real; Next: PEIement; end; Описание записи и указателя на запись не входит в описание объекта и предшествует ему. Состояние списка характеризуется значением указателя на его голову и числом элементов списка. Со списком ассоциируются процедуры создания пустого списка, пополнения списка числом, удаления головного элемента списка. type TRealList = object { поля } Head: PEIement; {указатель на головной элемент} Number: integer; {число элементов списка } { методы } procedure Nul; {создает пустой список } procedure Add(R: real); {добавляет к списку число R} function Del: real; {удаляет из списка число и возвращает его значение} end; Описание методов должно располагаться после описания объекта. Имена методов составные, они складываются из имени объекта и названия метода. Вот пример описания метода Nul объекта TRealList: procedure TRealLJst.Nul; begin Head := nil; Number := 0; end; После того как объект описан, его экземпляры можно использовать в программе. Рассмотрим программу, которая создает пустой список, вносит в него три числа, последнее число удаляет и показывает его значение на экране. var L: TRealList; begin L.Nul; L.Add(3.5); L.Add(4.6); L.Add(2.1); writeln(L.Del); end. Считается, что объектам присущ ряд свойств, которые собственно и делают их объектами. Одно из них – скрывать в себе детали, которые несущественны для использования объекта, называется инкапсуляцией. Чтобы увидеть, что такое инкапсуляция, спроектируем простое меню в одной строке экрана. Меню обеспечивает перебор пунктов, позволяет зафиксировать выбор или отказаться от выбора нажатием клавиши Escape. После выбора одного из пунктов в программу возвращается какое-то значение, связанное c выбранным пунктом, например, символ. При отказе от выбора в программу возвращается #27. Перед началом работы меню ему надо передать названия пунктов и возвращаемые символы. Это можно сделать в форме строки вида: 'Первое (а) Второе (b) Третье (с)', где за названием пункта следует в скобках возвращаемый символ. Состояние меню характеризуется координатами меню на экране, номером отмеченного пункта, общим количеством пунктов, а также перечнем названий пунктов и возвращаемых символов. Работа с объектом-меню состоит из инициализации (метод Init) и выбора пункта (метод Select). Меню должно уметь рисовать себя на экране (метод Draw). Вспомогательный характер носят: определение начала и длины называния пункта в строке Items (методы LeftBoard и Len) и выделение возвра­щаемого значения для выбранного пункта меню (метод WhatSel). type TMenu = object X.Y: integer; {координаты меню} Items: string; {строка названий и возвращаемых символов} MaxItem: integer; {общее количество пунктов} Selected: integer; {номер отмеченного пункта} procedure Init(TheItems: string); {заполняет поле Items, подсчитывает количество пунктов, делает выбранным первый пункт} procedure Select(var What: integer); {позволяет выбрать пункт меню и возвращает номер выбранного пункта. При отказе от выбора возвращает 0} procedure Draw; {рисует меню, выделяя выбранный пункт цветом} function LeftBoard(Item: integer): integer; {возвращает начало названия пункта Item в строке Items} function Len(Item: integer): integer; {возвращает длину названия пункта Item в строке Items} function WhatSel: char; {возвращает символ выбранного пункта} end; Приведем пример программы, использующей TMenu. var M: TMenu; С: char; begin M.lnit {'Первое (а) Второе (b) Третье (с)'); M.Select (С); end. Сопоставляя объекты с модулями, можно найти нечто общее. Описание объекта напоминает интерфейсный раздел модуля, описание методов – раздел реализации. И модуль, и объект обладают свойством инкапсуляции и объединяют в себе данные и процедуры. И модули, и объекты являются строительными блоками при создании программ. На этом их сходство заканчивается и начинается различие. Объект, в отличие от модуля, не является единицей трансляции и должен быть описан внутри программы или модуля. Остальные отличия можно уяснить, лишь продолжив изучение объектов. Наследование позволяет создавать новые объекты, изменяя или дополняя свойства прежних. Объект-наследник получает все поля и методы предка, но может добавить собственные поля, добавить собственные методы или перекрыть своими методами одноименные унаследованные методы. Вернемся к объекту TMenu. После оконча­ния работы с меню оно оставалось на экране, хотя уже никому не было нужно. Создадим новый объект TNeatMenu, наследующий TMenu, который в отличие от своего предка будет восстанавливать вид экрана. Для этого добавим новое поле Store, где будет храниться прежний экран во время дей­ствия меню, перекроем метод Init, и добавим метод Done. type TNeatMenu = object (TMenu) Store: array[1..4000] of byte; procedure Init (aX, aY: integer; TheItems: string); {сохраняет экран и вызывает Init предка} procedure Done; {восстанавливает состояние экрана} end; constructor TNeatMenu.lnit (aX, aY: integer; TheItems: string); begin move (mem [$b800:0], Store, 4000); TMenu.Init (aX, aY, Theltems); end; destructor TNeatMenu.Done; begin move (Store, mem [$b800:0], 160*25); end; Как видно из примера, наследник не содержит описания полей и методов предка. Вместо этого просто указывается имя предка в скобках после слова object. Из методов наследника можно вызывать методы предка, чем мы и воспользовались – не переписывать Init полностью. Объект TNeatMenu можно наследовать дальше, порождая разные виды меню. Отметим, что для создания наследника не требуется иметь исходный код предка, достаточно, чтобы объект-предок был в составе оттранслированного модуля. Предположим, нам надоел прежний вид меню, и мы хотим видеть его в форме столбца в правой части экрана. Для этого достаточно изменить метод Draw объекта TNeatMenu. Но если мы опишем потомка TNeatMenu (назовем его TVertMenu), который перекроет только метод Draw, то не до­стигнем цели. Дело в том, что методы Init и Select объекта TVertMenu унаследованы от TNeatMenu и будут вызывать не новый, а прежний Draw. Это естественно, т. к. во время трансляции объекта TMenu в их код была заложена ссылка на TNeatMenu.Draw. Отчасти выйти из положения можно, перекрыв не только Draw, но также Select и Init. При этом мы не отделаемся простым вызовом метода предка из метода потомка, как в TNeatMenu, а будем вынуждены полностью воспроизвести код методов. Это не только громоздко, но и просто невозможно, когда исходного кода предка нет в нашем распоряжении. Хороший выход из этой плохой ситуации открывают так называемые виртуальные методы. Если объявить методы Draw виртуальными, то связь между ними и вызывающими их процедурами будет устанавливаться не во время трансляции (раннее связывание), а во время выполнения программы (позднее связывание). Результат позднего связывания зависит от типа того объекта, чей метод обратился к виртуальному методу. Так, экземпляр объекта TMenu вызовет TMenu.Draw и нарисует себя в верхней строке, а экземпляр объекта TVertMenu вызовет TVertMenu.Draw и нарисует себя в правой части экрана. Чтобы воспользоваться виртуальными методами, надо соблюсти ряд формальностей. Во-первых, в описании объекта после заголовка виртуального метода надо добавлять слово virtual. Во-вторых, заголовки виртуальных методов предка и потомка должны в точности совпадать, причем оба метода должны быть виртуальными. Это означает, что, проектируя TMenu, мы должны были заранее предвидеть возможность его развития и сделать Draw виртуальным. В-третьих, инициализация экземпляра объекта должна выполняться методом особого вида, который называется конструктор. Обычно на конструктор возлагается работа по инициализации экземпляра объекта: присвоение полям исходных значений, открытие файлов, первоначальный вывод на экран и т. п. В TNeatMenu роль конструктора исполнял метод Init. Помимо действий, заложенных в него программистом, конструктор выполняет подготовку механизма позднего связывания виртуальных методов. Это значит, что до вызова любого виртуального метода должен быть выполнен какой-нибудь конструктор. Превратить обычный метод в конструктор очень просто, надо лишь заменить слово procedure словом constructor в заголовке метода. Упомянув о конструкторе, необходимо познакомиться и с деструктором. Его роль выполнять действия, завершающие работу с объектом: закрыть файлы, очистить динамическую память, восстановить экран и т. п. (т. е. противоположна роли конструктора). Заголовок метода-деструктора начинается со слова destructor. В объекте TMenu нет завершающих действий, а в объекте TNeatMenu есть, это восстановление экрана, которое выполняет метод Done. Если бы, проектируя объект TneatMenu, мы уже знали о виртуальных методах, то описали бы его так: type TNeatMenu = object (TMenu) Store: array [1..40001 of byte; constructor Init (aX, aY: integer; Theltems: string); destructor Done; end; a TVertMenu – так: type TVertMenu = object (TNeatMenu) procedure Draw; virtual; end; procedure TVertMenu.Draw; var Item: integer; begin for Item := 1 to MaxItem do begin gotoXY(X, Y + Item – 1); if Item = Selected then TextColor (Yellow) else TextColor (White); write (copy (Items, LeftBoard (Item), Len (Item))); end {for}; gotoXY (80,25); end; Теперь работа с экземпляром М объекта TNeatMenu, со­стоит из трех частей: M.lnit ('Первое (а) Второе (в) Третье (с)'); M.Select (с); M.Done; Окончательно значение деструктора будет выяснено при рассмотрении объектов в динамической памяти. Что же делает конструктор для подготовки позднего связывания? Он устанавливает связь между экземпляром объекта и таблицей виртуальных методов (VMT) объекта. Для каждого виртуального метода VMT содержит его адрес. Вызов виртуального метода делается не прямо, а через VMT: в начале по имени метода определяется его адрес, а затем по этому адресу передается управление. У каждого объектного типа собственная таблица. Именно это позволяет одному и тому же оператору Draw вызывать совершенно разные процедуры, ведь VMT объекта TNeat-Menu содержит адрес метода TNeatMenu.Draw, a VMT объекта TVertMenu содержит адрес метода TVertMenu.Draw. Понять механизм раннего и позднего связывания поможет рис. 19.12, который относится к рассмотренному примеру. Рис. 19.12. Механизмы раннего и позднего связывания Чтобы разместить объект в динамической памяти, надо описать указатель на него. Это похоже на описание динамических записей, например: type PMenu = TMenu; TMenu = object... Выделение памяти для динамического объекта выполняется процедурой NEW. Сразу после этого делается инициализация объекта, поэтому для объектов процедура NEW выглядит так: NEW (указатель на объект, конструктор). Высвобождение динамической памяти, занятой объектом, выполняется процедурой DISPOSE. Перед этим выполня­ются действия, завершающие работу с объектом, поэтому для объектов процедура DISPOSE выглядит так: DISPOSE (указатель на объект, деструктор). Нельзя освободить память, занятую динамическим объектом, если у него нет деструктора, хотя бы и пустого. Вот пример работы с динамическим объектом типа TNeatMenu. var М: PNeatMenu; n: integer; begin new(M, Init(' Первое Второе Третье Десерт')); M.Select (n); dispose (M, Done); end. Применим полученные знания для построения сложного иерархического меню. Нажатие клавиши будет разворачивать подсвеченный пункт в подменю или, если пункт находится на самом нижнем уровне, заканчивать работу. Нажа­тие клавиши Escape будет сворачивать подменю или заканчивать работу, если работа велась на самом верхнем уровне. Для начала необходимо решить, каким образом задавать структуру иерархического меню. Сделаем это при помощи скобочной формы, смысл которой ясен из следующего примера. Рис. 19.13. Схема иерархического меню. Скобочная форма: Первое (Борщ (а) Окрошка (b) Щи (с)) Второе (Каша (Гречневая (d) Манная (е)) Вареники (f)) Третье (Чай (g) Компот (h)) В скобочной форме за названием пункта меню следует описание соответствующего подменю, в такой же скобочной форме. Если пункт конечный, в скобках после него указан возвращаемый символ. Создадим составное меню в виде объекта TCompMenu, наследника TNeatMenu. Несмотря на кажущуюся сложность поставленной задачи, потребуется перекрыть только два метода Init и Select, и добавить лишь одно поле. Теперь основная задача метода Init – разобрать скобочную форму, данную ему в виде параметра, заполнить поле Items, и сохранить описания всех подменю в специально предназначенном для этого поле SubMenus. SubMenus – это массив строк, каждая из которых хранит описание одного подменю. Например, результатом разбора формы, приве­денной выше, будет Items =' Первое ( ) Второе ( ) Третье ( )', SubMenus [1] = 'Борщ (а) Окрошка (Ь) Щи (с)', SubMenus [2] = 'Каша (Гречневая (d) Манная (е)) Вареники(f)', SubMenus 13] = 'Чай (g) Компот (h)'. Метод TCompMenu.Select будет отличаться от Select предка только реакцией на нажатие клавиши. Она теперь зависит от того, является ли выбранный пункт конечным или нет. В первом случае, как раньше, надо закончить работу, вернув в программу номер пункта. Во втором случае надо рекурсивным образом заставить отработать подменю выбранного пункта и только потом закончить работу. Приведем описанный фрагмент метода Select: #13: if pos ('('.SubMenus [Selected] ) = 0 then What := WhatSel else begin X1 := X + LeftBoard (Selected); {функция NEW} SubMenu:=new(PCompMenu,Init(X1.Y+1,SubMenus[Selected])); SubMenu^.Select(WhatSub); dispose (SubMenu, Done); if WhatSub # 27 then What := WhatSub; end {else}; Пользуясь случаем, познакомимся с функциональной формой оператора NEW. Ее синтаксис задается следующей схемой: NEW (тип указателя, конструктор): указатель на объект. Для ООП характерен полиморфизм. Он выражается в том, что под одним именем скрываются различные действия, со­держание которых зависит от типа объекта. Впервые полиморфизм проявился, когда методы Select и Init, вызывая Draw, рисовали себя no-разному, в зависимости от типа владельца методов. Полиморфизм полезен, т. к. позволяет весьма экономно делать некоторые вещи. Например, мы хотим, чтобы меню самого нижнего уровня были вертикальными. Для этого достаточно изменить в методе TCompSelect всего один опе­ратор, вместо SubMenu := new(PCompMenu,Init(X1, Y+1, SubMenus [Selected])); надо написать if pos ('))', SubMenus [Selected] ) = 0 then SubMenu:= new (PVertMenu, Init(X1,Y+1,SubMenus[Selected])) else SubMenu:= new (PCompMenu, Init(X1,Y+1,SubMenus[Selected])) Т. е., если дочернее меню не имеет подменю, выполнить TVertMenu.Init, иначе придется выполнить TCompMenu.Init. Конструктор устанавливает связь с соответствующей VMT и благодаря этому операторы SubMenu^.Select (WhatSub); dispose (SubMenu, Done); будут вызывать виртуальные методы нужного объекта. Вы, вероятно, заметили, что полиморфизм является прямым следствием механизма позднего связывания. Содержание этого раздела носит частный характер и ка­сается особенностей представления объектов в среде ТП. Внутреннее представление объекта похоже на запись. Поля предка располагаются перед полями потомка. Если объект имеет виртуальные методы, конструктор или де­структор, транслятор добавляет 2-байтовое поле – смеще­ние VMT в сегменте данных. Это поле располагается после обычных полей и наследуется потомками. Tmenu X,Y:integer; Items: string; Maxltem: integer; Selected: integer; Смещение VMT TneatMenu X,Y: integer; Items: string; Maxltem: integer; Selected: integer; Смещение VMT Store: array [1..4000] of byte; Конструктор сначала устанавливает указатель на VMT, а потом выполняет свои операторы. Это позволяет вызывать виртуальные методы прямо из конструктора. Каждый объект, имеющий виртуальные методы, констракторы или дестракторы, имеет и VMT, которая хранится в сег­менте данных. 1-е слово VMT содержит истинный размер экземпляров. Это необходимо для правильного распределения и осво­бождения динамической памяти. 2-е слово содержит отрицательный размер экземпляра. Оно используется для блокирования вызовов методов не­инициализированного объекта. Перед вызовом виртуаль­ного метода проверяется, что первое слово VMT не равно нулю, а сумма первого и второго слова равна нулю. Далее в VMT следуют указатели на виртуальные методы объекта. Для работы с объектами в ТП имеются следующие стан­дартные функции. SizeOf (переменная или тип) – возвращает истинный размер объекта, который может быть больше от суммарного размера полей на 2 байта. ТуреОf (переменная или тип) : pointer – применяется только к объектам, имеющим VMT, и возвра­щает указатель на нее. Для проверки со­впадения типов можно использовать: TypeOf (х) = TypeOf (у). При инициализации объекта в динамической памяти воз­можны 2 вида ошибок констрактора: 1) он не смог распределить память для объекта. Если функция обработки ошибок кучи возвращает 0, воз­никает ошибка времени выполнения. Если – 1, процедура NEW возвращает nil; 2) он не смог распределить память для динамических пере­менных. В этом случае функция Fail, которая вызывается только из констрактора, высвобождает занятую ранее память. Благодаря инкапсуляции объекты так хорошо изолируются друг от друга, что бывает нелегко заставить их сообщать­ся между собой в программе. Конечно, это касается неза­висимых экземпляров объектов, а не тех, что являются по­лями или переменными методов других объектов. Обычно связь в этом случае устанавливается при помо­щи указателей, что делает объектную программу похожей на структуру данных в динамической памяти. Опыт ра­боты с массивами, списками, деревья­ми показывает, что чем они регулярнее, тем проще с ними обращаться. Это относится к объектным программам и подтверждается практикой. Непрямым способом обмена информацией между эк­земплярами объектов является передача через «карман» – внешнюю переменную, которой один объект присваивает значение, а другой считывает. Хотя этот способ прост, он лишает программу структурности, что не нравится многим программистам. Вопросы для самоконтроля: 1. Назовите основные концепции объектно-ориентированного программирования. 2. Каковы основные подходы структурного программирования? 3. Что такое объект? 4. Какими состояниями характеризуется объект? 5. Что такое идентификация объекта? 6. Что такое транзакция? 7. Что такое интерфейс объекта? 8. Что такое время жизни объекта? 9. Что такое класс? 10. Что такое экземпляр класса? 11. Каковы основные принципы ООП: инкапсуляция, наследование, полиморфизм? 12. Перечислите основные отношения между классами. 13. Что такое матаклассы и метаданные? 14. Что такое конструкторы и деструкторы? 15. Что такое стандартные директивы: private, public и virtual? Каково их назначение? 16. Приведите синтаксис описания класса на языке Turbo Pascal. Литература: 1. Абрамов С.А., Зима Е.В. Начала программирования на языке Паскаль. – М. : Наука. Гл. ред. физ.-мат. лит., 1987. – 112 с. 2. Боон К. ПАСКАЛЬ для всех / Пер. с гол. – М. : Энергоиздат, 1988. – 190 с. 3. Епанешников А., Епанешников В. Программирование в среде TurboPascal 7.0. – М. : "ДИАЛОГ-МИФИ", 1995. – 288 с. 4. Зуев Е.А. Язык программирования Turbo Pascal 6.0. – М. : Унитех, 1992. – 298 с. 5. Йенсен К., Вирт Н. Паскаль: руководство для пользователя / Пер. с англ. и предисл. Д.Б. Подшивалова. – М. : Финансы и статистика, 1989. – 255 с. 6. Поляков Д.Б., Круглов И.Ю. Программирование в среде Турбо Паскаль (версия 5.5): Справ.-метод. пособие. – М.: Изд-во МАИ, 1992. – 576 с. 7. Программное обеспечение микроЭВМ. В 11 кн. Кн. 7. Программиро­вание на языке ПАСКАЛЬ: Учеб. пособие для ПТУ / В.Ф. Шаньгин, Л.М. Под­дубная; Под ред. В.Ф. Шаньгина. – 2-е изд., перераб. и доп. – М. : Высш. шк., 1991. – 142 с. 8. Турбо Паскаль 7.0. – Киев : Торгово-издательское бюро ВНВ, 1996, – ­448 с. 9. Фаронов В.В. Турбо Паскаль (в 3-х книгах). Кн. 3. Практика программирования. – Часть 2. – М. : Учебно-инженерный центр "МВТУ-ФЕСТО ДИДАКТИК", 1993. – 304 с. 10. Сербулов Ю.С., Павлов И.О., Лемешкин А.В. Объектно-ориентированное программирование. Учеб. Пособ. для вузов. – Воронеж: Издательство «Научная книга», 2005. – 132 с. 11. Бондарев В. М., Рублинецкий В. И., Качко Е. Г. Основы программирования. – Харьков: Фолио, 1997. – 368 с. 12. Рубенкинг Н. Программирование в Delphi для «чайников». – К.: «Диалектика», 1996. – 304 с. 13. Калверт Ч. Программирование в Windows: Освой самостоятельно за 21 день / Пер. с англ. – М.: БИНОМ, 1995. – 496 с. 14. Культин Н. Б. Программирование в Turbo Pascal 7.0 и Delphi. – СПб.: BHV – Санкт-Петербург, 1998. – 240 с. 15. Сидоров М. Е., Трушин О. В. Школа работы на IBM PC. Часть 2: Программирование в среде Turbo Pascal. – Уфа, 1996. – 160 с. 16. Сурков Д. А., Сурков К. А., Вальвачев А. Н. Программирование в среде Borland Pascal для Windows. –М.: Высш. шк., 1996. – 432 с. 17. Фаронов В. В. Турбо-Паскаль 7.0. Начальный курс: Учебное пособие. – М.: Нолидж, 1997. – 616 с. 18. Федоров А. Рогаткин Дм. Borland Pascal в среде Windows. – Киев: «Диалектика», 1993. – 656 с.
«Основы программирования» 👇
Готовые курсовые работы и рефераты
Купить от 250 ₽
Решение задач от ИИ за 2 минуты
Решить задачу
Найди решение своей задачи среди 1 000 000 ответов
Найти
Найди решение своей задачи среди 1 000 000 ответов
Крупнейшая русскоязычная библиотека студенческих решенных задач

Тебе могут подойти лекции

Смотреть все 588 лекций
Все самое важное и интересное в Telegram

Все сервисы Справочника в твоем телефоне! Просто напиши Боту, что ты ищешь и он быстро найдет нужную статью, лекцию или пособие для тебя!

Перейти в Telegram Bot