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

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ẽ và 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ơn và gầ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);
và 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ên và thanh 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àmoperator+
. - Để nạp chồng toán tử
<<
, bạn tạo hàmoperator<<
. - Để nạp chồng toán tử
[]
, bạn tạo hàmoperator[]
.
Hàm này có thể là:
- Một phương thức của lớp (member function).
- 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ênprivate
hoặcprotected
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ớpVector2D
. Khi bạn viếtv1 + v2
, C++ sẽ gọiv1.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ộtVector2D
mới là tổng của hai vector. Từ khóaconst
cuối hàm chỉ ra rằng phép toán này không làm thay đổi đối tượngv1
.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==
và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ủaoperator==
, giúp tái sử dụng mã nguồn. Từ khóaconst
ở 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<<
và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ùngfriend
. Lý do là toán hạng bên trái của chúng là một đối tượng stream (ostream
hoặcistream
), không phải là đối tượng của lớpVector2D
của chúng ta.operator<<
nhậnostream&
(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ậnistream&
(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àmfriend
. Bạn cần hai phiên bản: một chovector * scalar
và một choscalar * vector
vì thứ tự toán hạng là quan trọng. Khi là hàmfriend
, 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) ++
và --
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àmfriend
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àmfriend
.- Phương thức:
obj1 + obj2
dịch thànhobj1.operator+(obj2)
. Ưu điểm là dễ dàng truy cập các thành viên củaobj1
. 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ợpscalar + obj
nếuscalar
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ủascalar
, điều này thường không thực tế). - Hàm Friend:
obj1 + obj2
dịch thànhoperator+(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ặcobj + scalar
bằng cách nạp chồng các phiên bản khác nhau của hàmoperator+
. Nhược điểm: Cầnfriend
để truy cập thành viên private/protected hoặc sử dụng getter/setter public.
- Phương thức:
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àng và phù 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 minh và có 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áchvoid printInfo() const;
// Để in thông tin sách theo định dạng yêu cầustring getBookId() const;
// Getter cho mã sách để dễ dàng tìm kiếmstring getStatus() const;
// Getter cho trạng thái để tính tổng kết
- Thành viên dữ liệu (Member variables):
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.
- 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
3. Cấu trúc chương trình chính (main
)
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ùngnumeric_limits
để clear buffer sau khi đọc số)
Đọ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ọigetline
tiếp theo (ví dụ: dùngcin.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àovector
.
- Đọc Tên sách (dùng
- Đọc số lượng sách
Đọ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 đó.
- Tìm kiếm sách tương ứng trong
- Đọc tên độc giả (dùng
- Đọc số lượng độc giả
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ứcprintInfo()
.
- Lặp qua
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.
- Khai báo các biến đếm cho từng trạng thái:
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ếnbookId
. Bên trong vòng lặp là code tìm kiếm và cập nhật trạng thái sách chobookId
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.
Comments