Menu
CoddyTech

Hàm ảo thuần túy

Thuộc phần Lập trình hướng đối tượng trong hành trình C++ của Coddy. Bài học 59 trên 104.

Hàm ảo thuần túy là hàm ảo không có phần triển khai trong lớp cơ sở. Hàm này được khai báo bằng cách gán = 0 cho khai báo hàm. Điều này cho trình biên dịch biết rằng các lớp dẫn xuất phải tự triển khai hàm.

class Shape {
public:
    virtual double area() = 0;  // Hàm ảo thuần túy
    virtual ~Shape() = default;
};

Khác với các hàm ảo thông thường cung cấp hành vi mặc định, hàm ảo thuần túy xác định một hợp đồng: mọi lớp dẫn xuất cụ thể phải triển khai hàm này thì mới có thể khởi tạo đối tượng. Lớp cơ sở chỉ khai báo việc gì cần làm, chứ không phải cách thức thực hiện.

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;
    }
};

Cả Circle và Rectangle đều phải triển khai area() vì hàm này là hàm thuần ảo trong Shape. Nếu lớp dẫn xuất không triển khai tất cả các hàm thuần ảo, lớp đó cũng trở thành lớp trừu tượng và không thể được khởi tạo.

Hàm ảo thuần túy rất cần thiết khi lớp cơ sở không thể cung cấp cách triển khai mặc định có ý nghĩa. Mỗi hình đều có diện tích, nhưng không có cách hợp lý nào để tính “diện tích hình dạng tổng quát” nếu không biết loại hình cụ thể.

challenge icon

Thử thách

Dễ

Hãy xây dựng một hệ thống xử lý thanh toán để minh họa cách các hàm thuần ảo thực thi một hợp đồng cho nhiều phương thức thanh toán. Bạn sẽ tạo một lớp cơ sở trừu tượng định nghĩa những việc mọi bộ xử lý thanh toán phải làm, sau đó triển khai các kiểu thanh toán cụ thể đáp ứng hợp đồng đó.

Bạn sẽ sắp xếp code trong ba tệp:

  • PaymentProcessor.h: Định nghĩa lớp trừu tượng PaymentProcessor, làm khuôn mẫu cho mọi phương thức thanh toán. Lớp này cần có:
    • Một thành viên std::string accountId có phạm vi protected
    • Một hàm khởi tạo để khởi tạo mã tài khoản
    • Một phương thức thuần ảo processPayment(double amount): mỗi kiểu thanh toán phải triển khai phương thức này theo cách riêng
    • Một phương thức thuần ảo getProcessorName() trả về một std::string
    • Một hàm hủy ảo
  • PaymentMethods.h: Triển khai hai bộ xử lý thanh toán cụ thể kế thừa từ PaymentProcessor:

    CreditCardProcessor:

    • Một thành viên riêng double feePercentage (tỷ lệ phí giao dịch)
    • Một hàm khởi tạo nhận mã tài khoản và tỷ lệ phí
    • Triển khai processPayment() để tính phí (amount * feePercentage / 100), sau đó in ra:
      Credit Card [<accountId>]: Charged $<amount> (Fee: $<fee>)
    • Triển khai getProcessorName() để trả về "CreditCard"

    BankTransferProcessor:

    • Một thành viên riêng std::string bankName
    • Một hàm khởi tạo nhận mã tài khoản và tên ngân hàng
    • Triển khai processPayment() để in ra:
      Bank Transfer [<accountId>] via <bankName>: Transferred $<amount>
    • Triển khai getProcessorName() để trả về "BankTransfer"
  • main.cpp: Đọc bốn đầu vào, mỗi đầu vào nằm trên một dòng riêng:
    1. Mã tài khoản thẻ tín dụng
    2. Tỷ lệ phí thẻ tín dụng (double)
    3. Mã tài khoản ngân hàng
    4. Tên ngân hàng

    Tạo cả hai bộ xử lý thanh toán và lưu chúng vào một mảng các con trỏ PaymentProcessor*. Xử lý khoản thanh toán 100.0 qua từng bộ xử lý, in tên bộ xử lý trước mỗi giao dịch:

    Processing with <processorName>:
    <processPayment output>

    In một dòng trống giữa các bộ xử lý. Dọn dẹp các đối tượng đã cấp phát động khi hoàn tất.

Ví dụ, với các đầu vào CC-4521, 2.5, BA-7890 và 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

Vì PaymentProcessor có các hàm thuần ảo, bạn không thể khởi tạo trực tiếp lớp này: chỉ có thể tạo các lớp triển khai cụ thể đáp ứng hợp đồng. Điều này đảm bảo mỗi phương thức thanh toán cung cấp logic xử lý riêng.

Tự làm thử

#include <iostream>
#include <string>
#include "PaymentMethods.h"

using namespace std;

int main() {
    // Đọc dữ liệu đầu vào
    string ccAccountId;
    double feePercentage;
    string bankAccountId;
    string bankName;
    
    getline(cin, ccAccountId);
    cin >> feePercentage;
    cin.ignore();
    getline(cin, bankAccountId);
    getline(cin, bankName);
    
    // TODO: Tạo mảng gồm 2 phần tử là con trỏ PaymentProcessor*
    
    // TODO: Tạo các đối tượng CreditCardProcessor và BankTransferProcessor
    // và lưu chúng vào mảng
    
    // TODO: Duyệt mảng và thực hiện với từng bộ xử lý:
    // 1. In ra "Processing with <processorName>:"
    // 2. Gọi processPayment với số tiền 100.0
    // 3. In một dòng trống giữa các bộ xử lý (không in sau bộ xử lý cuối cùng)
    
    // TODO: Giải phóng các đối tượng được cấp phát động
    
    return 0;
}
quiz iconTự kiểm tra

Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.

Tất cả bài học trong Lập trình hướng đối tượng

Tự luyện tập: Trình biên dịch C++ online