Класс в ООП

Класс в ООП - это абстрактный тип данных, который включает в себя не только данные, но и функции и процедуры. 

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

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

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

Определение метода при описании класса.

Объявление метода при описании, а его описание - при вызове.  

Методы, определенные внутри класса, являются неявно встроенными.

Пример. 

class A

    {

    int x, y;

    int sum ( ) { return (x + y) ; }

    } ;  

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

Пример. 

Class B

    {

    int x, y;

   int sum ( )

    } ;

int B::sum ( ) { return (x + y) ; } 

Также как и структуры, классы можно задавать либо статически, либо динамически.

Например,

    статически - Toplist    foo;

    динамически - Toplist *bar ; bar=new Toplist; 

Для статических  и динамических классов применимы  те же правила и принципы, что  и для статических и динамических переменных. 

Объект - экземпляр  класса.

Наследование. 
 

Классы содержат данные и методы. В ООП методы и данные одного класса могут передаваться другим классам, т.е. объекты могут  наследовать свойства друг друга. Класс  наследует свойства другого класса, обладает теми же возможностями, что  и класс, от которого он порожден. Этот принцип называется наследованием (inheritance). Порожденный класс называется потомком (descendant), а тот, от кого он порожден - предком (ancestor). Благодаря новым свойствам, которыми дополняется потомок, порожденный класс может обладать большими возможностями, чем его предок. 

Механизм наследования обеспечивает возможность многократного  применения программного кода. Таким  образом, классы могут быть представлены в виде иерархии. Библиотека VLC (Visual Component Library) в Delphi как раз и является такой иерархической системой классов.  

Наследование - механизм, позволяющий объектам класса наследовать  характеристики (данные и методы) более  простых и общих типов (классов); - средство получения новых классов  из существующих. 
 
 

Инкапсуляция.  
 

Инкапсуляция (encapsulation) - объединение данных с функциями, предназначенными для манипулирования этими данными (т.е. поведением) в новом типе - КЛАССЕ . 

Пример.

Представьте, что  Вам надо написать программу, которая  выполняла бы дует духового и струнного инструментов. Для этого определите классы Духовой инструмент и Струнный инструмент. Для класса Духовой инструмент определите, что имеется мундштук и что в него надо дуть, чтоб получить звук. Для класса Струнный инструмент определите, что по струнам надо ударять, чтоб получить звук. Оба класса уже способны "играть музыку", и что это свойство было унаследовано от предка. Они унаследовали метод PlayMusic, который был объявлен и реализован как метод класса Музыкальный инструмент. 

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

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

Полиморфизм.  
 

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

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

Например, метод класса Музыкальных инструментов - PlayMusicForAnOrchestra - может быть определен как общим метод, который может использоваться с любой категорией инструментов. Этот метод написан таким образом, что не важно, какой именно инструмент получает задание играть, однако для классов, описывающих конкретные инструменты, данный метод должен быть переопределен (override), что даст возможность определить конкретные действия, учитывая особенности конкретного инструмента.

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

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

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

Инкапсуляция –  это способ представления класса в виде «черного ящика». Это значит, что конечному пользователю класса (программисту) доступен лишь определенный набор функций и переменных для  работы с классом. Часто ограничение  доступа применяется для записи значений в переменные класса через  функции, при запрещенном непосредственном доступе к переменным.  

Класс в языке  С++ задается с помощью ключевого слова class, за которым следует его имя и в фигурных скобках {} дается его описание. После определения класса ставится точка с запятой. Ниже приведен пример описания класса для хранения координат графических примитивов:  

class CPos

{

int sp_x, sp_y; //координата начала

int ep_x, ep_y; //координата конца

};  

Каждый класс имеет  специальные функции, которые называются конструктор и деструктор. Конструктор  класса вызывается всякий раз, когда  объект создается в памяти ЭВМ  и служит обычно для инициализации  данных класса. Конструктор имеет  то же имя, что и имя класса. Деструктор вызывается при удалении класса из памяти и используется, как правило, для освобождения ранее выделенной памяти под какие-либо данные этого  класса. Имя деструктора совпадает  с именем класса, но перед ним  ставится символ ‘~’. Рассмотрим пример реализации конструктора и деструктора  для класса CPos.  

