Назад | Начало урока | Вперед
Содержание

Глава 19

Наследование

В этой главе...

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

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

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

Вверх

Что происходит при наследовании кодов

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

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

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

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

ПроизводныйКласс : БазевыйКласс

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

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

Класс Политик
{

Функции
ДаватьОбещания()
БратьДеньги()
}

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

класс ЧестныйПолитик : public Политик
{

Public:
ГоворитьПравду();
}

Поскольку этот класс является производным от класса Политик, процедуры
ДаватьОбещания() и БратьДеньги() являются его составной частью. Но в дополнение к этому он содержит в себе также метод ГоворитьПравлу. Ничего сложного, не так ли?

Вверх

Зачем делать то, что уже сделано?

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

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

Совет:

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

class MyPen : public Pen

Вверх

Наследование открытых, закрытых и защищенных элементов

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

(В действительности в языке C++ есть еще несколько дополнительных правил, которые касаются наследования открытых, закрытых и защищенных членов класса. О них речь идет в разделе "Защищенное и закрытое наследование" ниже в этой главе.)

Вверх

Перегрузка функций-членов

При необходимости можно изменить поведение элементов, которые были наследованы.
Чтобы сделать это, заново объявите для производного класса функции-члены, работа которых должна отличаться от работы тех же функций-членов базового класса. При этом сами имена функций-членов не меняются. Эта возможность называется перегрузкой (overriding) и является одним из самых мощных и часто используемых средств языка C++.

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

__gc class ShapeObject
{

public:
void Draw(Graphics *poG);
void PrintStats();
private:
int nUseCount;
};

Если теперь вы хотите создать отдельный класс, представляющий линии, можете наследовать его из класса ShapeObject, изменив порядок действий, выполняемых функцией Draw (воспользовавшись возможностью перегрузки):

__gc class LineObject : public ShapeObject
{

public:
void Draw(Graphics *poG);
};

Созданный таким образом класс LineObject будет иметь те же свойства и характеристики, что и класс ShapeObject, за исключением функции Draw, работа которой будет отличаться от работы функции Draw класса ShapeObject.

Вверх

Родительские связи

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

Предположим, например, что функции Draw класса LineObject необходимо вызвать
функцию Draw класса ShapeObject. Вот как это делается:

void LineObject::Draw(Graphics *poG)
{

//Вызов функции Draw базового класса
ShapeObject::Draw(poG);
//Создание пера для рисования
Pen *poPen = new Pen(Color::Red);
//Рисование линии
poG->DrawLine(poPen, m_nXFrom, m_nYFrom, m_nXTo, m_nYTo);
}

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

Вверх

А теперь немного практики

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


//Наследование
#include "stdafх.h"
#using <mscorlib.dll>
using namespace System;

//Базовый класс. В нем содержится информация о цене и функция,
//отображающая значение цены на экране
class Base
{

public:
int nPrice;
void PrintMe();

//Функция базового класса, отображающая цену на экране
void Base::PrintMe()
{

Console: :WriteLine (S"Base {0} ", nPrice.ToString ());
}
}

//Производный класс, в котором функция PrintMe будет
//определена по-своему
class Derived : public Base
{

public:
void PrintMe();
}

//Функция производного класса, которая вызывает функцию
//базового класса
void Derived::PrintMe()
{

Console::WriteLine(S"Derived");
//Вызов функции базового класса
Base::PrintMe ();
}

