Bài 35.3: Nạp chồng toán tử trong C++

Chào mừng các bạn quay trở lại với series blog C++ của FullhouseDev! Hôm nay, chúng ta sẽ khám phá một tính năng cực kỳ mạnh mẽtiện lợi trong C++: nạp chồng toán tử (Operator Overloading).

Bạn đã quen với việc sử dụng các toán tử như +, -, *, /, ==, <<, >> cho các kiểu dữ liệu cơ bản như int, float, double đúng không? Thật tuyệt vời khi bạn có thể viết int sum = a + b; hay cout << value;.

Nhưng nếu bạn đang làm việc với các kiểu dữ liệu tự định nghĩa của riêng mình, chẳng hạn như một lớp biểu diễn phân số, số phức, vector 2D, hay ma trận? Làm thế nào để cộng hai vector lại với nhau chỉ bằng dấu +? Hay in một đối tượng phân số ra màn hình chỉ bằng cout << phanSo;?

Nạp chồng toán tử chính là câu trả lời! Nó cho phép bạn định nghĩa lại ý nghĩa của hầu hết các toán tử C++ khi chúng được sử dụng với các đối tượng của lớp (hoặc struct) do bạn tạo ra. Điều này giúp mã nguồn của bạn trở nên trực quan hơn, dễ đọc hơngần gũi hơn với cách viết toán học thông thường.

Hãy tưởng tượng bạn có hai đối tượng Vector2D v1(1, 2);Vector2D v2(3, 4);. Nhờ nạp chồng toán tử +, bạn có thể viết:

Vector2D v3 = v1 + v2; // Kết quả là Vector2D(4, 6)

Thay vì phải gọi một phương thức add:

Vector2D v3 = v1.add(v2);

Rõ ràng cách dùng toán tử trông tự nhiênthanh lịch hơn nhiều phải không?

Nạp chồng toán tử hoạt động như thế nào?

Về cơ bản, khi bạn nạp chồng một toán tử, bạn đang tạo ra một hàm đặc biệt mà tên của nó là từ khóa operator theo sau là ký hiệu của toán tử mà bạn muốn nạp chồng.

Ví dụ:

  • Để nạp chồng toán tử +, bạn tạo hàm operator+.
  • Để nạp chồng toán tử <<, bạn tạo hàm operator<<.
  • Để nạp chồng toán tử [], bạn tạo hàm operator[].

Hàm này có thể là:

  1. Một phương thức của lớp (member function).
  2. Một hàm không phải là phương thức của lớp (non-member function), thường là hàm friend để có thể truy cập các thành viên private hoặc protected của lớp.

Việc chọn là phương thức hay hàm không phải phương thức phụ thuộc vào toán tử và cách nó được sử dụng.

Cú pháp cơ bản

1. Nạp chồng toán tử như một phương thức của lớp:

return_type operator op (parameters)
{
    // Triển khai logic
}
  • return_type: Kiểu dữ liệu trả về của phép toán.
  • op: Ký hiệu của toán tử (ví dụ: +, -, ==).
  • parameters: Các tham số đầu vào. Đối với toán tử hai ngôi (binary operator) khi là phương thức, tham số chính là toán hạng thứ hai (bên phải) của phép toán. Toán hạng thứ nhất (bên trái) chính là đối tượng gọi phương thức. Đối với toán tử một ngôi (unary operator), thường không có tham số.

2. Nạp chồng toán tử như một hàm không phải là phương thức (thường dùng friend):

friend return_type operator op (parameters)
{
    // Triển khai logic
}
  • friend: Từ khóa cho phép hàm này truy cập các thành viên private/protected của lớp.
  • parameters: Đối với toán tử hai ngôi, hàm này sẽ nhận hai tham số tương ứng với toán hạng thứ nhất (bên trái) và toán hạng thứ hai (bên phải). Đối với toán tử một ngôi, nó nhận một tham số.

Các ví dụ minh họa chi tiết

