Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Комплект Информатика / Курс лекций.doc
Скачиваний:
128
Добавлен:
22.05.2015
Размер:
4.8 Mб
Скачать

Контрольные вопросы

1. С какой целью производится оценка эффективности алгоритмов?

2. Какие характеристики используются при оценке эффективности алгоритмов?

3. Что выражает временная эффективность алгоритма? Поясните на примере.

4. Что выражает асимптотическая зависимость количества базовых операций от размера входных данных? Поясните на примере.

5. Что выражает амортизированная эффективность алгоритма? Поясните на примере.

6. Как провести оценку правильности алгоритма?

Лекция № 19 Концепции традиционного программирования

Цель лекции

Изучить основные концепции традиционного программирования.

План лекции

1. Эволюция и классификация языков программирования.

2. Концепции традиционного программирования.

1 Эволюция и классификация языков программирования

Первоначально процесс программирования предусматривал запись программистом всех алгоритмов непосредственно на машинном языке. Такой подход усугублял и без того трудную задачу разработки алгоритмов и слишком часто приводил к ошибкам, которые необходимо было обнаружить и исправить (процесс, известный как отладка) до того, как работу можно было считать законченной.

Первым шагом на пути к облегчению задачи программирования был отказ от использования цифр для записи команд и операндов непосредственно в той форме, в которой они используются в машине. С этой Цель лекциию при разработке программ стали широко применять мнемоническую запись различных команд вместо их шестнадцатеричного представления. Например, вместо цифрового кода команды загрузки регистра программист мог теперь написать LD (от Load), а вместо кода команды копирования содержимого регистра в память мог использовать мнемоническое обозначение ST (от Story). Для записи операндов были разработаны правила, в соответствии с которыми программист мог присваивать некоторым областям памяти описательные имена (идентификаторы) и использовать их при записи команд программы вместо адресов соответствующих ячеек памяти. Одним из специфических вариантов является присвоение мнемонических имен регистрам центрального процессора, например R0, R1, R2,...

Используя идентификаторы для ячеек памяти и мнемонические обозначения для команд, программисты смогли значительно повысить читабельность написанных ими последовательностей машинных команд. Давайте вернемся, например, к программе на машинном языке, приведенной в конце раздела 2.2. Эта программа суммировала содержимое ячеек с адресами '6С’ и '6D', после чего помещала результат в ячейку с адресом '6Е'. Напомним, что в шестнадцатеричном виде соответствующая последовательность команд имеет следующий вид:

156С 166D 5056 306Е С000

Если мы присвоим имя PRICE ячейке с адресом '6С’, имя TAX — ячейке адресом '6D' и имя TOTAL — ячейке с адресом '6Е', то сможем переписать ту же самую программу с использованием мнемонических записей команд так, как показано ниже:

LD R5,PRICE LD R6,TAX ADDI R0,R5,R6 ST R0,TOTAL HLT

Большинство согласится, что второй способ записи текста программы намного лучше отражает ее смысл, чем первый (оставаясь, впрочем, также не вполне понятным). Заметим, что мнемоническая запись ADDI здесь использована для команды сложения двух целых чисел, в то время как команду сложения двух чисел с плавающей точкой можно мнемонически обозначить как ADDF.

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

Кроссплатформенное программное обеспечение. При решении многих задач типичная прикладная программа вынуждена полагаться на операционную систему. Например, она может обратиться к диспетчеру окна для организации взаимодействия с пользователем или диспетчеру файлов для считывания данных с устройств массовой памяти. К сожалению, различные операционные системы выполняют такие запросы по-разному. Поэтому если программа предназначена для рассылки и выполнения в сети, объединяющей машины разного типа, которые имеют различные операционные системы, то она должна быть независимой как от операционных систем, так и от типа используемых машин. Чтобы отметить этот уровень независимости, используется термин кроссплатформенное программное обеспечение. Иными словами, кроссплатформенное программное обеспечение — это программы, которые не зависят ни от операционной системы, ни от аппаратного обеспечения, а значит, могут выполняться на разных компьютерах, объединенных в сеть.

В свое время разработка языков ассемблера считалась гигантским шагом вперед в поисках более совершенных технологий программирования. Многие считали, что они представляют собой совершенно новое поколение языков программирования. Со временем языки ассемблера стали называть языками программирования второго поколения, а к первому поколению были отнесены сами машинные языки.

Хотя языки второго поколения имели много преимуществ по сравнению с машинными языками, они все же не могли обеспечить завершенную среду программирования. Помимо всего прочего, применяемые в языке ассемблера языковые конструкции, по существу, совпадают с конструкциями соответствующих машинных языков. Разница заключается лишь в синтаксическом способе их выражения. По этой причине программы, написанные на языке ассемблера, являются принципиально машинно-зависимыми, т.е. команды в этих программах выражаются в терминах определенных машинных атрибутов. Программу на языке ассемблера достаточно сложно выполнить на другой машине, поскольку для этого ее нужно переписать с учетом новой конфигурации регистров и набора команд.

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

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

