Компания ALT Linux
Опубликован: 07.03.2015 | Доступ: свободный | Студентов: 2135 / 487 | Длительность: 24:14:00
Лекция 10:

Объектно-ориентированное программирование

10.4.2 Раннее и позднее связывание

Обрабатывая вызов метода какого-либо класса, компилятор сначала ищет метод с указанным именем внутри данного класса. Если метод с таким именем не определён внутри класса, то компилятор обращается к базовому классу и ищет его там. Если найдёт, то подставит в точки вызова адрес метода из родительского класса. Если не найдёт, то поднимается всё выше по иерархии наследования.

Методы, которые вызываются так, являются статическими — в том смысле, что компилятор разбирает ссылки на них во время компиляции. Этот подход экономит ресурсы в момент выполнения программы, однако иногда приводит к нежелательным результатам. Рассмотрим для примера иерархию из двух классов: класса vector, представляющего собой двумерный вектор, и производный от него класс spatial_vector, уже знакомый нам по прежним примерам. Нам будут нужны два метода у каждого из классов: метод info(), выводящий текстовое сообщение и сообщающий, чему равен модуль вектора, и метод abs(), собственно вычисляющий значение модуля. Наследование одного класса от другого в данном случае представляется вполне логичным: в производном классе достаточно будет добавить ещё одну переменную, модифицировать конструктор и функцию вычисления модуля.

#include <iostream>
#include <math.h>
using namespace std;
class vector
{
protected :
	double x, y;
public :
	vector ( double x, double y ) { this->x=x; this->y=y; }
	double abs ( ) { return sqrt ( x*x + y*y ); }
	void info ( ) { cout << "Модуль вектора равен " << abs ( ) << endl; }
};
class spatial_vector : public vector
{
	protected :
	double z;
public :
	spatial_vector ( double x, double y, double z );
	double abs ( ) { return sqrt ( x*x + y*y + z*z ); }
};
spatial_vector::spatial_vector ( double x, double y, double z ) : vector ( x, y )
{
	this->z=z;
}
main ( )
{
	cout << "Создаём вектор на плоскости с координатами 1,2\n ";
	vector a(1, 2 );
	a.info ( );
	cout << "Создаём пространственный вектор с координатами 1,2,3\n ";
	spatial_vector b(1, 2, 3 );
	b.info ( );
}

В действительности же данный код генерирует весьма странный результат:

Создаём вектор на плоскости с координатами 1,2
Модуль вектора равен 2.23607
Создаём пространственный вектор с координатами 1,2,3
Модуль вектора равен 2.23607

Мы корректно переопределили метод abs() в производном классе (можно легко в этом убедиться, вызвав его непосредственно), однако для производного класса функция info() выдала явно неверное значение, не посчитав в модуле третью координату. Проблема в том, что родительский метод info() "не знает", что функция abs() переопределена в классе-потомке.

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

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

Адрес виртуального метода известен только в момент выполнения программы. Когда происходит вызов виртуального метода, его адрес берётся из таблицы виртуальных методов своего класса. Таким образом, вызывается то, что нужно.

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

#include <iostream>
#include <math.h>
using namespace std;
class vector
{
protected :
	double x, y;
public :
	vector ( double x, double y ) { this->x=x; this->y=y; }
	virtual double abs ( ) { return sqrt ( x*x + y*y ); }
	void info ( ) { cout << "Модуль вектора равен " << abs ( ) << endl; }
};
class spatial_vector : public vector
{
	protected :
	double z;
public :
	spatial_vector ( double x, double y, double z );
	double abs ( ) { return sqrt ( x*x + y*y + z * z ); }
};
spatial_vector::spatial_vector ( double x, double y, double z ) : vector ( x, y )
{
	this->z=z;
}
main ( )
{
	cout << "Создаём вектор на плоскости с координатами 1,2\n ";
	vector a(1, 2 );
	a.info ( );
	cout << "Создаём пространственный вектор с координатами 1,2,3\n ";
	spatial_vector b(1, 2, 3 );
	b.info ( );
}

Будучи выполнен, пример наконец выдаёт ожидаемый ответ:

Создаём вектор на плоскости с координатами 1,2
Модуль вектора равен 2.23607
Создаём пространственный вектор с координатами 1,2,3
Модуль вектора равен 3.74166

10.4.3 Множественное наследование

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

class A{...};
class B{...};
class C : public A, public B{...};

При этом класс C унаследует как содержимое класса А, так и класса B. При вызове конструктора будут сначала вызваны конструкторы базовых классов (в порядке следования). Деструкторы, как всегда, имеют противоположный порядок вызова.

При множественном наследовании автоматически включается позднее связывание.

10.4.4 Указатели на базовые классы

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

point * p = new vertex();

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

Вызвать по такому указателю метод, присутствующий лишь в производном классе напрямую нельзя, но можно косвенно, с использованием приведения типов:

class parent
{
public :
	void parent_method ( ) {}
};
class child : public parent
{
public :
	void child_method ( ) {}
};
main ( )
{
	parent *p = new child ( );
	p->parent_method ( );
	( ( child *) p )->child_method ( );
}

Типичное использование указателя на базовый класс, которому присвоен адрес объекта производного класса — хранение либо передача нескольких разнотипных объектов, имеющих общий класс-предок. Например, во многих библиотеках виджетов (графических элементов управления) инструментальная панель, которая может содержать в себе кнопки, надписи, выпадающие списки и т. д., является универсальным контейнером, хранящим указатели на объект базового класса (например, класса widget), от которого унаследованы конкретные элементы управления (классы button, text, list и т. д.). Благодаря возможности использовать указатель на базовый класс, панель реализует один единственный набор методов для добавления и удаления разнотипных элементов, а также для обращения к ним.

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

10.4.5 Абстрактные классы

Иногда, когда функция объявляется в базовом классе, она не выполняет никаких значимых действий, поскольку часто базовый класс не определяет законченный тип, а нужен чтобы построить иерархию. Например, метод paint(), объявленный в классе widget и выполняющий отрисовку виджета на экране, должен переопределяться в классах-потомках, с тем, чтобы выводить на экран изображение кнопки в классе button или текстовую надпись в классе text. Изображение же абстрактного виджета тоже абстрактно, и метод в базовом классе не несёт практической нагрузки.

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

Чисто виртуальные методы не определяются в базовом классе. У них нет тела, а есть только декларации об их существовании.

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

virtual тип имя_функции (список параметров) = 0;

Как можно заметить, функцию делает чисто виртуальной приравнивание её описания к нулю.

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

Сергей Радыгин
Сергей Радыгин

Символы кириллицы выводит некорректно. Как сделать чтобы выводился читабельный текст на русском языке?

Тип приложения - не Qt,

Qt Creator 4.5.0 основан на Qt 5.10.0. Win7.

 

Юрий Герко
Юрий Герко

Кому удалось собрать пример из раздела 13.2 Компоновка (Layouts)? Если создавать проект по изложенному алгоритму, автоматически не создается  файл mainwindow.cpp. Если создавать этот файл вручную и добавлять в проект, сборка не получается - компилятор сообщает об отсутствии класса MainWindow. Как правильно выполнить пример?