Глава 6. Основы объектно-ориентированного программирования 


Мы поможем в написании ваших работ!



ЗНАЕТЕ ЛИ ВЫ?

Глава 6. Основы объектно-ориентированного программирования



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

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

6.1. Понятие классов в С++

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

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

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

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

Класс в языке С++ задается с помощью ключевого слова 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;
};

Теперь дочерние классы CLine, CRect и CEllipse можно образовывать от двух базовых CPos и CProp, которые являются не связанными друг с другом. Для того чтобы построить класс на основе двух базовых они указываются друг за другом через запятую следующим образом:

class CLine: public CPos, public CProp
{
public:
CLine(): CPos(), CProp() {}
CLine(int x1, int y1, int x2, int y2, int w, int clr):
CPos(x1,y1,x2,y2), CProp(w,clr) {}
~CLine() {}

void Draw() {SetWidth(width); SetColor(color);
MoveTo(sp_x, sp_y); LineTo(ep_x, ep_y);
}
};

Аналогичным образом строятся классы CRect и CEllipse. Здесь следует отметить, что конструктор CLine(int x1, int y1, int x2, int y2, int w, int clr) класса CLine вызывает конструкторы двух базовых классов, которые перечислены через запятую с указанием в них конкретных переменных. Работа с функциями класса CLine через его представитель имеет следующий вид:

CLine line;
line.SetProperty(1,0);
line.SetParam(10,10,20,20);
line.Draw();

Благодаря полиморфизму, функции SetProperty() и SetParam() базовых классов вызываются непосредственно из класса CLine.



Поделиться:


Последнее изменение этой страницы: 2017-02-05; просмотров: 282; Нарушение авторского права страницы; Мы поможем в написании вашей работы!

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 3.136.154.103 (0.039 с.)