Лекция 6:

к оглавлению
оглавление

Наследование в языке С++

Модификаторы наследования

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

Когда один класс наследуется другим, класс, который наследуется, называется производным классом. Наследующий класс называют базовым классом.

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

class Parent {....};
class Child : [модификатор наследования] Parent {....};

При определении класса потомка за его именем следует разделитель-двоеточие (:), затем – необязательный модификатор наследования и имя родительского класса. Модификатор наследования определяет видимость наследуемых переменных и методов для пользователей и возможных потомков самого класса-потомка. Другими словами, он определяет, какие права доступа к переменным и методам класса-родителя будут "делегированы" классу-потомку. При реализации наследования, область "видимости" принадлежащих классу данных и методов можно определять выбором ключевого слова private (собственный), public (общедоступный) или protected (защищенный), которые могут произвольно чередоваться внутри описания класса. С двумя первыми модификаторами доступа мы уже знакомы, – private описывает закрытые члены класса, доступ к которым имеют только методы–члены этого класса, public предназначен для описания общедоступных элементов, доступ к которым возможен из любого места в программе. Особый интерес представляют элементы, обладающие модификатором доступа protected. Модификатор protected используется тогда, когда необходимо, чтобы некоторые члены базового класса оставались закрытыми, но были бы доступны для производного класса. Protected эквивалентен private с единственным исключением: защищенные члены базового класса доступны для членов всех производных классов этого базового класса.

То, как изменяется доступ к элементам базового класса из методов производного класса, в зависимости от значения модификаторов наследования, приведено в таблице.

    Модификатор наследования
Модификатор доступа public protected private
public public protected private
protected protected protected private
private нет доступа нет доступа нет доступа

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

[к началу]

Конструкторы и деструкторы при наследовании

Базовый класс, производный класс или оба могут иметь конструкторы и/или деструкторы.

Если и у базового и у производного классов есть конструкторы и деструкторы, то конструкторы выполняются в порядке наследования, а деструкторы – в обратном порядке. Т.е. если А базовый класс, В – производный из А, а С – производный из В (А-В-С), то при создании объекта класса С вызов конструкторов будет иметь следующий порядок: конструктор А - конструктор В - конструктор С. Вызов деструкторов при разрушении этого объекта произойдет в обратном порядке: деструктор С - деструктор В - деструктор А.

Понять закономерность такого порядка не сложно, поскольку базовый класс "не знает" о существовании производного класса, любая инициализация выполняется в нем независимо от производного класса, и, возможно, становится основой для инициализации, выполняемой в производном классе. С другой стороны, поскольку базовый класс лежит в основе производного, вызов деструктора базового класса раньше деструктора производного класса привел бы к преждевременному разрушению производного класса.

class BaseClass
{
public:
 BaseClass() {cout << ' Работа конструктора базового класса \n';}
 ~BaseClass() {cout << ' Работа деструктора базового класса \n';}
};

class DerivedClass: public BaseClass
{
public:
 DerivedClass() {cout << ' Работа конструктора производного класса \n';}
 ~DerivedClass() {cout << ' Работа деструктора производного класса \n';}
};

main()
{
 DerivedClass obj;
}

Эта программа выводит следующее

Работа конструктора базового класса
Работа конструктора производного класса
Работа деструктора производного класса
Работа деструктора базового класса

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

конструктор_производного_класса (список формальных параметров)
 : конструктор_базового_класса (список фактических параметров)
{
 ... // тело конструктора производного класса
}

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

class BaseClass
{
 int i;
public:
 BaseClass (int ii) {i=ii;}
 ~BaseClass() {cout << ' Работа деструктора базового класса \n';}
};

class DerivedClass: public BaseClass
{
 int n;
public:
 DerivedClass (int nn, int m): BaseClass (m) {n=nn;}
 ~DerivedClass() {cout << ' Работа деструктора производного класса \n';}
};

main()
{
 DerivedClass obj(2,3);
}

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

class BaseClass
{
 int j, i;
public:
 BaseClass (int jj, int ii) {j=jj; i=ii;}
 ~BaseClass() {cout << ' Работа деструктора базового класса \n';}
};

class DerivedClass: public BaseClass
{
 int n;
public:
 DerivedClass (int nn);
 ~DerivedClass() {cout << ' Работа деструктора производного класса \n';}
};

DerivedClass :: DerivedClass (int nn): BaseClass (nn/2, nn%2)
{ n=nn; }

main()
{
 DerivedClass obj(15);
}

Еще раз обратите внимание на то, что в расширенной форме объявления конструктора производного класса описывается вызов (!!!) конструктора базового класса.

[к началу]

Пример построения классов и наследования

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

struct Point
{
int X;
int Y;
};

Но пиксел на экране монитора, кроме координат своего положения, обладает еще и возможностью "светиться". Расширим структуру:

enum Boolean {false, true}; // false = 0, true = 1
struct Point
{
 int X;
 int Y;
 Boolean Visible;
};

Тип Boolean хорошо знаком программистам на Паскале. Этот код использует перечисляемый тип enum для проверки true (истина) или false (ложь). Так как значения перечисляемого типа начинаются с 0, то Boolean может иметь одно из двух значений: 0 или 1 (ложь или истина).

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

enum Boolean {false, true}; // false = 0, true = 1
class Point
{
protected:
 int X;
 int Y;
 Boolean Visible;
public:
 int GetX(void) { return X; }
 int GetY(void) { return Y; }
 Boolean isVisible (){ return Visible;}
 Point (const Point& cp); // прототип конструктора копирования
 Point (int newX =0, int new Y =0); // прототип конструктора
};

Point :: Point (int NewX, int NewY) // конструктор
{
 X = newX; Y = newY; Visible = false;
}