class CPos

{

public:

CPos() {printf(“Вызов конструктора.\n”);}

~CPos() {printf(“Вызов деструктора.\n”);}  

int sp_x, sp_y; //координата начала

int ep_x, ep_y; //координата конца

};  

Здесь ключевое слово  public используется для обеспечения общего доступа к функциям и переменным класса.  

Для создания нового экземпляра класса в памяти ЭВМ используется оператор new языка С++, а для удаления – оператор delete. Использование данных операторов для создания экземпляра класса CPos и его удаления выглядит следующим образом:  

CPos *pos_ptr = new CPos(); //создание объекта

delete pos_ptr; //удаление объекта  

В результате выполнения этих двух строк программы на экране появятся сообщения:  

Вызов конструктора.

Вызов деструктора.  

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

CPos pos;  

В этом случае переменная pos называется представителем класса, у которого также вызывается конструктор при его создании и деструктор при его удалении из памяти.  

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

class CPos

{

public:

CPos(int x1, int y1, int x2,int y2)

{

sp_x = x1; sp_y = y1;

ep_x = x2; ep_y = y2;

}

~CPos() {}  

int sp_x, sp_y;

int ep_x, ep_y;

};  

и процесс создания экземпляра класса принимает вид:  

CPos *pos_ptr = new CPos(10,10,20,20);  

или  

CPos pos(10,10,20,20);  

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

class CPos

{

public:

CPos() {}

CPos(int x1, int y1, int x2,int y2)

{

sp_x = x1; sp_y = y1;

ep_x = x2; ep_y = y2;

}

~CPos() {}  

int sp_x, sp_y;

int ep_x, ep_y;

};  

В классах помимо переменных, конструкторов и деструкторов можно задавать описания и обычных  функций, которые, в этом случае, называются методами. Например, в классе CPos для задания значений координат примитива целесообразно добавить функцию для ввода значений в переменные sp_x, sp_y, ep_x и ep_y. Это позволит, во-первых, не запоминать программисту имена этих переменных, а оперировать только одной функцией и, во-вторых, в самой функции можно реализовать необходимые проверки на истинность переданных значений координат перед их присваиванием переменным. Такую функцию можно описать в классе следующим образом:  

class CPos

{

public:

CPos() {}

~CPos() {}  

void SetParam(int x1, int y1, int x2, int y2)

{

if(x1 >= 0 && x1 <= MAX_SIZE) sp_x = x1;

if(y1 >= 0 && y1 <= MAX_SIZE) sp_y = y1;

if(x2 >= 0 && x2 <= MAX_SIZE) ep_x = x2;

if(y2 >= 0 && y2 <= MAX_SIZE) ep_y = y2;

}  

int sp_x, sp_y;

int ep_x, ep_y;

};  

В приведенном примере  реализована функция SetParam(), которая перед присваиванием значений переменных выполняет проверку на их истинность. Здесь некоторое неудобство представляет то, что данная функция полностью описана в классе CPos, а описание большого числа функций в одном классе делает текст программы трудночитаемым. Поэтому обычно в классах записывают лишь прототипы функций, а их реализации приводят отдельно после описания класса. Для того чтобы описать реализацию функции SetParam() вне класса CPos перед именем функции ставится имя класса с оператором глобального разрешения ‘::’ как показано ниже:  

void CPos::SetParam(int x1, int y1, int x2, int y2)

{

if(x1 >= 0 && x1 <= MAX_SIZE) sp_x = x1;

if(y1 >= 0 && y1 <= MAX_SIZE) sp_y = y1;

if(x2 >= 0 && x2 <= MAX_SIZE) ep_x = x2;

if(y2 >= 0 && y2 <= MAX_SIZE) ep_y = y2;

}  

а перед ней должно идти следующее определение класса:  

class CPos

