Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Методические указания.doc
Скачиваний:
9
Добавлен:
18.04.2015
Размер:
619.52 Кб
Скачать
  1. Преобразование типов

Логика преобразования типов основана на следующем достаточно простом положении: объект производного типа может рассматриваться как объект его базового типа; обратное утверждение неверно. Компилятор может неявно выполнить преобразование объекта производного типа к объекту типа базового:

class Base { /* … */ };

class Derived : public Base { /* … */ };

Derived derived;

Base base = derived;

Обратное преобразование должно быть определено программистом:

Derived tmp = base; // ошибка, если для Derived

// не определен конструктор Derived(Base&)

На практике в таких случаях значительно удобнее иметь дело не с самими объектами, а с указателями (ссылками) на них. Гораздо чаще, чем преобразование типов, встречается преобразование указателей (ссылок) на них.

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

class Base { /* … */ };

class Derived : public Base { /* … */ };

то принципы преобразования очень просты; неявно может быть выполнено преобразование указателя типа Derived * к указателю типа Base *; обратное преобразование обязательно должно быть явным. Другими словами при общем наследовании объект производного типа может рассматриваться как объект базового типа.

В случае личного наследования:

class Base { /* … */ };

class Derived : Base { /* … */ };

преобразование указателя на производный класс к указателю на базовый класс необходимо производить явно.

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

Лекция №5.

  1. Виртуальные функции

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

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

class Figure {

protected:

int lineColor;

int lineThickness;

int areaColor;

public:

FigureType type;

Figure ( int color1, int color2, int width=NORM_WIDTH);

void drawElement ();

};

Определим тип фигур:

enum FigureType { LINE, POLYGON, CIRCLE };

Для удобства, создадим структуру, указывающую координаты точки на экране:

struct Coord { int x,y; };

Реальные фигуры составят классы, производные от класса Figure:

class Line : public Figure

{

Coord beg, end;

public:

Line (Coord b, Coord e, int color1, int color2);

void drawLine ();

};

class Polygon : public Figure

{

int numOfVertices;

int * vertices;

public:

Polygon (int num, int * coord, int color1, int color2);

void drawPolygon ();

};

class Circle : public Figure

{

Coord centre;

int radius;

public:

Circle (Coord c, int rad, int color1, int color2);

void drawCircle ();

};

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

Реализация всех функций-членов классов достаточно очевидна. Рассмотрим дефиницию важной для нас функции:

void Figure::drawElement ()

{

switch (type)

{

case LINE:

((Line*)this)->drawLine();

break;

case POLYGON:

((Polygon*)this)->drawPolygon();

break;

case CIRCLE:

((Circle*)this)->drawCircle();

break;

}

}

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

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

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

Применительно к нашему примеру естественно объявить виртуальными функции рисования геометрических фигур. Изменения, вносимые в этом случае в нашу программу, будут следующими: во-первых, изменится определение класса Figure, т.к. нет больше необходимости в поле type типа FigureType (как и в самом этом типе), а вместо функции

void drawElement ();

появится функция

virtual void draw () {}

Служебное слово virtual означает, что функция draw () может иметь свои версии для различных классов, производных от Figure. В классе Figure функция draw () ничего делать не должна, так как непонятно, что собой представляет объект типа Figure. Теперь создадим свои варианты функции draw() для классов, производных от Figure. Эти варианты заменят функции drawLine(), drawPolygon(), drawCircle():

class Line : public Figure

{

// …

virtual void draw ();

}:

class Polygon : public Figure

{

// …

virtual void draw ();

}:

class Circle : public Figure

{

// …

virtual void draw ();

}:

Все версии виртуальной функции draw () должны иметь один и тот же тип, то есть одно и то же имя, список аргументов и тип результата. В производных классах служебное слово virtual не обязательно, хотя для ясности его лучше не опускать. Кратко отличие виртуальной функции от обычной можно сформулировать так: при вызове через указатель или ссылку обычная функция определяется типом указателя или ссылки, а виртуальная – истинным типом объекта, на который указывает указатель или ссылка. Если вы не хотите использовать механизм виртуальных функций, то при обращении к нужной функции необходимо использовать ее полное имя, например, Circle::draw ().

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

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