Point :: Point (const Point& cp) // конструктор копирования
{
 X = cp.X; Y = cp.Y; Visible = cp.Visible;
}

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

Point Center(320, 120); // объект Center типа Point
Point *point_ptr; // указатель на тип Point
point_ptr = &Center; // указатель показывает на Center

Задание аргументов по умолчанию при описании прототипа конструктора дает нам возможность вызывать конструктор без аргументов или с неполным списком аргументов:

Point aPoint ();
Point Row[80]; // массив из объектов типа Point
Point bPoint (100);

Пока мы может создавать объекты класса Point и определять их координаты, но не можем пока их показывать. Так что необходимо дополнить класс Point соответствующими методами.

class Point
{
 ...
public:
 ...
 void Show();
 void Hide();
 void MoveTo(int newX, int newY);
};

void Point::Show()
{
 Visible = true;
 putpixel (X,Y,getcolor());
}

void Point::Hide()
{
 Visible = false;
 putpixel (X,Y,getbkcolor());
}

void Point::MoveTo (int newX, int newY)
{
 Hide ();
 X = newX;
 Y = newY;
 Show ();
}

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

Point pointA (50,50);
pointA.Show ();
pointA.MoveTo (100,130);
pointA.Hide ();

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

Давайте попробуем создать класс Circle для окружности. Окружность, в известном смысле, является жирной точкой. Она имеет все, что имеет точка (позицию X и Y и видимое/невидимое состояние) плюс радиус. По-видимому, класс Circle появляется, чтобы иметь только отдельный элемент Radius, однако не забывайте обо всех элементах, которые наследует Circle, являясь классом, порожденным из Point. Circle имеет X, Y, а также Visible, даже если их не видно в определении класса для Circle.

class Circle: public Point
{
 int Radius; // private по умолчанию
public:
 Circle (int initX, int initY, int initR);
 void Show ();
 void Hide ();
 void Expand (int deltaR);
 void Contract (int deltaR);
 void MoveTo (int newX, int newY);
};

Circle::Circle (int initX, int initY, int initR) // конструктор
 :Point (initX, initY) // вызов конструктора базового класса
{
 Radius = initR;
}

void Circle::Show ()
{
Visible = true;
circle (X,Y, Radius);
}

void Circle::Hide () // скрыть = зарисовать цветом фона
{
 Visible = false;
 unsigned int tempColor = getcolor ();
 setcolor (getbkcolor());
 circle (X,Y, Radius);
 setcolor (tempColor);
}

void Circle::Expand (int deltaR)
{
 Hide();
 Radius += deltaR;
 Show();
}

void Circle::Contract (int deltaR)
{
 Expand (-deltaR);
}

void Circle::MoveTo (int newX, int newY)
{
 Hide ();
 X = newX;
 Y = newY;
 Show ();
}

main()
{
 int graphDr = DETECT, graphMode;
 initgraph ( &graphDr, &graphMode, "");
 
 Circle C (150,200,50); // создать объект окружность с центром в т.(150, 200) и радиуса 50
 C.Show(); // показать окружность
 getch();
 C.MoveTo (300,100); // переместить
 getch();
 C.Expand (50); // растянуть
 getch();
 C.Contract (70); // сжать
 getch();
 
 closegraph();
}

Поскольку класс Circle – производный от класса Point, то, соответственно, класс Circle наследует из класса Point состояния X, Y, Visible, а также методы isVisible(), GetX(), GetY(). Что касается методов Show(), Hide() и MoveTo() класса Circle, то необходимость их переопределения продиктована спецификой объектов нового класса, поскольку, например, показать окружность, – это не то же самое, что показать точку.

Заметьте, что методы Circle требуют доступа к различным элементам данных в классах Circle и Point. Рассмотрим Circle::Expand. Она требует доступа к Radius. Нет проблем. Radius определен как private в самом Circle. Поэтому Radius доступен любой функции элементов из класса Circle.

Теперь рассмотрим Circle::Hide и Circle::Show. Они требуют доступа к Visible из своего базового класса Point. В этом примере Visible имела protected доступ в Point. А Circle порождается с доступом public из Point. Поэтому, Visible имеет protected доступ в пределах Circle. Заметим, что если бы Visible определялась как private в Point, то она была бы недоступна для функций элементов Circle. Можно было бы сделать Visible c доступом public. Однако в таком случае Visible сделалась бы доступной для функций не являющихся элементами.

[к началу]

Совместимость типов

Наследование предъявляет свои требования к правилам совместимости типов.

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

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

Например,

Point APoint, *ptrPoint;
Circle ACircle, *ptrCircle;

При наличии этих объявлений следующие присваивания являются законными:

APoint = ACircle;

Обратные присваивания незаконны.

Родительскому объекту можно присваивать объект любого порожденного им класса.

Чтобы было проще запомнить путь совместимости типов, давайте рассуждать таким образом:

1. источник должен полностью заполнять объект назначения,
2. порожденные типы содержат все, что содержат родительские типы посредством наследования. Следовательно, порожденный тип будет иметь или тот же размер, или (обычно) размер больший, чем его предок, но никогда не меньший размер.

Присваивание родительского объекта порожденному может оставить некоторые поля потомка неопределенными после присваивания. Что весьма опасно.

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

Совместимость типов также действует между указателями на классы. Указатели на потомков можно присваивать указателям на предков по тем же общим правилам, как и для экземпляров объекта.

Формальный параметр объекта заданного класса может использовать в качестве фактического параметра объект такого же класса или любого порожденного класса. Например,

void Proc (Point param)

Тогда фактические параметры могут иметь тип Point, Circle и любой другой порожденный от них тип.

[к началу]
назад КОНЕЦ ШЕСТОЙ СЕРИИ вперед