Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Лекции по Технологии разработки ПО 2005.doc
Скачиваний:
0
Добавлен:
01.05.2025
Размер:
833.54 Кб
Скачать

Виртуальные и дружественные функции

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

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

class Base { // Базовый класс

public:

void show() //Обычная функция

{ cout << "Base\n"; }

};

//---------------------------------------------------------------------------

class Derv1 : public Base { //Производный класс 1

public:

void show()

{ cout << "Derv1\n"; }

};

//---------------------------------------------------------------------------

class Derv2 : public Base { //Производный класс 2

public:

void show()

{ cout << "Derv2\n"; }

};

//---------------------------------------------------------------------------

int main() {

Derv1 d1; //Объект производного класса 1

Derv2 d2; //Объект производного класса 2

Base* ptr; //Указатель на базовый класс

ptr = &d1; ptr->show(); //Адрес dv1 занести в указатель баз. Класса и //Выполнить show()

ptr = &d2; ptr->show(); //Адрес dv2 занести в указатель баз. класса и //Выполнить show()

return 0;

}

В главной функции мы создали объекты производных классов, а также указатель на базовый класс. Затем адрес объекта порождённого класса мы заносим в указатель базового класса: ptr = &d1;. Здесь мы присваиваем адрес объекта одного типа указателю на другой тип. Это вполне возможно. Дело в том, что указатели на объекты порождённых классов совместимы по типу с указателями на объекты базового класса.

Здесь нас более всего интересует, какая же, собственно, функция выполняется в строке: ptr->show();. Функция базового класса или производного класса? Программа выведет: Base. Компилятор не смотрит на содержимое указателя ptr, а выбирает тот метод, который удовлетворяет типу указателя (в данном случае это метод базового класса).

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

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

Проблема доступа к методам, переопределённым в производных классах, через указатель на базовый класс решается посредством использования виртуальных методов. Чтобы сделать некоторый метод виртуальным, надо в базовом классе предварить его заголовок спецификатором virtual. После этого он будет восприниматься как виртуальный во всех производных классах.

Сделаем соответствующее изменение в нашей программе.

class Base { //Базовый класс

public:

virtual void show() //Виртуальная функция

{ cout << "Base\n"; }

};

… тоже самое

В результате, программа выведет:

Derv1

Derv2

Таким образом мы добились, что один и тот же вызов ptr->show(); запускает разные функции в зависимости от содержимого ptr. Компилятор выбирает функцию, удовлетворяющую тому, что занесено в указатель (т.е. на что указывает ptr), а не типу указателя, как было в предыдущей программе. Такой подход называется поздним, или динамическим, связыванием.

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

Для реализации динамического связывания компилятор создаёт таблицу виртуальных методов (vtbl), а к каждому объекту добавляет скрытое поле ссылки (vptr) на таблицу vtbl. Это требует больше ресурсов, но даёт выигрыш в возможностях и гибкости.

Д/З

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

Напиши на доске главную функцию.

class Base {

protected:

int x;

public:

Base() { x=0; }

Base(int xx) { x=xx; }

void show() { cout << "\nX = " << x; }

virtual void modify() { x = x * 2; }

};

//---------------------------------------------------------------------------

class Pro : public Base {

public:

Pro(int px) { x = px; }

void modify() { x = x / 2; }

};

//---------------------------------------------------------------------------

int main() {

Base b1(10); Pro p1(10);

Base* ptr;

ptr=&b1; ptr->modify(); ptr->show(); // =20

ptr=&p1; ptr->modify(); ptr->show(); // =5

getch(); return 0;

}

Если бы, мы не сделали метод modify() виртуальным, то вывод программы был бы таким: х=20; х=20.

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

Без использования указателей

С использованием указателей

class Tovar {

private:

char name[N];

int number;

float cena;

public:

void dobav_zap() {

cout << "\n Vvedite naimenovanie tovara: "; cin >> name;

cout << " Vvedite nomer tovara: "; cin >> number;

cout << " Vvedite stoimost tovara: "; cin >> cena;

}

};

class TovarProd : public Tovar {

private:

int srok;

int temp;

public:

void dobav_zap() {

Tovar::dobav_zap();

cout << "\n Vvedite srok godnosti: "; cin >> srok;

cout << " Vvedite temperaturu hranenija: "; cin >> temp;

}

};

class TovarProm : public Tovar {

};

int menu();

//---------------------------------------------------------------------------

int main() {

const int K = 100;

TovarProd tpd[K];

TovarProm tpm[K];

int otv=0, i=0, j=0;

do {

otv = menu();

if(otv==1) {

tpd[i].dobav_zap();

i++;

}

if(otv==2) {

tpm[j].dobav_zap();

j++;

}

}while(otv!=4);

return 0;

}

class Tovar {

private:

char name[N];

int number;

float cena;

public:

virtual void dobav_zap() {

cout << "\n Vvedite naimenovanie tovara: "; cin >> name;

cout << " Vvedite nomer tovara: "; cin >> number;

cout << " Vvedite stoimost tovara: "; cin >> cena;

}

};

class TovarProd : public Tovar {

private:

int srok;

int temp;

public:

void dobav_zap() {

Tovar::dobav_zap();

cout << " Vvedite srok godnosti: "; cin >> srok;

cout << " Vvedite temperaturu hranenija: "; cin >> temp;

}

};

class TovarProm : public Tovar {

};

int menu();

//---------------------------------------------------------------------------

int main() {

Tovar* mptr[100]; //массив указателей

int otv=0, i=0;

do {

otv = menu();

if(otv==1) {

mptr[i] = new TovarProd;

mptr[i]->dobav_zap();

i++;

}

if(otv==2) {

mptr[i] = new TovarProm;

mptr[i]->dobav_zap();

i++;

}

}while(otv!=4);

return 0;

}

В примере с указателями мы создаём массив указателей базового класса, тем самым получаем массив, элементы которого способны хранить информацию о продуктовых товарах и информацию о промтоварах. В зависимости от выбора пользователя каждому элементу массива выделяется столько памяти, сколько необходимо либо для АТД TovarProd, либо для АТД TovarProm. Обратите внимание и на то, что отпадает необходимость создавать и обрарбатывать два массива.