Поскольку при нормальной работе программы рисования геометрических фигур в ней не должно существовать объектов типа Figure, а только объекты, производные от него, то функция Figure::draw () могла быть определена для контроля ошибок следующим образом:

Figure::draw ()

{

cout<<"Ошибка: попытка нарисовать figure";

exit(1);

}

В C++ существует более удобный и надежный способ контроля того, чтобы объект типа Figure не использовался в явном виде в программе. Версия виртуальной функции, которая не должна никогда быть использована, называется чисто виртуальной функцией и объявляется через присваивание декларации функции значения 0:

class Figure

{

// …

virtual void draw ()=0;

};

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

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

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

Лекция №6.

  1. Виртуальные деструкторы

Рассмотрим простой пример:

class Base

{

public:

Base ();

~Base ();

};

class Derived : public Base

{

char * str;

public:

Derived (int arg){ str=new char [arg]; }

~Derived (){ delete str; }

};

А теперь рассмотрим следующий фрагмент программы:

// …

Base * bp= new Derived (10);

delete bp;

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

class Base

{

// …

virtual ~Base (){ /* … */ }

};

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

  1. Шаблоны

Предположим, что нам необходимо написать функцию, которая возвращает "среднее" (в некотором смысле) значение двух своих аргументов. Назовем эту функцию Mid (). В С++ существует три способа решения этой задачи:

а) Определить макрос вида

#define Mid(x,y) (((x)+(y))/2)

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

б) Для каждого из возможных сочетаний типов операндов определить свою версию функции Mid(). Например:

int Mid ( int x, int y){ return (x+y)/2; }

int Mid ( float x, float y){ return (x+y)/2; }

int Mid ( Light x, Light y){ return (x+y)/2; }

Мы предполагаем, что операции + и / переопределены для типа Light. Этот способ обеспечивает полную безопасность использования функций Mid(), но необходимость написание трех идентичных вариантов может спровоцировать программиста на применение макроподстановки. К тому же, если появляется необходимость в четвертой функции, оперирующей с аргументами другого типа, то программист должен скопировать код функции с изменением типов операндов.

в) C++ позволяет создать настраиваемый на различные типы шаблон функции Mid ():

template <class Type> Type Mid (Type a, Type b) { return (a+b)/2; }

Имя типа Type как бы становится параметром макроподстановки, но все необходимые действия при этом выполняются компилятором. Существует и еще одно отличие от макроподстановки: обработка template-функции не приводит к включению кода функции в код программы при каждом обращении. Вместо этого генерируется обычное обращение к подпрограмме с предварительным помещением аргументов в стек. Параметрами шаблона могут быть несколько имен типов:

template <class T1, class T2> T1& myFunction (T1&, T2&);

В качестве примера настраиваемого класса рассмотрим концепцию "массив с заданным диапазоном изменения индекса".

template <class Data> class Array {

Data* a;

int size;

int lowerBound;

public:

Array (int sz, int lb=0);

~Array();

Data& operator[] (int);

};

Объявление класса Array как template-класса с параметром Data говорит о том, что элементами массива типа Array могут быть объекты любого типа. Встает вопрос: как отличить в программе класс-шаблон, созданный для различных типов? Для этого необходимо использовать модификацию имени класса: наименование типа в угловых скобках после имени класса. Объявление функций-членов класса Array осуществляется следующим образом:

template <class Data>

Array<Data>::Array (int sz, int lb=0)

{

a = new Data [size=sz];

lowerBound = lb;

}

template <class Data>

Array<Data>::~Array (){ delete a; }

Используется Array при создании конкретного объекта следующим образом:

Array<int> theArray (5,1);