Опубликован: 05.01.2015 | Доступ: свободный | Студентов: 2058 / 0 | Длительность: 63:16:00
Лекция 4:

Абстрактные типы данных

Дружественная (friend) функция - это функция, которая, не будучи членом класса, имеет доступ к его приватным членам.

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

float X() const { return x; }
float Y() const { return y; }
      

Эти функции объявлены с ключевым словом const, так как они не модифицируют данные-члены объекта, для которого вызываются. Мы часто включаем в классы языка C++ подобные функции. Обратите внимание, что если бы использовалось другое представление данных, например, полярные координаты, то реализовать эти функции было бы труднее, но эта трудность была бы прозрачной для клиента. Преимущества подобной гибкости можно также задействовать в функциях-членах - если в реализации функции distance из программы 4.1 вместо обращений a.x использовать обращения a.X() , то этот код не придется переписывать при изменении представления данных. Помещая эти функции в приватную часть класса, можно обеспечить такую гибкость даже в тех классах, где не требуется доступ клиента к данным.

Во многих приложениях главная цель в создании класса - определение нового типа данных, наиболее соответствующего потребностям приложения. В таких ситуациях часто требуется использовать этот тип данных таким же образом, как и встроенные типы данных языка C++, например, int или float. Эта тема рассматривается более подробно в разделе 4.8. Один важный инструмент в языке С++, помогающий нам достичь этой цели, называется перегрузкой операций (operator overloading) - она позволяет указать, что к объекту класса могут применяться фундаментальные операции, и что в точности эти операции должны делать. Например, предположим, что требуется считать две точки совпадающими, если расстояние между ними меньше чем 0,001. Добавив в класс код

friend int operator==(POINT a, POINT b)
  { return distance(a, b) < .001; }
      

можно с помощью операции == проверять, равны ли две точки (согласно приведенному определению).

Другой операцией, для которой обычно желательна перегрузка, является операция << из класса ostream. При программировании на языке C++ обычно предполагается, что эту операцию можно использовать для вывода значений любого объекта; в действительности же так можно делать, только если в классе определена перегруженная операция <<. В классе POINT это можно сделать следующим образом:

ostream& operator<<(ostream& t, POINT p)
  {
    cout << "(" << p.X() << "," << P.Y() << ")";
    return t;
  }
      

Эта операция не является ни функцией-членом, ни даже дружественной: для доступа к данным она использует общедоступные функции-члены X() и Y() .

Как понятие класса языка C++ соотносится с моделью "клиент-интерфейс-реализация" и абстрактными типами данных? Оно обеспечивает непосредственную языковую поддержку, но очень часто для этого существует несколько различных подходов. Общепринято следующее правило: объявления общедоступных функций в классе образуют его интерфейс. Другими словами, представление данных хранится в приватной части класса, где оно недоступно для программ, использующих класс (клиентских программ). Все, что клиентские программы "знают" о классе - это общедоступная информация о его функциях-членах (имя, тип возвращаемого значения и типы аргументов). Чтобы подчеркнуть важность интерфейса (определяемого посредством класса), мы в этой книге сначала рассматриваем интерфейс (наподобие программы 4.3), и уже затем рассматриваем реализацию - в данном случае это программа 4.1. Дело в том, что такой порядок упрощает анализ других реализаций, с другими представлениями данных и другими реализациями функций, а также тестирование и сравнение реализаций, позволяя делать это без каких-либо изменений клиентских программ.

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

Программа 4.3. Интерфейс абстрактного типа данных POINT

В соответствии с общепринятым правилом, интерфейс, относящийся к реализации АТД в виде класса, получают путем устранения приватных частей и замены реализаций функций их объявлениями (сигнатурами). Данный интерфейс именно так и получен из программы 4.1. Мы можем использовать различные реализации, имеющие один и тот же интерфейс, без внесения изменений в код клиентских программ, работающих с этим АТД.

class POINT
  {
    private:
      // Программный код, зависящий от реализации 
    public:
      POINT();
      float distance(POINT) const;
  };
      