//Ожидание, пока пользователь не остановит выполнение
//программы
void HangOut()
{

Conscle::WriteLine(L"Нажмите Enter, чтобы завершить
выполнение программы");
Console::ReadLine();
}

//С этой строки начинается выполнение программы
#ifdef _UNICODE
int wmain(void)
#else
int main(void)
#endif
{

Base oBaseClass;
Derived oDerivedClass;

oBaseCiass.nPrice = 1;
oDerivedClass.nPrice = 7;

Console::WriteLine(S"Вызов функции базового класса");
cBaseClass.PrintMe();

Console::WriteLine(S"3ызов функции производного класса");
oDerivedClass.PrintMe ();
HangCut ();
return 0;

}

Как же процесс наследования отображается на конструкторах
и деструкторах

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

Когда вы создаете объект производного класса, вызывается конструктор базового класса.При этом компилятор в первую очередь ищет конструктор, установленный по умолчанию (тот, который не имеет параметров).
Иногда возникает необходимость в создании специализированных конструкторов. Например вы уже могли видеть, что объект класса Реn может создаваться с указанием цвета либо с указанием цвета и ширины пера. Если на основе класса Реn вы создадите производный класс, то наверняка при создании объектов нового класса вам захочется использовать параметризованные конструкторы базового. Сделать это можно с помощью конструктора производного класса. Для этого просто укажите конструктор базового класса, который должен вызываться при вызове конструктора производного класса, и список используемых параметров:
Например:

Derived::Derived():base(....);

Например:

NewPen::NewPen(int Width, int Style) : Pen(Color::Red, nWidth);

В данном случае вызов конструктора NewPen (int, int) сопровождается вызовом
Конструктора базового о класса Pen, который устанавливает цвет и ширину пера.

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

Вверх

Универсальный указатель

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

LineObject *poShape;

Если какой-то объект является производным от класса ShapeObject, то для ссылки на него так же можно использовать указатель poShape::

poShape = new LineObject();

«Ну и что?» - спросите вы, а мы ответим: "Это очень важно, поскольку отсюда следует, что вы можете использовать один и тот же указатель для ссылки на множество различных объектов. Так, вам не обязательно заранее знать, какой объект создаст пользователь данного класса: специальный, производный или еще какой-то. Вы должны использовать один и тот же указатель, и он будет работать с производными объектами так же хорошо, как и с базовыми".

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

Увидеть этот прием в действии вы сможете ниже в -этой главе на примере программы
Draw8. В список ArrayList будут добавляться объекты LineObject и CircleObject. a
затем для доступа к ним вы просто используете такой код (ну хорошо, слово просто в данном случае, пожалуй, является не самым подходящим):

static_cast(poEnumerator->Current)->Draw(poG);

Поскольку классы LineObject и CircleObject являются производными от класса
ShapeObject и поскольку класс ShapeObject имеет функцию-член Draw, этот код будет работать независимо от того, является ли текущий элемент списка объектом класса LineObject или объектом класса CircleObject.

Почему указатель может ссылаться на объекты разных классов?

Как вы знаете, C++ следит за корректностью использования типов данных. Например, если у вас есть указатель типа int*, то он не может ссылаться на значение типа float . Однако в случае с производными классами все обстоит несколько иначе.

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

Если компилятор имеет дело с указателем на базовый класс, то он знает, как в нем найти различные члены данных и функции-члены по их местоположению в этом классе.

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

А что произойдет, если в производном классе будет перегружена какая-либо функция базового класса? Об этом речь пойдет ниже.

Защищенное и закрытое наследование

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

В табл 19.1 показано, какие права доступа приобретают элементы производного класса при использовании в процессе наследования ключевых слов public, private и
protected.

Таблица 19.1. Влияние слов public, private и protected на процесс наследования

Используемое слово Члены базового класса объявлены как ... Члены производного класса наследуются как ...
public public
protected
private
public
protected
Недоступны для наследования
protected public
protected
protected
public
protected
Недоступны для наследования
private public
protected
private
private
private
Недоступны для наследования

Вверх

Виртуальная реальность

Виртуальные функции используются тогда, когда указатель на объект иногда ссылается на базовый класс, а иногда — на производный. К счастью, все это не так сложно, как выглядит поначалу. Чуть выше, в разделе "Универсальный указатель", вы уже видели фрагмент кода из программы DrawS, где список ArrayList использовался для хранения объектов CircleObject и LineObject. Это, собственно, и все, о чем пойдет речь.

Конечно, здесь есть одна маленькая загвоздка. Предположим, что в производном классе вы перегрузили одну из функций-членов, изменив тем самым ее поведение. Например, в классе LineObject функция Draw была перегружена таким образом, чтобы в процессе ее выполнения на экране рисовались линии. А в классе CircleObject та же функция была изменена так, чтобы ее вызов сопровождался рисованием кругов. Теперь, если вы используете указатель типа ShapeObject для ссылки на один из этих объектов и вызовете функцию Draw, вы не получите тот результат, который вам нужен: будет вызвана функция Draw базового класса.

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

И тут на помощь приходят виртуальные функции. Когда их видят указатели, они знают, что им придется вызывать перегруженные функции-члены производного класса. Если программисты говорят между собой о полиморфизме как о важнейшем преимуществе объектно-ориентированного программирования, на самом деле речь идет о виртуальных функциях.

Когда вы делаете функцию Draw виртуальной, упомянутая выше проблема исчезает. Если у вас есть объект LineObject, но на него ссылается указатель типа ShapeObject, вызов функции Draw будет означать вызов именно функции LineObject: : Draw, а не ShapeObject: : Draw. Более того, при этом не возникает никакой путаницы. Так, если вы ссылаетесь на объект LineObject с помощью указателя типа LineObject, то при вызове функции Draw также будет вызываться функция LineObject: : Draw. Другими словами, с виртуальными функциями происходят только правильные вещи.

Тест на виртуальность

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

Если ответы на все четыре вопроса будут положительными, вам нужна виртуальная функция.

Декларация ваших виртуальных намерений

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

class Base
{

public:
int Price;
virtual void PrintMe();
};

Этот код говорит о том, что в классе Base есть виртуальная функция PrintMe.
Предположим, что на основе этого класса вы создаете производный:

class Derived : public Base
{

public:
virtual void PrintMe();
};

Теперь компилятор сможет корректно вызывать функцию PrintMe, если вы используете
один и тот же указатель для ссылки на объекты этих двух классов.

Вам не обязательно набирать слово virtual перед названием функции при объявлении производного класса. Однако лучше это сделать, поскольку в дальнейшем при чтении кодов программы вам легче будет определить, что в данном случае вы имеете дело с виртуальной функцией.
Например, класс Derived можно объявить так:

class Derived : public Base
{

public:
void PrintMe();
}

Однако следующий код более понятен:

class Derived : public Base
{

public:
virtual void PrintMe();
}

Когда без этого не обойтись

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

В программе используются два класса: Bass и Derived. Класс 3ase содержит функ-
цию-член PrintMe, которая не является виртуальной, и виртуальную функцию-член
PrintMeV:

class Base
{

public:
void PrintMe f);
virtual void PrintMeV();
} ;

