Чистые виртуальные функции
Часть раздела Объектно-ориентированное программирование путешествия по C++ на Coddy — урок 59 из 104.
Чистая виртуальная функция — это виртуальная функция, которая не имеет реализации в базовом классе. Она объявляется путем присваивания = 0 объявлению функции. Это сообщает компилятору, что производные классы должны предоставить свою собственную реализацию.
class Shape {
public:
virtual double area() = 0; // Чистая виртуальная функция
virtual ~Shape() = default;
};В отличие от обычных виртуальных функций, которые обеспечивают поведение по умолчанию, чистые виртуальные функции определяют контракт: любой конкретный производный класс должен реализовать эту функцию, чтобы иметь возможность создавать экземпляры. Базовый класс просто объявляет, что должно быть сделано, а не как.
class Circle : public Shape {
double radius;
public:
Circle(double r) : radius(r) {}
double area() override {
return 3.14159 * radius * radius;
}
};
class Rectangle : public Shape {
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double area() override {
return width * height;
}
};Оба класса Circle и Rectangle должны реализовать area(), так как эта функция является чисто виртуальной в Shape. Если производный класс не реализует все чисто виртуальные функции, он также становится абстрактным, и создать его экземпляр будет невозможно.
Чисто виртуальные функции необходимы в тех случаях, когда базовый класс не может предоставить осмысленную реализацию по умолчанию. У каждой фигуры есть площадь, но не существует разумного способа вычислить «площадь абстрактной фигуры», не зная конкретного типа фигуры.
Задание
ЛегкоДавайте создадим систему обработки платежей, которая демонстрирует, как чисто виртуальные функции обеспечивают соблюдение контракта между различными методами оплаты. Вы создадите абстрактный базовый класс, который определяет, что должен делать каждый процессор платежей, а затем реализуете конкретные типы платежей, которые выполняют этот контракт.
Вы организуете свой код в трех файлах:
PaymentProcessor.h: Определите абстрактный классPaymentProcessor, который служит шаблоном для всех методов оплаты. Этот класс должен иметь:- Защищенный член
std::string accountId - Конструктор, который инициализирует идентификатор аккаунта
- Чисто виртуальный метод
processPayment(double amount)— каждый тип платежа должен реализовывать его по-своему - Чисто виртуальный метод
getProcessorName(), который возвращаетstd::string - Виртуальный деструктор
- Защищенный член
PaymentMethods.h: Реализуйте два конкретных процессора платежей, наследуемых отPaymentProcessor:CreditCardProcessor:- Приватный член
double feePercentage(процент комиссии за транзакцию) - Конструктор, принимающий идентификатор аккаунта и процент комиссии
- Реализуйте
processPayment()для расчета комиссии (amount * feePercentage / 100), затем выведите:Credit Card [<accountId>]: Charged Credit Card [<accountId>]: Charged $<amount> (Fee: $<fee>)lt;amount> (Fee: Credit Card [<accountId>]: Charged $<amount> (Fee: $<fee>)lt;fee>) - Реализуйте
getProcessorName(), чтобы он возвращал"CreditCard"
BankTransferProcessor:- Приватный член
std::string bankName - Конструктор, принимающий идентификатор аккаунта и название банка
- Реализуйте
processPayment()для вывода:Bank Transfer [<accountId>] via <bankName>: Transferred Bank Transfer [<accountId>] via <bankName>: Transferred $<amount>lt;amount> - Реализуйте
getProcessorName(), чтобы он возвращал"BankTransfer"
- Приватный член
main.cpp: Считайте четыре входных значения (каждое на отдельной строке):- Идентификатор аккаунта кредитной карты
- Процент комиссии по кредитной карте (double)
- Идентификатор банковского счета
- Название банка
Создайте оба процессора платежей и сохраните их в массиве указателей
PaymentProcessor*. Обработайте платеж в размере100.0через каждый процессор, отображая имя процессора перед каждой транзакцией:Processing with <processorName>: <processPayment output>Выведите пустую строку между процессорами. Очистите динамически выделенные объекты по завершении.
Например, при входных данных CC-4521, 2.5, BA-7890 и National Bank:
Processing with CreditCard:
Credit Card [CC-4521]: Charged $100 (Fee: $2.5)
Processing with BankTransfer:
Bank Transfer [BA-7890] via National Bank: Transferred $100Поскольку PaymentProcessor имеет чисто виртуальные функции, вы не можете создать его экземпляр напрямую — можно создавать только конкретные реализации, которые выполняют контракт. Это гарантирует, что каждый метод оплаты предоставляет свою собственную специфическую логику обработки.
Шпаргалка
Чистая виртуальная функция объявляется путем присваивания = 0 виртуальной функции, указывая на то, что она не имеет реализации в базовом классе:
class Shape {
public:
virtual double area() = 0; // Чистая виртуальная функция
virtual ~Shape() = default;
};Чистые виртуальные функции создают контракт, который производные классы должны реализовать. Базовый класс определяет, что должно быть сделано, а не как.
Производные классы должны переопределить все чистые виртуальные функции, чтобы можно было создавать их экземпляры:
class Circle : public Shape {
double radius;
public:
Circle(double r) : radius(r) {}
double area() override {
return 3.14159 * radius * radius;
}
};Если производный класс не реализует все чистые виртуальные функции, он также становится абстрактным, и его экземпляры не могут быть созданы.
Используйте чистые виртуальные функции, когда базовый класс не может предоставить осмысленную реализацию по умолчанию.
Попробуйте сами
#include <iostream>
#include <string>
#include "PaymentMethods.h"
using namespace std;
int main() {
// Чтение входных данных
string ccAccountId;
double feePercentage;
string bankAccountId;
string bankName;
getline(cin, ccAccountId);
cin >> feePercentage;
cin.ignore();
getline(cin, bankAccountId);
getline(cin, bankName);
// TODO: Создайте массив указателей PaymentProcessor* из 2 элементов
// TODO: Создайте объекты CreditCardProcessor и BankTransferProcessor
// и сохраните их в массиве
// TODO: Пройдите циклом по массиву и для каждого процессора:
// 1. Выведите "Processing with <processorName>:"
// 2. Вызовите processPayment с суммой 100.0
// 3. Выведите пустую строку между процессорами (но не после последнего)
// TODO: Освободите динамически выделенную память
return 0;
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП
Внешние файлыСборка и компиляция C++Заголовочные файлы и файлы исходного кодаПространства имен и область видимостиВведение в ООП на C++Классы и объектыУказатель 'this'Методы (функции-члены)Атрибуты (члены данных)Основы конструкторов и деструкторовИтоги — Простой калькулятор4Свойства классов
Члены экземпляра против статическихГеттеры и сеттерыКонстантные функции-членыКлючевое слово mutableСтатические методы и переменныеДружественные функции и классыИтоги — Менеджер банковских счетов7Наследование
Основы наследованияУровни доступа при наследованииПорядок вызова конструкторов и деструкторовПереопределение методовВиртуальные функции и VTableМножественное наследованиеВиртуальное наследованиеИтоги — Иерархия сотрудников10Обзор STL
Обзор и философия STLКонтейнеры STLИтераторыАлгоритмы STLФункторы и лямбда-выраженияИтоги: частота слов13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonFactory и Abstract FactoryПаттерн BuilderПаттерн ObserverПаттерн Strategy2Управление памятью
Стек и кучаУказатели и ссылкиДинамическая память (new/delete)Умные указатели в C++RAII в C++Итоги — Менеджер динамического массива5Инкапсуляция
Спецификаторы доступа в C++Спецификаторы доступа: подробный разборСокрытие данныхStruct против ClassВложенные и внутренние классыИтоги — Система учета студентов8Полиморфизм
Полиморфизм компиляции и времени выполненияПерегрузка функцийВиртуальные функции: повторениеЧистые виртуальные функцииАбстрактные классыПроектирование интерфейсов на C++Dynamic Casting и RTTIИтоги: Калькулятор фигур11Продвинутые концепции ООП
Композиция против наследованияМиксины через CRTPИдиома PimplСтирание типовEnum Classes и строгая типизацияОбработка исключений в ООППользовательские иерархии исключений14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикRAII как паттерн3Конструкторы и деструкторы
Конструктор по умолчаниюПараметризованный конструкторКонструктор копированияКонструктор перемещенияСписки инициализации конструктораДелегирующие конструкторыГлубокое погружение в деструкторыПравило трех / пяти / нуляПовторение — класс String6Перегрузка операторов
Введение в перегрузку операторовПерегрузка арифметических операторовПерегрузка операторов сравненияОператоры потоковПерегрузка оператора присваиванияПерегрузка операторов [] и ()Операторы преобразования типовИтоги — класс Matrix9Шаблоны
Шаблоны функцийШаблоны классовСпециализация шаблоновВариативные шаблоныОсновы SFINAE и Type TraitsИтоги — Обобщенный контейнер