С помощью классов C++ можно определить функции, которые позволяют клиентским программам работать с данными без непосредственного доступа к ним. Для этого создаются функции-члены, возвращающие требуемые данные. Например, можно предоставить клиентским программам интерфейс, в котором определены такие операции, как извлечь имя клиента или добавить запись о клиенте. Самое важное следствие из такой организации заключается в том, что те же самые клиентские программы можно использовать даже в том случае, если потребуется изменить формат списков почтовых адресов. Или, говоря по-другому, можно изменять представление данных и реализацию функций, имеющих доступ к этим данным, без необходимости модификации клиентских программ.

Подобные реализации типов данных в виде классов иногда называют конкретными типами данных (concrete data type). Однако в действительности тип данных, который подчиняется этим правилам, удовлетворяет также и нашему определению абстрактного типа данных (определение 4.1) - различие между ними состоит в тонкостях определения таких слов, как "доступ", "обращаться" и "определять", но мы оставим эти нюансы теоретикам языков программирования. Действительно, в определении 4.1 точно не сформулировано, что такое интерфейс или как должны быть описаны тип данных и операции. Такая неопределенность является неизбежной, поскольку попытка точно выразить эту информацию в наиболее общем виде потребует использования формального математического языка и, в конце концов, приведет к трудным математическим вопросам. Этот вопрос является центральным при проектировании языка программирования. Вопросы спецификации будут рассматриваться позже, после изучения нескольких примеров абстрактных типов данных.

Применение классов языка C++ для реализации абстрактных типов данных в сочетании с соглашением, что интерфейс состоит из определений общедоступных функций, не является идеальным вариантом, поскольку интерфейс и реализация не являются полностью разделенными. При изменении реализации приходится перекомпилировать программы-клиенты. Некоторые альтернативные варианты рассматриваются в разделе 4.5.

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

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

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

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

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

В отличие от этого, классы языка C++ позволяют не только использовать разные реализации операций, но и создавать их на основе разных структур данных. Повторяю: ключевая характеристика АТД заключается в том, что он позволяет производить изменения, не модифицируя клиентские программы - поскольку доступ к типу данных разрешен только через интерфейс. Ключевое слово private в определении класса блокирует прямой доступ клиентских программ к данным. Например, можно было бы включить в стандартную библиотеку языка C++ реализацию класса string, созданную на базе, скажем, представления строки в виде связного списка, и использовать эту реализацию без изменения клиентских программ.

На протяжении всей настоящей главы мы рассмотрим многочисленные примеры реализаций АТД с помощью классов языка C++ . После четкого уяснения этих понятий в конце главы мы вернемся к обсуждению теоретических и практических следствий из них.

Упражнения

  • 4.1. Предположим, что необходимо подсчитать количество пар точек, находящихся внутри квадрата со стороной d. Для решения этой задачи напишите две разные версии клиента и реализации: (1) модифицируйте соответствующим образом функцию-член distance; (2) замените функцию-член distance функциями-членами X и Y.
  • 4.2. Добавьте в класс точки (программа 4.3) функцию-член, которая возвращает расстояние до начала координат.
  • 4.3. В программе 4.3 модифицируйте реализацию АТД точки таким образом, чтобы точки были представлены полярными координатами.
  • 4.4. Напишите клиентскую программу, которая считывает из командной строки целое число N и заполняет массив N точками, среди которых нет двух равных друг другу. Для проверки равенства или неравенства точек используйте перегруженную операцию ==, описанную в тексте настоящей главы.
  • 4.5. Используя представление на базе связного списка, наподобие программы 3.14, преобразуйте интерфейс обработки списков из раздела 3.4 "Элементарные структуры данных" (программа 3.12) в реализацию АТД на базе классов. Протестируйте полученный интерфейс, изменив клиентскую программу (программа 3.13) так, чтобы она использовала этот интерфейс; затем перейдите к реализации на базе массивов (см. упражнение 3.52).
Александра Боброва
Александра Боброва

Я прошла все лекции на 100%.

Но в https://www.intuit.ru/intuituser/study/diplomas ничего нет.

Что делать? Как получить сертификат?

Никита Андриянов
Никита Андриянов
Владимир Хаванских
Владимир Хаванских
Россия, Москва, Высшая школа экономики
Вадим Рычков
Вадим Рычков
Россия, Москва, МГТУ Станкин