Добавил:
ivanov666
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз:
Предмет:
Файл:Программирование основы языка С++. Учебное пособие
.pdf
Harmful). Это поистине исторический документ, оказавший заметное
влияние на дальнейшее развитие программирования.
Это интересно! Эдсгер Вибе Дейкстра (нидерл. Edsger
Wybe Dijkstra (11 мая 1930, Роттердам, Нидерланды –
6 августа 2002, Нюэнен, Нидерланды) – нидерландский
ученый, труды которого оказали влияние на развитие
информатики и информационных технологий; один из
разработчиков концепции структурного программирования, исследователь формальной верификации и распределенных вычислений. Тьюринговский лауреат
(1972). Он активно участвовал в разработке языка
программирования Алгол и написал первый компилятор
Алгол-60. Будучи одним из авторов концепции структурного программирования,
он проповедовал отказ от использования инструкции GOTO. Также ему принадлежит идея применения «семафоров» для синхронизации процессов в многозадачных
системах и алгоритм нахождения кратчайшего пути на ориентированном графе
с неотрицательными весами ребер, известный как алгоритм Дейкстры. В 2002 г.
получил ежегодную премию, вручаемую Симпозиумом по принципам распределенных
вычислений (англ. Symposium on Principles of Distributed Computing) Ассоциации вычислительной техники «за публикацию, оказавшую наибольшее влияние на область
распределенных вычислений»; в знак признания заслуг ученого с 2003 года эта премия носит название премии Дейкстры. Автор нескольких книг и множества статей, самые известные публикации – книги «Дисциплина программирования»,
«Заметки по структурному программированию», статья «О вреде оператора
GOTO». Судьба этого документа очень интересна. Дело в том, что Дейкстра дал
статье совсем другое название: «Доводы против оператора GO TO» (A Case against
the GO TO Statement). Однако в момент публикации произошло нечто непонятное
– статья почему-то загадочным образом превратилась в «Письмо к редактору»,
причем прежнее название столь же загадочно исчезло. Что произошло на самом
деле? Дейкстра объяснил таинственное превращение статьи в письмо лишь много
лет спустя, в 2001 г., за год до смерти. Журнал Communications of the ACM опубликовал мой текст под названием «Оператор GOTO считается вредным». В последующие годы его часто цитировали. К сожалению, зачастую это делали люди,
которые видели в нем не больше, чем сказано в заголовке. Этот заголовок стал
краеугольным камнем моей славы…Как все это случилось? Я отправил статью под
названием «Доводы против оператора GO TO». Чтобы ускорить публикацию, редактор превратил мою статью в «Письмо к редактору». При этом он придумал
для статьи новое название, которое изобрел сам. Редактором был Никлаус Вирт.
11

Цель структурного программирования – повысить производительность труда программистов, в том числе при разработке больших и
сложных программных комплексов, сократить число ошибок, упростить
отладку, модификацию и сопровождение программного обеспечения.
Такая цель была поставлена в связи с ростом сложности программ и
неспособностью разработчиков и руководителей крупных программных
проектов справиться с проблемами, возникшими в 1960–1970 гг. в связи
с развитием программных средств. Структурное программирование
призвано, в частности, устранить беспорядок и ошибки в программах, вызванные трудностями чтения кода, несистематизированным,
неудобным для восприятия и анализа исходным текстом программы.
Такой текст нередко характеризуют как «спагетти-код».
Спагетти-код – плохо спроектированная, слабо структурированная,
запутанная и трудная для понимания программа, содержащая много
операторов goto (особенно переходов назад), исключений и других
конструкций, ухудшающих структурированность. Самый распространенный антипаттерн программирования. Спагетти-код назван так потому, что ход выполнения программы похож на миску спагетти, то есть
извилистый и запутанный. Иногда называется «кенгуру-код» (kangaroo
code) из-за множества инструкций jump. В настоящее время термин
применяется не только к случаям злоупотребления goto, но и к любому
«многосвязному» коду, в котором один и тот же небольшой фрагмент
исполняется в большом количестве различных ситуаций и выполняет
много различных логических функций. Спагетти-код может быть отлажен и работать правильно и с высокой производительностью, но он
крайне сложен в сопровождении и развитии. Доработка спагетти-кода
для добавления новой функциональности иногда несет значительный
потенциал внесения новых ошибок. По этой причине становится практически неизбежным рефакторинг – главное лекарство от спагетти.
Оператор goto – начиная с 1970-х гг. оператор безусловного перехода
goto оказался в центре систематической и всевозрастающей критики.
Неправильное и необдуманное использование оператора goto в исходном тексте программы приводит к получению запутанного, неудобочитаемого «спагетти-кода». По тексту такого кода практически
невозможно понять порядок исполнения и взаимозависимость фрагментов. Дейкстра заметил, что качество программного кода обратно
пропорционально количеству операторов goto в нем. Статья приобрела
12

широкую известность, в результате чего взгляды на использование
оператора goto были существенно пересмотрены. Код с goto трудно
форматировать, так как он может нарушать иерархичность выполнения (парадигму структурного программирования) и потому отступы,
призванные отображать структуру программы, не всегда могут быть
выставлены правильно. Кроме того, оператор goto мешает оптимизации
компиляторами управляющих структур.
Некоторые способы применения goto могут создавать проблемы с
логикой исполнения программы:
если некоторая переменная инициализируется (получает значе-
•
ние) в одном месте и потом используется далее, то переход в точку
после инициализации, но до использования, приведет к тому, что
будет выбрано значение, которое находилось в памяти, выделенной
под переменную, до момента выделения (и которое, как правило,
является произвольным и случайным);
передача управления внутрь тела цикла приводит к пропуску кода
•
инициализации цикла или первоначальной проверки условия;
аналогично, передача управления внутрь процедуры или функции
•
приводит к пропуску ее начальной части, в которой производится
инициализация (выделение памяти под локальные переменные).
Доводы против оператора goto оказались столь серьезными, что в
структурном программировании его стали рассматривать как крайне
нежелательный. Это нашло отражение при проектировании новых языков программирования. Например, goto запрещен в Java и Ruby. В ряде
современных языков он все же оставлен из соображений эффективности в тех редких случаях, когда применение goto оправданно. Так, goto
сохранился в Аде – одном из наиболее продуманных с точки зрения
архитектуры языков за всю историю. Однако в языках высокого уровня,
где этот оператор сохранился, на его использование, как правило, накладываются жесткие ограничения, препятствующие использованию
наиболее опасных методов его применения: например, запрещается
передавать управление извне цикла, процедуры или функции внутрь.
Стандарт языка C++ запрещает обход инициализации переменной с
помощью goto.
Принципы структурного программирования
Принцип 1. Следует отказаться от использования оператора безусловного перехода goto.
13

Принцип 2. Любая программа строится из трех базовых управляющих
конструкций: последовательность, ветвление, цикл:
последовательность – однократное выполнение операций в том
•
порядке, в котором они записаны в тексте программы.
ветвление – однократное выполнение одной из двух или более
•
операций, в зависимости от выполнения заданного условия.
цикл – многократное исполнение одной и той же операции до
•
тех пор, пока выполняется заданное условие (условие продолжения
цикла).
Принцип 3. В программе базовые управляющие конструкции могут
быть вложены друг в друга произвольным образом. Никаких других
средств управления последовательностью выполнения операций не
предусматривается.
Принцип 4. Повторяющиеся фрагменты программы можно оформить
в виде подпрограмм (процедур и функций). Таким же образом (в виде
подпрограмм) можно оформить логически целостные фрагменты программы, даже если они не повторяются. В этом случае в тексте основной
программы, вместо помещенного в подпрограмму фрагмента, вставляется инструкция «Вызов подпрограммы». При выполнении такой инструкции работает вызванная подпрограмма. После этого продолжается
исполнение основной программы, начиная с инструкции, следующей
за командой «Вызов подпрограммы».
Принцип 5. Каждую логически законченную группу инструкций следует оформить как блок. Блоки являются основой структурного программирования. Блок – это логически сгруппированная часть исходного
кода, например, набор инструкций, записанных подряд в исходном коде
программы. Понятие блок означает, что к блоку инструкций следует
обращаться как к единой инструкции. Блоки служат для ограничения
области видимости переменных и функций. Блоки могут быть пустыми
или вложенными один в другой. Границы блока строго определены.
Принцип 6. Все перечисленные конструкции должны иметь один вход
и один выход. Произвольные управляющие конструкции (такие, как
в блюде спагетти) могут иметь произвольное число входов и выходов.
Ограничив себя управляющими конструкциями с одним входом и одним
выходом, мы получаем возможность построения произвольных алгоритмов любой сложности с помощью простых и надежных механизмов.
Принцип 7. Разработка программы ведется пошагово, методом
«сверху вниз» (top-down method) – то есть, сначала пишется текст ос-
14

новной программы, в котором, вместо каждого связного логического
фрагмента текста, вставляется вызов подпрограммы, которая будет выполнять этот фрагмент. Вместо настоящих, работающих подпрограмм, в
программу вставляются фиктивные части – заглушки, которые, говоря
упрощенно, ничего не делают. Заглушка удовлетворяет требованиям
интерфейса заменяемого фрагмента (модуля), но не выполняет его
функций или выполняет их частично. Затем заглушки заменяются или
дорабатываются до настоящих полнофункциональных фрагментов (модулей) в соответствии с планом программирования. На каждой стадии
процесса реализации уже созданная программа должна правильно работать по отношению к более низкому уровню. Полученная программа
проверяется и отлаживается. После того, как программист убедится,
что подпрограммы вызываются в правильной последовательности (то
есть общая структура программы верна), подпрограммы-заглушки последовательно заменяются на реально работающие, причем разработка
каждой подпрограммы ведется тем же методом, что и основной программы. Разработка заканчивается тогда, когда не останется ни одной
заглушки. Такая последовательность гарантирует, что на каждом этапе
разработки программист одновременно имеет дело с обозримым и понятным ему множеством фрагментов, и может быть уверен, что общая
структура всех более высоких уровней программы верна.
Достоинства структурного программирования. Следование принципам структурного программирования сделало тексты программ, даже
довольно крупных, нормально читаемыми. Серьезно облегчилось понимание программ, появилась возможность разработки программ в нормальном промышленном режиме, когда программу может без особых
затруднений понять не только ее автор, но и другие программисты. Это
позволило разрабатывать достаточно крупные для того времени программные комплексы силами коллективов разработчиков, и сопровождать эти комплексы в течение многих лет, даже в условиях неизбежных
изменений в составе персонала.
1. Структурное программирование позволяет значительно сократить
число вариантов построения программы по одной и той же спецификации, что значительно снижает сложность программы и, что еще важнее,
облегчает понимание ее другими разработчиками.
2. В структурированных программах логически связанные операторы находятся визуально ближе, а слабо связанные – дальше, что
15

позволяет обходиться без блок-схем и других графических форм изображения алгоритмов (по сути, сама программа является собственной
блок-схемой).
3. Упрощается процесс тестирования и отладки структурированных
программ.
4. Ясность и удобочитаемость программ – улучшение удобочитаемости структурных программ объясняется тем, что отсутствие оператора
goto позволяет читать программу сверху донизу без разрывов, вызванных передачами управления. В итоге можно сразу (одним взглядом)
обнаружить условия, необходимые для модификации того или иного
фрагмента программы.
1.2.3. Объектно-ориентированное программирование. Объектноориентированное программирование (ООП) – методология программирования, основанная на представлении программы в виде совокупности
объектов, каждый из которых является экземпляром определенного
класса, а классы образуют иерархию наследования. Идеологически
ООП – подход к программированию как к моделированию информационных объектов, решающий на новом уровне основную задачу структурного программирования: структурирование информации с точки
зрения управляемости, что существенно улучшает управляемость самим
процессом моделирования, что, в свою очередь, особенно важно при
реализации крупных проектов.
Управляемость для иерархических систем предполагает минимизацию избыточности данных (аналогичную нормализации) и их целостность, поэтому созданное удобно управляемым – будет и удобно пониматься. Таким образом, через тактическую задачу управляемости
решается стратегическая задача – транслировать понимание задачи
программистом в наиболее удобную для дальнейшего использования
форму. Основные принципы структурирования в случае ООП связаны
с различными аспектами базового понимания предметной задачи, которое требуется для оптимального управления соответствующей моделью:
абстрагирование для выделения в моделируемом предмете важного
•
для решения конкретной задачи по предмету, в конечном счете –
контекстное понимание предмета, формализуемое в виде класса;
инкапсуляция для быстрой и безопасной организации собственно
•
иерархической управляемости: чтобы было достаточно простой
команды «что делать», без одновременного уточнения как именно
делать, так как это уже другой уровень управления;
16

наследование для быстрой и безопасной организации родственных
•
понятий: чтобы было достаточно на каждом иерархическом шаге
учитывать только изменения, не дублируя все остальное, учтенное
на предыдущих шагах;
полиморфизм для определения точки, в которой единое управление
•
лучше распараллелить или наоборот – собрать воедино.
То есть фактически речь идет о прогрессирующей организации информации согласно первичным семантическим критериям: «важное/
неважное», «ключевое/подробности», «родительское/дочернее», «единое/множественное». Прогрессирование, в частности, на последнем
этапе дает возможность перехода на следующий уровень детализации,
что замыкает общий процесс.
Обычный человеческий язык в целом отражает идеологию ООП,
начиная с инкапсуляции представления о предмете в виде его имени и
заканчивая полиморфизмом использования слова в переносном смысле,
что в итоге развивает выражение представления через имя предмета до
полноценного понятия-класса.
Основные понятия объектно-ориентированного программирования
Абстракция данных – означает выделение значимой информации и
исключение из рассмотрения незначимой. В ООП рассматривают лишь
абстракцию данных (нередко называя ее просто «абстракцией»), подразумевая набор наиболее значимых характеристик объекта, доступных
остальной программе.
Инкапсуляция – свойство системы, позволяющее объединить данные
и методы, работающие с ними, в классе. Одни языки (например, С++,
Java или Ruby) отождествляют инкапсуляцию с сокрытием, но другие
(Smalltalk, Eiffel, OCaml) различают эти понятия.
Наследование – свойство системы, позволяющее описать новый
класс на основе уже существующего с частично или полностью заимствующейся функциональностью. Класс, от которого производится
наследование, называется базовым, родительским или суперклассом.
Новый класс – потомком, наследником, дочерним или производным
классом.
Полиморфизм подтипов (в ООП называемый просто «полиморфизмом») – свойство системы, позволяющее использовать объекты с одинаковым интерфейсом без информации о типе и внутренней структуре
объекта. Другой вид полиморфизма – параметрический – в ООП называют обобщенным программированием.
17

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

динамическое связывание требуется не более чем для 20 % вызовов, но
некоторые ООП-языки используют его постоянно
2. Значительная глубина абстракции – ООП-разработка часто приводит к созданию «многослойных» приложений, где выполнение объектом требуемого действия сводится к множеству обращений к объектам
более низкого уровня. В таком приложении происходит очень много
вызовов методов и возвратов из методов, что, естественно, сказывается
на производительности.
3. Наследование «размывает» код – код, относящийся к «конечным»
классам иерархии наследования, которые обычно и используются программой непосредственно, находится не только в самих этих классах, но
и в их классах-предках. Относящиеся к одному классу методы фактически описываются в разных классах. Это приводит к двум неприятным
моментам:
снижается скорость трансляции, так как компоновщику приходится
•
подгружать описания всех классов иерархии;
снижается производительность программы в системе со страничной
•
памятью – поскольку методы одного класса физически находятся
в разных местах кода, далеко друг от друга, при работе фрагментов
программы, активно обращающихся к унаследованным методам,
система вынуждена производить частые переключения страниц.
4. Инкапсуляция снижает скорость доступа к данным – запрет на
прямой доступ к полям класса извне приводит к необходимости создания и использования методов доступа. И написание, и компиляция, и
исполнение методов доступа сопряжены с дополнительными расходами.
5. Динамическое создание и уничтожение объектов – динамически создаваемые объекты, как правило, размещаются в куче, что менее
эффективно, чем размещение их на стеке и, тем более, статическое
выделение памяти под них на этапе компиляции.
1.2.4 Функциональное программирование. Функциональное программирование – раздел дискретной математики и парадигма программирования, в которой процесс вычисления трактуется как вычисление
значений функций в математическом понимании последних (в отличие от функций как подпрограмм в процедурном программировании).
Противопоставляется парадигме императивного программирования,
которая описывает процесс вычислений как последовательное изменение состояний (в значении, подобном таковому в теории автоматов).
19

При необходимости, в функциональном программировании вся совокупность последовательных состояний вычислительного процесса
представляется явным образом, например, как список. Функциональное
программирование предполагает обходиться вычислением результатов
функций от исходных данных и результатов других функций, и не предполагает явного хранения состояния программы. Соответственно, не
предполагает оно и изменяемость этого состояния (в отличие от императивного, где одной из базовых концепций является переменная,
хранящая свое значение и позволяющая менять его по мере выполнения
алгоритма).
На практике отличие математической функции от понятия «функции» в императивном программировании заключается в том, что императивные функции могут опираться не только на аргументы, но и
на состояние внешних по отношению к функции переменных, а также иметь побочные эффекты и менять состояние внешних переменных. Таким образом, в императивном программировании при вызове
одной и той же функции с одинаковыми параметрами, но на разных
этапах выполнения алгоритма, можно получить разные данные на выходе из-за влияния на функцию состояния переменных. А в функциональном языке при вызове функции с одними и теми же аргументами
мы всегда получим одинаковый результат: выходные данные зависят
только от входных. Это позволяет средам выполнения программ на
функциональных языках кешировать результаты функций и вызывать
их в порядке, не определяемом алгоритмом и распараллеливать их без
каких-либо дополнительных действий со стороны программиста (что
обеспечивают функции без побочных эффектов – чистые функции).
Лямбда-исчисление являются основой для функционального программирования, многие функциональные языки можно рассматривать как
«надстройку» над ними.
Первым функциональным языком был Лисп, созданный Джоном
Маккарти в период его работы в Массачусетском технологическом институте в конце пятидесятых и реализованный, первоначально, для IBM
700/7000. В Лиспе впервые введено множество понятий функционального языка, хотя при этом в языке применяется не только парадигма
функционального программирования. Дальнейшим развитием Лиспа
стали такие языки как Scheme и Dylan.
20
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]
