          ЧТО ТАКОЕ ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ

              Биарне Строустрап, AT&T Bell Laboratories

     Не все языки программирования могут быть объектно-ориентированны-
ми. Да, были  сделаны  утверждения,  что  APL,  Ada, Clu, C++, Loops и
Sмalltalk объектно-ориентированные языки. Мне приходилось слышать дис-
куссии об  объектно-ориентированном  программировании  на  C,  Pascal,
Modula-2  и Chill. Можно ли говорить об объектно-ориентированном прог-
раммировании на языках Fortran и Cobol? Я думаю,  что  в общем то да.
     Слово "объектно-ориентированный" стало в области программирования
синонимом слово "хорошо". В печати чаще всего встречаются элементы та-
кого вида:  "Ada - это хорошо; объектной ориентированности - это хоро-
шо; отсюда следует, что Ada - объектно-ориентированный язык".
     В этой статье содержится взгляд на то,  что означает термин "объ-
ектно-ориентированный" в контексте языков программирования общего наз-
начения.  Примеры представлены на C++,  отчасти,  чтобы познакомить  с
С++,  отчасти потому что С++ один из немногих языков, допускающих абс-
трактные типы  данных,  объектно-ориентированное  программирование,  а
также  оставляет возможной традиционную технику написания программ.  Я
не затрагиваю вопросы  конкурентоспособности  и  аппаратной  поддержки
языковых конструкций более высокого уровня.


                      ПОДХОДЫ В ПРОГРАММИРОВАНИИ

     Техника  объектно-ориентированного программирования может служить
образцом написания "хороших" программ для круга проблем.  Если  термин
"объектно-ориентированный язык" что-нибудь да означает, то прежде все-
го,  что  это  язык,  обладающих  механизмами, поддерживающими объект-
но-ориентированный стиль.
     Здесь имеется важное различие: язык поддерживает стиль программи-
рования, если располагает средствами сделать удобным его использование
(действительно простым и эффективным). Язык не поддерживает  некоторую
технологию программирования, если для ее использования приходится зат-
рачивать  дополнительные  усилия;  в  этом случае язык только обладает
средствами для следования технологии. Например, можно писать  структу-
рированные программы  на  языке Fortran или строго типизированные - на
С,  или использовать абстрактные типы данных - в Modula2,  но это  до-
вольно сложно делать,  потому что эти языки не поддерживают упомянутые
возможности.
     Поддержка некоторой парадигмы программирования состоит в  наличии
не только очевидных средств языка для следования ей, но и в более тон-
ких видах проверок на стадиях компиляции и выполнения, позволяющих вы-
являть ненамеренные отклонения от этой парадигмы. Примерами лингвисти-
ческой поддержки парадигмы являются проверка типов, выявление двусмыс-
ленностей,  проверки  во  времени выполнения. Такие внелингвистические
средства, как библиотеки поддержки  или  технологии  программирования,
также могут оказывать существенную поддержку парадигмы.
     Не  обязательно,  чтобы  один язык был лучше другого, так как они
могут обладать  различными  (непересекающимися)  возможностями.  Здесь
важно  не то, какой гаммой средств обладает язык, а насколько они дос-
таточны для поддержания желаемого  стиля  программирования  в  области
приложений. Более точно, важно, чтобы:
     -  было возможно получать решения, комбинируя средства языка, без
привлечения каких-либо неординарных других средств;
     - все средства были естественно  и  "элегантно"  интегрированы  в
язык;
     - было как можно меньше специальных средств;
     -  наличие какого-либо средства не приводило к значительному уве-
личению размеров программ, которые этим средством не пользуются;
     - пользователю необходимо было знать только необходимые для напи-
сания программы подмножества языка.
     Последние два принципа можно перефразировать как "то, чего вы  не
знаете,  не  должно вам навредить". Если имеются сомнения о полезности
некоторого средства, лучше его опустить. Значительно  легче  давать  к
языку новую возможность, чем изъять ее или изменить, поскольку она ре-
ализована в компиляторе и описана в литературе.
     ПРОЦЕДУРНОСТЬ. Изначальной и, вероятно, все еще самой общей пара-
дигмой  программирования  является следующая: "Решите, какие процедуры
вы желаете; используйте лучше из алгоритмов, которые можете найти".
     При этом  внимание  фокусируется на определение процедуры:  выбор
алгоритма,  необходимого для выполнения желаемых вычислений.  В языках
эта  парадигма обеспечивается возможностями передачи функциям парамет-
ров и обратного возврата значений.  литература,  касающаяся этой пара-
дигмы,  полна дискуссиями о том,  как передавать параметры, как разли-
чать различные виды аргументов и различные  типы  функций  (процедуры,
процедуры-функций, макросы и пр.) и т.д.
     Fortran  является  первым  процедурным  языком; Algol - 60, Algol
-68, С, Pascal - более поздние проекты, продолжающие эту традицию.
     В качестве примера хорошего  процедурного  стиля  можно  привести