Hãy xây dựng một lớp Vector2D đơn giản và nạp chồng một vài toán tử phổ biến để thấy rõ sức mạnh của kỹ thuật này.

#include <iostream>
#include <cmath> // Để dùng sqrt và pow

class Vector2D {
private:
    double x;
    double y;

public:
    // Constructor
    Vector2D(double x_val = 0.0, double y_val = 0.0) : x(x_val), y(y_val) {}

    // Getter methods (optional but good practice)
    double getX() const { return x; }
    double getY() const { return y; }

    // ----------------------------------------------
    // Ví dụ Nạp chồng Toán tử (Member Functions)
    // ----------------------------------------------

    // Nạp chồng toán tử + (Cộng hai vector)
    // Là phương thức, nên tham số 'other' là toán hạng bên phải
    Vector2D operator+(const Vector2D& other) const {
        cout << "Using member operator+" << endl;
        return Vector2D(this->x + other.x, this->y + other.y);
    }

    // Nạp chồng toán tử - (Trừ hai vector)
    Vector2D operator-(const Vector2D& other) const {
        cout << "Using member operator-" << endl;
        return Vector2D(this->x - other.x, this->y - other.y);
    }

    // Nạp chồng toán tử - (Phủ định vector - Unary Minus)
    // Toán tử một ngôi, không có tham số
    Vector2D operator-() const {
        cout << "Using member unary operator-" << endl;
        return Vector2D(-this->x, -this->y);
    }

    // Nạp chồng toán tử == (So sánh bằng)
    // Trả về bool, nhận vector khác để so sánh
    bool operator==(const Vector2D& other) const {
        // So sánh x và y có bằng nhau không (lưu ý so sánh double cần tolerance trong thực tế)
        // Với ví dụ đơn giản này, ta so sánh trực tiếp
        cout << "Using member operator==" << endl;
        return (this->x == other.x) && (this->y == other.y);
    }

    // Nạp chồng toán tử != (So sánh khác)
    // Có thể cài đặt dựa trên toán tử ==
    bool operator!=(const Vector2D& other) const {
        cout << "Using member operator!=" << endl;
        return !(*this == other); // Gọi toán tử == đã nạp chồng
    }

    // Nạp chồng toán tử += (Cộng và gán)
    // Thường trả về tham chiếu đến đối tượng hiện tại (*this) để cho phép chuỗi phép gán
    Vector2D& operator+=(const Vector2D& other) {
        cout << "Using member operator+=" << endl;
        this->x += other.x;
        this->y += other.y;
        return *this; // Trả về tham chiếu đến đối tượng đã được sửa đổi
    }

    // ----------------------------------------------
    // Ví dụ Nạp chồng Toán tử (Non-Member / Friend Functions)
    // ----------------------------------------------

    // Nạp chồng toán tử << (Output stream)
    // Cần là friend để truy cập các thành viên private của Vector2D
    // Luôn nhận ostream& làm tham số đầu tiên (toán hạng bên trái)
    // Luôn nhận const Vector2D& làm tham số thứ hai (toán hạng bên phải)
    // Trả về ostream& để cho phép chuỗi output (cout << v1 << v2;)
    friend ostream& operator<<(ostream& os, const Vector2D& v) {
        os << "Vector2D(" << v.x << ", " << v.y << ")";
        return os;
    }

    // Nạp chồng toán tử >> (Input stream)
    // Cần là friend
    // Luôn nhận istream& làm tham số đầu tiên
    // Luôn nhận Vector2D& (không const) làm tham số thứ hai vì cần sửa đổi đối tượng
    // Trả về istream& để cho phép chuỗi input (cin >> v1 >> v2;)
    friend istream& operator>>(istream& is, Vector2D& v) {
        cout << "Enter Vector2D (x y): ";
        is >> v.x >> v.y;
        return is;
    }

