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

[ Миронченко ] Императивное и объектно-ориентированное програмирование на Turbo Pascal и Delphi

.pdf
Скачиваний:
71
Добавлен:
25.04.2014
Размер:
3.16 Mб
Скачать

371

Sie schufen starke Wunder | noch seitdem in Etzels Land.

Песнь о Нибелунгах, средневерхненемецкий

In alten mæren wnders vil geseit

von heleden lobebæren

von grozer arebeit

von frevde vn– hochgeciten

von weinen vn– klagen

von kvner recken striten

mvget ir nv wnder horen sagen

Ez whs <inBvregonden>

 

ein vil edel magedin

daz in allen landen

niht schoners mohte sin

Chriemhilt geheizen

div wart ein schone wip

dar vmbe mvsin degene

vil verliesen den lip

Ir pflagen dri kunige

edel un– rich

Gunther un– Gernot

die rechen lobelich

vn– Giselher der iunge

ein wetlicher degen

div frowe was ir swester

die helde hetens inir pflegen

Ein richiv chuniginne

frov Vte ir mvter hiez

ir vater der hiez Dancrât

der in div erbe liez

sit nach sime lebene

ein ellens richer man

der ovch insiner iugende

 

grozer eren vil gewan

Die herren waren milte

von arde hoh erborn

mit kraft vn– mazen chvne

die rechen vz erchorn

da zen Bvrgonden so was ir lant genant

si frvmten starchiv wnder

 

sit in Etzelen lant

Работа с текстовым файлом

Тексты поэмы приводятся в html-формате, поэтому если вы его не знаете, то вам придется разобраться с тем, как писать html-файлы. Не пугайтесь, ничего сложного в этом нет.

Сначала вам придется преобразовать текст из html-формата в какой-то более подходящий. Но учтите, что в древнеанглийском тексте есть буквы, которые не входят во множество символов ASCII, поэтому либо вы должны каждой букве древнеанглийского текста поставить в соответствие некоторый не встречающийся в тексте символ, либо попытаться перевести html-файл в текстовый файл на основе кодировки Unicode (двухбайтовые целые).

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

372

Глава 19: Проблемы ООП. Везенспрограммирование

19.1. История языков программирования

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

Год

 

 

Событие

 

Характеристика языка

1941

Конрад Цузе в Германии построил

 

 

 

машину

Z3

на

основе

 

 

 

электромеханических реле

 

 

 

1943

В

Англии

построена

первая

 

 

 

электронная ЭВМ – Colossus.

 

 

1944

Джон

Эккерт

выдвинул

концепцию

 

 

 

программы, хранимой в памяти

 

 

1945

В Америке построена ЭВМ ENIAC

 

 

1949

Первый язык ассемблера

 

Ассемблер

 

1951

Грейс Хоппер разработала первый

 

 

 

транслятор для программы,

 

 

 

записанной в удобной

 

 

 

 

 

алгебраической форме

 

 

 

 

1955

Fortran

 

 

 

Процедурный

 

1958

ALGOL 58

 

 

 

Процедурный

 

1959

LISP

 

 

 

 

Функциональный

 

1961

GPSS

 

 

 

 

Логический (декларативный)

1967

Simula 67

 

 

 

ОО-расширение

императивных

 

 

 

 

 

 

языков

 

1970

Prolog

 

 

 

 

Логический (декларативный)

1971

Pascal

 

 

 

 

Процедурный

 

1972

C

 

 

 

 

Процедурный

 

1972

Smalltalk 72

 

 

 

ОО-расширение императивных языков

1981

1-й персональный компьютер (IBM PC)

 

 

1986

C++

 

 

 

 

ОО-расширение императивных языков

1986

Object Pascal

 

 

 

ОО-расширение императивных языков

1988

CLOS

 

 

 

 

ОО-расширение LISP

 

1996

Java

 

 

 

 

ОО-расширение императивных языков

2002

C#

 

 

 

 

ОО-расширение императивных языков

Как видите, все концепции программирования, которые используются на сегодняшний день, появились до 1967 года. Через 22 года после появления машины ENIAC, которая работала в миллионы раз медленнее, чем современные ПК, которая постоянно ломалась и занимала целые комнаты, уже были сформулированы все концепции программирования, используемые на сегодняшний день, и написаны

373

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

19.2. Обзор некоторых объектно-ориентированных языков

Чтобы оценить современное состояние ООП, мы рассмотрим вкратце основные ОО-языки.

Simula

Первый объектно-ориентированный язык – Simula 67 - появился еще в 1967 году. Он был создан Кристеном Нигардом (Kristen Nygaard) и Оле-Йоханом Далом (OleJohan Dahl) из Норвежского Вычислительного Центра (Norsk Regnesentral) и Университета Осло. В 1986 году Simula 67 был переименован в Simula.

