<< Предыдущая Оглавление Следующая >>


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 класс можно использовать как базовый в механизме наследования. Это свойство полезно использовать для запрета создания экземпляров класса, что бывает необходимым, если он является лишь промежуточным звеном в иерархии объектов и не представляет ценности как отдельный объект.


<< Предыдущая Оглавление Следующая >>