Лекции (1129116), страница 19
Текст из файла (страница 19)
Пример 1.
Например, мы пишем класс, над объектами которого осмысленно применение операций "+" и "-" (унарных и бинарных). В С++ есть еще такие операции, как "+=" и "-=", и естественно, что хотелось бы перекрыть и эти операции. Как реализовать перекрытие?
class X{
X operator +(X&);
}
Такой оператор разбираться будет следующим образом. Выражение a=b+c будет эквивалентно выражению a=b.operator+(c) (такой синтаксис тоже допустим). Но операция "+" в принципе должна быть симметричной (а в данном случае это не так), поэтому бинарные операции имеет смысл описывать не как функции члены, а как глобальные функции: X operator+(X& a, X& b). Теперь возникает вопрос, а если мы хотим инкапсулировать структуру данных X, т.е. мы хотим оставить публичными только функции-члены, как нам тогда описать эту операцию "+" (она внешняя)?
Пример 2.
Есть и другая проблема. Каким образом реализовать контейнер (class Container)? Контейнер – это структура данных, содержащая в себе другие однотипные объекты данных (например, контейнером является массив, список, очередь, хэш-таблица). Детали реализации контейнеров могут сильно влиять на приложение. Поэтому нам хорошо бы иметь единообразный интерфейс для всех реализаций контейнера, чтобы можно было бы менять реализацию контейнера, и при этом не менять использующие его классы.
Когда речь идет о контейнере, то очень хотелось бы перебирать элементы контейнера с первого до последнего (т.е. чтобы можно было бы реализовать что-то вроде цикла for(i=0; i<n; i++){a[i]=…} ). Для этого обычно вводится класс Iterator:
class Iterator{
public:
Iterator(Container&);
X& GetFirst(); // Пусть контейнер содержит объекты класса X
X& GetCurrent();
X& GetNext();
private:
… // В этой части будет содержаться ссылка на объект класса Container,
и другие детали, например, индекс
}
Когда мы хотим переделать контейнер, то мы должны изменить реализацию класса Container и реализацию класса Iterator. Интерфейс класса Iterator остается одним и тем же. Возникает независимость кода этих классов от реализации. Если мы не будем использовать концепцию Итератора, то мы не сможем писать инкапсулированные контейнеры. Если мы не использовали Итератор, и вдруг решили переделать контейнер, то мы должны будем переделать любой код, который использовал объекты контейнера. Если изначально контейнер сопровождается итератором, то переделка тривиальна.
Почему нельзя реализовать методы класса Iterator прямо в классе Container? Очень часто, например, при сортировке массива, требуется два индекса i и j, с помощью которых осуществляется доступ к элементам. Но если индекс будет в классе Container, то мы не сможем осуществить одновременное индексирование одного контейнера по двум разным индексам.
Главная проблема в том, что итератор – внешний по отношению к контейнеру класс. Как ему использовать внутренние детали реализации контейнера?
Два приведенных примера показывают, что концепция полной защиты класса извне не совсем корректна. Для разрешения возникающих проблем, введена специальная концепция друзей
Для чего даже введено специальное ключевое слово friend. Это слово очень подходит, потому что, во-первых, оно очень дружелюбное, а во вторых, к друзьям-классам применимы те же понятия, что и к друзьям вообще: например, друзей приобретают, но в друзья не навязываются. Что такое друг класса? Класс может внутри себя явным образом разрешить доступ извне для некоторых функций и для некоторых классов. Права доступа делегируются, но не приобретаются. Например, чтобы разрешить оператору "+" использовать члены класса Х, нужно в любом месте класса Х написать: friend X operator + (X& a, X& b). Причем нужно указать полный прототип оператора из-за возможностей перегрузки. После такого объявления, оператор "+" может обращаться абсолютно ко всем членам класса Х. Почти всегда переопределяемые симметричные бинарные операции имеет смысл делать глобальными. Но операции типа "+=" имеет смысл делать членами класса, потому что у этих операций никакой симметрии нет.
Класс контейнер можно описать так:
class Container{
...
friend class Iterator;
...
}
В данном случае можно было бы обойтись и без концепции друзей. Мы уже говорили о том, что класс ведет себя еще и как модуль. Модули могут быть вложенными.
class Container{
...
public:
Container();
class Iterator{
...
};
...
}
Но возникает проблема: какие права доступа будет иметь класс Iterator по отношению к классу Container? Ответ простой - такие же, как и остальные классы, т.е. только к публичным членам класса. Страуструп вместо того, чтобы сказать, что внутренние классы имеют все права, предпочел концепцию друзей, потому иначе наследники класса Iterator тоже получили бы права доступа к членам Container. И в данном случае, все равно надо Iterator сделать другом Container.
Концепция друзей не распространяется на наследников, т.е. наследующий класс должен тоже явно указывать своих друзей, если в этом есть необходимость. Заметьте также, что концепция друга не транзитивна (друг моего друга не мой друг).
Пользоваться Итератором очень просто, например:
for(Container::Iterator i(C); !i.end(); i.GetNext(i)){ A = i.GetCurrent(); ...};
Концепция друзей позволяет гибко реализовывать различные стратегии работы с типами данных. Подобных возможностей нет в традиционных процедурных языках программирования.
Язык Java.
В языке Java практически ничего не было изменено. Хотя с точки зрения синтаксиса, средства языка Java побогаче. Спецификатор доступа должен стоять перед каждым членом класса: Base::protected int i. Всего спецификаторов в Java четыре - private, protected, public и пустой спецификатор (т.е. отсутствие ключевого слова), который означает пакетный доступ. Концепции друзей в Java нет. Семантика первых трех спецификаторов точно такая же.
Private скрывает детали реализации не от злоумышленников (от них все равно не скроешься), а от незнания (чтобы случайно не была нарушена целостность структуры данных). От себя защищаться не нужно, нужно защищаться от клиентских модулей. В С++ пришлось вводить друзей из-за того, что классы слишком обособлены друг от друга, и private слишком строг - он укрывает классы, принадлежащие одному программисту друг от друга, хотя его функция скрывать классы от клиентских модулей. Эти проблемы возникают из-за проблем раздельной компиляции, которые достались языку С++ в наследство от языка Си. Все недостатки С++ (90%) достались от языка Си.
С этой точки зрения, Java совершенно свободный язык, и разработчики обратили внимание на то, что есть механизм классов как модулей, и есть механизм раздельной компиляции, который реализуется через понятие пакета. Пакет - это некоторая совокупность файлов, которая где-то и как-то локализуется (подробнее об этом будет рассказано позже). Если член класса описан с пакетным доступом, то это означает, что только классы из данного пакета имеют доступ к этому члену класса. Как правило, пакет разрабатывает один человек, и нет смысла защищаться от себя. В Java механизм доступа более глубокий, потому что более проработаны проблемы раздельной компиляции.
Рассмотрим механизм protected-членов. Для этого сначала необходимо сказать несколько слов о наследовании. Рассмотрим класс Base у которого есть наследники C1 и С2. У класса С1 есть наследник С3. Функция-член класса С3 в С++ имеет доступ к защищенным членам классов С1,С2, Base. В Java такой возможности нет, т.е. из С3 можно обращаться только к прямым предкам (С1 и Base).
Глава 6. Раздельная трансляция.
Различают несколько видов трансляции:
1. Цельная трансляция. Этот вид трансляции реализован в таких языках, как Алгол-60, Паскаль, и некоторых других. Компилятору предъявляется вся программа целиком. Понятно, что проект может быть лишь небольшого масштаба - это либо конкретные алгоритмы, либо студенческие программы.
2. Пошаговая трансляция. Программа предъявляется компилятору небольшими частями, и он тут же эти части транслирует. Примером пошагового транслятора является транслятор Бейсика в первых IBM машинах, который автоматически загружался из ROM, если не происходила загрузка с дискеты или с другого какого-либо устройства. Пошаговая трансляция достаточна только для языков с примитивной структурой.
3. Инкрементная трансляция. Это расширение пошаговой трансляции. программа разбивается на куски (большие и более значимые), и каждый кусок транслируется отдельно. С инкрементной трансляцией связана динамическая трансляция с языка Java. Транслятор с этого языка преобразовывает файл с расширением .java в файл с расширением .class, который является программой на байт-коде, которая в свою очередь, интерпретируется виртуальной Java-машиной. Интерпретация всегда связана с неэффективностью, поэтому используется динамическая трансляция, когда часть байт-кода "налету" транслируется в машинный код. это наиболее эффективно, когда, например, выполняется некоторый цикл, который нет смысла интерпретировать каждый раз.
4. Раздельная независимая трансляция. Программа разбивается на физически независимые куски и компилятору в каждый момент времени доступен только один кусок. Эта трансляция употреблялась в языке Fortran. Каждая подпрограмма (процедура или функция) и сама программа транслировались раздельно. Этот вид трансляции также используется в языках Ассемблер и Си.
5. Раздельная зависимая трансляция. Язык и/или система программирования устроены таким образом, что компилятору доступен не только текст модуля, но и информация о других модулях. Зависимая трансляция обладает более богатыми возможностями.
Раздельная трансляция необходима, потому что, даже если человек разрабатывает программу один, все равно ему удобно разбивать программу на логические части. Промышленный язык программирования невозможен без раздельной трансляции.
Чем различается зависимая трансляция от независимой. Когда речь идет о сегментации программы на физические модули, возникает понятие контекста трансляции. языки с независимой трансляцией характеризуются тем, что контекст трансляции должен обеспечиваться самим программистом. Связи между физическими модулями, которые и составляют контекст трансляции, задаются вручную программистом (например с помощью спецификатора extern). Т.е. такая модель трансляции очень напоминает ассемблерную модель. Отличие только в том, что при описании внешних имен, нужно описывать тип этого имени.
Лекция 16
Нас будут интересовать как раз раздельная независимая трансляция и раздельная зависимая трансляция. Следует сразу условиться о терминологии: мы для краткости под «раздельной трансляцией» будем понимать «раздельную зависимую трансляцию», соответственно, раздельная независимая трансляция так и будет использоваться.
Раздельная независимая трансляция
Независимая трансляция – наследие старых времен. Из языков, которые мы рассматриваем, ее поддерживает только C++.
Мы уже говорили, что при любой раздельной трансляции возникает некий контекст трансляции, который всегда не пуст.
При независимой трансляции, когда компилятору доступен только один модуль, естественно, что КТ должен поставляться самими программистом. Реально КТ состоит из объявлений имен внешних имен и типов, и эти объявления должны точно совпадать с определяемыми объектами. Программисту приходится дублировать информацию, причем безо всякого контроля. А везде, где нет контроля возникают ошибки.
Например, если мы пишем:
M1:
extern int i;
M2:
double i;
то хорошо, если загрузчик сообщит об ошибке, да и то он в этом случае может среагировать только, если размеры отводимой памяти - различны. В случае функций это не страшно.
В языках с независимой трансляцие ошибки несовпадания контекстов являются достаточно труднообнаружимым явлением.
К тому же, как правило, в языках с разделенной трансляцией достаточно убогие средства именования объектов между различными модулями, ведь все имортируемые объекты становятся непосредственно видимыми. Разумеется, программисты как-то искали выходы.
Прежде всего за счет неких технологий:
Именование
Например, в любой серьезной библиотеке на C/C++, возьмем, к примеру:
Xlib, Xtoolkit, Motif
все имена в этих библиотеках начинаются с префиксов, соответственно:
X, Xt, Xm
это нужно не для удобочитаемости, а для того, чтобы не было конфликта имен в библиотеках, которые разрабатываются независимо.
Include-файлы
Как преодолеть ошибки, связанные с некорректностью указания КТ? Разумеется, с помощью include-файлов. Иначе говоря, на механизме include-файлов программисты на C/C++ практически реализовали механизм экспорта/импорта. То есть, если у нас есть модуль M.C, то по технологическим правилам (которые разделяют практически все программисты), его интерфейс должен быть представлен в header файле M.h. Header играет практически роль файла определений. С помощью некоторых ухищрений можно достигнуть и полной инкапсуляции.
Посмотрим на библиотеку Xtoolkit. В ней есть тип XtAppContext. Интересно, что структура этого типа недоступна программистам на Xtoolkit. Он является инкапсулированным, это достигается путем объявления его через указатель:
typedef _XtAppContext * XtAppContext
Больше информации об этом типе нет. Язык С такое обращение воспринимает нормально. Как обращаться с этим типом? Через набор функций, определенном в том же header файле, где определен и сам XtAppContext. Чтобы достать реальную структуру этого типа придется забраться в «модуль реализации», то есть в .c файл, где реально описан этот тип. Разработчики Xtoolkit могут свободно изменять этот тип, программисту будет достаточно лишь перекомпоновать свою программу.