русс | укр

Языки программирования

ПаскальСиАссемблерJavaMatlabPhpHtmlJavaScriptCSSC#DelphiТурбо Пролог

Компьютерные сетиСистемное программное обеспечениеИнформационные технологииПрограммирование

Все о программировании


Linux Unix Алгоритмические языки Аналоговые и гибридные вычислительные устройства Архитектура микроконтроллеров Введение в разработку распределенных информационных систем Введение в численные методы Дискретная математика Информационное обслуживание пользователей Информация и моделирование в управлении производством Компьютерная графика Математическое и компьютерное моделирование Моделирование Нейрокомпьютеры Проектирование программ диагностики компьютерных систем и сетей Проектирование системных программ Системы счисления Теория статистики Теория оптимизации Уроки AutoCAD 3D Уроки базы данных Access Уроки Orcad Цифровые автоматы Шпаргалки по компьютеру Шпаргалки по программированию Экспертные системы Элементы теории информации

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


Дата добавления: 2013-12-23; просмотров: 1341; Нарушение авторских прав


Ссылки на производные классы

Указатели и ссылки на производные типы

Передача параметров в базовый класс

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

порожденный конструктор(список_аргументов) : базовый1(список_аргументов), базовый2{список_аргументов), ..., базовыйN(список_аргументов)

{

}

Здесь под базовый1, ... базовыйN обозначены имена базовых классов, наследуемые производ­ным классом. Обратим внимание, что с помощью двоеточия конструктор производного класса отделяется от списка конструкторов базового класса. Список аргументов, ассоциированный с базовыми классами, может состоять из констант, глобальных переменных или параметров конст­руктора производного класса. Поскольку инициализация объекта происходит во время выполне­ния программы, можно использовать в качестве аргумента любой идентификатор, определенный в области видимости класса.

Следующая программа иллюстрирует, как передаются аргументы базовому классу из производ­ного класса:

#include <iostream.h>

class X

{

protected:

int a;

public:

X(int i) { a = i; }

};

class Y

{

protected:

int b;

public:

Y(int i) { b = i; }



};

// Z наследует как от X, так и от Y

class Z: public X, public Y

{

public:

Z(int х, int у) : Х(х), Y(у) { cout << "Initializing\n"; }

int make_ab() { return a*b; }

};

int main()

{

Z i(10, 20);

cout << i.make_ab();

return 0;

}

Обратим внимание, что конструктор класса Z фактически не использует параметры прямо. Вме­сто этого в данном примере они просто передаются конструкторам классов X и Y. Вместе с тем при необходимости класс Z может использовать эти и другие аргументы непосредственно.

Прежде чем переходить к виртуальным функциям и полиморфизму, следует объяснить один из их важнейших атрибутов. Начнем с указателей. В общем случае указатель одного типа не может указы­вать на объект другого типа. Из этого правила, однако, есть исключение, которое относится только к производным классам. В C++ указатель на базовый класс может указывать на объект производного класса, полученного из этого базового класса. Предположим, например, что имеется базовый класс B_class и его производный класс D_class. В C++ любой указатель типа B_class* может также указы­вать на объект типа D_class. Например, если имеются следующие объявления переменных:

B_class *p; // указатель на объект типа B_class

B_class B; // объект типа B_class

D_class D; // объект типа D_class

то следующие присваивания абсолютно законны:

р = &B; // р указывает на объект типа B_class

р = &D; /* р указывает на объект типа D_class, являющийся объектом, порожденным от B_class */

Используя указатель р, можно получить доступ ко всем членам D, которые наследованы от B. Однако специфические члены D не могут быть получены с использованием указателя р (по крайней мере до тех пор, пока не будет осуществлено приведение типов). Это является след­ствием того, что указатель «знает» только о членах базового типа и не знает ничего о специфи­ческих членах производных типов.

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

#include <iostream.h>

#include <string.h>

class CName

{

char name[80];

public:

void put_name(char *s) { strcpy(name, s); }

void show_name() { cout << name << " "; }

};

class CBook: public CName