Simula 67 – наследник языка Simula 1, созданного в начале 60-х годов для моделирования дискретных событий. В отличие от своего предшественника, Simula 67

– это многоцелевой ОО-язык. Оба языка являются расширениями языка ALGOL 60 – одного из самых популярных языков в то время.

Класс в Simula состоит из полей, методов и тела класса.

Тело класса в чем-то заменяет конструктор класса. При создании объекта выполняются операторы тела класса.

В Simula реализованы все основные понятия ООП – наследование, полиморфизм, абстрактные классы.

Кроме того, в Simula есть понятие сопрограмм (мы его рассматривали в главе рекурсия). Сопрограммы реализованы на основе классов. С их помощью разработчики Simula добились аналога параллельности работы (хотя сопрограммы – это не параллелизм, т.к. они предусматривают жесткую зависимость сопрограмм друг от друга). Позднее, когда появились мультипотоковые ОС, поддержку параллельности перенесли в них.

Все ОО-языки – потомки Simula 67, поэтому значение языка поистине огромно. Simula – не мертвый язык – он по сей день используется для разработки ПО (в

основном в Скандинавии).

Smalltalk

Вторым ОО-языком стал Smalltalk. Базовые концепции ООП Smalltalk позаимствовал из Simula, но в то же время в этом языке вводится много новых понятий:

В Smalltalk все переменные – объекты (даже числа).

Есть базовый класс - object.

Сами классы – объекты метаклассов.

У классов есть собственные методы.

Поля классов могут быть только закрытыми.

ВSmalltalk нет статической проверки типов, поэтому все необходимые проверки применимости операций проводятся во время работы программы. Именно это

374

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

Delphi (Object Pascal)

Известно много различных объектно-ориентированных расширений для языка Pascal. Мы дадим оценку лишь для Delphi, который мы рассмотрели достаточно подробно.

Delphi строился как расширение языка Turbo Pascal и первоначально назывался Object Pascal. Поэтому Delphi позволяет писать программный код как в процедурном, так и в ОО-стиле.

Delphi многое перенял от Smalltalk. То, что все классы происходят от TObject разумно и в практическом смысле, т.к. позволяет писать методы, которые будут общими для объектов любых классов и в концептуальном, т.к. в любом случае каждый объект содержит по крайней мере ссылку на себя и ссылку на дескриптор класса, поэтому сделать класс, объекты которого содержат эти поля и еще несколько базовых методов, представляется довольно разумным.

Использование ссылок позволяет обойтись без указателей, которые используются

впроцедурном программировании.

ВDelphi есть все базовые возможности ООП:

инкапсуляция с делением полей на private, public, protected.

Одиночное наследование

Полиморфизм, реализованный с помощью динамического связывания

ВDelphi очень естественно реализована возможность получения информации о типе объекта во время выполнения (RTTI – Run Time Type Information), - в базовом классе TObject можно получать ссылки на дескриптор класса.

Метаклассы и операторы is, as позволяют с легкостью орудовать на уровне классов, а не объектов, поэтому легко достигается общность программ.

Интерфейсы позволяют сделать более структурированной иерархию классов без использования множественного наследования (см. раздел о С++).

Свойства реализованы довольно громоздко, но в принципе идея неплохая. Недостатки:

1. Возможность доступа к private-членам класса в других классах, реализованных в том же модуле. Это – пережиток модульного прошлого Delphi. Чтобы избавиться от этого недостатка, в Delphi 2005 ввели определители видимости элементов strict

private, strict protected.

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

3.Большой набор базовых функций в классе TObject: многие из них являются подметодами для других методов, и сами по себе нужны крайне редко.

4.Думаю, что разработчикам Delphi не стоило стремиться к возможно более полной совместимости с ТР. Из-за этого Delphi перенял оператор goto и тип object, который устарел т.к. были введены классы. Можно вспомнить также такое понятие, как «типизированная константа», которая на самом деле – переменная, хотя и

375

объявляется в разделе const. Зачем было называть переменную, инициализированную значением типизированной константой, непонятно. Все эти недостатки справедливо вызывают нарекания у многих программистов.

5. Нет переменных класса Сложнее вопрос, нужны ли указатели и встроенный ассемблер (мы его не

рассматривали). С одной стороны, т.к. в Delphi иерархия классов идет от TObject и работа с объектами ведется только при помощи ссылок, то указатели, по большому счету, для написания ОО-приложений не нужны. С другой стороны, указатели и встроенный ассемблер могут очень помочь, если требуется достичь большей эффективности приложений - например, для написания компиляторов.

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