Следуя такому подходу, специалисты по компьютерам стали разрабатывать языки программирования, которые больше подходили для целей разработки программного обеспечения, чем низкоуровневые языки ассемблера. В результате появились языки программирования третьего поколения, которые отличались от предыдущих поколений тем, что их языковые конструкции имели более высокий уровень и были машинно-независимыми. Наиболее известными примерами ранних языков третьего поколения являются FORTRAN (FORmula TRANslator — переводчик формул), который был предназначен для научных и инженерных расчетов, и COBOL (COmmon Business-Oriented Language — язык общего назначения деловой ориентации), разработанный специалистами военный морского флота США для решения экономических задач.

В общем случае язык программирования третьего поколения представляет собой определенный набор языковых конструкций достаточно высокого уровня, предназначенный для разработки программного обеспечения. По существу, точно так же был разработан и наш псевдокод, описанный в главе 4. Каждая из языковых конструкций была разработана так, чтобы ее можно было реализовать в виде последовательности низкоуровневых примитивов, существующих в машинных языках. Рассмотрим следующий оператор:

Total ← Price + Tax

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

идентификаторвыражение

После того как необходимый набор примитивов высокого уровня будет определен, пишется программа, называемая транслятором (translator — переводчик). Она предназначена для перевода программ, записанных с использованием примитивов языка высокого уровня, на машинный язык. Подобный транслятор похож на программу-ассемблер второго поколения, за исключением того, что ему часто приходится объединять (или компилировать, от англ. compile) несколько машинных инструкций в короткие последовательности команд, предназначенные для имитации выполнения отдельных примитивов высокого уровня. Именно поэтому подобные программы-переводчики часто называют компиляторами. Разработку первого компилятора приписывают Грейс Хоппер (Grace Hopper), которая играла ведущую роль в продвижении концепции языков программирования высокого уровня. Действительно, идея писать программы в форме, близкой к естественному языку, была настолько революционной, что многие руководители поначалу отвергали ее.

Популярной альтернативой трансляторам являются интерпретаторы (interpreters), предложенные как еще один способ выполнения программ, написанных на языках программирования третьего поколения. Эти программы подобны трансляторам, однако они выполняют команды программы непосредственно после их перевода, а не записывают, подобно трансляторам, переведенный код в виде выполняемого модуля, предназначенного для последующего использования. Это означает, что вместо создания копии программы на машинном языке, которую необходимо будет выполнить позже, интерпретатор немедленно выполняет все переведенные им инструкции.

С появлением языков программирования третьего поколения Цель лекции обеспечения машинной независимости программ была в основном достигнута. Поскольку операторы в языках третьего поколения не привязаны к особенностям какой-то конкретной машины, они легко могут быть скомпилированы на любом компьютере. Теоретически программа, написанная на языке третьего поколения, может быть выполнена на любой машине за счет использования соответствующего компилятора.

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

Проблема переноса программ с одной машины на другую заключается в отсутствии общей точки зрения на то, что именно считать стандартом данного языка программирования. В связи с этим Американский национальный институт стандартов (ANSI) и Международная организация по стандартизации (ISO) приняли и опубликовали стандарты для многих популярных языков программирования. В других случаях применяются неформальные стандарты, которые являются следствием популярности того или иного диалекта языка, а также желания многих разработчиков компиляторов создавать продукты, совместимые с другими, подобными им.

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

Total ← Price + Tax,

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

Классификация языков программирования по поколениям требует распределения их по линейной шкале (рис. 1) в соответствии с той степенью свободы от компьютерной тарабарщины, которую данный язык предоставляет программисту. Это позволяет ему мыслить понятиями, связанными непосредственно с решаемой задачей. В действительности развитие языков программирования происходило несколько иначе. Оно протекало по разным направлениям, связанным с альтернативными подходами к процессу программирования (называемыми парадигмами программирования). Таким образом, историческую схему развития языков программирования наиболее точно можно изобразить составной диаграммой, показанной на рис. 2, на которой отдельные линии, символизирующие различные парадигмы программирования, появляются и развиваются независимо друг от друга. В частности, на рисунке показаны четыре независимых направления, соответствующие функциональной, объектно-ориентированной, императивной и декларативной парадигмам программирования, а также представлены различные относящиеся к ним языки. Месторасположение названия языка на линии соответствует времени его появления относительно других языков. Однако это вовсе не означает, что каждый последующий язык обязательно является наследником предыдущего.

