Функция, определенная в базовом классе, может быть переопределена в выведенном классе.
При вызове функции для объекта вызывается функция из того класса, какого типа объект.
Пример 1.
Вызов функции при помощи оператора «.»
class
base {
public:
void func() {
std::cout << “Функция func() из класса base\n”
}
};
class derived1: public base {
public:
void func() {
std::cout << “Функция func() из класса derived1\n”
}
};
class derived2: public base {
public:
void func() {
std::cout << “Функция func() из класса derived2\n”
}
};
int main() {
base b;
derived1 d1;
derived2 d2;
b.func(); // Вызов функции func() из класса base
d1.func(); // Вызов функции func() из класса derived1
d2.func(); // Вызов функции func() из класса derived2
return 0;
}
Эта программа выводит на экран следующие строки:
Функция func() из класса base
Функция func() из класса derived1
Функция func() из класса derived2
Пример 2. Переопределяется только функция с той же сигнатурой (имя и список параметров (с типом))
…
class derived1: public base {
public:
void func(int n) {
std::cout << “Функция func() из класса derived1\n”
}
};
…
Эта программа выводит на экран следующие строки:
Функция func() из класса base
Функция func() из класса base
Функция func() из класса derived2
Виртуальная функция (virtual function) — это функция-член класса, объявленная в базовом классе с ключевым словом virtual и переопределенная в производном классе.
Пример 3. Вызов виртуальной функции при помощи указателя на базовый класс:
class base {
public:
virtual void vfunc() {
std::cout << “Функция vfunc() из класса base\n”
}
};
class derived1: public base {
public:
virtual void vfunc() {
std::cout << “Функция vfunc() из класса derived1\n”
}
};
class derived2: public base {
public:
virtual void vfunc() {
std::cout << “Функция vfunc() из класса derived2\n”
}
};
int main() {
base *p, b;
derived1 d1;
derived2 d2;
b.vfunc(); // Вызов функции vfunc() из класса base
d1.vfunc(); // Вызов функции vfunc() из класса derived1
d2.vfunc(); // Вызов функции vfunc() из класса derived2
// Указатель на объект базового класса:
p = &b; // Присваиваем p адрес объекта b
p->vfunc(); // Вызов функции vfunc() из класса base
// Указатель на объект класса derived1:
p = &d1; // Присваиваем p адрес объекта d1 (можем, т.к. D1 – типа derived1 – выведен на базе base)
p->vfunc(); // Вызов функции vfunc() из класса derived1
// Указатель на объект класса derived2:
p = &d2;
p->vfunc(); // Вызов функции vfunc() из класса derived2
return 0;
}
Эта программа выводит на экран следующие строки:
Функция vfunc() из класса base
Функция vfunc() из класса derived1
Функция vfunc() из класса derived2
Функция vfunc() из класса base
Функция vfunc() из класса derived1
Функция vfunc() из класса derived2
Вопрос. Как компилятор узнает, функцию из какого класса вызвать?
Подсказка: не из p (p – всего-лишь адрес)
Ответ. Узнает из содержимого памяти, на которую указывает p
Вопрос. Что находится в области памяти, которую занимает объект типа какого-то класса?
Ответ. Там находятся члены класса.
Class base – не содержит членов класса => не занимает памяти?
В памяти, на которую указывает p, надо хранить информацию о виртуальных функциях.
Эта информация помещается в области памяти, предшествующей адресу, на который указывает p.
Таблица виртуальных функций
|
Сигнатура функции |
Адрес функции |
|
vfunc() |
Адрес в памяти, где находится код функции base::vfunc() |
p=&b (начинается память с адресом &b)
…
|
Сигнатура функции |
Адрес функции |
|
vfunc() |
Адрес в памяти, где находится код функции derived1::vfunc() |
p=&d1 (начинается память с адресом &d1)
...
|
Сигнатура функции |
Адрес функции |
|
vfunc() |
Адрес в памяти, где находится код функции derived2::vfunc() |
p=&d2 ((начинается память с адресом &d2)
…
В примере 3 внутри класса base объявлена виртуальная функция vfunc().
Эта функция будет виртуальной для derived1 и derived2 даже если в них она объявлена без ключевого слова virtual.
Для виртуальной функции вариант вызываемой функции определяется типом объекта, на который ссылается указатель p.
Виртуальную функцию можно вызвать обычным способом, используя имя объекта и оператор «.», но полиморфизм достигается только при обращении к ней через указатель.
Вопрос. Для чего нужны виртуальные функции?
Ответ. Для вызова функции выведенного класса по указателю на базовый класс.
(Если указатель имеет тип указателя на базовый класс, то при вызове виртуальной функции по этому указателю вызовется функция из выведенного класса
(того класса, тип которого имеет объект, на который указывает указатель)).
--------------------------------------------------------------
Виртуальные функции могут вызываться и с помощью ссылки на объект базового класса.
Пример 4. Вызов виртуальной функции через ссылку на объект базового класса: (на базе примера 3)
void f(base &r) {
r.vfunc();
}
int main() {
base b;
derived d1;
derived d2;
f(b); // Функции f() передается объект класса base
f(d1); // Функции f() передается объект класса derived1
f(d2); // Функции f() передается объект класса derived2
return 0;
}
Эта программа выводит на экран те же сообщения, что и программа из примера 1.
----------------------------------------------
Атрибут virtual наследуется неограниченное количество раз, то есть виртуальная функция при наследовании каждый раз остается виртуальной (см. Пример 5)
Пример 5. Наследование атрибута virtual:
class base {
public:
virtual void vfunc() {
std::cout << “Функция vfunc() из класса base\n”
}
};
class derived1: public base {
public:
void vfunc() {
std::cout << “Функция vfunc() из класса derived1\n”
}
};
class derived2: public derived1 {
public:
// Функция vfunc() остается виртуальной:
void vfunc() {
std::cout << “Функция vfunc() из класса derived2\n”
}
};
int main() {
base *p, b;
derived1 d1;
derived2 d2;
// Указатель на объект базового класса:
p = &b;
p→vfunc(); // Вызов функции vfunc() из класса base
// Указатель на объект класса derived1:
p = &d1;
p→vfunc(); // Вызов функции vfunc() из класса derived1
// Указатель на объект класса derived2:
p = &d2;
p→vfunc(); // Вызов функции vfunc() из класса derived2
return 0;
}
Результатом работы этой программы будет печать тех же строк, что и в примерах 1 и 2.
---------------------------------------------
Виртуальные функции являются иерархическими, то есть их не обязательно замещать (см. Пример 6)
Пример 6. Выведенный класс derived2 не замещает функцию vfunc():
class base {
public:
virtual void vfunc() {
std::cout << “Функция vfunc() из класса base\n”
}
};
class derived1: public base {
public:
void vfunc() {
std::cout << “Функция vfunc() из класса derived1\n”
}
};
class derived2: public base {
public:
// Функция vfunc() не замещается в классе derived2, используется версия из класса base
};
int main() {
base *p, b;
derived1 d1;
derived2 d2;
// Указатель на объект базового класса:
p = &b;
p→vfunc(); // Вызов функции vfunc() из класса base
// Указатель на объект класса derived1:
p = &d1;
p→vfunc(); // Вызов функции vfunc() из класса derived1
// Указатель на объект класса derived2:
p = &d2;
p→vfunc(); // Вызов функции vfunc() из класса base
return 0;
}
Эта программа выводит на экран следующие строки:
Функция vfunc() из класса base
Функция vfunc() из класса derived1
Функция vfunc() из класса base
--------------------------------------
Если виртуальная функция базового класса не переопределена в выведенном классе, то в таблице виртуальных функций указан адрес виртуальной функции из базового класса.
Чисто виртуальная функция (pure virtual function) – виртуальная функция, не имеющая определения в базовом классе.
Для страховки от неправильного вызова ее часто объявляют равной нулю:
virtual <тип> <имя функции> (<список параметров>) = 0;
Пример 7. Ситуация, когда определение виртуальной функции в базовом классе не имеет смысла.
class number {
protected:
int val;
public:
void setval(int i) { val = i; }
virtual void show() = 0; // Функция show() является чисто виртуальной. Запись «=0” не является обязательной, может отсутствовать, но требуется дисциплиной программирования
};
// Функция не реализована:
//void number::show() {
// // Реализация
//}
class hextype: public number {
public:
void show() {
std::cout << hex << val << “\n”;
}
};
class dectype: public number {
public:
void show() {
std::cout << val << “\n”;
}
};
class octtype: public number {
public:
void show() {
std::cout << oct << val << “\n”;
}
};
int main() {
dectype d;
hextype h;
octtype o;
d.setval(20);
d.show(); // Выводит десятичное число 20
h.setval(20);
h.show(); // Выводит шестнадцатеричное число 14
o.setval(20);
o.show(); // Выводит восьмеричное число 24
return 0;
}
Класс, содержащий хотя бы одну чисто виртуальную функцию, называется абстрактным.
Пример 8. Абстрактный класс нельзя инстанциировать (создать объект этого типа) (на базе Примера 7)
...
int main() {
number n; // Компилятор сообщит об ошибке: инстанциирование абстрактного класса
dectype d;
hextype h;
octtype o;
d.setval(20);
d.show(); // Выводит десятичное число 20
h.setval(20);
h.show(); // Выводит шестнадцатеричное число 14
o.setval(20);
o.show(); // Выводит восьмеричное число 24
return 0;
}
-----
Поскольку абстрактный класс содержит одну или несколько функций, не имеющих определения (то есть чисто виртуальных), то его объекты создать невозможно.
Следовательно, абстрактные классы можно использовать только как основу для производных классов.
Указатели и ссылки на абстрактный класс создать можно, это используется для поддержки динамического полиморфизма.
Пример 9. Указатели и ссылки на абстрактный класс (на базе Примера 7)
int main() {
number *p; // Указатель на класс number
number &r; // Ссылка на класс number
dectype d;
hextype h;
octtype o;
p = &d;
p->setval(20);
p->show(); // Выводит десятичное число 20
r = d;
r.setval(20);
r.show(); // Выводит десятичное число 20
p = &h;
p->setval(20);
p->show(); // Выводит шестнадцатеричное число 14
r.setval(20);
r.show(); // Выводит шестнадцатеричное число 14
p = &o;
p->setval(20);
p->show(); // Выводит восьмеричное число 24
r.setval(20);
r.show(); // Выводит восьмеричное число 24
return 0;
}