{

char phone_num[80];

public:

void put_phone(char *num) { strcpy(phone_num, num); }

void show_phone() { cout << phone_num << "\n"; }

};

int main()

{

CName *p_name, name;

CBook *p_book, book;

p_name = &name;

// доступ к CName через указатель

p_name->put_name("Ivanov I. I.");

// доступ к CBook через указатель

p_name = &book;

p_name->put_name("Petrov P. P.");

// показать каждое имя соответствующего объекта

name.show_name();

book.showname();

cout << "\n";

/* поскольку put_phone и show_phone не являются частью базового класса, они не доступны через указатель на базовый класс и доступ должен осуществляться или напрямую, или, как показано ниже, через указатель на порожденный класс */

p_book = &book;

p_book->put_phone("555 555-1234");

p_name->show_name(); // в данной строке могут использоваться р и dp

p_book->show_phone();

return 0;

}

В этом примере указатель p_name определен как указатель на класс CName. Однако он может указы­вать также на объект производного класса CBook и может использоваться для доступа к членам производного класса, которые были определены в базовом классе. Вместе с тем следует запом­нить, что этот указатель не может использоваться для доступа к членам, специфическим для про­изводного класса, до тех пор, пока не выполнено приведение типов. Именно поэтому доступ к функции show_phone() осуществляется с использованием указателя p_book, являющегося указателем на производный класс.

Если необходимо получить доступ к элементам производного класса с помощью указателя, имеющего тип указателя на базовый класс, необходимо воспользоваться приведением типов. На­пример, в следующей строке кода осуществляется вызов функции show_phone() класса CBook:

((CBook*)p_name)->show_phone();

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

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

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

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

Виртуальная функция объявляется в базовом классе с использованием ключевого слова virtual. Когда же она переопределяется в производном классе, повторять ключевое слово virtual нет не­обходимости, хотя и в случае его повторного использования ошибки не возникнет.

В качестве первого примера виртуальной функции рассмотрим следующую короткую программу:

#include <iostream.h>

class Base

{

public:

virtual void who() { cout << “Base\n”; }

};

class first: public Base

{

public:

void who() { cout << "First\n"; }

};

class second: public Base

{

public:

void who() { cout << "Second\n"; }

};

int main()

{

Base base_object, *p;

first first_object;

second second_object;

p = &base_object;

p->who(); // доступ к who класса Base

p = &first_object;

p->who(); // доступ к who класса first

p = &second_obect;

p->who(); // доступ к who класса second

return 0;

}

Программа выдаст следующий результат:

Base

First

Second

Проанализируем подробно эту программу, чтобы понять, как она работает. Как можно видеть, в классе Base функция who() объявлена как виртуальная. Это означает, что эта функция может быть переопределена в производных классах. В каждом из классов first и second функция who() переопределена. В функции main() определены три переменные. Первой является объект base_object класса Base. После этого объявлен указатель р на класс Base, затем объекты first_object и second_object, относящиеся к двум производным классам. Далее указателю p при­своен адрес объекта base_object и вызвана функция who(). Поскольку эта функция объявлена как виртуальная, то C++ определяет на этапе исполнения, какую из версий функции who() употребить, в зависимости от того, на какой объект указывает указатель р. В данном случае им является объект типа Base, поэтому исполняется версия функции who(), объявленная в классе Base. Затем указате­лю p присвоен адрес объекта first_object. (Как известно, указатель на базовый класс может быть ис­пользован с любым производным классом.) После того, как функция who() была вызвана, C++ снова анализирует тип объекта, на который указывает p, для того, чтобы определить версию фун­кции who(), которую необходимо вызвать. Поскольку p указывает на объект класса first, то используется соответствующая версия функции who(). Аналогично, когда указателю p присвоен адрес объекта second_object, то используется версия функции who(), объявленная в классе second.

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

#include <iostream.h>

class Base

{

public:

virtual void who() { cout << "Base\n"; }

};

class first: public Base

{

public:

void who() { cout << "First\n"; }

};

class second: public Base

{

public:

void who() { cout << "Second\n"; }

};

void show_who(Base &r)