Рисунок 1 - Схематическое представление поколений языков программирования

Рисунок 2 - Эволюция парадигм языков программирования

Императивная (imperative paradigm), или процедурная парадигма (procedural paradigm), представляет традиционный подход к процессу программирования. Действительно, именно в соответствии с этой парадигмой построен цикл обработки команды центрального процессора: "извлечь-декодировать-выполнить". Как следует из названия, императивная парадигма определяет процесс программирования как запись последовательности команд, которая при выполнении выполнит обработку данных, необходимую для получения желаемого результата. Таким образом, для решения задачи императивная парадигма предлагает попытаться найти алгоритм ее решения.

В противоположность этому, декларативная парадигма (declarative paradigm) во главу угла ставит вопрос "Что представляет собой задача?", а не "Какой алгоритм нужен для решения задачи?" Основная проблема здесь состоит в том, чтобы создать и реализовать общий алгоритм решения задач. После этого задачи можно формулировать в виде, совместимом с этим алгоритмом, а затем применять его. В этом случае роль программиста заключается в точной формулировке задачи, а не в поисках и реализации алгоритма ее решения.

Основной трудностью в разработке декларативных языков программирования является выбор базового алгоритма решения задач. По этой причине ранние декларативные языки были узкоспециализированными по своей природе и ориентированными на специфические приложения. Например, декларативный подход уже многие годы применяется для моделирования систем (экономических, физических, политических и т.п.) в целях проверки выдвинутых гипотез. В этом случае базовый алгоритм, в сущности, является процессом моделирования течения времени посредством многократно повторяющегося вычисления значений параметров (роста внутреннего продукта, торгового дефицита и т.д.) исходя из вычисленных ранее значений. Использование декларативного языка для выполнения такого моделирования сводится, прежде всего, к реализации алгоритма, выполняющего указанную повторяющуюся процедуру. В результате программисту остается лишь описать взаимоотношения моделируемых параметров. Далее базовый алгоритм моделирования просто имитирует течение времени, используя указанные соотношения для выполнения требуемых вычислений.

Не так давно декларативная парадигма нашла свое новое применение — благодаря осознанию того факта, что применение методов математической формальной логики позволяет создавать простые алгоритмы решения задач, подходящие для использования в системах декларативного программирования общего назначения. Результатом пристального внимания ученых к декларативной парадигме явилось появление дисциплины логического программирования.

Функциональная парадигма (functional paradigm) рассматривает процесс разработки программ как конструирование ее из неких "черных ящиков", каждый из которых получает некоторые исходные данные (на входе) и вырабатывает соответствующий результат (на выходе). Математики называют такие "ящики" функциями, поэтому этот подход называется функциональной парадигмой. Языковые конструкции функциональных языков программирования состоят из элементарных функций, на основе которых программист должен создавать более сложные функции, необходимые для решения поставленной задачи. Таким образом, согласно функциональной парадигме, программист решает задачу, рассматривая исходные данные, требуемые результаты и преобразование, которое необходимо выполнить, чтобы получить результаты из исходных данных. Решение требуемой задачи, вероятнее всего, можно получить, разбивая исходное преобразование на более простые преобразования, порождающие промежуточные результаты, служащие, в свою очередь, исходными данными для других простых преобразований. Короче говоря, в соответствии с функциональной парадигмой процесс программирования заключается в конструировании требуемых функций в виде вложенных друг в друга совокупностей более простых функций.

Например, на рис. 3 показано, как можно построить функцию вычисления среднеарифметического нескольких чисел из трех более простых функций. Первая из них — Sum — получает на вход список чисел и вычисляет их сумму; вторая — Count — получает список чисел и подсчитывает их количество; третья — Divide — получает на вход два числа и вычисляет их частное. На языке LISP (популярном функциональном языке программирования) эта конструкция может быть записана в виде следующего выражения:

(Divide (Sum Numbers) (Count Numbers))

Использование в этом выражении вложенных структур отражает, что исходные данные для функции Divide являются результатами выполнения функций Sum и Count. В качестве другого примера предположим, что у нас есть функция Sort, которая сортирует список чисел, и функция First, которая находит первое число в этом списке. В этом случае приведенное ниже выражение позволяет извлечь из списка List наименьшее из чисел:

(First (Sort List))

В данном случае использование вложенных структур означает, что результат функции Sort является исходной информацией для функции First. Таким образом, список сначала сортируется, а затем из отсортированного списка извлекается первое число.

Рисунок 3 - Функция вычисления среднеарифметического для списка чисел, построенная из более простых функций Sum, Count и Divide

