Абстрактные классы и члены классов. Абстрактные типы данных

Последнее обновление: 04.08.2018

Кроме обычных классов в C# есть абстрактные классы . Абстрактный класс похож на обычный класс. Он также может иметь переменные, методы, конструкторы, свойства. Единственное, что при определении абстрактных классов используется ключевое слово abstract :

Abstract class Human { public int Length { get; set; } public double Weight { get; set; } }

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

Human h = new Human();

Зачем нужны абстрактные классы? Допустим, в нашей программе для банковского сектора мы можем определить две основных сущности: клиента банка и сотрудника банка. Каждая из этих сущностей будет отличаться, например, для сотрудника надо определить его должность, а для клиента - сумму на счете. Соответственно клиент и сотрудник будут составлять отдельные классы Client и Employee. В то же время обе этих сущности могут иметь что-то общее, например, имя и фамилию, какую-то другую общую функциональность. И эту общую функциональность лучше вынести в какой-то отдельный класс, например, Person, который описывает человека. То есть классы Employee (сотрудник) и Client (клиент банка) будут производными от класса Person. И так как все объекты в нашей системе будут представлять либо сотрудника банка, либо клиента, то напрямую мы от класса Person создавать объекты не будем. Поэтому имеет смысл сделать его абстрактным:

Abstract class Person { public string Name { get; set; } public Person(string name) { Name = name; } public void Display() { Console.WriteLine(Name); } } class Client: Person { public int Sum { get; set; } // сумма на счету public Client(string name, int sum) : base(name) { Sum = sum; } } class Employee: Person { public string Position { get; set; } // должность public Employee(string name, string position) : base(name) { Position = position; } }

Затем мы сможем использовать эти классы:

Client client = new Client("Tom", 500); Employee employee = new Employee ("Bob", "Apple"); client.Display(); employee.Display();

Или даже так:

Person client = new Client("Tom", 500); Person employee = new Employee ("Bob", "Операционист");

Но мы НЕ можем создать объект Person, используя конструктор класса Person:

Person person = new Person ("Bill");

Однако несмотря на то, что напрямую мы не можем вызвать конструктор класса Person для создания объекта, тем не менее конструктор в абстрактных классах то же может играть важную роль, в частности, инициализировать некоторые общие для производных классов переменные и свойства, как в случае со свойством Name. И хотя в примере выше конструктор класса Person не вызывается, тем не менее производные классы Client и Employee могут обращаться к нему.

Абстрактные члены классов

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

  • Свойства

    Индексаторы

Абстрактные члены классов не должны иметь модификатор private. При этом производный класс обязан переопределить и реализовать все абстрактные методы и свойства, которые имеются в базовом абстрактном классе. При переопределении в производном классе такой метод или свойство также объявляются с модификатором override (как и при обычном переопределении виртуальных методов и свойств). Также следует учесть, что если класс имеет хотя бы одный абстрактный метод (или абстрактные свойство, индексатор, событие), то этот класс должен быть определен как абстрактный .

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

Абстрактные методы

Например, сделаем в примере выше метод Display абстрактным:

Abstract class Person { public string Name { get; set; } public Person(string name) { Name = name; } public abstract void Display(); } class Client: Person { public int Sum { get; set; } // сумма на счету public Client(string name, int sum) : base(name) { Sum = sum; } public override void Display() { Console.WriteLine($"{Name} имеет счет на сумму {Sum}"); } } class Employee: Person { public string Position { get; set; } // должность public Employee(string name, string position) : base(name) { Position = position; } public override void Display() { Console.WriteLine($"{Position} {Name}"); } }

Абстрактные свойства

Следует отметить использование абстрактных свойств. Их определение похоже на определение автосвойств. Например:

Abstract class Person { public abstract string Name { get; set; } } class Client: Person { private string name; public override string Name { get { return "Mr/Ms. " + name; } set { name = value; } } } class Employee: Person { public override string Name { get; set; } }

В классе Person определено абстрактное свойство Name. Оно похоже на автосвойство, но это не автосвойство. Так как данное свойство не должно иметь реализацию, то оно имеет только пустые блоки get и set. В производных классах мы можем переопределить это свойство, сделав его полноценным свойством (как в классе Client), либо же сделав его автоматическим (как в классе Employee).

Отказ от реализации абстрактных членов

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

Abstract class Person { public abstract string Name { get; set; } } abstract class Manager: Person { }

Пример абстрактного класса

Xрестоматийным примером является система геометрических фигур. В реальности не существует геометрической фигуры как таковой. Есть круг, прямоугольник, квадрат, но просто фигуры нет. Однако же и круг, и прямоугольник имеют что-то общее и являются фигурами:

// абстрактный класс фигуры abstract class Figure { // абстрактный метод для получения периметра public abstract float Perimeter(); // абстрактный метод для получения площади public abstract float Area(); } // производный класс прямоугольника class Rectangle: Figure { public float Width { get; set; } public float Height { get; set; } public Rectangle(float width, float height) { this.Width = width; this.Height = height; } // переопределение получения периметра public override float Perimeter() { return Width * 2 + Height * 2; } // переопрелеление получения площади public override float Area() { return Width * Height; } }

Доброго времени суток, хабравчане!

Следующий пост является изложением моих размышлений на тему природы классов и АТД. Эти размышления дополнены интересными цитатами из книг гуру разработки программного обеспечения

Введение

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

Что же означает слово “абстрактный”? В первую очередь понятие “абстрактность” означет сосредоточение внимания на чем-то важном и, при этом, нам нужно отвлечься от неважных, на данный момент, деталей. Определение абстрактности хорошо раскрыто в книге Гради Буча (“Grady Booch”). Звучит определение так:

Абстракция – это выделение и придание совокупности объектов общих свойств, которые определяют их концепутальные границы и отличают от всех других видов объектов.
Иными словами, абстракция позволяет “пролить свет” на нужные нам данные объектов и, при этом, “затенить” те данные, которые нам не важны.

Итак, что же будет, если слить понятия “тип данных” и “абстракция” воедино? Мы получим тип данных, который предоставляет нам некий набор операций, обеспечивающих поведение объектов этого типа данных, а также этот тип данных будет скрывать те данные, с помощью которых реализовано данное поведение. Отсюда, приходим к понятию АТД:

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

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

Преимущества АТД

Использование АТД имеет массу преимуществ (все описанные преимущества можно найти в книге Стива Макконнелла «Совершенный код”):

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

Стив Макконнелл рекомендует представлять в виде АТД низкоуровнеые типы данных, такие как стек или список. Спросите себя, что представляет собой этот список. Если он представляет список сотрудников банка, то и рассматривайте АТД как список сотрудников банка.

Итак, мы разобрались, что такое АТД и назвали преимущества его применения. Теперь стоит отметить, что при разработке классов в ООП следует думать, в первую очередь, об АТД. При этом, как сказал Стив МакКоннелл, Вы программируете не на языке, а с помощью языка. Т.е Вы будете программировать выходя за рамки языка, не ограничиваясь мыслями в терминах массивов или простых типов данных. Вместо этого Вы будете думать на высоком уровне абстракции (например, в терминах электронных таблицах или списков сотрудников). Класс – это не что иное как дополнение и способ реализации концепции АТД. Мы можем даже представить класс в виде формулы:
Класс = АТД + Наследование + Полиморфизм.
Так почему же следут думать об АТД, при разработке классов. Потому что, сперва мы должны решить какие операции будут составлять интерфейс будущего класса, какие данные скрыть, а к каким предоставить открытый доступ. Мы должны подумать об обеспечении высокой информативности интерфейса, легкости оптимизации и проверки кода, а также о том, как бы нам предоставить правильную абстракцию, чтобы можно было думать о сущностях реального мира, а не о низкоуровнеых деталях реализации. Я считаю, что именно после определения АТД мы должны думать о вопросах наследования и полиморфизма.

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

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

Использованные источники:

Стив Макконнелл – “Совершенный код”;
Роберт Седжвик – «Алгоритмы на Java».

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

Все вычислительные системы основаны на уровнях абстракции: определенные физические свойства кремния и других материалов позволяют принять абстрактную модель бита, который может принимать двоичные значения 0-1; затем на динамических свойствах значений определенного набора битов строится абстрактная модель машины; далее, на основе принципа работы машины под управлением программы на машинном языке строится абстрактная модель языка программирования; и, наконец, строится абстрактное понятие алгоритма, реализуемое в виде программы на языке C++. Абстрактные типы данных дают возможность продолжать этот процесс дальше и разрабатывать абстрактные механизмы для определенных вычислительных задач на более высоком уровне, чем это обеспечивается системой C++, разрабатывать абстрактные механизмы , ориентированные на конкретные приложения и подходящие для решения задач в многочисленных прикладных областях, а также создавать абстрактные механизмы более высокого уровня, в которых используются эти базовые конструкции. Абстрактные типы данных предоставляют в наше распоряжение расширяемый до бесконечности набор инструментальных средств для решения все новых и новых задач.

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

Для разработки нового уровня абстракции потребуется (1) определить абстрактные объекты, с которыми необходимо манипулировать, и операции , которые должны выполняться над ними; (2) представить данные в некоторой структуре данных и реализовать операции ; (3) и (самое главное) обеспечить, чтобы эти объекты было удобно использовать для решения прикладных задач. Эти пункты применимы и к простым типам данных, так что базовые механизмы для поддержки типов данных, которые были рассмотрены в "Элементарные структуры данных" , можно адаптировать для наших целей. Однако язык C++ предлагает важное расширение механизма структур, называемое классом ( class ). Классы исключительно полезны при создании уровней абстракции и поэтому рассматриваются в качестве основного инструмента, который используется для этой цели в оставшейся части книги.

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

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

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

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

В отличие от этого, программа 4.1 содержит реализацию абстрактного типа данных, соответствующего типу данных из программы 3.3, но с использованием класса языка C++, в котором сразу определены как данные, так и связанные с ними операции . Программа 4.2 является клиентской программой, работающей с этим типом данных. Эти две программы выполняют те же самые вычисления, что и программы 3.3 и 3.8. Они иллюстрируют ряд основных свойств классов, которые мы сейчас рассмотрим.

Когда мы пишем в программе определение наподобие int i, мы указываем системе зарезервировать область памяти для данных (встроенного) типа int , к которой можно обращаться по имени i. В языке C++ для подобных сущностей имеется термин объект . При записи в программе такого определения, как POINT p, говорят, что создается объект класса POINT , к которому можно обращаться по имени p. В нашем примере каждый объект содержит два элемента данных, с именами x и у. Как и в случае структур, к ним можно обращаться по именам вроде p.y.

Элементы данных x и у называются данными-членами класса. В классе могут быть также определены функции-члены, которые реализуют операции , связанные с этим типом данных. Например, класс , определенный в программе 4.1, имеет две функции-члена с именами POINT и distance .

Клиентские программы, такие как программа 4.2, могут вызывать функции-члены, связанные с объектом, указывая их имена точно так же, как и имена данных, находящихся в какой-нибудь структуре struct. Например, выражение p.distance(q) вычисляет расстояние между точками p и q (такое же расстояние должен возвращать и вызов q.distance(p) ). Функция POINT() - первая функция в программе 4.1 - является особой функцией-членом, называемой конструктором: у нее такое же имя, как и у класса, и она вызывается тогда, когда требуется создать объект этого класса.

Программа 4.1. Реализация класса POINT (точка)

В этом классе определен тип данных , который состоит из набора значений, представляющих собой "пары чисел с плавающей точкой" (предполагается, что они интерпретируются как точки на декартовой плоскости), а также две функции-члена, определенные для всех экземпляров класса POINT : функция POINT() , которая является конструктором, инициализирующим координаты случайными значениями от 0 до 1, и функция distance(POINT) , вычисляющая расстояние до другой точки. Представление данных является приватным ( private ), и обращаться к нему или модифицировать его могут только функции-члены. Сами функции-члены являются общедоступными ( public ) и доступны для любого клиента. Код можно сохранить, например, в файле с именем POINT .cxx.

#include class POINT { private: float x, у; public: POINT() { x = 1.0*rand()/RAND_MAX; у = 1.0*rand()/RAND_MAX; } float distance(POINT a) { float dx = x-a.x, dy = y-a.y; return sqrt(dx*dx + dy*dy); } };

Программа 4.2. Программа-клиент для класса POINT (нахождение ближайшей точки)

Эта версия программы 3.8 является клиентом, который использует АТД POINT , определенный в программе 4.3. Операция new создает массив объектов POINT (вызывая конструктор POINT () для инициализации каждого объекта случайными значениями координат). Выражение a[i].distance(a[j]) вызывает для объекта a[i] функцию-член distance с аргументом a[j] .

#include #include #include "POINT.cxx" int main(int argc, char *argv) { float d = atof(argv); int i, cnt = 0, N = atoi(argv); POINT *a = new POINT[N]; for (i = 0; i < N; i++) for (int j = i+1; j < N; j++) if (a[i].distance(a[j]) < d) cnt+ + ; cout << cnt << " пар в радиусе " << d << endl; }

Определение POINT p в программе-клиенте приводит к выделению области памяти под новый объект и затем (с помощью функции POINT() ) к присвоению каждому из двух его элементов данных случайного значения в диапазоне от 0 до 1.

Этот стиль программирования, который иногда называется объектно-ориентированным программированием, полностью поддерживается конструкцией class языка C++. Класс можно считать расширением понятия структуры, где не только объединяются данные, но и определяются операции с этими данными. Может существовать много разных объектов, принадлежащих одному классу, но все они подобны в том, что их данные-члены могут принимать один и тот же набор значений, и с этими данными-чле-нами может выполняться одна и та же совокупность операций - в общем, это экземпляры одного и того же типа данных. В объектно-ориентированном программировании объекты предназначены для обработки своих данных-членов (в отличие от использования независимых функций для обработки данных, хранимых в объектах).

Мы рассматриваем описанный выше пример небольшого класса просто чтобы познакомиться с основными чертами классов; поэтому он далеко не полон. В реальном коде для класса точки у нас будет намного больше операций. Например, в программе 4.1 отсутствуют даже операции , позволяющие узнавать значения координат x и y . Как мы увидим, добавление этих и других операций - довольно простая задача. В части 5 мы более подробно рассмотрим классы для точки и других геометрических абстракций, например, линий и многоугольников.

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

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

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

Рассмотрим реализацию понятия даты с использованием struct для того, чтобы определить представление даты date и множества функций для работы с переменными этого типа:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

#include
struct date
{
int month; // месяц
int day; // день
int year; // год
};
void set_date(date* f, int d, int m, int y)
{
f->day = d;
f->month = m;
f->year = y;
}
void print_date(date* f)
{
printf("%d.%d.%d" , f->day, f->month, f->year);
}
int main()
{
date today;
set_date(&today, 2, 4, 2014);
print_date(&today);
getchar();
return 0;
}


Результат выполнения

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

#include
struct date
{
int month; // месяц
int day; // день
int year; // год
void set_date(int d, int m, int y)
{
day = d; month = m; year = y;
}
void print_date(void );
};
void date::print_date(void )
{
printf("%d.%d.%d" , day, month, year);
}
int main()
{
date today;
today.set_date(2, 4, 2014);
today.print_date();
getchar();
return 0;
}

Функции-члены и данные-члены

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

Определение функций-членов может осуществляться двумя способами:

  • описание функции непосредственно при описании структуры;
  • описание функции вне структуры.

Функции-члены, которые определены внутри структуры, являются неявно встроенными (). Как правило, только короткие, часто используемые функции-члены, должны определяться внутри структуры:

void set(int m, int d, int y) {month = m; day = d; year = y;};



Поскольку разные структуры могут иметь функции-члены с одинаковыми именами, при определении функции члена необходимо указывать имя структуры, связывая их с помощью оператора разрешения контекста (двойное двоеточие)::
тип АТД::имя(список аргументов) {
тело функции-члена; }

  • тип — тип возвращаемого значения функции-члена
  • АТД — имя абстрактного типа данных (имя структуры или класса)
  • имя — имя функции-члена

void date::print_date(void )
{ printf("%d.%d.%d" ,day, month, year);}

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

Функции-члены, одной и той же структуры могут быть полиморфными, то есть перегруженными.

Права доступа

Концепция структуры в языке С++ (в отличие от Си) позволяет членам структуры быть общими, частными или защищенными:

  • public – общие;
  • private – частные;
  • protected – защищенные.

Использование ключевого слова protected связано с понятием наследования .

Использование ключевого слова private ограничивает доступ к членам, которые следуют за этой конструкцией. Члены private могут использоваться только несколькими категориями функций, в привилегии которых входит доступ к этим членам. В основном это функции-члены той же структуры.
Ключевое слово public образует интерфейс к объекту структуры.

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

Изменим структуру date так, чтобы скрыть представление данных (инкапсуляция данных):

1
2
3
4
5
6
7
8

struct date
{
private :
int month, day, year;
public :
void set(int , int , int );
void print();
};

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

Все вычислительные системы основаны на уровнях абстракции: определенные физические свойства кремния и других материалов позволяют принять абстрактную модель бита, который может принимать двоичные значения 0-1; затем на динамических свойствах значений определенного набора битов строится абстрактная модель машины; далее, на основе принципа работы машины под управлением программы на машинном языке строится абстрактная модель языка программирования; и, наконец, строится абстрактное понятие алгоритма, реализуемое в виде программы на языке C++. Абстрактные типы данных дают возможность продолжать этот процесс дальше и разрабатывать абстрактные механизмы для определенных вычислительных задач на более высоком уровне, чем это обеспечивается системой C++, разрабатывать абстрактные механизмы , ориентированные на конкретные приложения и подходящие для решения задач в многочисленных прикладных областях, а также создавать абстрактные механизмы более высокого уровня, в которых используются эти базовые конструкции. Абстрактные типы данных предоставляют в наше распоряжение расширяемый до бесконечности набор инструментальных средств для решения все новых и новых задач.

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

Для разработки нового уровня абстракции потребуется (1) определить абстрактные объекты, с которыми необходимо манипулировать, и операции , которые должны выполняться над ними; (2) представить данные в некоторой структуре данных и реализовать операции ; (3) и (самое главное) обеспечить, чтобы эти объекты было удобно использовать для решения прикладных задач. Эти пункты применимы и к простым типам данных, так что базовые механизмы для поддержки типов данных, которые были рассмотрены в "Элементарные структуры данных" , можно адаптировать для наших целей. Однако язык C++ предлагает важное расширение механизма структур, называемое классом ( class ). Классы исключительно полезны при создании уровней абстракции и поэтому рассматриваются в качестве основного инструмента, который используется для этой цели в оставшейся части книги.

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

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

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

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

В отличие от этого, программа 4.1 содержит реализацию абстрактного типа данных, соответствующего типу данных из программы 3.3, но с использованием класса языка C++, в котором сразу определены как данные, так и связанные с ними операции . Программа 4.2 является клиентской программой, работающей с этим типом данных. Эти две программы выполняют те же самые вычисления, что и программы 3.3 и 3.8. Они иллюстрируют ряд основных свойств классов, которые мы сейчас рассмотрим.

Когда мы пишем в программе определение наподобие int i, мы указываем системе зарезервировать область памяти для данных (встроенного) типа int , к которой можно обращаться по имени i. В языке C++ для подобных сущностей имеется термин объект . При записи в программе такого определения, как POINT p, говорят, что создается объект класса POINT , к которому можно обращаться по имени p. В нашем примере каждый объект содержит два элемента данных, с именами x и у. Как и в случае структур, к ним можно обращаться по именам вроде p.y.

Элементы данных x и у называются данными-членами класса. В классе могут быть также определены функции-члены, которые реализуют операции , связанные с этим типом данных. Например, класс , определенный в программе 4.1, имеет две функции-члена с именами POINT и distance .

Клиентские программы, такие как программа 4.2, могут вызывать функции-члены, связанные с объектом, указывая их имена точно так же, как и имена данных, находящихся в какой-нибудь структуре struct. Например, выражение p.distance(q) вычисляет расстояние между точками p и q (такое же расстояние должен возвращать и вызов q.distance(p) ). Функция POINT() - первая функция в программе 4.1 - является особой функцией-членом, называемой конструктором: у нее такое же имя, как и у класса, и она вызывается тогда, когда требуется создать объект этого класса.

Программа 4.1. Реализация класса POINT (точка)

В этом классе определен тип данных , который состоит из набора значений, представляющих собой "пары чисел с плавающей точкой" (предполагается, что они интерпретируются как точки на декартовой плоскости), а также две функции-члена, определенные для всех экземпляров класса POINT : функция POINT() , которая является конструктором, инициализирующим координаты случайными значениями от 0 до 1, и функция distance(POINT) , вычисляющая расстояние до другой точки. Представление данных является приватным ( private ), и обращаться к нему или модифицировать его могут только функции-члены. Сами функции-члены являются общедоступными ( public ) и доступны для любого клиента. Код можно сохранить, например, в файле с именем POINT .cxx.

#include class POINT { private: float x, у; public: POINT() { x = 1.0*rand()/RAND_MAX; у = 1.0*rand()/RAND_MAX; } float distance(POINT a) { float dx = x-a.x, dy = y-a.y; return sqrt(dx*dx + dy*dy); } };

Программа 4.2. Программа-клиент для класса POINT (нахождение ближайшей точки)

Эта версия программы 3.8 является клиентом, который использует АТД POINT , определенный в программе 4.3. Операция new создает массив объектов POINT (вызывая конструктор POINT () для инициализации каждого объекта случайными значениями координат). Выражение a[i].distance(a[j]) вызывает для объекта a[i] функцию-член distance с аргументом a[j] .

#include #include #include "POINT.cxx" int main(int argc, char *argv) { float d = atof(argv); int i, cnt = 0, N = atoi(argv); POINT *a = new POINT[N]; for (i = 0; i < N; i++) for (int j = i+1; j < N; j++) if (a[i].distance(a[j]) < d) cnt+ + ; cout << cnt << " пар в радиусе " << d << endl; }

Определение POINT p в программе-клиенте приводит к выделению области памяти под новый объект и затем (с помощью функции POINT() ) к присвоению каждому из двух его элементов данных случайного значения в диапазоне от 0 до 1.

Этот стиль программирования, который иногда называется объектно-ориентированным программированием, полностью поддерживается конструкцией class языка C++. Класс можно считать расширением понятия структуры, где не только объединяются данные, но и определяются операции с этими данными. Может существовать много разных объектов, принадлежащих одному классу, но все они подобны в том, что их данные-члены могут принимать один и тот же набор значений, и с этими данными-чле-нами может выполняться одна и та же совокупность операций - в общем, это экземпляры одного и того же типа данных. В объектно-ориентированном программировании объекты предназначены для обработки своих данных-членов (в отличие от использования независимых функций для обработки данных, хранимых в объектах).

Мы рассматриваем описанный выше пример небольшого класса просто чтобы познакомиться с основными чертами классов; поэтому он далеко не полон. В реальном коде для класса точки у нас будет намного больше операций. Например, в программе 4.1 отсутствуют даже операции , позволяющие узнавать значения координат x и y . Как мы увидим, добавление этих и других операций - довольно простая задача. В части 5 мы более подробно рассмотрим классы для точки и других геометрических абстракций, например, линий и многоугольников.

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




Top