Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Методичка ООП.doc
Скачиваний:
23
Добавлен:
08.11.2018
Размер:
1.4 Mб
Скачать
  1. Инкапсуляция

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

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

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

В объектно-ориентированных языках программирования имеется особая структура, которая называется классом. В Object Pascal классом называется такая структура, которая может иметь в своем составе поля, методы и свойства. Определенный таким образом тип данных также называют объектным типом. В приведенном примере класс с именем TMyObject имеет в своем составе поле MyField целого типа и метод MyMethod – функцию без параметров, возвращающую целый тип:

Type TMyObject = class MyField: integer; Function MyMethod: integer; End;

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

Переменные типа класс называют экземплярами класса или объектами. Объявляются они обычным образом:

Var Obj: TMyObject;

Это объявление выглядит как объявление статической переменной, но на самом деле переменная типа экземпляр класса в языке Object Pascal всегда является указателем. Память под сам объект распределяется отдельно. Как это осуществляется, будут рассмотрены позже, в разделе «Конструкторы и деструкторы». Статических объектов в языке Object Pascal нет, и это позволяет опускать символ «^», используемый при объявлении обычных указателей.

Между терминами класс и объект существует четкая граница: класс – это описание, объект – то, что создано в соответствии с этим описанием. Традиционно имена классов в языке Object Pascal начинают с буквы «Т», от «Type».

Доступ к полям и методам объекта синтаксически выглядит так же, как доступ к полям записи:

Obj.MyMethod := 10; A := Obj.MyMethod;

Символ «^», который используется при работе с указателями, при этом также опускается, хотя и подразумевается.

Рассмотрим, из чего состоит класс. Поля класса аналогичны полям записи. Это – данные, уникальные для каждого созданного в программе экземпляра класса. Описанный выше класс TMyObject имеет одно поле – MyField. В отличие от полей, методы у двух объектов одного класса общие. Методы – это процедуры или функции, описанные внутри класса и предназначенные для операций над его полями. От обычных процедур и функций методы отличаются тем, что при вызове им неявно передается указатель на тот объект, который их вызвал. Поэтому обрабатываться будут поля именно того объекта, который вызвал метод. Внутри метода указатель на вызвавший его объект доступен под зарезервированным именем Self.

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

Рассмотрим пример. Объявим класс следующим образом:

TSimpleObject = class Fx, Fy: integer; Procedure SetXY(x,y: integer); Procedure GetXY(var x,y: integer); End;

Объявим три объекта этого класса:

Var O1,O2,O3: TSimpleObject;

Предположим, что все три объекта созданы. Тогда, анализируя память, отведенную под выполняющуюся программу (рис. 3), обнаружим в ней: три указателя – O1, O2, O3, три экземпляра полей класса, на которые указывают соответствующие указатели, и единственный экземпляр методов класса.

При вызове метода класса, например:

O2.SetXY(10,20)

в него неявно передается указатель на тот объект, для которого он вызван. В данном случае – O2. И указатель self внутри метода SetXY будет указывать именно на этот объект. В данном случае – O2.

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

Function TMyObject.MyMethod; begin ... end;

Доступ к элементам класса внутри метода класса возможен с использованием указателя self, например:

Function TMyObject.MyMethod; begin Self.Fx := 10;

... end;

Указатель Self можно опускать. Оба эти варианта семантически эквивалентны:

Function TMyObject.MyMethod; begin Fx := 10; //эквивалентно Self.Fx := 10; ... end;

Дополним рассмотренный выше пример реализацией методов:

TSimpleObject = class Fx, Fy: integer; Procedure SetXY(x,y: integer); Procedure GetXY(var x,y: integer); End;

Procedure TSimpleObject.SetXY(x,y: integer); //также неявно передается указатель Self Begin Fx := x; //указатель Self перед Fx опущен, но подразумевается Self.Fy := y; //указатель Self перед Fу есть, но мог бы быть опущен End;

Procedure TSimpleObject.GetXY(var x,y: integer); Begin x := Self.Fx; //указатель Self перед Fx есть, но мог бы быть опущен y := Self.Fy; //указатель Self перед Fy опущен, но подразумевается End;

Var O1, O2, O3: TSimpleObject; //объявляем три объекта – то есть набора полей x1,y1: integer;

Begin ... //предположим, что все три объекта созданы O1.SetXY(10,20); //устанавливаем значения полей первого объекта O2.SetXY(11,22); //устанавливаем значения полей второго объекта O3.SetXY(-10,0); //устанавливаем значения полей третьего объекта O2.GetXY(x1,y1); //x1,y1 примут значения 11,22 т.к. работаем с объектом О2 ... End;