    // Ví dụ nạp chồng toán tử nhị phân không phải là phương thức (thay vì là phương thức)
    // Để minh họa cú pháp khác
    friend Vector2D operator*(const Vector2D& v, double scalar) {
        cout << "Using friend operator* (vector * scalar)" << endl;
        return Vector2D(v.x * scalar, v.y * scalar);
    }

    // Thêm một phiên bản khác cho phép scalar * vector
    friend Vector2D operator*(double scalar, const Vector2D& v) {
         cout << "Using friend operator* (scalar * vector)" << endl;
        return Vector2D(v.x * scalar, v.y * scalar);
    }

    // Lưu ý: Toán tử gán (=) thường không cần nạp chồng tường minh
    // vì C++ cung cấp toán tử gán sao chép mặc định nếu không có con trỏ/resource cần quản lý đặc biệt.
    // Tuy nhiên, bạn CÓ THỂ nạp chồng nó nếu cần logic đặc biệt.
};

int main() {
    Vector2D v1(1.0, 2.0);
    Vector2D v2(3.0, 4.0);

    // Sử dụng toán tử + (Member function)
    Vector2D v_sum = v1 + v2;
    cout << "v1 + v2 = " << v_sum << endl;

    // Sử dụng toán tử - (Member function)
    Vector2D v_diff = v1 - v2;
    cout << "v1 - v2 = " << v_diff << endl;

    // Sử dụng toán tử - (Unary member function)
    Vector2D v_neg = -v1;
    cout << "-v1 = " << v_neg << endl;

    // Sử dụng toán tử == (Member function)
    if (v1 == v2) {
        cout << "v1 is equal to v2" << endl;
    } else {
        cout << "v1 is not equal to v2" << endl;
    }

    Vector2D v3(1.0, 2.0);
    if (v1 == v3) {
        cout << "v1 is equal to v3" << endl;
    }

    // Sử dụng toán tử != (Member function)
    if (v1 != v2) {
        cout << "v1 is not equal to v2 (using !=)" << endl;
    }

    // Sử dụng toán tử += (Member function)
    Vector2D v4(5.0, 6.0);
    v4 += v1; // Tương đương v4 = v4 + v1 (nếu operator+= không tồn tại)
    cout << "v4 after v4 += v1: " << v4 << endl;

    // Sử dụng toán tử << (Friend function)
    cout << "Printing v1 using <<: " << v1 << endl;
    cout << "Printing v2 using <<: " << v2 << endl;

    // Sử dụng toán tử >> (Friend function)
    Vector2D v_input;
    cin >> v_input;
    cout << "You entered: " << v_input << endl;

    // Sử dụng toán tử * (Friend function - scalar multiplication)
    Vector2D v_scaled1 = v1 * 2.5; // Gọi operator*(Vector2D, double)
    cout << "v1 * 2.5 = " << v_scaled1 << endl;

    Vector2D v_scaled2 = 3.0 * v2; // Gọi operator*(double, Vector2D)
    cout << "3.0 * v2 = " << v_scaled2 << endl;


    return 0;
}