{

r.who();

}

int main()

{

Base base_object;

first first_object;

second second_object;

show_who(base_object); // доступ к who класса Base

show_who(first_object); // доступ к who класса first

show_who(second object); // доступ к who класса second

return 0;

}

Эта программа выводит на экран те же самые данные, что и предыдущая версия. В данном при­мере функция show_who() имеет параметр типа ссылки на класс Base. В функции main() вызов виртуальной функции осуществляется с использованием объектов типа Base, first и second. Вызываемая версия функции who() в функции show_who() определяется типом объекта, на кото­рый ссылается параметр при вызове функции.

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

На первый взгляд переопределение виртуальной функции в производном классе выглядит как специальная форма перегрузки функции. Но это не так, и термин перегрузка функции не приме­ним к переопределению виртуальной функции, поскольку между ними имеются существенные раз­личия. Во-первых, функция должна соответствовать прототипу. Как известно, при перегрузке обычной функции число и тип параметров должны быть различными, а при переопределе­нии виртуальной функции интерфейс функции должен в точности соответствовать прототипу. Если же такого соответствия нет, то такая функция просто рассматривается как перегруженная и она утрачивает свои виртуальные свойства. Кроме того, если отличается только тип возвращаемо­го значения, то выдается сообщение об ошибке. (Функции, отличающиеся только типом возвра­щаемого значения, порождают неопределенность.) Другим ограничением является то, что вирту­альная функция должна быть членом, а не другом класса, для которого она определена. Тем не менее, виртуальная функция может быть другом другого класса. Хотя деструктор может быть виртуальным, но конструктор виртуальным быть не может.

В силу различий между перегрузкой обычных функций и переопределением виртуальных фун­кций будем использовать для последних термин переопределение (overriding).

Если функция была объявлена как виртуальная, то она и остается таковой вне зависимости от количества уровней в иерархии классов, через которые она прошла. Например, если класс second получен из класса first, а не из класса Base, то функция who() останется виртуальной и будет вызываться корректная ее версия, как показано в следующем примере:

class second: public first

{

public:

void who() { cout << "Second derivation\n"; }

};

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

#include <iostream.h>

class Base

{

public:

virtual void who() { cout << "Base\n"; }

};

class first: public Base