Ни в коем случае не следует указывать в методах класса перед именами полей имя самого класса или имя конкретного объекта класса. Связь с конкретным объектом класса происходит через указатель self. Если же указать имя конкретного объекта, то данный метод будет работать только для единственного экземпляра класса, а именно для указанного.

Указатель self явно используется редко, хотя и постоянно подразумевается. Явное использование указателя self необходимо, например, когда объект создает подчиненный себе объект другого класса и передает ему указатель на себя, чтобы предоставить возможность вызывать собственные методы.

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

unit Unit1;

interface //секция интерфейса модуля

type

//Описание класса TSimpleObject = class Fx, Fy: integer; Procedure SetXY(x,y: integer); Procedure GetXY(var x,y : integer); End;

Var O: TSimpleObject;

implementation //секция реализации модуля

//Реализация методов класса Procedure TSimpleObject.SetXY(x,y: integer); //также неявно передается указатель Self Begin Fx := x; //указатель Self перед Fx опущен, но подразумевается Self.Fy := y; //указатель Self перед Fу есть, но мог бы быть опущен End;

...

end.

Разрешено опережающее объявление классов, как в следующем примере:

type

TFirstObject = class; //опережающее объявление класса TFirst

TSecondObject = class //используем его имя до полного объявления First: TFirstObject; //класс TSecondObject имеет //в качестве поля объект класса TFirstObject

... end;

TFirstObject = class //полное объявление класса TFirst Second: TSecondObject; //класс TFirstObject имеет //в качестве поля объект класса TSecondObject

... end;

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

TObStack = class procedure InStack(Elem: TElem); function OutStack(var Elem: TElem): boolean; ... end;

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

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

procedure InStack(var Stack: TStack; var TopStack: integer; Elem: TElem); // Stack: TStack – массив // TopStack: integer – индекс текущей вершины стека // Elem: TElem – добавляемый в стек элемент

function OutStack(var Stack: TStack; var TopStack: integer; var Elem: TElem): boolean; // Stack: TStack – массив // TopStack: integer – индекс текущей вершины стека // Elem: TElem – добавляемый в стек элемент

При использовании динамики потребуется передавать динамический указатель на вершину стека:

procedure InStack(var TopStack: TTopStack; Elem: TElem); // TopStack: TTopStack динамический указатель на вершину стека // Elem: TElem – добавляемый в стек элемент

function OutStack(var TopStack: TTopStack; var Elem: TElem): boolean; // TopStack: TTopStack динамический указатель на вершину стека // Elem: TElem – добавляемый в стек элемент

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

Рассмотрим, как будут выглядеть классы, реализующие статический и динамический стеки:

Стек в массиве:

interface

type

TSObStack = class //массив элементов типа TElem Stack: array[1..100] of TElem; //индекс вершины стека Top: integer; procedure InStack(Elem: TElem); function OutStack(var Elem: TElem): boolean; ... end;

implementation

procedure TSObStack.InStack(Elem: TElem);1 begin Top := Top+1; Stack[Top] := Elem; end;

function TSObStack.OutStack(var Elem: TElem): boolean; begin if (Top>0) then begin Elem := Stack[Top]; Top := Top-1; OutStack := true; end else OutStack := false; end;

...

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

Стек на основе динамики

interface

TRef = ^TERef; TERef = record Next: TRef; Elem: TElem; End;

TDObStack = class Top: TRef //динамический указатель на вершину стека procedure InStack(Elem: TElem); function OutStack(var Elem: TElem): boolean; ... end;

implementation

procedure TDObStack.InStack(Elem: TElem); var r: TRef; begin new(r); r^.Elem := Elem; r^.Next := Top; Top := r; end;

function TDObStack.OutStack(var Elem: TElem): boolean; var r: TRef; begin if (Top<>nil) then begin Elem := Top^.Elem; r := Top^.Next; dispose(Top); Top := r; OutStack := true; end else OutStack := false; end; ...

Описание класса также не является полным.

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

Var DStack: TDObStack; SStack: TSObStack; ... a: TElem; begin ... //создание объектов

//Добавление элементов в стеки выполняется одинаково DStack.InStack(a); //добавление в динамический стек SStack.InStack(a); //добавление в статический стек

... //работа с объектами

//Чтение элементов из стеков тоже выполняется одинаково if not(DStack.OutStack(a) writeln(‘Динамический стек пуст’); if not(SStack.OutStack(a) writeln(‘Статический стек пуст’);

... //уничтожение объектов end;

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