функцию извлечения квадратного корня. Функция точно получает результат
для данного аргумента. Для этого она выполняет хорошо понятные матема-
тические вычисления.

     double sqrt (double arg)
     {
        //команды для вычисления корня
     }
     void some_function ()
     {
        double root2=sgrt(2);
        //...

     Процедурное  программирование использует функции для создания по-
рядка в лабиринте алгоритмов.
     ЛОКАЛИЗАЦИЯ ДАННЫХ.  Со временем основное внимание при разработке
программние все  более  и  более  начало перемещаться с проектирования
процедур к организации данных. Среди других причин этого, следует ука-
зать увеличение размеров программ.  Множество связанных процедур и об-
рабатываемых данных часто называют модулем. Парадигма программирования
здесь следующая:  "Решите, какие модули вы желаете; разбейте программу
таким образом,  чтобы данные были спрятаны в модулях". Когда нет необ-
ходимости группировать процедуры вокруг данных,  соблюдается процедур-
ный стиль.  Фактически,  технология  проектирования  хороших  процедур
должна теперь применяться к каждой процедуре модуля.
     Наиболее  общим  примером упрятывания данных является модуль, ис-
пользующий стек. Хорошее решение требует:
     - пользовательского интерфейса к стеку (например, функции push ()
и pop ();
     - доступа к стеку только с использованием принятого интерфейса;
     - инициализации стека перед первым его использованием.
     Возможным внешним интерфейсом к модулю со стеком может быть:

     //определение интерфейса к модулю
     //стек символов
     char pop ();
     void push (chav);
     const stack_size=100;

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

     #include "stack.h"
     Static char v[stack_size]; //"static" означает, что переменная
                               //является локализованной в этом
                              //файле (модуле
     static char* P=V;       //стек вначале пуст
     char pop ()
     {
       //проверить, не пуст ли стек, и pop
     }
       //проверить, не переполнен ли стек, и push
     }

     Несложно преобразовать стековое представление в связанный список.
В любом случае у пользователя нет доступа к представлению данных  (по-
тому  что  V  и P определены как static, т.е. локализованы в файле или
модуле, в котором декларированы). Такой стек можно использовать следу-
ющим образом:

     # include "stack.h"
     void some_function ()
     {
       char c = pop(push ('c'));
       if (c!='c') error ("impossible");
     }

     Изначально Pascal не обеспечивает удовлетворительные средства по-
добные возможности группирования,-единственным образом спрятать имя от
остальной части программы является его локализация  в  процедуре.  Это
приводит  к  странному гнездованию данных и излишней"нагрузки" на гло-
бальные данные.
     С языком С дела в этом смысле обстоят лучше. Как  было  показано,
вы  можете  определить модуль, группируя процедуры и определения соот-
ветствующих данных в простом исходном файле. А затем программист может
управлять и использованием данных в остальных  частях  программы  (имя
становится недоступным, если его описать в программе, как static). Та-
ким  образом в С может быть достигнута степень модульности. Однако та-
кой путь, основанный на static-декларациях, является довольно  низкоу-
ровневым.
     В  одном  из  Pascal-подобном  языке - Modula-2 - пошли несколько
дальше. В нем формализация понятия модуля происходит путем определения
модуля как фундаментальной конструкции с хорошо определенными деклара-
циями, явным контролем всей совокупности используемых  имен  (средства
импорта/экспорта),  механизмом инициализации модуля и множеством обще-
известных допускаемых стилей программирования.
     Другими словами, в С имеются средства программы на модули,  в  то
время как Modula-2 поддерживает эту возможность.
     АБСТРАКЦИЯ ДАННЫХ. Модульное программирование ведет к централиза-
ции  контроля  за всеми данными определенного типа в модуле управления
типами данных. Если вам необходимо два стека, то вы можете  определить
управляющий модуль с интерфейсом такого вида:

     //stack_id это тип; никаких других подробностей
     //stack или stack_ids здесь доступны:
     class stack_id;
     //организовать стек и вернуть его идентификатор
     stack_id create_stack (int size);
     //обратиться, когда стек больше не нужен
     destroy_stack (stack_id);
     void push (stack_id,char);
     char pop (stack_id);

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

     void f ()
     {
       stack_id s1;
       stack_id s2;

       sl=create_stack (200);
       //Oops:забыли создать S2

       char c1=pop(s1,push(s1,'a'));
       if (c1!='c')error("impossible");

       chavc2=pop(s2,push(s2,'a'));
       if(c2!='c')error("impossible");

       destroy(s2);
       //Oops:забыли разрушить s1
     }

     Другими словами, концепция модуля, поддерживающая парадигму лока-
лизацию данных, обладает возможностями абстракции данных, но  не  под-
держивает ее.
     АБСТРАКТНЫЕ ТИПЫ ДАННЫХ. В таких языках, как Ada, Clu, С++, проб-
лема  абстрактных типов данных решается путем предоставления пользова-
телю возможностей определения типов, которые ведут себя почти таким же
образом, как встроенные типы. Типы такого вида часто называются  абст-
рактными типами данных, хотя я предпочитаю называть их типами, опреде-
ленными  пользователем.  (Как  отмечал Дуг Маклрой (Doud Mellroy), эти
типы не являются абстрактными, они так же реальны, как  типы  inf  или
real. Другое определение абстрактных типов данных требует математичес-
кого  определения всех типов (как встроенных, так и определенных поль-
зователем). В этой статье мы будем понимать под такими  типами  данных
конкретные  спецификации  таких  действительно  абстрактных объектов.)
Здесь парадигма программирования  следующая:"Решите,  какие  типы  вам
нужны; обеспечьте полный набор операций для каждого типа".
     Когда  нет нужды более чем в одном типе, достаточно стиля локали-
зации данных с использованием модульности. Общими примерами, определя-
емых пользователем типов данных являются  арифметические  типы  рацио-
нальных и комплексных чисел:

     class complex{
        double re,im;
     public:
        complex(double r,double ){re=r;im=i;}
     // преобразование float complex:
        complex(double r){re=r;im=0}
        friend complex operator+(complex,complex);
     // бинарный минус:
        friend complex operator-(complex,complex);
     // унарный минус:
        friend complex operator * (complex);
        friend complex operator/(complex,complex);
     //...
     }

     Декларация  комплексного  класса (определенный пользователем тип)
классифицирует представление комплексного числа и  множество  операций
под  такими  числами. Представление является личным, т.е. re и im дос-
тупны только функциям,  специфицированным  в  декларации  комплексного
класса. Такие функции можно определить так:

     complex operator+(complex a1, complex a2)
     {
      return complex (a1.re+a2.re,a1.im+a2.im);
     }

и использовать так:

     complex a=2.3;
     complex b=1/a;
     complex c=a+b*complex(1,2,3);
     //...
     с=-(а/в)+2;

     Лучше  большую часть модулей (но не все) представлять как опреде-
ленные пользователем типы. В случаях, когда  программист  предпочитает
использовать  модульное представление даже при доступных средствах оп-
ределения типов, он сможет определить только единичный объект  требуе-
мого типа. Как альтернатива, язык может обеспечивать концепцию модуль-
ности в дополнение или в противовес концепции класса.
     ПРОБЛЕМЫ.  Определяемый пользователем тип является черным ящиком.
Будучи однажды определенным, он в действительности никак не взаимодей-
ствует с остальной частью программы. Единственным  способом  адаптации
типа  к  новым  применениям  является его переопределение. Это, часто,
очень негибкий способ.
     Рассмотрим  определяемый  для   использования   в   графике   тип
shape.Предположим  на время, что система должна поддерживать окружнос-
ти, треугольники и квадраты. Допустим также, что у вас есть ряд  клас-
сов:

     class point{/*...*/};          (point - точка)
     class color{/*...*/};          (color - цвет)

     Можно определить тип форма - shape - как:

     enum Kind{cirele,triangle,sguare};   (сircle -круг,
                                          triaugle -треугольник,
                                          squre - квадрат)

     class shape{
           point center;
           coljr col;
           kind k;
           //представление формы
     public:
           point where() {returu center;}
           void move (point to){cenre=to;draw();}
           void draw();
           void rotate (int);
           //другие операции
     };

     Тип  field,  k используется в таких операциях, как draw() - рисо-
вать и rotate() - вращать, для определения формы, с которой они  имеют
дело  (в  языках  типа  Pascal  можно использовать иную запись с тегом
k-tag k).
     Функцию draw() можно определить так:

     void shape draw()
     {
     switch (k){
     case circle:
         //рисовать круг
           break;
     case triangle:
        //рисовать треугольник
          break;
     case square:
        //рисовать квадрат
     }
     }

     Этот способ плох тем, что функции типа draw () должны  знать  все
используемые  виды форм. Поэтому код каждой такой функции должен моди-
фицироваться всякий раз, когда в систему добавляется новая форма.
     Если определяется новая форма, то все операции над типом  "форма"
следует пересмотреть и возможно изменить. У вас нет возможности ввести
новую форму до тех пор, пока вам не станут доступны тексты определения
всех операций. Вследствие того, что добавление новой формы влечет воз-
можное  переопределение  хода всех важных операций, этот процесс может
потребовать высокой квалификации и может  также  привести  к  внесению
ошибок в код операции, касающейся и других форм.
     Ваш выбор представления отдельной формы может быть строго ограни-
чен  требованиями настолько, что по крайней мере некоторые представле-
ния будут находиться в рамках фиксированного  образца  представляемого
определением общего типа "форма".
     Проблема состоит в том, что пропадает отличие между общими свойс-
твами,  присущими  всем  формам  (цвет,  свойство  быть нарисованной и
т.д.), и специфическими (круг есть форма, имеющая радиус;  рисуется  с
помощью функции circle-drawiug изображения окруности и т.д.).


               ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ

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

     class shape{
           point center;
           color col;
           //...
     public:
           point where () {return center;}
           void move (point to) {center=to;draw();}
           virtual void draw()ж
           мшкегфд мщшв кщефеу(int);
           //...
     };

     Отмеченные виртуальные функции это те, для которых можно специфи-
цировать общий интерфейс обращения, а особенности использования указы-
ваются для конкретных форм. (Термин "виртуальная"  принадлежит  языкам
Simula и C++ как обозначающий "может быть переопределена позже в клас-
се, полученном из класса, характеризующего общие свойства".) При таком
определении, мы можем написать общие функции манипулирования формами:

     void rotate-all (shape*V,int size,int augle)
         //вращать все элементы вектора "v"
         //значения "размеры-size","угол-augle"
        {
           tor (int i=0;i<size;i++)
           v[i].rotate(augle);
         }

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

     class circle:public shape {
         int radius;
     public:
         void draw (){/*...*/};
         void rotate (int){}//да, пустая
                           // функция
     };

     В C++ говорится, что класс окружностей является производным клас-
са форм и обратно, класс форм является базовым для класса окружностей.
В другой терминологии класс окружностей-это подкласс, форм-суперкласс.
     Парадигма программирования такова: "Решите, какие вам нужны клас-
сы; для каждого класса обеспечьте полный набор операций; сделайте общ-
ность свойств явной, используя "наследственность".
     Если нет общности свойств, абстракция данных теряет смысл. Тестом
оценки применимости объектно-ориентированного программирования являет-
ся количество типов с общими свойствами таких, что эта общность сможет
быть использована в механизме наследования свойств и виртуальных функ-
циях.
     В  некоторых  областях  программирования, таких как интерактивная
графика, имеются поистине большие возможности применения объектно-ори-
ентированного подхода. В других, таких как классические арифметические
типы и основанные на них вычисления, трудно  увидеть  необходимость  в
большем,  чем абстракция данных. (Однако, в более сложных разделах ма-
тематики можно  добиться  выигрыша,  используя  наследственность:  по-
ля-специальный вид колец, векторные пространства-модулей.)
     Поиск общих свойств среди типов далеко не тривиальный процесс. От
того,  как спроектирована сама система, зависит, насколько сможет быть
использована общность свойств. Общность должна все  время  быть  перед
глазами,  во  время  проектирования  системы,  как при специфицировани
классов (строя описания для других типов), так и прианализе того,  об-
ладают ли классы общими свойствами, которые могут быть приданы базово-
му классу. Hurapg (nygaard)[1] и Kerr(Kerr)[2]объясняют, что такое об-
ъектное  программирование, не прибегая к специфическим лингвистическим
конструкциям; (Carqill)[3] посвятил объектному программированию специ-
альное исследование.


                      ПОДДЕРЖКА АБСТРАКЦИИ ДАННЫХ

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

     class vector{
        int sz;
        int*v;
     public:
        void init(int size); //вызвать функцию init
                             //для инициализации
                            // sz и v перед первым
                            // использованием вектора
     //...

     };
     vektor v;
     //не используйте здесь v
     v.init(10);
     //здесь можно использовать v

     Но  этот способ чреват ошибками и неестествененен. Более предпоч-
тительное решение состоит  в  позволении  пользователю,  определяющему
тип,  проводить  инициализацию в функции специфицирования производного
типа.Такая функция превратит распределение памяти и инициализацию  пе-
ременной  в  единичную  операцию(часто  называемую  установкой) вместо
двух.  Такая  функция   установки   также   называется   конструктором
(coustructor).
     В случаях, когда конструирование новых типов не тривиально, часто
необходимо  проводить  противоположную операцию очистки объектов после
их последнего использования. В С++ функция очистки называется деструк-
тором (destructor). Рассмотрим тип "вектор" (vektor).

     class vektor{
        int sz;   //количество элементов
        int*v;   //указатель к целым(inteqers)
     public:
        vector(int); //конструктор
        vector();   //деструктор
        intzoperator[] (int index); //индекс-оператор
     };

     Конструктор типа "вектор" может  распределять  память,  например,
так:

     vector::vector(int s)
     {
        if(s<=0)error("неверна размерность вектора");
        sz=s;
        v=new int[s]; //распределить массив s целых
     }

     Деструктор типа "вектор" освобождает память:

     vector::~vector()
     {
         delete v; //освободить память,
                  // выделенную v
     }

     С++  не  поддерживает  сборку  мусора. В языке это компенсируется
тем, что типу позволено  свое  управление  памятью  без  вмешательства
пользователя.  Поскольку это является общим примером использования ме-
ханизма конструкторов (деструкторов), многие пользователи,  использую-
щие его, не касаются управления памятью.
     ПРИСВОЕНИЕ  И ИНИЦИАЛИЗАЦИЯ.Управление конструкцией (деструкцией)
объектов оказывается достаточным для многих типов,  но  не  для  всех.
Иногда  необходимо  контролировать  операции  копирования.  Рассмотрим
класс векторов:

     vector v1 (00);
     vector v2 =v1; //проинициализировать
                    //новый вектор v2 значениями
                    //вектора v1
     v1=v2;

     Должна быть возможность определять значение  инициализации  v2  и
 его  присвоение v1. Также должны быть средства, препятствующие подоб-
 ным операциям копирования; предпочтительнее, чтобы были доступны  обе
 альтернативы. Например:

     class vector {
        int*v;
        int sz;
     public:
        //...
        void operator=(vector z); //присвоение
        vector(vector z);         //инициализация
     };

     означает,  что определенные пользователем операции должны исполь-
 зоваться для интерпретации  векторного  присвоения  и  инициализации.
 Присвоение может быть определено так:

     vector::operator=(vector z a)
         //проверить разменность и скопировать
         //элементы
     {
         if(sz!=a.sz)
         error("неверная размерность для =");
         for(int i=0;i<sz;i++)v[i]=a.v[i];
     }

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

     vector::vector(vector z a)
         //инициализация вектора значениями другого вектора
     {
         sz=a.sz;       //та же размерность
         v=new int[sz]; //выделить память массиву элементов
               //скопировать элементы
               for(int i=0;i<sz;i++)v[i]=a.v[i];
     }

     С++ конструктор х(хz)определяет всю процедуру инициализации  объ-
 ектов  типа  х с помощью значения элементов другого объекта типа х. В
 дополнение и явной инициализации конструкторы вида x(xz) используются
 для обработки передаваемых по значению аргументов и возвращаемых фун-
 кциями значений.
     В С++ присвоению объекта класса ч можно воспрепятствовать, объяв-
 ляя операцию присвоения как личную:

     class x{
        void operator=(xz); //только члены х
                            //могут копировать х
        х(хz);
     public:
        ...
     };

     В языке Aga не поддерживаются конструкторы,  деструкторы,  перег-
рузка  путем  присвоения, определенный пользователем контроль передачи
параметров и возвращаемого функцией значения.  Эти  недостатки  строго
ограничивают класс типов, которые разрешается определять, и заставляет
программиста  обращаться  к  технике  локализации данных: пользователю
приходится проектировать и использовать модули управления типами вмес-
то соответствующих типов.
     ПАРАМЕТРИЗОВАННЫЕ ТИПЫ.Почему возникает необходимость  каким-либо
образом определить вектор целых чисел. Обычная ситуация, когда пользо-
вателю  требуется  вектор  элементов некоторого кипа, а это может ока-
заться несогласованным с определением типа вектор.  Следовательно  тип
вектор должен определяться таким образом, чтобы тип элемента был пара-
метром:

     class vector<classT>{
        //вектор элементов типа Т
     T*v;
     int sz;
     public;
        vector(int s)
     {
        if(s<=0)error("неверна размерность вектора");
        v=new T[sz=s]; //распределить память под массив
     }
     Tz operator[](int i);
     int size(){return sz;}
     //...
     };

     Теперь  можно определять и использовать векторы специфических ти-
пов:

     vector<int>v1(100);//v1-вектор из 100 целых чисел
     vector<complex>v2(200);//v2-вектор из 200 комплексныхчисел
     v2[i]=complex(v1[x],v1[y]);

     В  языках Ada и Clu поддерживаются параметризовнные типы. К сожа-
лению в С++ этого средства нет; приведенные примеры использовали  экс-
периментальные  конструкции языка. Когда в программе встречаются пара-
метризованные классы, они подменяются макросами. В  случае,  если  все
используемые типы явно определены, на стадии выполнения дополнительные
накладные расходы, связанные с использованием классов, не возникают.
     Обычно  параметризованный  тип  зависит по крайней мере от одного
аспекта параметра типа. Например, некоторые векторные операции  должны
допускать, что присвоение определено для объектов с определенным типом
параметра. Как вы можете в этом быть уверены? Один способ решения сос-
тоит  в установлении зависимости лицом, определяющим параметризованный
класс. Например, Т может быть типом, для которого определена  операция
"=".  Предпочтительненее не требовать этого или допускать специфициро-
вание типа параметра как частное специфицирование. Компилятор в состо-
янии обнаружить, была ли опущенная операция примененена и выдать сооб-
щение вида:

     cannot define
     vector<non_copy>::operator[](non_copy z):
        type noncopy does not have operator=

     Эта технология позволяет таким образом описывать типы, что  зави-
симости атрибутов параметрического типа обрабатываются на уровне инди-
видуальной  операции  типа.  Например,  можно определить операцию sort
(сортировка) для вектора.Операция  sort  может  использовать  операции
<,==,==  над  объектами параметрического типа. Возможность определения
вектора такого типа, что операция < для него не определена, сохраняет-
ся до тех пор, пока операция sort не была действительно использована.
     При использовании параметризованных типов проблема состоит в том,
что  каждая  актуализация  создает  независимый  тип.   Например   тип
vector<chav>  не связан с типом vector<complex>.В идеале должна сохра-
няться возможность выражать и использовать общие свойства типов, полу-
ченных из одного параметризованного типа. Например, и к vector<chav> и
к vector<cjmplex> применима функция size(), не зависящая от типа пара-
метра. Оказывается возможным, хотя это и не просто, выводить  общность
свойств из определения класса векторов с тем, чтобы допустить примене-
ние функции size() к любому вектору. И в этом преимущество языка, под-
держивающего  как  параметризацию  типов,  так и механизм наследования
свойств.
     ОБРАБОТКА ОШЫБОК.По мере увеличения размеров программ и интенсив-
ности использования библиотек, становится важной стандартизация  обра-
ботки ошибок (или исключительных ситуаций).
     В  языках  Ada,Algol-68,Clu есть средства стандартизованной обра-
ботки ошибок. В С++ таких средств, к сожалению нет. Если возникает не-
обходимость, ошибочные ситуации обрабатываются с использованием указа-
телей к функциям, ошибочных состояний и средств  библиотеки  языка  Си
signal  и  longjmp. Это все является явно неудовлетворительным, потому
что даже не может обеспечить стандартную схему обработки ошибок.
     Рассмотрим опять пример класса векторов. Что следует предпринять,
если оператору определения индекса будет передано значение,  выходящее
за допустимые пределы? Проектировщик векторного класса должен обладать
возможностями обеспечения для этой ситуации действий по умолчанию:

     class vector{
        ...
     except vector-range{
        //определить ошибочную ситуацию,
        //называемую vector-range и специфицировать
        //программу для ее обработки
        error("global:vector range error");
        exit(99);
     }
     }

     Вместо   обращения   к   функции    обработки    ошибок    error,
vector::operator[]() может генерировать код ошибки:

     intz vector::operator[](int i)
     {
        if (0<i//sz<=i)raise vector_range;
        return v[i];
     }

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

     void f(){
        vector v(10);
        try{    //здесь обрабатываются ошибки
                //определенной нише локальной
                //функцией обработки ошибок
     //...
     iut i=g(); //g может приводить к ошибке
                //размерности из-за потенциально
                //возможной ошибки размерности
     v[i]=7;    //вектора
     }
     except{
        vector::vector_range:
        error("f():ошибка ранга вектора");
        return
     }
                 //здесь ошибки обрабатываются
                 //глобальной функцией обработки
                 //ошибок, определенной в классе vector
     int i=g();  //g может приводить к ошибке
                //размерности из-за потенциально
                //возможной ошибки размерности
     v[i]=7;
     }

     Существует  много способов определения ошибочных ситуаций и пове-
дения функций обработки ошибок. Кратко изложенные здесь средства похо-
жи на соответствующие возможности языка Modula-2+. Этот стиль обработ-
ки ошибок можно реализовать таким способом, чтобы программа  обработки
ошибок  не  выполнялась  до тех пор, пока не возникнет соответствующая
ошибочная ситуация (исключая, возможно, стадию  начальной  инициализа-
ции).  Воспользоваться такими средствами можно в любых реализациях Lu,
используя функции Setjmp() и lougjmp() (см.руководство  по  библиотеке
Си для вашей операционной системы).
     Можно  ли  описанным  способом переложить всю обработку ошибочных
ситуаций на такой язык, как С++? К сожалению, нет. препятствие состоит
в том, что когда случается ошибка, нельзя освобождать стек вызовов  до
точки  обращения  к функции обработки ошибок. Чтобы это правильно сде-
лать в С++ используются вызываемые деструкторы, определенные в исполь-
зуемых программах. Это не отмечено в С-функции  lougjmp()  и  в  общем
случае не может быть обеспечено пользователем.
     ПРЕОБРАЗОВАНИЯ. определяемые пользователем преобразования, напри-
мер,   плавающих   чисел   в   комплексные   с   помощью  конструктора
complex(double), оказались неожиданно полезными в С++. Такие  преобра-
зования  могут  применяться явно или неявно с помощью компилятора там,
где это необходимо и не приводит к противоречиям:

     complex a=complex(1);
     complex b=1; //неявно:
                  //1->complex(1)
     a = b + complex (2);
     a = b + 2;   //неявно:
                  //2->complex(2)

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

     complex a=2;
     complex b=a+2; //интерпретируется как
                    //оператор + (а,complex(2))
     b = 2 + a;     //интерпретируется как
                    //оператор + (cjmplex(2),a)

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

     a + 2;//a.operator + (2)
     2 + a;//2.operator + (a),

     что приводит к необходимости модифицировать  класс  целых  чисел,
чтобы допустить возможность выражения 2 + а.
     Следует,  по возможности, при добавлении новых средств в систему,
избегать модификации существенного программного кода. Как правило, об-
ъектно-ориентированное программирование  предоставляет  более  удобные
средства пополнения системы без модификации существенного кода. В рас-
сматриваемом  случае,  однако, механизм абстракции данных обеспечивает
лучшее решение.
     ПОВТОРИТЕЛИ(iterators).  Язык,  который  поддерживает  абстракцию
данных,  должен  обеспечивать  способ определения управляющих структур
[4]. В частности, пользователям необходим механизм определения  циклов
с элементами, содержащимися в объекте некоторого определенного пользо-
вателем типа, вне зависимости от особенностей реализации пользователь-
ского  типа.  При наличии достаточно мощного механизма для определения
новых типов и операторов перегрузки это можно сделать без введения от-
дельного механизма определения управляющих структур.
     В случае векторов нет необходимости определения повторителя, пос-
кольку пользователю доступны средства упорядочения с помощью индексов.
Я приведу один способ для демонстрации техники.
     Существует несколько  стилей  итерирования.  Предпочитаемый  мной
опирается на перегрузку прикладного оператора функции ():

     class vector iterator{
        vector z v
        int i;
     public:
        vector_iterator(vector z r){i=0;v=r;}
        int operator ()()
        {return i<v.size()?v.elem(i++):0;}
     };

     (Этот  стиль  также  опирается на существование различных величин
для представления конца итерации. Часто, в частности, для типа  указа-
телей в языке С++, используется ).
     Теперь можно декларировать тип vector_iterator и использовать его
для векторов:

     vector v(sz);
     vector_iterator next(v);
     int i;
     while (i=next())print(i);

     Для  одного  объекта одновременно может быть активизировано более
одного повторителя, а также для одного типа можно иметь несколько  ти-
пов повторителей, так что есть средства организации различных тераций.
Повторитель  это довольно простая управляющая структура. Можно опреде-
лять и более общие механизмы.  Например,  стандартная  библиотека  С++
обеспечивает класс сопутствующих утилит (coroutine class)[s].
     Для  многих  включающих  типов, таких, как вектор, можно избежать
ввода отдельного типа повторителя путем определения механизма итераций
как части самого типа. Тип "вектор" может быть  определен  так,  чтобы
иметь "текущий элемент":

     class vector{
     int*v;
     int sz;
     int current;
  public:
     //...
     int next()
     {return (current++<sz)?v[current]:0;}
     int prev()
     {return(0<-current)?v[current]:0;}
  };

     Тогда интерацию можно организовать так:

     vector v(sz);
     int i;
     while (i=v.next())print(i);

     это  решение  не является таким общим, как механизм повторителей,
но оно позволяет избежать накладных расходов в  важном  случае,  когда
необходим  только один вид итерации и используется каждый момент време
ни только одна итерация.
     При необходимости можно применять более общие решения в  дополне-
ние  описанному.  Описанный способ требует от разработчика включающего
класса большей предусмотрительности, чем механизм повторителей. техни-
ка типа повторителей позволяет определять повторителей, которые  можно
связать с несколькими различными включающими типами. Тем самым обеспе-
чивается  механизм  итераций с единственным типом повторителя для раз-
личных включающих типов.
     ОСОБЕННОСТИ РЕАЛИЗАЦИИ.Изначально абстракция  данных  обеспечива-
лась средствами языков программирования, поддерживаемых компиляторами.
Однако  параметризованные  типы  лучше реализовать, используя редактор
связей и некоторую информацию о семантике языка, а обработку  исключи-
тельных ситуаций лучше обеспечивать на стадии выполнения. Можно приме-
нять  оба способа, чтобы уменьшить время компиляции и увеличить эффек-
тивность программ без нарушения общности механизмов программирования и
удобств написания программ.
     По мере расширения возможностей определения типов, программы  бу-
дут  все более зависеть от типов описания которых содержатся в библио-
теках (и не только ожидаемых в руководствах по  использованию  языка).
Это,  естественно,  накладывает  дополнительные требования на средства
описания того, что должно быть записано в библиотеку или  получено  из
нее,  что должно содержаться в библиотеке, какие ее компоненты исполь-
зуются программой и т.д.
     Для языков компилируемого типа важны возможности проведения  оце-
нок  увеличения  кода программ после внесения изменений. Очень сущест-
венно, чтобы редактор связей/загрузчик формировали оптимальный по раз-
мерам модуль и не присоединяли к нему большое количество  неиспользуе-
мых  программ,  обеспечивающих среду языка. В частности, если комплекс
библиотека /редактор связей/загрузчик включают в состав программы  код
каждой определенной для типа операции, тогда как программист использу-
ет одну или две, то это скорее плохо, чем просто бесполезно.
     Основными обеспечивающими функциями, которые необходимы при напи-
сании  объектно-ориентированных  программ,  являются функции механизма
определения класса со свойствами наследования черт класса и механизма,
реализующего подключение только функций  для  актуализированных  типов
объектов (когда актуальный тип неизвестен во время компиляции).
     Проектирование механизма подключения только  необходимых  функций
очень существенно. В дополнение отметим, что в случаях, когда доступно
объектно-ориентированное  программирование,  важны  средства поддержки
техники абстракции данных.
     Успех обеих технологий зависит от средств проектирования типов, а
также от легкости, гибкости и эффективности их использования.  Объект-
но-ориентированное  программирование  просто  значительно  увеличивает
гибкость использования определяемых пользователем  типов  и  сферу  их
применения по сравнению с технологией абстракции данных.
     МЕХАНИЗМ  ВЫЗОВА.  Ключевым средством языка для поддержки объект-
но-ориентированного программирования является  механизм,  регулирующий
подключение служебных функций для объекта. Например, при данном указа-
теле Р, как реализовать обращение р->f(arg)? Существуют различные спо-
собы решения.
     В  таких языках, как С++ и Simula, где широко используется стати-
ческая прверка типов, система поддержки типов может использовать  раз-
личные механизмы вызова. В С++ есть две альтернативы:
     1.  Обычное  обращение  к функции: нужная функция определяется во
время компиляции (просмотром таблицы символов компилятора);  обращение
к  ней  происходит  с помощью обычного call-механизма с использованием
аргумента, идентифицирующего объект,  для  которого  нужна  вызываемая
функция.  В  случаях,  когда стандартный call-механизм не эффективен в
достаточной мере, программист может указать, что функцию следует прямо
встраивать в код программы в месте обращения к ней. Это позволяет дос-
тигнуть эффективности макрорасширений, не нарушая стандартной семанти-
ки обращения к функциям. Такая оптимизация равно годится  и  в  случае
абстракции данных.
     2.ВИРТУАЛЬНОЕ  ОБРАЩЕНИЕ  К ФУНКЦИИ:вызов функции зависит от типа
объекта, который обычно можно определить только  во  время  выполнения
программы.  Типичной является ситуация, когда указатель Р связан с не-
которым базовым классом В, а объект относится к некоторому производно-
му классу D.Call-механизм должен"заглянуть"в объект, найти  помещенную
туда  компилятором  информацию и определить, к какой служебной функции
следует обратиться. Как только соответствующая функция,  скажем  D::F,
найдена,  для  обращения к ней использутся выше описанный механизм. На
стадии компиляции имя F преобразуется в индекс к таблице указателей  к
функциям.  Такой  виртуальный Call-механизм может быть сделан таким же
эффективным, как и обычный. В стандартной реализации языка С++, только
пять ссылок на дополнительную память. В  случаях,  когда  тип  объекта
можно определить во время компиляции, можно исключить даже эти наклад-
ные  расходы  и  использовать  средства встраивания кода функции в код
программы. Такие случаи являются достаточно общими и важными.
     В языках с неразвитыми  средствами  статической  проверки  типов,
следует  использовать третью, более проработанную альтернативу. В язы-
ках, подобных Smalltalk список всех служебных функций (называемых  ме-
тодами)  класса  хранится  таким образом, чтобы их можно было найти на
стадии выполнения программы.
     3.  ОБРАЩЕНИЕ  К СЛУЖЕБНОЙ УТИЛИТЕ:вначале ищется соответствующая
таблица имен сдужебных утилит с помощью анализа описания  объекта,  на
который  указывает  указатель Р. В этой таблице (или множестве таблиц)
ищется строка F, если объект содержит F(). Если F() обнаруживается, то
к ней происходит обращение; иначе обрабатывается  ошибочная  ситуация.
Этот  просмотр отличается от просмотра, проводимого на стадии компиля-
ций в языках со статической проверкой типов, потому что для  обращения
к служебной утилите используется таблица служебных утилит для актуаль-
ного  объекта.  Описанный способ является неэффективным по сравнению с
виртуальным Call-обращением, но более гибким. Так как чаще всего  ста-
тическая проверка аргументов типов не может быть выполнена при обраще-
нии к служебной утилите, использование механизма служебных утилит дол-
жно обеспечиваться динамической проверкой типов.
     ПРОВЕРКА ТИПОВ.Приведенный пример с формами показал  мощь  вирту-
альных  функций. Что еще предоставляет программисту механизм обращения
к служебной утилите? Он позволяет использовать любую служебную утилиту
для любого объекта.
     Эта возможность позволяет разработчику библиотеки функций  общего
назначения перенести ответственность по обработке типов на плечи поль-
зователя.  Естественно это облегчает проектирование библиотеки. Напри-
мер:

     class stack {  // предположим класс any
                    // содержит функцию next
     any*v;

     void push (anyx p)
     {
        p >next = v
        v = p;
     }
     any*pop ()
     {
        if(v==0)return error_obj;
        ani*r=v;
        v=v->next;
        return r;
     }
     };

     Теперь пользователь должен избегать смешения типов, например:

     Stack<anu*>cs;
     cs.push(new Saab 900);
     cs.push(new Saab 37B);
     plane*p=(plane*)cs.pop();
     p->takeoff();p=(plane*)cs.pop();
     p->takeoff();
        //ошибка на стадии выполнения;
        //Saab 900 это автомобиль
        //автомобиль не имеет служебной утилиты takeoff

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

     stack<plane*>cs;
     cs.push(new Saab 900);
        //Ошибка на стадии компиляции: смешение типов;
        //используется car*, ожидается plane*
     cs.push (new Saab 37 B);
     plane*p=cs.pop();
     p->takeoff(); //ошибкаЖ Saab 37 B это самолет
     p=cs.pop();
     p->takeoff();

     Использование статической проверки типов и механизма обращения  к
виртуальным  функциям приводит к стилю программирования, отличающемуся
от стиля, определяемого динамической проверкой типов и механизма мето-
дов.
     Например, классы в языках Simula или С++  специфицируют  фиксиро-
ванный  интерфейс  к  соответствующему  множеству объектов (или любому
производному классу), в то время как в языке Smalltalk классы специфи-
цируют базовое множество операций для объектов (или подкласса). Други-
ми словами, классы в языке Smalltalk определяют минимальные специфика-
ции, а пользователь может пытаться использовать  не  специфицированные
операции. В языке же С++ класс представляет собой точную спецификацию,
и  компилятор допускает только операции, специфицированные в определе-
нии класса.
     МЕХАНИЗМ НАСЛЕДОВАНИЯ. Рассмотрим язык, в котором реализована не-
которая форма просмотра служебных утилит без использования  механизмов
наследования.  Поддерживает  ли  такой  язык  объектно-ориентированное
программирование? Я думаю, что нет.
     Понятно, что можно делать интересные вещи, используя таблицу  ме-
тодов  для адаптации поведения объектов к определенным условиям. Одна-
ко, чтобы при этом избегать хаоса, необходимо выбрать некоторый способ
ассоциирования служебных утилит с допускаемыми или структурами  данных
для  представления объектов. Чтобы обеспечить пользователя информацией
о поведении объектов, необходимо также принять  некоторый  стандартный
способ  выражения общих черт поведения объектов. Механизм наследования
и представляет собой такой систематический стандартный путь решения.
     Рассмотрим язык, в котором обеспечивается механизм  наследования,
но  не  включены возможности виртуальных функций или служебных утилит.
Поддерживает ли такой язык объектно-ориентированное  программирование?
Я  думаю, что нет: в таком языке трудно предложить хорошее решение для
примера с формами.
     Однако такой язык будет более мощным, чем язык только с  абстрак-
цией  данных.  Это  утверждение подтверждается тем фактом, что большое
количество написанных на языках Simula и С++ программ, структурируется
с использованием иерархии классов без виртуальных функций. Возможность
выражения общности (факторизация) является поистине мощным  средством.
Например,  на  этом пути удается решить проблемы, касающиеся необходи-
мости иметь общее представление всех форм.
     Однако, при отсутствии средств виртуальных функций,  программисту
придется  обращаться  к  использованию групповых полей для определения
действительных типов объектов, что оставляет нерешенными проблемы  мо-
дульности.
     Отсюда  следует, что механизм производных классов (подклассы) яв-
ляется важным программистским инструментарием. Он  имеет  более  общее
применение,  чем только поддержка объектно-ориентированного программи-
рования. Это утверждение в частности верно, если связать механизм нас-
ледования в объектно-ориентированном программировании с идеей  о  том,
что  базовый класс представляет общее понятие, специальными видами ко-
торого являются производные классы. И это только один из аспектов  ис-
пользования  выразительной  мощности  механизма наследования, который,
однако, поддерживается языками, в которых все служебные функции  явля-
ются виртуальными.
     При  наличии контроля за наследуемыми свойствами, средство произ-
водных классов может служить мощным инструментарием для создания новых
типов. Оно позволяет добавлять и удалять свойства  класса.  Не  всегда
удается описать отношение между базовым и производными классами в тер-
минах специализации - дакторизация является лучшим термином.
     Средство производных классов является одним из програмистских ин-
струментариев.  Для него не существует строгих рекомендаций по исполь-
зованию - этот подход еще  слишком  молод  (на  20  лет  моложе  языка
Simula),  чтобы можно было говорить о том, что какие-то его применения
являются надуманными.
     МЕХАНИЗМ  МНОГОСЕТЕВОГО НАСЛЕДОВАНИЯ. Если класс А является базо-
вым для класса В, то В наследует атрибуты А, т.е. В обладает свойства-
ми А плюс, возможно, некоторыми другими  особенностями.  Приняв  такое
пояснение,  кажется очевидным, что могла бы оказаться полезной возмож-
ность существования двух базовых классов А1 и А2 для кдасса В. Это на-
зывается множественным наследованием.
     Примером множественного наследования  могут  служить  два  класса
библиотек-библиотека  объектов,  отображаемых на дисплее, и библиотека
задач. Программист может тогда создать такие классы:

     class my-displayed-task
     :public displayed, public task{
        //мое"хозяйство"
     };
     class my-task
     :public task{//мое "хозяйство"
                  //не выводимое на дисплей
     };
     class my-displayed
     :public displayed{//не задача
     };

     В случае единичного наследования программисту открыты только  две
из  этих трех возможностей. Это ведет к дублированию программного кода
или потере гибкости, а чаще всего-и того, и другого. В С++ этот пример
может быть реализован без значительных накладных расходов (по  времени
и  памяти), сравнимых с единичным наследованием и без отказа от стати-
ческой проверки типов [9].
     Неопределенности обнаруживаются во время компиляции:

     class A {public:f();...};
     class B {public:f();...};
     class C:public A, public B {...};

     void g(){
          c*p;
          p->f();///ошибка:двусмысленность

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

     class C:public A, public B{
     public:
        f()
        {
             //"хозяйство" класса С
            A::f();
            B::f();
         }
         ...
         }

     В дополнение к довольно простому понятию независимого множествен-
ного  наследования,  может появиться необходимость в более общем меха-
низме для выражения зависимостей между классами  (переплетающихся  при
множественном  наследовании).  В  С++ требование того, чтобы подобъект
мог быть разделяем объектом класса,  выражается  с  помощью  механизма
виртуального базового класса:

     class W {...};
     class B window //окно с границей
     :public virtual W
     {...};
     class M window //окно с меню
     :public virtual W
     {...};
     class BMW       //окно с границей и меню
     :public B window, public M window
     {...}

     Здесь  единичный  подобъект  "окно"  разделяется  подобъектами  В
window и M window класса BMW.В диалектике Lisp для облегчения програм-
мирования с использованием таких сложных иерархий классов  применяется
комбинирование служебных утилит. В С++ в этом нет необходимости.
     ИНКАПСУЛЯЦИЯ. Рассмотрим элемент класса (данные или функцию), ко-
торые  следует  защитить  от  неавторизованного доступа. Какие решения
приемлемы, чтобы ограничить доступ к этому элементу?
     очевидным ответом в языке, поддерживающем  объектно-ориентирован-
ное  программирование,  является  указание всех определенных для этого
объекта операций или всех функций класса.  При  таком  подходе  однако
возникает осложнение, связанное с тем, что невозможно указать полный и
окончательный список всех функций, имеющих право доступа к защищаемому
элементу.  Это  следует из того, что всегда можно добавить новую функ-
цию, путем получения производного класса из класса, содержащего  защи-
щаемый элемент, и определения для производного класса функции над этим
элементом. Этот подход сочетает достаточную защищенность от случайнос-
тей  (не  легко  "случайно" определить производный класс) с гибкостью,
необходимой для производства инструментария с использованием  иерархий
классов  (вы  можете сами себе гарантировать доступ к защищенному эле-
менту, порождая производный класс).
     К сожалению решение, принятое в языках, поддерживающих абстракцию
 данных, отличается от приведенного нами:"Перечислите все функции, для
 которых  необходим  доступ к элементу, в декларации класса". При этом
 ничего не говорится о том, что это за функции,-  они  не  обязательно
 должны быть функциями, описанными в классе. Неописанные в классе фун-
 кции  с  доступам к элементам класса в С++ называются дружественными.
 Упомянутый ранее класс Complex был определен с использованием дружес-
 твенных функций. Иногда важно, чтобы можно  было  определить  функцию
 как  дружественную  для  более чем одного класса. Доступность полного
 списка описанных в классе и дружественных  функций  является  большим
 преимуществом,  когда  вы пытаетесь понять поведение типа и в особен-
 ности, когда вы хотите модифицировать его.
     Рассмотрим пример, который демонстриует  некоторые  из  возможных
 путей инкансуляции в С++:

     class B{
             //члены класса по умолчанию
             //личного пользования
        int i1;
        void f1();
     protected:
        int i2;
     publik:
        int i3;
        void f3();
     friend void g (B*); //любая функция может
                         //быть определена как
                         //дружественная
     };

Личные и защищенные члены класса вообще не являются доступными:

     void h (B*p)
     {
        p->f1(); //ошибка: В::f1-личная
        р->f2(); //ошибка: B::f2-защищенная
        p->f3(); //окей  : B::f3-общего пользования
     }

     Запрещенные,  но  не  личные,  члены  доступны членам производных
классов:

     class D:public B{
     public:
        void g()
        {
           f1(); //ошибка: B::f1-личная
           f2(); //окей  : B::f2-защищенная,
                 //но D получен из В
           f3(); //окей  : B::f3-общего пользования
         }
     };

     Дружественные функции имеют доступ к личным и  защищенным  членам
класса, подобно описанным в классе функциям:

     void g(B*p)
     {
        p->f1(); //окей: B::f1-личная
                 //но g() дружественна В
        p->f2(); //окей: B::f2-защищенная,
                 //но g()дружественна В
        p->f3(); //окей :B::f3-общего пользования
     }

     Важность средств инкапсуляции резко возрастает по мере увеличения
размеров  программ, а также увеличения количества пользователей и рас-
ширения сферы их географического размещения. Детали о  механизмах  ин-
капсуляции   можно   найти   у   Шнайдера  (Snyder)[6]  и  Строустрапа
(Stroustrup)[7].


                        ОСОБЕННОСТИ РЕАЛИЗАЦИИ

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


                               ПРОБЛЕМЫ

     Язык,поддерживающий  технологию  локализации  данных,  абстракции
данных  и объектно-ориентированного программироввания, чтобы быть язы-
ком общего назначения должен также:
     -быть реализованным на традиционных ЭВМ;
     -выполняться в среде традиционных операционных систем;
     -быть конкурентоспособным с традиционными языками  программирова-
ния по эффективности при выполнении программ;
     -подходить для большей части возможных приложений.
     Это  означает,  что должны быть включены средства для эффективных
численных приложений (плавающая  арифметика  без  накладных  расходов,
иначе  Fortran окажется привлекательней). Должны быть включены возмож-
ности доступа к памяти (что необходимо для написания дрейверов). Долж-
ны быть возможности обращени[я к функциям  (call-обращения),  согласо-
ванные  с интерфейсами конкретных операционных систем. И, дополнитель-
но, должны быть возможности обращения к функциям, написанным на других
языках и наоборот, к функциям, написанным на  объектно-ориентированных
языках из других языков.
     Это  также  означает,  что объектно-ориентированный язык не может
полностью основываться не механизмах, которые эффективно не реализуют-
ся на традиционных архитектурах, и что все еще предполагается  исполь-
зование такого языка, как языка общего назначения. То же можно сказать
и о сборке мусора, которая может оказаться узким местом в части произ-
водительности и мобильности. Большинство объектно-ориентированных язы-
ков  используют сборку мусора, чтобы упростить проблемы программиста и
уменьшить сложность самого языка и  компилятора.  Однако  должна  быть
возможность  использовать сборку мусора в некритических ситуациях, од-
нако сохранять контроль за памятью, там, где это необходимо. Альтерна-
тивой является язык, не занимающийся сборкой  мусора,  но  позволяющий
проектировать типы, которые управляют используемой ими памятью. Приме-
ром может служить С++.
     Обработка  исключительных ситуаций и конкретное использование ре-
сурсов также представляют собой проблемы. Любое средство языка,  кото-
рое  реализуется  с  помощью редактора связей, скорее всего тоже будет
представлять проблему в части мобильности.
     Альтернативой включению в язык  низкоуровневых  средств  является
использование  в критических случаях специализированных языков низкого
уровня.
     Объектно-ориентированное программирование - это программирование,
использующее механизм наследования. Абстракция данных - это программи-
рование с использованием определяемых пользователем типов. С небольшим
исключением объектно-ориентированное программирование может  и  должно
быть обобщением абстракции данных.
     Эта  механика  нуждается  в надлежащей языковой полддержке, чтобы
быть эффективной. Для абстракции  данных  достаточно  только  языковой
поддержки;  для  объектно-ориентированного  программирования требуются
средства общеситсемного программного окружения. Чтобы обладать свойст-
вами языка общего назначения, язык должен позволять эффективно исполь-
зовать традиционные аппаратные средства.


                             БЛАГОДАРНОСТИ

     Ранняя  версия этой статьи представлялась на совещании Ассоциации
пользователей языка Simula-association of Simula Users - в Стокгольме,
прошедшем в августе 1986 г. Прошедшая там дискуссия внесла много улуч-
шений в стиль излождения и содержания статьи. Много конструктивных за-
мечаний сделали Брайен Керниган (Brian Kernighan) и  Рави  Сети  (Ravi
Sethi).


                           Л и т е р а т у р а

    1.K.Nygaard,"Basic Cocepts in Object-Oriented Programming",
      SIGPlan  Notices,Oct.1986,pp.128-132.

    2.R.Kerr,"Object-Based Programming:A.Foundation for Reliable
      Software,"Proc.14th Simulu Users'Conf.,Simula Information,Oslo,
      Norway,1986,pp.159-165;a short version is "A Materialistic View
      of the Sofware 'Engineering' Analogy, SIGPlan Notices, March
      1987,pp123-125.

    3.T.Cargill,"IPI:A Case Study in Object-Oriented Programming",
      SIGPlan Ntices, Nov.1986,pp.350-360.

    4.B.Liskov et al.,"Abstraction Mechanisms in Clu,"Comm.ACM,
      Aug.1977,pp.564-576.

    5.J.Shopiro,"Extending the C++ Task System for Real-Time Appli-
      cations,"Proc.Usenix C++ Workshop, Usenix, Santa Monica,Calif.,
      1987,pp.77-94.

    6.A.Snyder, "Encapsulation and Inheritance in Object-Oriented
      Programming Languages,"SIGPlan Notices,Nov.1986,pp.38-45.

    7.B.Stroustrup, The C++ Programming Language, Addison-Wesley,
      Reading,Mass.,1986.

    8.D.Weinreb and D.Moon, Lisp Manual,Symbolics,Cam-
      bridge, Mass.,1981.

    9.B.Stroustrup, "Multiple Inheritance for C++, "Proc. Spring
      European Unix Users Group Conf., EEUG,London,1987.



     Bjarne  Stroustrup является разработчиком языка С++. Его исследо-
вательские интересы включают распределенные системы, операционные сис-
темы, моделирование, методологию программирования и языки программиро-
вания.
     Stroustrup получил MS в области математики и вычислительной  тех-
ники  в университете Артуса (Aarhus) и PhD по вычислительной технике в
университете Кэмбриджа (Cambridge). Он является членом  исследователь-
ского   центра   вычислительной  техники  (Computer  Science  Research
Center), также членом IEEE и ACM.
     Вопросы, касающиеся этой статьи, направляйте автору в  АТ$Т  Bell
Gaboratovies, Rm.2C-324, 600 Monntain Ave.,Murray Hill, N J 07974.
