- •Определение объектно-ориентированного языка программирования (оояп). Основные свойства оояп - абстракция, инкапсуляция, наследование, полиморфизм.
- •Пример использования ооп (объектно-ориентированного проектирования)
- •Разработка класса в ооп: определение и реализация класса. Члены класса. Уровни видимости. Inline-функции. Основные этапы разработки класса
- •Определение класса
- •Рекомендации по поводу использования уровней видимости при определении класса
- •Методы класса: классификация методов, доступ к членам класса, неявный параметр this. Определение и реализация методов. Использование методов для экземпляров класса. [3, стр.6 – 7, 9-16]
- •Использование класса
- •Реализация класса
- •Пример: реализация класса Rational
- •Реализация методов класса
- •Использование класса
- •Пример использования класса
- •Перегрузка операторов
- •Выбор перегруженной функции
- •Примеры
- •Пример с ошибками
- •Друзья или члены
- •Преобразования типа: назначение, использование. Правила преобразования типа. Возможные проблемы.
- •Возникающие проблемы
- •Решение проблемы
- •Когда используются присваивание и копирующий конструктор
- •Типы отношений между классами. Контейнерные классы: определение, видимость членов класса. Реализация и вызов конструкторов и деструкторов вложенных классов. Реализация и использование методов.
- •Контейнерные классы: определение
- •Контейнерные классы: реализация методов
- •Контейнерные классы: реализация конструкторов
- •Простое наследование: правила определения производного класса
- •Пример производного класса
- •Указатели на классы: базовый и производный
- •Понятие статического и динамического связывания
- •Виртуальные функции
- •Пример: классы Точка и Окружность.
- •Пример использования динамического связывания: список
- •Понятие и назначение итераторов. Проектирование, реализация и использование итератора.
- •Шаблоны типа для функций
Простое наследование: правила определения производного класса
Правила объявления производных классов. Две проблемы: могут ли (и как) методы производного класса обращаться к членам базового класса (доступность изнутри производного класса) и как члены базового класса, унаследованные производным классом, могут быть доступны извне производного класса.
Первая проблема решается уровнем видимости членов в базовом классе:
class B{
private:
// закрытая часть класса; не доступна никому, кроме методов класса (в том числе
// не доступна и из производного класса)
protected:
// защищенная часть класса; доступна из методов базового и производного класса
// (но не доступна извне класса)
public:
// открытая часть класса; доступна везде
};
Вторая проблема решается способом наследования:
class D: тип_наследования B{
. . . };
тип_наследования: одно из private, protected, public.
Тип наследования не смягчает, но усиливает защиту данных: члены класса, имевшие менее защищенный уровень видимости в базовом классе, в производном классе приобретают уровень видимости, определяемый типом наследования.
-
Уровень видимости в базовом классе
Тип наследования
private
protected
public
protected
private
protected
protected
public
private
protected
public
Пример производного класса
Вернемся еще раз к классу Окружность, описанному выше. Окружность можно рассматривать как точку, имеющую некоторый размер. В соответствии с этим, можно сформулировать следующее отношение между классами:
Окружность ЕСТЬ Точка, имеющая некоторый (возможно, нулевой) размер.
В такой ситуации класс Точка является базовым для класса Окружность. В результате класс Окружность:
наследует состояние и методы, определенные в базовом классе Точка;
должен определить собственные состояние и/или методы, отсутствующие в базовом классе;
может переопределить какие-то методы базового класса.
Так, класс Окружность наследует часть состояния класса Точка (координаты) и метод вычисления расстояния между точками (центрами двух окружностей); определяется собственная часть состояния (радиус) и свой собственный метод, отсутствовавший в классе Точка (пересечение окружностей); наконец, переопределяет метод вывода.
Рассмотрим определение и реализацию указанных классов.
Определение класса Точка:
class Point{
protected:
int x, y;
public:
// конструкторы
Point():x(0), y(0){}
Point(int x0):x(x0),y(x0){}
Point(int x0, int y0):x(x0), y(y0){}
Point(const Point &p):x(p.x), y(p.y){}
void print();
float distance(Point p);
};
Реализация класса:
void Point::print()
{ cout << '(' << p.x << ", " << p.y << ')'; }
float Point::distance(Point p)
{ float dx = x - p.x;
float dy = y - p.y;
return sqrt(dx * dx + dy * dy); }
Определение производного класса Окружность:
class Circle: public Point{
private:
int rad;
public:
// конструкторы
Circle();
Circle(int r);
Circle(int x, int r);
Circle(int x, int y, int r);
Circle(Point p, int r);
// методы
void print();
int intersect(Circle c);
};
В определении класса приводятся только те его члены, которые отличают производный класс от базового. В нашем случае приводятся радиус окружности, собственные методы определения пересечения окружностей и вывода и, конечно же, необходимые конструкторы.
Теперь рассмотрим реализацию методов. Начнем с конструкторов.
Проблема, возникающая здесь, заключается в следующем. Конструктор должен инициализировать состояние экземпляра класса. Доступ к собственной части состояния проблем не вызывает. Но как быть с частью состояния, унаследованной от базового класса? Если состояние базового класса включено в область protected, оно доступно из производного класса, и тогда конструктор может быть реализован следующим образом:
Circle::Circle(){x = 0; y = 0; rad = 0;} // пустой конструктор
Если же состояние базового класса включено в private область, оно не доступно из производного класса, но также должно быть инициализировано. Состояние класса инициализируют конструкторы класса. Следовательно, необходимо иметь возможность из конструктора производного класса вызвать конструктор базового класса. Эта возможность реализуется следующим образом:
<имя_производного_класса>::<имя_производного_класса>(<параметры>):
<имя_базового_класса>(<аргументы>) // вызов конструктора базового класса
{< дополнительные операции >}
Инициализация собственного состояния может быть реализована в соответствии с правилами контейнерных классов, тогда конструктор производного класса может выглядеть и так:
<имя_производного_класса>::<имя_производного_класса>(<параметры>):
<имя_базового_класса>(<аргументы>),
<имя_члена_данного1_класса>(<значение>),
...
{< дополнительные операции>}
И опять же действует правило по умолчанию: если вызов конструктора базового класса отсутствует, по умолчанию вызывается пустой конструктор базового класса.
В соответствии с изложенным, реализация конструкторов класса Окружность может быть следующей:
Circle::Circle():rad(0){} // Вызывается пустой конструктор базового класса
Circle::Circle(int r):rad(r){}
Circle::Circle(int x, int r):Point(x), rad(r){}
Circle::Circle(int x, int y, int r):Point(x, y), rad(r){}
Circle::Circle(Point p, int r):Point(p), rad(r){}
Унаследованные от базового класса методы не требуют реализации. Так, метод distance()из базового класса Point наследуется классом Circle и будет определен в производном классе без каких-либо дополнительных усилий.
В реализации собственных методов производного класса может возникнуть необходимость использовать (вызвать) метод базового класса. Поскольку он наследуется производным классом, его вызов ничем не отличается от вызова собственных методов класса. Так, например, метод определения, пересекаются ли окружности, должен вызвать метод определения расстояния между точками базового класса. Реализация метода может выглядеть следующим образом:
int Circle::intersect(Circle c)
{ return distance(c) < rad + c.rad; }
Проблемы могут возникнуть, если вызываемый метод базового класса имеет такое же имя, как и использующий его метод производного класса. Например, в методе вывода print() производного класса используется метод с таким же именем базового класса.
Если метод будет реализован следующим образом:
void Circle::print()
{ print(); // (!) Метод какого класса вызывается?
cout << " with rad " << rad; }
В этом случае возникает вопрос: метод какого класса будет вызван в предложении (!)? В соответствии с правилами языка С++, использование имени члена класса предполагает использование неявного параметра this, т.е. конструкция (!) интерпретируется как this->print(). Так как рассматривается реализация производного класса, параметр this определяет экземпляр производного класса; следовательно, здесь вызывается метод производного класса – и мы получаем бесконечную рекурсию! Чтобы вызвать метод базового класса, нужно это указать явно, используя оператор :: и имя базового класса. Тогда корректная реализация метода будет выглядеть следующим образом:
void Circle::print()
{ Point::print(); // Вызывается метод print() базового класса
cout << " with rad " << rad; }
Использование классов:
Circle c1(3), c2(2, 2);
cout << "distance between " << c1 << " and " << c2 << " is " << c1.distance(c2) << endl;
cout << c1 << " and " << c2;
if(c1.intersect(c2))
cout << " are intersected " << endl;
else
cout << " are not intersected " << endl;