Итак, мы рассмотрели реализацию первой части определения инкапсуляции – объединение данных и кода. Во второй половине определения оговаривается, что непосредственный доступ к данным не рекомендован. Действительно, данные представляют собой особенности реализации и должны быть скрыты от внешнего мира. Они изменяются в результате вызова методов, которые представляют собой интерфейс. Если в рассмотренном выше примере будет позволен свободный доступ к данным – к массиву или к динамическим структурам, это сведет на нет все преимущества, и вызовет высокую вероятность ошибки. Представьте, например, что будет, если полю Top объекта стека в массиве присвоить значение -200. Для сокрытия особенностей реализации используется механизм так называемых областей видимости. Рассмотрим пока две из них: общие (public) и личные (private).

Директива, определяющая видимость указывается перед описанием элемента класса и относится ко всем элементам класса до следующей директивы области видимости. По умолчанию элементы класса в языке Object Pascal имеют область видимости public.

TSampleObject = class // элементы с областью видимости public ... private // элементы с областью видимости private ... public // элементы с областью видимости public ... private // опять элементы с областью видимости private ... end;

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

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

Дополним описание классов динамического и статического стека директивами областей видимости:

TSObStack = class private // это особенности реализации, они должны быть скрыты Stack: array[1..100] of TElem; Top: integer; public //это два интерфейсных метода, они должны быть доступны из //внешнего мира procedure InStack(Elem: TElem); function OutStack(var Elem: TElem): boolean; ... end;

TDObStack = class private // это особенности реализации, они должны быть скрыты Top: Tref public //это два интерфейсных метода, они должны быть доступны из //внешнего мира procedure InStack(Elem: TElem); function OutStack(var Elem: TElem): boolean; ... end;

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

    1. Инкапсуляция в C++

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

Например:

struct CMyClass1 { int MyField; int MyMethod(); };

Или:

class CMyClass2 { int MyField; int MyMethod(); };

Имя класса становится идентификатором нового типа. В языке C++ принято давать классам имена, начинающиеся с «C» от class.

Допускается неполное объявление класса, которое разрешает некоторые ссылки к именам классов до того, как классы будут полностью определены:

class CSimple;

В языке C++ в отличие от Object Pascal возможно создавать как статические, так и динамические объекты:

struct CSample { int Data; int ReadData(); };

Sample s1; // статическая переменная Sample s3[10]; // статический массив Sample* s2; // указатель

Если объект статический, то обращаться к его элементам следует, указав имя объекта и имя соответствующего элемента через «.» :

s1.ReadData();

Если это указатель, указывающий на уже созданный объект, то к его элементам обращаются с помощью оператора «->» :

s2->ReadData();

При определении методов класса, называемых также функциями-элементами, указывается имя класса через «::». Например:

struct CMyClass { int MyField; int MyMethod(); };

int CMyClass::MyMethod() { return MyField; }

Также можно определить метод непосредственно при объявлении класса:

struct CMyClass { int MyField; int MyMethod(){return MyField;}; };

Для того чтобы связать методы с данными, соответствующими конкретному экземпляру класса, в функцию-элемент передается скрытый аргумент, представляющий собой указатель на объект, для которого вызывается функция-элемент. В самой функции этот указатель доступен под именем this (аналог self в Object Pascal).

int CMyClass::MyMethod() { return this->MyField; //аналогично return MyField; }

При обращении к элементам класса в функциях-элементах «this», как правило, опускается, но подразумевается.

Как уже было сказано выше, вторая половина определения инкапсуляции требует сокрытия данных. Так же, как и в Object Pascal, это реализовано через области видимости.

Имеются три ключевых слова, определяющих возможности доступа к элементам класса:

public: - элементы, следующие за этим ключевым словом, доступны из любой точки программы;

private: - элементы доступны только методам того же класса (а не во всем модуле, как в Object Pascal);

protected: - будет рассмотрена позднее.

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

Например:

class CDate { private: int month, day, year; public: void set(int m, int d, int y); void get(int* m, int* d, int* y); void print(); };

В этом примере данные имеют видимость private, то есть, доступны только из методов класса. А методы имеют область видимости public, что позволяет вызывать их из любого места программы.

Область видимости по умолчанию зависит от ключевого слова, с которым был объявлен класс. Если класс объявлен как class, то по умолчанию его элементы имеют область видимости private. Если класс объявлен как struct или union, то все его элементы по умолчанию являются общедоступными. Поэтому, строго говоря, только структура, объявленная как class, может называться классом. Struct и union являются просто структурами, позволяющими объединять в своем составе код и данные.