Превосходство функциональной парадигмы программирования над императивной проявляется в том, что она стимулирует модульный подход к конструированию программ. Действительно, то, что программы рассматриваются как функции, которые, в свою очередь, должны состоять из других функций, вынуждает программиста думать в терминах модулей. По этой причине сторонники функционального программирования утверждают, что этот подход приводит к созданию более высокоорганизованных программ, чем в случае применения императивной парадигмы. Более того, многие утверждают, что функциональная парадигма является естественной средой для метода, предусматривающего построение программ из "строительных блоков". Данный подход напоминает скорее конструирование программ из заранее подготовленных блоков, нежели выполнение всей работы с нуля. Такому способу программирования отдают предпочтение в основном специалисты по разработке больших пакетов программ. Эти же аргументы приводятся и в защиту объектно-ориентированной парадигмы.

Объектно-ориентированная парадигма (object-oriented paradigm), которая предполагает применение методов объектно-ориентированного программирования (ООП), — это еще один подход к процессу разработки программного обеспечения. В рамках этого подхода элемент данных рассматривается как активный "объект", а не как пассивный элемент, как это принято в традиционной императивной парадигме. Поясним это на примере списка имен. В традиционной императивной парадигме этот список рассматривается просто как совокупность некоторых данных. Любая программа, получающая на вход этот список, должна содержать алгоритм выполнения над ним требуемых действий. Таким образом, список является пассивным объектом, поскольку он обрабатывается управляющей программой, а не обрабатывает себя сам. Однако при объектно-ориентированном подходе список рассматривается как объект, содержащий некоторую совокупность данных вместе с набором процедур для их обработки. Этот набор может включать процедуры для вставки в список нового элемента, удаления элемента из списка или сортировки списка. Поэтому программа, получающая доступ к списку для его обработки, не обязана содержать алгоритм для выполнения указанных действий. При необходимости она просто выполняет процедуры, предоставляемые самим объектом. В этом смысле объектно-ориентированная программа вместо сортировки списка (как при императивной парадигме) скорее просит список отсортировать самого себя.

Язык Visual Basic. Visual Basic — это объектно-ориентированный язык программирования, разработанный компанией Microsoft в качестве инструмента, с помощью которого пользователи операционной системы Microsoft Windows могли бы создавать собственные графические интерфейсы пользователя (GUI). В действительности Visual Basic - это нечто больше, чем просто язык программирования. Это— мощный интегрированный пакет разработки программного обеспечения, позволяющий программисту создавать графический интерфейс пользователя из заранее определенных компонентов (таких как кнопки, флажки опций, текстовые поля, полосы прокрутки и т.п.) и настраивать работу этих компонентов в приложении, описывая их реакцию на различные события. Например, если речь идет о кнопке, программист должен описать, что должно случиться, если пользователь щелкнет на ней. В главе 6 мы увидим, что эта стратегия создания программ из готовых компонентов представляет собой важнейшую современную тенденцию в области разработки программного обеспечения.

Популярность операционной системы Windows и удобство пакета для разработки программ Visual Basic способствовали тому, что язык Visual Basic в настоящее время стал одним из наиболее известных и широко используемых языков программирования.

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

Многие из преимуществ объектно-ориентированного проектирования являются следствием модульной структуры, которая возникает как естественный побочный эффект от применения объектно-ориентированного подхода. В рамках этого подхода каждый объект реализуется в виде отдельного, точно определенного элемента. После того как свойства некоторой сущности будут определены подобным образом, полученное определение можно повторно использовать всякий раз, когда возникнет потребность в этой сущности. По этой причине сторонники объектно-ориентированного программирования утверждают, что объектно-ориентированная парадигма предоставляет естественную среду для конструирования программного обеспечения из "строительных блоков". Они предсказывают появление программных библиотек, содержащих определения различных объектов, с помощью которых новое программное обеспечение можно будет собирать точно так же, как обычные промышленные изделия собирают из готовых компонентов.

Объектно-ориентированная парадигма оказывает все большее влияние на область компьютерных наук, поэтому в разделе 5.5 мы детально обсудим ее особенности. Кроме того, в последующих главах мы вновь и вновь будем встречаться с проявлениями этой парадигмы. В частности, будет показано, какое влияние оказала объектно-ориентированная парадигма на методы разработки программного обеспечения (глава 6) и проектирования баз данных (глава 9), а в главе 7 мы увидим, как объектно-ориентированный подход к разработке программного обеспечения естественным образом обобщает результаты исследований в области структур данных.

Наконец, следует заметить, что процедуры объекта, описывающие, как объект должен отвечать на различные сообщения, в сущности, представляют собой небольшие императивные программные единицы. Поэтому большинство объектно-ориентированных языков программирования обладают свойствами императивных языков. Например, распространенный объектно-ориентированный язык C++ был создан добавлением к императивному языку Си объектно-ориентированных свойств.