Эти слова относились к Delphi 7. В Delphi 2005, стремясь подстроиться под платформу .NET, был введен целый ворох средств, которые не приносят ничего принципиально нового, но при этом загромождают язык. Тем не менее, как ни старались разработчики Delphi, перещеголять С++ по количеству ненужных нововведений им не удалось.

С++

С++ - один из наиболее популярных ОО-языков, созданный в 1986 году Бьярном Страуструпом. Он представляет собой расширение языка С, разработанного Деннисом Ритчи для разработки ОС Unix в 1972 году.

Язык С создавался для системного программирования – он позволил программистам меньше кода писать на ассемблере, что их несомненно радовало. В С есть механизмы, которые вообще не являются процедурным программированием – например, макросы. В ТР их нет, поэтому давайте рассмотрим это понятие более подробно.

Макросы

Макросы - параметризованные символические константы.

Перед началом компиляции компилятор заменяет вхождения макроса на строку, вставляя в нее параметры.

Макросы можно создавать с помощью директивы #define. В примере вводится 3 макроса aPb1, aPb2, aPb3.

Пример 1: Макросы в С++.

#include <iostream.h>; //подключение библиотеки iostream.h

#define aPb1(a,b) (a*b) #define aPb2(a,b) a*b #define aPb3(a,b) ((a)*(b))

void main() //основная подпрограмма

{

 

double c = (double)10/aPb1(1+2,4);

//c=10/(1+2*4)

376

double d = (double)10/aPb2(1+2,4); //d=10/1+2*4 double e = (double)10/aPb3(1+2,4); //е=10/((1+2)*4) cout<<"c ="<<c<<" d ="<<d<<" e "<<e;

}

Например, разберем определение макроса aPb1:

#define aPb1(a,b) (a*b) aPb1(a,b) – заголовок макроса

(a*b) – значение, на которое макрос будет заменен.

Вчастности, аРb1(1+2,3+4) преобразуется в (1+2*3+4).

Втеле программы демонстрируется различие между тремя макросами, которые

описаны в программе. Из трех макросов лишь aPb3 правильно реализует умножение чисел.

(double) – это операция приведения типов. Дело в том, что отношение двух целых чисел в С – целое число, поэтому без приведения типов не обойтись.

Пример, который я привел – далеко не самый хитрый. Например, в каком порядке будет компилятор выполнять замены в следующем выражении:

aPb3(aPb3(1,2),3)?

Ответ: вначале будет подставлена строка вместо вложенного макроса, а потом будет заменен внешний макрос, т.е. результат будет такой:

aPb3(aPb3(1,2),3) -> aPb3(((1)*(2)),3) -> ((((1)*(2)))*(3))

Поэтому для работы с макросами мало знать, что это такое, - надо знать еще дополнительные правила, одно из которых - очередность подстановки.

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

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

Базовые ОО-возможности C++

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

ВС++ основным понятием является класс. В С++ есть возможность ограничивать доступ к элементам класса. Для этого служат ключевые слова private, public, protected.

377

Рrivate означает, что элемент класса доступен лишь внутри класса. Это разумнее, чем давать возможность доступа к private-элементу внутри модуля.

Есть в С++ статические функции (то же самое, что функции класса в Delphi) и статические переменные. В Delphi статических переменных нет, что, как мы уже говорили, не очень логично. С помощью статических переменных можно, например, контролировать количество созданных объектов данного класса. Без статических переменных сделать это проблематично.

В С++ есть одиночное наследование, динамическое связывание и полиморфизм. Но в С++ нет базового класса, аналога TObject в Delphi. Это создает проблемы:

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

Расширенные возможности С++

Наследование (одиночное), инкапсуляция, полиморфизм есть в любом ОО-языке, начиная с Simula 67. Сейчас мы рассмотрим некоторые особенности ООП на С++. По необходимости мы ограничимся лишь констатацией фактов без подробных примеров и объяснений.

1.Множественное наследование

ВС++ кроме обычного наследования есть наследование множественное. В таком

случае классу-наследнику перейдут все поля и методы, которые объявлены как public и protected хотя бы в одном из классов-наследников. Возможность кажется заманчивой, но на самом деле проблем с множественным наследованием больше, чем реальной пользы от него.

A

ВС

D

Рис. 20.1 Ромбовидное наследование

Разработчикам пришлось добавить механизм виртуальных базовых классов для того, чтобы избежать проблем ромбовидного наследования.

2.Перегрузка операций и операторов

ВС++ можно перегружать операторы. Например, в С++ можно, проектируя класс

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

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

378

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

3. Обобщенное программирование

Под обобщенным программированием понимают написание программ, которые могут использоваться не для конкретных, а для любых классов.