Giải thích các ví dụ:

  • operator+ (Member Function): Được định nghĩa bên trong lớp Vector2D. Khi bạn viết v1 + v2, C++ sẽ gọi v1.operator+(v2). Toán hạng bên trái (v1) là đối tượng gọi phương thức, toán hạng bên phải (v2) được truyền qua tham số other. Hàm trả về một Vector2D mới là tổng của hai vector. Từ khóa const cuối hàm chỉ ra rằng phép toán này không làm thay đổi đối tượng v1.
  • operator- (Member Function - Binary): Tương tự như operator+.
  • operator- (Member Function - Unary): Đây là toán tử một ngôi (chỉ có một toán hạng, ví dụ -v1). Khi là phương thức, nó không nhận tham số nào. Nó trả về một vector mới với các thành phần bị đổi dấu.
  • operator==operator!= (Member Functions): Các toán tử so sánh thường là phương thức. operator== trả về bool và nhận vector khác để so sánh. operator!= có thể cài đặt bằng cách phủ định kết quả của operator==, giúp tái sử dụng mã nguồn. Từ khóa const ở cuối là quan trọng vì phép so sánh không nên làm thay đổi các đối tượng.
  • operator+= (Member Function): Toán tử gán kết hợp (như +=, -=, *=) thường được nạp chồng như phương thức và trả về tham chiếu đến chính đối tượng hiện tại (*this). Điều này cho phép các biểu thức như v1 += v2 += v3;.
  • operator<<operator>> (Friend Functions): Đây là hai toán tử đặc biệt thường phải được nạp chồng như hàm không phải là phương thức, và thường dùng friend. Lý do là toán hạng bên trái của chúng là một đối tượng stream (ostream hoặc istream), không phải là đối tượng của lớp Vector2D của chúng ta.
    • operator<< nhận ostream& (luồng đầu ra) làm tham số thứ nhất và const Vector2D& (đối tượng cần in) làm tham số thứ hai. Nó trả về ostream& để có thể nối tiếp các lệnh in (cout << obj1 << obj2;).
    • operator>> nhận istream& (luồng đầu vào) làm tham số thứ nhất và Vector2D& (đối tượng cần đọc dữ liệu vào) làm tham số thứ hai. Lưu ý tham số thứ hai không có const vì chúng ta cần thay đổi giá trị của đối tượng. Nó trả về istream&.
  • operator* (Friend Functions): Ví dụ này minh họa việc nạp chồng toán tử nhị phân (* để nhân vector với một số vô hướng) bằng hàm friend. Bạn cần hai phiên bản: một cho vector * scalar và một cho scalar * vector vì thứ tự toán hạng là quan trọng. Khi là hàm friend, cả hai toán hạng đều được truyền vào làm tham số.

Toán tử tiền tố (Prefix) và hậu tố (Postfix) ++--

Nạp chồng toán tử tăng (++) và giảm (--) có một cú pháp đặc biệt để phân biệt giữa dạng tiền tố (prefix, ++v) và hậu tố (postfix, v++). Cả hai thường được nạp chồng như phương thức.

  • Tiền tố (++v): Cú pháp thông thường: return_type& operator++();. Trả về tham chiếu đến đối tượng sau khi đã tăng.
  • Hậu tố (v++): Cú pháp có thêm một tham số int dummy: return_type operator++(int);. Tham số int này chỉ dùng để phân biệt với dạng tiền tố và không có giá trị sử dụng thực tế. Dạng hậu tố thường trả về một bản sao của đối tượng trước khi nó được tăng.

Hãy thêm vào lớp Vector2D của chúng ta:

// Bên trong class Vector2D {...}:

// Nạp chồng toán tử ++ (Tiền tố - Prefix)
// Tăng x và y, trả về tham chiếu đến chính đối tượng
Vector2D& operator++() {
    cout << "Using prefix operator++" << endl;
    this->x++;
    this->y++;
    return *this;
}

// Nạp chồng toán tử ++ (Hậu tố - Postfix)
// Tạo bản sao trước khi tăng, tăng x và y, trả về bản sao cũ
Vector2D operator++(int) { // Tham số int là dummy để phân biệt
    cout << "Using postfix operator++" << endl;
    Vector2D temp = *this; // Lưu trạng thái hiện tại
    this->x++;             // Tăng giá trị thật
    this->y++;
    return temp;           // Trả về bản sao lưu trữ trước đó
}

// Tương tự cho -- nếu cần

Và sử dụng chúng trong main:

int main() {
    // ... (phần code trước) ...

    Vector2D v5(10.0, 20.0);
    cout << "Original v5: " << v5 << endl;

    // Sử dụng toán tử ++ (Tiền tố)
    Vector2D v6 = ++v5;
    cout << "After v6 = ++v5:" << endl;
    cout << "  v5: " << v5 << endl; // v5 đã tăng
    cout << "  v6: " << v6 << endl; // v6 nhận giá trị sau khi tăng

    Vector2D v7(100.0, 200.0);
    cout << "Original v7: " << v7 << endl;

    // Sử dụng toán tử ++ (Hậu tố)
    Vector2D v8 = v7++;
    cout << "After v8 = v7++:" << endl;
    cout << "  v7: " << v7 << endl; // v7 đã tăng
    cout << "  v8: " << v8 << endl; // v8 nhận giá trị trước khi tăng

    return 0;
}

Một số toán tử KHÔNG THỂ nạp chồng

Không phải tất cả các toán tử trong C++ đều có thể nạp chồng. Dưới đây là danh sách các toán tử bị cấm nạp chồng:

  • Toán tử thành viên truy cập: .
  • Toán tử con trỏ thành viên truy cập: .*
  • Toán tử phạm vi phân giải: ::
  • Toán tử điều kiện ba ngôi: ?:
  • Toán tử sizeof
  • Toán tử typeid
  • Toán tử static_cast, dynamic_cast, reinterpret_cast, const_cast
  • Từ khóa noexcept (là một toán tử trong ngữ cảnh nhất định, nhưng không thể nạp chồng)
  • Từ khóa alignof (tương tự như noexcept)

Lý do C++ cấm nạp chồng các toán tử này thường liên quan đến việc chúng là các toán tử cơ bản, cốt lõi của ngữ pháp C++ hoặc liên quan đến kiểm tra kiểu và thông tin compile-time mà không thể thay đổi hành vi.

Nên dùng Member Function hay Friend Function?

Quyết định nạp chồng toán tử như một phương thức hay hàm friend phụ thuộc vào toán tử và ngữ cảnh:

  • Toán tử gán (=, +=, -=, v.v.): Luôn luôn phải là phương thức của lớp.
  • Toán tử truy cập chỉ mục ([]), toán tử gọi hàm (()), toán tử con trỏ (->), toán tử tiền/hậu tố tăng/giảm (++, --): Thường là phương thức của lớp vì chúng hoạt động trực tiếp trên một đối tượng duy nhất.
  • Toán tử nhập/xuất stream (<<, >>): Hầu như luôn luôn là hàm friend vì toán hạng bên trái là đối tượng stream (không phải đối tượng của lớp bạn).
  • Các toán tử nhị phân khác (+, -, *, /, ==, !=, <, >, v.v.): Có thể là phương thức hoặc hàm friend.
    • Phương thức: obj1 + obj2 dịch thành obj1.operator+(obj2). Ưu điểm là dễ dàng truy cập các thành viên của obj1. Nhược điểm: Toán hạng bên trái phải là đối tượng của lớp bạn. Không thể xử lý trường hợp scalar + obj nếu scalar không phải là lớp của bạn (trừ khi bạn nạp chồng toán tử trong lớp của scalar, điều này thường không thực tế).
    • Hàm Friend: obj1 + obj2 dịch thành operator+(obj1, obj2). Ưu điểm: Cả hai toán hạng đều là tham số, cho phép xử lý các trường hợp như scalar + obj hoặc obj + scalar bằng cách nạp chồng các phiên bản khác nhau của hàm operator+. Nhược điểm: Cần friend để truy cập thành viên private/protected hoặc sử dụng getter/setter public.

Trong nhiều trường hợp, nạp chồng các toán tử nhị phân như +, -, *, / dưới dạng hàm friend (hoặc hàm không phải friend nếu không cần truy cập private members) mang lại sự linh hoạt cao hơn, cho phép các toán hạng thuộc các kiểu khác nhau đứng ở vị trí bên trái hoặc bên phải. Tuy nhiên, nạp chồng dưới dạng phương thức cũng rất phổ biến và đủ cho nhiều trường hợp.

Tại sao lại sử dụng Nạp chồng Toán tử?