{

public:

CPos() {} ~

CPos() {}  

void SetParam(int x1, int y1, int x2, int y2);  

int sp_x, sp_y;

int ep_x, ep_y;

};  

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

CPos::CPos()

{

//операторы конструктора 

}  

CPos::~CPos()

{

//операторы деструктора 

}  

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

CPos* pos_ptr = new CPos();

CPos pos;  

pos_ptr->SetParam(10,10,20,20);

pos.SetParam(10,10,20,20);  

Таким же образом  можно обращаться и к переменным класса:  

pos_ptr->sp_x = 10;

pos.sp_x = 20;  

Здесь можно заметить, что значения переменных sp_x, sp_y, ep_x и ep_y могут быть заданы как непосредственно при обращении к ним, так и с помощью функции SetParam(). В результате проверка, реализованная в данной функции, может быть проигнорирована программистом. Часто такая ситуация недопустима, например, при использовании готовых классов библиотек MFC, VCL, OWL и др. В связи с этим в классах для переменных и функций предусмотрена возможность установки разных уровней доступа, которые определяются тремя ключевыми словами: public, private и protected.  

Ключевое слово  public означает общий доступ к переменным и функциям класса. Уровень доступа private указывает на частный способ доступа к элементам класса и устанавливается по умолчанию при описании класса. Частный уровень доступа дает возможность обращаться к переменным и функциям только внутри класса и запрещает извне, например, через представители или указатели на класс. Режим доступа protected также как и private запрещает доступ к элементам класса через представители и указатели, но разрешает обращаться к ним из дочерних классов при наследовании.  

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

class CPos

{

public:

CPos() {}

~CPos() {}  

void SetParam(int x1, int y1, int x2, int y2);  

private:

int sp_x, sp_y;

int ep_x, ep_y;

};  

Здесь раздел private ограничивает доступ пользователю класса к переменным sp_x, sp_y, ep_x и ep_y только функцией SetParam(). Следует также отметить, что отсутствие раздела public вначале описания класса привело бы к тому, что все функции класса CPos имели бы область видимости private. В результате доступ к конструктору и деструктору был бы запрещен, и создание нового объекта стало бы невозможным. Аналогичная картина имеет место и в режиме доступа protected, но в отличие от private класс можно использовать как базовый в механизме наследования. Это свойство полезно использовать для запрета создания экземпляров класса, что бывает необходимым, если он является лишь промежуточным звеном в иерархии объектов и не представляет ценности как отдельный объект.

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

Предположим, создается  дочерний класс с именем CLine для работы с линией на основе базового CPos. Для этого после имени дочернего класса CLine ставится символ ‘:’, а затем пишется имя базового класса CPos с указанием уровня доступа:  

class CPos

{

public:

CPos() {}

CPos(int x1, int y1, int x2, int y2) {SetParam(x1,y1,x2,y2);}

~CPos() {}  

void SetParam(int x1, int y1, int x2, int y2);  

protected:

int sp_x, sp_y;

int ep_x, ep_y;

};  

class CLine : public CPos

{

public:

CLine() {}

CLine(int x1,int y1, int x2, int y2) {SetParam(x1,y1,x2,y2);}

~CLine() {}  

void Draw() {MoveTo(sp_x,sp_y); LineTo(ep_x,ep_y);}

};  

В результате наследования с уровнем доступа public класс CLine имеет доступ ко всем переменным и функциям класса CPos, которые не являются частными (private). Ключевое слово public перед именем класса CPos означает, что все общие (public) элементы этого класса остаются с таким же уровнем доступа и в классе CLine. Следует также отметить, что описание класса CPos должно предшествовать описанию класса CLine, а переменные sp_x, sp_y, ep_x и ep_y должны быть описаны в разделе protected для возможности их использования в функции Draw() дочернего класса CLine и в то же время не доступными извне.  