На процедурном уровне обобщенность достигается путем применения бестиповых указателей (когда мы с вами писали сортировку, которая упорядочивала что угодно и как угодно, мы занимались «обобщенным программированием»).

В Delphi обобщенность на уровне ООП достигается за счет трех фундаментальных идей:

1.Все классы – наследники TОbject.

2.Для каждого объекта можно узнать его тип во время выполнения программы.

3.Есть метаклассы.

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

Но в С++ нет ни базового класса, ни метаклассов, - вместо этого для того, чтобы узнавать тип объекта, вводится несколько вспомогательных операторов. Для передачи классов вводятся шаблоны (это развитие идеи макросов).

Можно было бы говорить о С++ еще долго – в нем есть столько всего ненужного!

– но мне кажется, что у вас уже и так сложилось некоторое впечатление о нем.

Java

Теперь поговорим о Java. Этот язык был разработан фирмой Sun Microsystems в 1996 году. Синтаксис языка взят из С, поэтому Java часто называют расширением языка С, иногда – «рафинированным С++». На самом деле по части ООП Java во многом похож на Delphi.

ВJava, как и в Delphi:

используются ссылки на объект, а не сами объекты

есть базовый класс - Object.

Наследование – только одиночное.

Есть интерфейсы.

Поддерживается механизм RTTI (Run Time Type Information), позволяющий определять тип объекта во время выполнения.

Метаклассов в Java нет, что не очень логично, т.к. из-за этого возможности Java по части обобщенного программирования существенно снижаются.

379

Java создавался как чистый ОО-язык, поэтому:

1.В Java вообще нельзя использовать указатели.

2.Макросов нет.

3.Нет оператора goto.

Однако в Java вводится неструктурный оператор, который позволяет выходить

сразу из нескольких циклов (т.е. аналог break, но многоуровненый). Чтобы достичь общности, были введены шаблоны. Кроме того, синтаксис довольно путаный (все-таки Java – расширение С) и при этом он стал громоздкий (в отличие от С).

ОО-возможности Java примерно такие же, как и в Delphi, но отказ от средств, которые присущи процедурному программированию, делает Java языком с ограниченной областью применения.

19.3. Компиляция или интерпретация

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

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

Можно было бы решить проблему так: передавать исходный код (быть может, некоторым образом закодированный, чтобы его никто не смог прочитать), а затем компилировать проект на той машине, на которой будет выполняться проект. Но компиляция больших проектов занимает много времени, поэтому такой выход – не самый лучший.

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

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

В качестве примера рассмотрим JVM (Java Virtual Machine).

Исходный код на языке Java компилируется в программу на байт-коде – промежуточном низкоуровневом языке. Выполняется этот программный код с помощью JVM – интерпретатора байт-кода. Для того чтобы программы, написанные на Java, могли выполняться на компьютере, на нем должна быть установлена JVM.

Конечно, не следует считать, что программы на Java должны обязательно преобразовываться в байт-код, а потом интерпретироваться – в принципе можно было

380

бы написать и компилятор для языка Java. Просто фирма Sun, разработавшая язык Java изначально делала упор на интерпретируемость языка.

19.4. Недостатки объектно-ориентированных языков

Описание самоорганизующихся систем

Некогда вы были обезьяной, и даже теперь еще человек больше обезьяны, чем иная из обезьян

Фридрих Ницше «Так говорил Заратустра»

Основное преимущество ООП перед модульным программированием в том, что программист может мыслить в терминах классов и объектов, что гораздо ближе к реальности, чем мыслить модулями, которые представляются просто как коллекции подпрограмм. Более того, принципы ООП могут реализовываться на основе логических и функциональных языков, таких как Prolog и Lisp.

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

Классы более-менее справляются, когда надо моделировать неживые объекты, но при описании самоорганизующихся систем возникают проблемы. Система, состоящая из классов похожа на механизм, состоящий из подмеханизмов, каждый из которых может непосредственно влиять на другие. Но любой объект с внутренней организацией обладает определенной самостоятельностью, и не дает возможности вызывать свои функции извне – он может получать информацию, затем обрабатывать ее как ему вздумается и, если ему хочется, возвращать кому-то ответ. Заставить его что-то обязательно отдать и, тем более, полностью руководить им, извне нельзя. На программистском языке можно сказать так: у существа вообще нет открытых полей и методов. Более того, организм не может получить полный доступ даже к своим собственным составным частям: я могу двигать ногами и головой, дышать носом, ощущать определенные изменения моего самочувствия, но не способен влиять на биохимические процессы в клетках. Существо может влиять на свое внутреннее

состояние, но непосредственного контроля над каждым внутренним процессом у

него нет.

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

Объекты языкового уровня и объекты времени выполнения

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