Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Программирование основы языка С++. Учебное пособие

.pdf
Скачиваний:
0
Добавлен:
07.09.2026
Размер:
2 Мб
Скачать
☆
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
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]