Mục tiêu chính của nạp chồng toán tử là làm cho code sử dụng các kiểu dữ liệu tự định nghĩa trở nên:

  • Dễ đọc và trực quan hơn: Code trông giống như các biểu thức toán học hoặc logic thông thường.
  • Thanh lịch hơn: Giảm bớt việc phải gọi các phương thức với tên dài dòng.
  • Nhất quán hơn: Sử dụng cùng một ký hiệu (+, -, <<) cho các phép toán tương tự trên các kiểu dữ liệu khác nhau.

Lưu ý khi sử dụng Nạp chồng Toán tử

  • Giữ vững ý nghĩa: Hãy nạp chồng toán tử sao cho ý nghĩa của nó là rõ ràngphù hợp với ý nghĩa gốc của toán tử. Đừng nạp chồng toán tử + để thực hiện phép nhân, điều đó sẽ gây nhầm lẫn nghiêm trọng!
  • Tuân thủ quy tắc ngôn ngữ: Đừng cố gắng thay đổi thứ tự ưu tiên (precedence) hay tính kết hợp (associativity) của toán tử thông qua nạp chồng. Chúng đã được C++ định sẵn.
  • Cẩn thận với toán tử gán: Nếu lớp của bạn quản lý tài nguyên (ví dụ: cấp phát bộ nhớ động), bạn cần nạp chồng toán tử gán sao chép (=) và toán tử gán di chuyển (= &&) (hoặc tuân theo quy tắc "Rule of Three/Five") để tránh các lỗi như double deletion hoặc shallow copy.
  • Hiệu quả: Đôi khi, việc nạp chồng toán tử có thể tạo ra các đối tượng tạm thời không cần thiết (ví dụ: trả về đối tượng mới thay vì tham chiếu). Hãy cân nhắc hiệu quả, đặc biệt là với các toán tử như +=.

Nạp chồng toán tử là một kỹ thuật mạnh mẽ giúp nâng cao khả năng biểu đạt của C++ và làm cho mã nguồn của bạn thân thiện hơn với người đọc. Việc sử dụng nó một cách thông minhcó trách nhiệm sẽ giúp bạn viết code C++ tốt hơn rất nhiều.

Bài tập ví dụ: C++ Bài 24.A3: Quản lý thư viện (OOP)

Quản lý thư viện (OOP)

Đề bài

Thư viện FullHouse Dev muốn quản lý thông tin sách và tình trạng mượn sách của độc giả. Quy tắc quản lý như sau:

  • Mỗi cuốn sách có các thông tin:

    • Tên sách
    • Tác giả
    • Năm xuất bản
    • Mã sách
    • Tình trạng (Có sẵn, Đang mượn, Đã đặt trước)
  • Mỗi độc giả có thể mượn tối đa 3 cuốn sách cùng một lúc.

Hãy nhập thông tin các sách và tình trạng mượn sách của độc giả theo quy tắc trên.

Input Format

  • Dòng đầu ghi số sách (không quá 100 sách)
  • Mỗi sách ghi trên 4 dòng:

    • Tên sách
    • Tác giả
    • Năm xuất bản
    • Mã sách
  • Dòng tiếp theo ghi số độc giả (không quá 100 độc giả)

  • Mỗi độc giả ghi trên 2 dòng:
    • Tên độc giả
    • Danh sách mã sách đã mượn (tối đa 3 mã sách, cách nhau bởi dấu phẩy)

Output Format

Ghi ra danh sách sách đã được cập nhật tình trạng gồm các thông tin:

  • Mã sách
  • Tên sách
  • Tác giả
  • Năm xuất bản
  • Tình trạng

Dòng cuối ghi tổng số sách theo từng tình trạng (Có sẵn, Đang mượn, Đã đặt trước).

Ví dụ