{

public:

void who() { cout << "First\n”; }

};

class second: public Base {};

int main()

{

Base base_object, *p;

first first_object;

second second_object;

p = &base_object; p->who();

p = &first_object; p->who();

p = &second_object; p->who();

return 0;

}

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

Base

First

Base

Надо иметь в виду, что характеристики наследования носят иерархический характер. Чтобы проиллюстрировать это, предположим, что в предыдущем примере класс second порожден от класса first вместо класса Base. Когда функцию who() вызывают, используя указатель на объект типа second (в котором функция who() не определялась), то будет вызвана версия функции who(), объявленная в классе first, поскольку этот класс — ближайший к классу second. В общем случае, когда класс не переопределяет виртуальную функцию, C++ использует первое из определений, которое он находит, идя от потомков к предкам.

5.8 Для чего нужны виртуальные функции?

Как отмечалось в начале главы, виртуальные функции в комбинации с производными типами позволяют языку C++ поддерживать полиморфизм времени исполнения. Этот полиморфизм ва­жен для объектно-ориентированного программирования, поскольку он позволяет переопреде­лять функции базового класса в классах-потомках с тем, чтобы иметь их версию применительно к данному конкретному классу. Таким образом, базовый класс определяет общий интерфейс, кото­рый имеют все производные от него классы, и вместе с тем полиморфизм позволяет производным классам иметь свои собственные реализации методов. Благодаря этому полиморфизм часто опре­деляют фразой «один интерфейс — множество методов».

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

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

Чтобы понять всю мощь идеи «один интерфейс — множество методов», рассмотрим следую­щую короткую программу. Она создает базовый класс figure. Этот класс используется для хране­ния размеров различных двумерных объектов и для вычисления их площадей. Функция set_dim() является стандартной функцией-членом, поскольку ее действия являются общими для всех произ­водных классов. Однако функция show_area() объявляется как виртуальная функция, поскольку способ вычисления площади каждого объекта является специфическим. Программа использует класс figure для вывода двух специфических классов square и triangle.

#include <iostream.h>

class figure

{

protected:

double x, y;

public:

void set_dim(double i, double j) { x = i; У = j; }

virtual void show_area() { cout << "No area computation defined for this class.\n"; }

};

class triangle: public figure

{

public:

void show_area()

{

cout << "Triangle with height " << x << " and base " << y << " has an area of " << x * 0.5 * у;

}

};

class square: public figure

{

public:

void show_area()

{

cout << "Square with dimensions “ << x << "x" << y << " has an area of " << x * у;

}

};

int main()

{

figure *p; /* создание указателя базового типа */

triangle t; /* создание объектов порожденных типов */

square s;

p = &t; p->set_dim(10.0, 5.0); p->show_area();

p = &s; p->set_dim(10.0, 5.0); p->show_area() ;

return 0;

}

Как можно видеть на основе анализа этой программы, интерфейс классов square и triangle является одинаковым, хотя оба обеспечивают свои собственные методы для вычисления площади каж­дой из фигур. На основе объявления класса figure можно вывести класс circle, вычисляющий пло­щадь, ограниченную окружностью заданного радиуса. Для этого необходимо создать новый производный класс, в котором реализовано вычисление площади круга. Вся сила виртуальной функции основана на том факте, что можно легко вывести новый класс, разделяющий один и тот же общий интерфейс с другими родственными классами. В качестве примера здесь показан один из способов реализации:

class circle: public figure

{

public:

void show_area()

{

cout << "Circle with radius " << x << " has an area of " << 3.14 * x * x;

}

};

Прежде чем использовать класс circle, посмотрим внимательно на определение функции show_area(). Обратим внимание, что она использует только величину х, которая выражает ради­ус. Как известно, площадь круга вычисляется по формуле pR2. Однако функция set_dim(), опре­деленная в базовом классе figure, требует не одного, а двух аргументов. Поскольку класс circle не нужда­ется во второй величине, то как нам быть в данной ситуации?

Имеются два пути для решения этой проблемы. Первый заключается в том, чтобы вызвать set_dim(), используя в качестве второго параметра фиктивный параметр, который не будет ис­пользован. Недостатком такого подхода служит необходимость запомнить этот исключительный случай, что по существу нарушает принцип «один интерфейс — множество методов».

Лучшее решение данной проблемы связано с использованием параметра y в set_dim() со значе­нием по умолчанию. В таком случае при вызове set_dim() для круга необходимо указать только радиус. При вызове set_dim() для треугольника или прямоугольника укажем обе величины:

#include <iostream.h>

class figure

{

protected:

double x, y;

public:

void set_dim(double i, double j=0) { x = i; У = j; }

virtual void show_area() { cout << "No area computation defined for this class.\n"; }

};

int main()

{

figure *p; /* создание указателя базового типа */

circle с;

p = &с;

p->set_dim(9.0); p->show_area();

return 0;

}

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



<== предыдущая лекция | следующая лекция ==>
Множественное наследование | Чисто виртуальные функции и абстрактные типы


Карта сайта Карта сайта укр


Уроки php mysql Программирование

Онлайн система счисления Калькулятор онлайн обычный Инженерный калькулятор онлайн Замена русских букв на английские для вебмастеров Замена русских букв на английские

Аппаратное и программное обеспечение Графика и компьютерная сфера Интегрированная геоинформационная система Интернет Компьютер Комплектующие компьютера Лекции Методы и средства измерений неэлектрических величин Обслуживание компьютерных и периферийных устройств Операционные системы Параллельное программирование Проектирование электронных средств Периферийные устройства Полезные ресурсы для программистов Программы для программистов Статьи для программистов Cтруктура и организация данных


 


Не нашли то, что искали? Google вам в помощь!

 
 

© life-prog.ru При использовании материалов прямая ссылка на сайт обязательна.

Генерация страницы за: 0.014 сек.