Класс Derived создается путем наследования класса Base. При этом обе функции перегружаются:

class Derived : public Base
{

public :
void PrintMe();
virtual void PrintMeV();
};

Работа функции main начинается с создания объекта класса Вазе. Чтобы продемонстрировать свойства виртуальной функции, для доступа к объекту используется указатель:

Base *poBase = new Base (); Затем с его помощью вызываются функции-члены:
poBase->PrintMe();
poBase->PrintMeV();

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

poBase = new Derived();
poBase->PrintMe() ;
poBase->PrintMeV();

Когда этот код будет выполняться, вы увидите, что будет вызвана функция-член PrintMe класса Дажe несмотря на то, что указатель poBase в данный момент ссылается на объект класса Derived. Это происходит потому, что функция PrintMe не является виртуальной. В то же время вызванная функция PrintMeV будет относиться к классу Derived. Почему? Потому что она является виртуальной. (На рис. 19.1 показан результат выполнения приведенных выше кодов.)

. . .

Рис. 19.1. Использование виртуальных функций позволяет указателям находить функции-члены производных классов

После того как вы объявили какую-то функцию виртуальной, она становится таковой для всех производных классов. Например, функция PrintMeV была объявлена как виртуальная для класса Base. Следовательно, она также будет виртуальном и для производного класса Derived. Если вы создадите новый класс (назвав его, допустим, DerivedKiddo), производный от класса Derived, его функция PrintMeV также будет виртуальной. Независимо от того, как много классов будут наследовать коды других классов (а на количество классов, создаваемых путем наследования, ограничений нет), компилятор всегда сможет правильно определит - функцию какого класса вы хотите вызвать.
Вот полная распечатка кодов программы, о которой только что шла речь:
// Inherit2
// Illustrates virtual functions

#include "stdafx.h"
#using <mscorlib.dll>
using namespace System;

//This is the base class.
class Base
{

public:
void PrintMe();
virtual void PrintMeV();
};

//Print it, letting us know it is the base function
void Base::PrintMe()
{

Console::WriteLine(S"Non-virtual base");
}

void Base::PrintMeV()
{

Console::WriteLine(S"Virtual base");
}

//Derived is inherited from Base.
class Derived : public Base
{

public:
void PrintMe();
virtual void PrintMeV();
};

//Let us know the derived one was called.
void Derived::PrintMe()
{

Console::WriteLine(S"Non-virtual derived");
}

void Derived::PrintMeV()
{

Console::WriteLine(S"Virtual derived");
}

//Hang out until the user is finished
void HangOut()
{

Console::WriteLine(L"Hit the enter key to stop the program");
Console::ReadLine();
}

// This is the entry point for this application
#ifdef _UNICODE
int wmain(void)
#else
int main(void)
#endif
{

//Create a base class dynamically
Base *poBase = new Base();

//Call the two functions
Console::WriteLine(S"Using the base class");
poBase->PrintMe();
poBase->PrintMeV();

delete poBase;

//Now use the base class pointer to point to the derived class
poBase = new Derived();

//Call the two functions
Console::WriteLine(S"Using the derived class");
poBase->PrintMe();
poBase->PrintMeV();

HangOut();
return 0;

}