Dữ liệu vào:
3
Lap trinh C++
Nguyen Van A
2020
S001
Python co ban
Tran Thi B
2019
S002
Hoc Java
Le Van C
2021
S003
2
Nguyen Van D
S001,S002
Tran Thi E
S003
Dữ liệu ra:
S001 Lap trinh C++ Nguyen Van A 2020 Dang muon
S002 Python co ban Tran Thi B 2019 Dang muon
S003 Hoc Java Le Van C 2021 Dang muon
Tong so sach: Co san: 0, Dang muon: 3, Da dat truoc: 0
Giải thích ví dụ mẫu:
  • Đầu vào: Lap trinh C++ Nguyen Van A 2020 S001
  • Đầu ra: S001 Lap trinh C++ Nguyen Van A 2020 Dang muon

Dòng này hiển thị thông tin chi tiết về sách S001, bao gồm mã sách, tên sách, tác giả, năm xuất bản và tình trạng. Tổng số sách theo từng tình trạng cũng được tính toán và hiển thị ở dòng cuối. Chào bạn, đây là hướng dẫn giải bài tập Quản lý thư viện theo hướng OOP trong C++ mà không cung cấp code hoàn chỉnh:

1. Phân tích các đối tượng (Entities)

Dựa vào đề bài, chúng ta có thể xác định các đối tượng chính cần quản lý:

  • Sách (Book): Có các thuộc tính là Tên sách, Tác giả, Năm xuất bản, Mã sách, Tình trạng.
  • Độc giả (Reader): Có các thuộc tính là Tên độc giả và danh sách các Mã sách đã mượn.

Đề bài yêu cầu quản lý thông tin sách và cập nhật tình trạng dựa trên việc mượn sách của độc giả.

2. Thiết kế các Class

Chúng ta sẽ tạo các class tương ứng với các đối tượng đã phân tích:

  • Class Book:

    • Thành viên dữ liệu (Member variables):
      • string title;
      • string author;
      • int publicationYear;
      • string bookId;
      • string status; // Lưu trữ "Co san", "Dang muon", "Da dat truoc"
    • Hàm khởi tạo (Constructor): Để dễ dàng tạo đối tượng Book khi đọc input. Có thể có hàm khởi tạo mặc định hoặc hàm khởi tạo nhận các tham số title, author, year, id. Trạng thái mặc định khi tạo sách là "Co san".
    • Phương thức (Methods):
      • void setStatus(const string& newStatus); // Để cập nhật trạng thái sách
      • void printInfo() const; // Để in thông tin sách theo định dạng yêu cầu
      • string getBookId() const; // Getter cho mã sách để dễ dàng tìm kiếm
      • string getStatus() const; // Getter cho trạng thái để tính tổng kết
  • Class Reader:

    • Lưu ý: Class Reader không nhất thiết phải lưu trữ toàn bộ thông tin độc giả và sách mượn nếu mục tiêu chỉ là cập nhật trạng thái sách. Chúng ta chỉ cần thông tin về độc giả để biết sách nào được mượn. Tuy nhiên, để tuân thủ OOP và nếu bài toán mở rộng, việc có class Reader là hợp lý. Nhưng cho bài này, ta có thể đơn giản hóa: chỉ cần đọc tên độc giả và danh sách mã sách mượn để xử lý trực tiếp. Để code ngắn gọn như yêu cầu, ta có thể bỏ qua việc tạo một class Reader và xử lý thông tin mượn sách trực tiếp trong hàm main hoặc một hàm xử lý riêng.