Класс CLine содержит два конструктора, деструктор и функцию Draw() для рисования линии на экране. При этом процедура задания координат графического объекта целиком находится в базовом классе CPos и по мере необходимости используется в дочернем классе CLine. Такое разделение оказывается удобным, т.к. при описании работы с новыми графическими объектами процедура работы с их координатами будет оставаться одной и той же и находится в одном классе. Если бы в данном случае использовался структурный подход к программированию, то алгоритм работы с координатами графических примитивов пришлось бы прописывать каждый раз для всех типов объектов, что привело бы к заметному усложнению текста программы.  

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

CLine* line_ptr = new CLine();  

или  

CLine line;  

При создании нового объекта CLine вызывается сначала конструктор CPos() базового класса, а затем конструктор дочернего – CLine(). Таким образом, создается как бы два объекта: CPos и CLine, но они представляются как единое целое объекта CLine.  

В представленном классе CLine предусмотрено два конструктора: с параметрами и без них. В случае вызова конструктора с параметрами  

CLine line(10,10,20,20);  

вызывается конструктор  CPos() базового класса, а затем конструктор CLine(int x1, int y1, int x2, int y2) дочернего, в котором выполняется функция SetParam() для записи значений координат графического объекта.  

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

class CLine : public CPos

{

public:

CLine() : CPos()

{

}

CLine(int x1,int y1, int x2, int y2) : CPos(x1,y1,x2,y2)

{

}

~CLine() {}  

void Draw() {MoveTo(sp_x,sp_y); LineTo(ep_x,ep_y);}

};  

В приведенном примере  конструктор CLine() будет вызывать конструктор CPos() базового класса, а конструктор CLine(int x1, int y1, int x2, int y2) конструктор CPos(int x1, int y1, int x2, int y2). При этом функция SetParam() в CLine(int x1, int y1, int x2, int y2) может быть опущена, т.к. необходимая инициализация переменных будет выполнена при вызове конструктора CPos(int x1, int y1, int x2, int y2) базового класса.  

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

class CPos

{

protected:

CPos() {}

CPos(int x1, int y1, int x2, int y2) {SetParam(x1,y1,x2,y2);}

~CPos() {}  

public:

void SetParam(int x1, int y1, int x2, int y2);  

protected:

int sp_x, sp_y;

int ep_x, ep_y;

};  

Функции классов  CPos и CLine можно вызывать, например, через представитель класса CLine, следующим образом:  

CLine line;

line.SetParam(10,10,20,20);

line.Draw();  

Обратите внимание, что благодаря полиморфизму, функция  SetParam(), заданная в классе CPos, вызывается через представитель line как будто она определена в классе CLine. В результате, единожды объявленная функция SetParam() может быть многократно использована в разных классах, производных от CPos.  

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

class CRect : public CPos

{

public:

CRect() : CPos()

{

}

CRect(int x1,int y1, int x2, int y2) : CPos(x1,y1,x2,y2)

{

}

~CRect() {}  

void Draw() {Rectangle(sp_x,sp_y,ep_x,ep_y);}

};  

class CEllipse : public CPos

{

public:

CEllipse() : CPos()

{

}

CEllipse(int x1,int y1, int x2, int y2) : CPos(x1,y1,x2,y2)

{

}

~CEllipse() {}  

void Draw() {Ellipse(sp_x,sp_y,ep_x,ep_y);}

};  

В результате построения объектов получается следующая иерархия (рис. 6.1).  

  

Рис. 6.1. Иерархия классов  графических примитивов  

У каждого из представленных дочерних объектов CLine, CRect и CEllipse имеется один базовый объект CPos. Вместе с тем язык С++ предоставляет возможность создавать дочерние объекты на основе нескольких базовых, что приводит к концепции множественного наследования.  

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

class CProp

{

protected:

CProp() {}

CProp(int wdt, int clr) {SetProperty(wdt,clr);}

~CProp();  

public:

void SetProperty(int wdt, int clr)

{

if(wdt >= 0 && wdt <= MAX_WIDTH) width = wdt;

if(clr >= 0 && clr <= MAX_COLOR) color = clr;

}

protected:

int width, color;

};  

Класс в ООП