Вверх

Абстрагирование от деталей

Предположим, вам нужно создать программу, рисующую геометрические фигуры. Заранее известно, что будет много разных фигур, таких как круги, линии и квадраты. Для хранения информации обо всех отображаемых фигурах можно использовать список ArrayList. Следовательно, вам нужен базовый класс, из которого путем наследования можно создать производный классы для представления всех этих фигур. Почему? Помните, что указатель на базовый класс может ссылаться также и на объекты производных классов. Имея базовый класс, вы можете сохранять все производные объекты в списке ArrayList, извлекать их оттуда с помощью указателя на базовый класс, вызывать функцию Draw для рисования фигур на экране. Сделав функцию-член Draw виртуальной, вы обеспечиваете корректность выполнения программы: круги будут рисоваться как круги, линии как линии, а квадраты как квадраты. Итак, вам нужно создать базовый класс ShapeObject, который станет основой для создания производных объектов LineObject и CircleObject, а также других объектов, предназначенных для отображения фигур на экране.

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

Чтобы обобщить эту концепцию и не допустить непреднамеренного прямого использования класса ShapeObject, вы можете сделать его абстрактным базовым классом. Абстрактный базовый класс представляет некую концепцию (отсюда и его название — абстрактный), но сам не несет никакой функциональности. Вы никогда не сможете создать объект такого класса: если попытаетесь это сделать, компилятор сразу же выскажет все, что думает по этому поводу. Этот класс может быть использован только для создания на его основе производных классов. А сами производные классы должны определить функциональность для каждой функции-члена, наследованной от абстрактного базового класса.

Чтобы сообщить компилятору, что создаваемый класс должен быть абстрактным, нужно
все функции-члены этого класса сделать исключительно виртуальными. Этот термин означает, что интерфейс класса объявляется, но не определяется. Чисто технически это выполняется следующим образом: при объявлении функций-членов в конце строки наберите = 0;

//Объявление абстрактного базового класса
gc class ShapeObject
{

virtual void Initialize!) = 0;
virtual void Draw(Graphics *poG) = 0;
};

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

Класс, функции-члены которого объявлены таким образом, не может иметь собственных объектов, т.е. вы не сможете создать объект класса ShapeObject, набрав код наподобие такого:

ShapeObject foo;

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

Готов ли класс к тому, чтобы обзавестись потомством?

Классы к наследованию нужно подготовить. Вот небольшой список, с которым следует сверяться, чтобы определить, все ли сделано для того, чтобы какой-либо класс смог стать базовым (родительским) и произвести на свет парочку производных (дочерних) классов. (Готовы ли ползунки и погремушки? И как насчет детской коляски и кроватки?)

Искусство абстрагирования

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

Как она будет работать? Как и ранее, для сохранения информации об отображаемых фигурах будет использоваться список ArrayList. Но теперь в этот список будут помещаться объекты классов LineObject и CircleObject:

Console::WriteLine("Туре с to create a circle and 1 to create a line " ) ;

//Отображаемое сообщение переводится так: "Наберите с, чтобы
//создать круг, и 1, чтобы создать линию"
pszMore = Console::ReadLine();
if (pszMore->Equals(S"c">)
{

//Создание объекта, представляющего круг
poShape = new CircleObject();
}
else
i
//Создание объекта, представляющего линию
poShape = new LineObject();
}

Оба класса являются производными от абстрактного базового класса ShapeObject.
И все эти классы имеют виртуальные функции-члены Initialize и Draw, которые поразному определяются для каждого производного класса. Так, для класса LineObject функции-члены с такими именами считывают значения координат двух точек и рисуют между ними прямую линию. Для класса CircleOoject такие же функции считывают координаты центра круга и длину радиуса и, исходя из этих данных, рисуют круг на экране.

Как и ранее, при извлечении объектов из списка нужно указать, к какому классу они относятся. В данном случае вы указываете, что все объекты относятся к классу ShapeObject, и затем вызываете функции-члены, полагаясь на то, что компилятор сможет разобраться, к какому классу (LineObject или CircleObject) на самом деле относится текущий объект и какая функция должна быть вызвана:

IEnumerator *poEnumerator = m_paShapes->GetEnumerator();
while (poEnumerator->MoveNext())
{

//Вызов функции, отображающей фигуру на экране
static_cast(poEnumerator->Current)->Draw(poG);
}

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

// Draw8
// Uses inheritance and virtual functions

#include "stdafx.h"

#using <mscorlib.dll>
#using <System.dll>
#using <System.Windows.Forms.dll>
#using <System.Drawing.dll>