3. Cấu trúc chương trình chính (main)

  1. Khai báo và bao gồm các thư viện cần thiết:

    • iostream (cho nhập/xuất)
    • vector (để lưu danh sách sách)
    • string (để làm việc với chuỗi)
    • sstream (để phân tích chuỗi mã sách mượn)
    • limits (có thể cần dùng numeric_limits để clear buffer sau khi đọc số)
  2. Đọc thông tin sách:

    • Đọc số lượng sách n.
    • Tạo một vector<Book> để lưu trữ các đối tượng sách.
    • Lặp n lần:
      • Đọc Tên sách (dùng getline vì có thể có khoảng trắng).
      • Đọc Tác giả (dùng getline).
      • Đọc Năm xuất bản (int). Lưu ý: Sau khi đọc số, còn ký tự newline trong buffer. Cần xử lý newline này trước khi gọi getline tiếp theo (ví dụ: dùng cin.ignore()).
      • Đọc Mã sách (dùng getline).
      • Tạo một đối tượng Book mới với thông tin vừa đọc, gán trạng thái mặc định là "Co san".
      • Thêm đối tượng Book vào vector.
  3. Đọc thông tin mượn sách của độc giả và cập nhật trạng thái sách:

    • Đọc số lượng độc giả m.
    • Lặp m lần:
      • Đọc tên độc giả (dùng getline).
      • Đọc danh sách mã sách đã mượn dưới dạng một chuỗi duy nhất (ví dụ: "S001,S002") (dùng getline).
      • Sử dụng stringstream để phân tích chuỗi mã sách đã mượn, tách các mã sách dựa vào dấu phẩy (,).
      • Với mỗi mã sách tách được:
        • Tìm kiếm sách tương ứng trong vector<Book>. Có thể dùng vòng lặp hoặc thuật toán tìm kiếm (nhưng vòng lặp đơn giản là đủ cho tối đa 100 sách).
        • Nếu tìm thấy sách có mã tương ứng, gọi phương thức setStatus("Dang muon") của đối tượng sách đó.
  4. In danh sách sách đã cập nhật:

    • Lặp qua vector<Book>.
    • Với mỗi đối tượng Book, gọi phương thức printInfo().
  5. Tính toán và in tổng kết:

    • Khai báo các biến đếm cho từng trạng thái: countAvailable = 0, countBorrowed = 0, countReserved = 0.
    • Lặp qua vector<Book>.
    • Với mỗi đối tượng Book, kiểm tra trạng thái (getStatus()) và tăng biến đếm tương ứng.
    • In dòng tổng kết theo định dạng yêu cầu, sử dụng các biến đếm vừa tính.

4. Chi tiết về Parsing chuỗi mã sách mượn (ví dụ: "S001,S002")

  • Đọc toàn bộ chuỗi vào một string strBorrowed.
  • Tạo stringstream ss(strBorrowed);
  • Khai báo string bookId;
  • Dùng vòng lặp while (getline(ss, bookId, ',')): Vòng lặp này sẽ đọc từng phần của stringstream, tách bởi dấu phẩy, và gán vào biến bookId. Bên trong vòng lặp là code tìm kiếm và cập nhật trạng thái sách cho bookId hiện tại.

5. Lưu ý về cin.ignore()

Khi bạn đọc một số (int) bằng cin >>, ký tự xuống dòng (\n) sau số vẫn còn lại trong bộ đệm nhập. Nếu lệnh tiếp theo là getline, nó sẽ đọc ngay ký tự xuống dòng này và coi đó là một dòng trống. Để tránh điều này, sau khi đọc số bằng cin >>, bạn nên thêm dòng cin.ignore(numeric_limits<streamsize>::max(), '\n'); để loại bỏ hết phần còn lại của dòng hiện tại (bao gồm cả ký tự xuống dòng) khỏi bộ đệm.

6. Ưu điểm của cách tiếp cận này

  • OOP: Dữ liệu và hành vi liên quan đến sách được gói gọn trong class Book.
  • Sử dụng STD: Tận dụng các container (vector), chuỗi (string), và công cụ xử lý I/O và chuỗi (iostream, sstream).
  • Modularity: Các chức năng được chia thành các phương thức rõ ràng (setStatus, printInfo).

Bằng cách làm theo các bước và thiết kế trên, bạn có thể tự viết code C++ hoàn chỉnh cho bài tập này. Chú trọng vào việc cài đặt chi tiết các phương thức và xử lý I/O cẩn thận.

Làm thêm nhiều bài tập miễn phí tại đây

Comments

There are no comments at the moment.