using namespace System;
using namespace System::Drawing;
using namespace System::Windows::Forms;
using namespace System::Collections;

//Stores shape information
//An abstract base class
__gc class ShapeObject
{

public:
virtual void Initialize() = 0;
virtual void Draw(Graphics *poG) = 0;
};

//Stores line information
__gc class LineObject : public ShapeObject
{

public:
virtual void Initialize();
virtual void Draw(Graphics *poG);
private:
int m_nXFrom;
int m_nYFrom;
int m_nXTo;
int m_nYTo;
};

//Initialize values for this line object
void LineObject::Initialize()
{

//Read the x and y positions and store them in the structure
Console::WriteLine(S"What is the starting x position?");
m_nXFrom = Int32::Parse(Console::ReadLine());
Console::WriteLine(S"What is the starting y position?");
m_nYFrom = Int32::Parse(Console::ReadLine());
Console::WriteLine(S"What is the ending x position?");
m_nXTo = Int32::Parse(Console::ReadLine());
Console::WriteLine(S"What is the ending y position?");
m_nYTo = Int32::Parse(Console::ReadLine());
}

//Draws the line
void LineObject::Draw(Graphics *poG)
{

//Create a pen with which to draw
Pen *poPen = new Pen(Color::Red);
//Draw the line
poG->DrawLine(poPen, m_nXFrom, m_nYFrom, m_nXTo, m_nYTo);
}

//Stores circle information
__gc class CircleObject : public ShapeObject
{

public:
virtual void Initialize();
virtual void Draw(Graphics *poG);
private:
int m_nXCenter;
int m_nYCenter;
int m_nRadius;
};

//Initialize values for this circle object
void CircleObject::Initialize()
{

//Read the center position and radius and store them in the structure
Console::WriteLine(S"What is the center x position?");
m_nXCenter = Int32::Parse(Console::ReadLine());
Console::WriteLine(S"What is the center y position?");
m_nYCenter = Int32::Parse(Console::ReadLine());
Console::WriteLine(S"What is the radius?");
m_nRadius = Int32::Parse(Console::ReadLine());
}

//Draws the line
void CircleObject::Draw(Graphics *poG)
{

//Create a pen with which to draw
Pen *poPen = new Pen(Color::Blue);
//Draw the line
poG->DrawEllipse(poPen, m_nXCenter-m_nRadius, m_nYCenter-m_nRadius, m_nXCenter+m_nRadius, m_nYCenter+m_nRadius);
}

//Stores all of the lines to draw
__gc class DisplayObject
{

public:
void Add();
void Draw(Graphics *poG);
DisplayObject();
private:
ArrayList *m_paShapes;
};

//Initialize the DisplayObject structures
DisplayObject::DisplayObject()
{

m_paShapes = new ArrayList();
}

//Add a new object
void DisplayObject::Add()
{

bool fFinished = false;
String *pszMore;
ShapeObject *poShape;

while (!fFinished)
{

//What shall we create?
Console::WriteLine("Type c to create a circle and l to create a line");
pszMore = Console::ReadLine();

if (pszMore->Equals(S"c"))
{

//We'll create a circle
poShape = new CircleObject();
}
else
{
//We'll create a line
poShape = new LineObject();
}

//Initialize it
poShape->Initialize();
//Store it in the structure
m_paShapes->Add(poShape);

//Should we add another one?
Console::WriteLine("Hit y to enter a new shape");
pszMore = Console::ReadLine();
if (!pszMore->Equals(S"y"))
{

fFinished = true;
}
}
}

//Draw all of the shapes
void DisplayObject::Draw(Graphics *poG)
{

//Go through each of the shape objects
IEnumerator *poEnumerator = m_paShapes->GetEnumerator();
while (poEnumerator->MoveNext())
{
//Call the draw method for the object
static_cast(poEnumerator->Current)->Draw(poG);
}
}

// This is the entry point for this application
#ifdef _UNICODE
int wmain(void)
#else
int main(void)
#endif
{

//Structures for drawing graphics
Form *poForm = new Form();
Graphics *poGraphics = poForm->CreateGraphics();
DisplayObject *poDisplay = new DisplayObject();

//Add the shapes
poDisplay->Add();

//Move the display surface to the far left
poForm->SetDesktopLocation(0,0);
//Show the display surface
poForm->Show();

//Draw the shapes
poDisplay->Draw(poGraphics);

//Free up the graphics surface
poGraphics->Dispose();

//Hang out until the user closes the form
Application::Run(poForm);

}


Назад | Начало урока | Вверх | Вперед
Содержание