Chương 5: Operator Overloading (Nạp Chồng Toán Tử)¤
1. Giới thiệu¤
Trong lập trình hướng đối tượng, khi xây dựng các lớp biểu diễn các khái niệm toán học (phân số, số phức, ma trận,...), nếu không có operator overloading, ta phải gọi hàm một cách rườm rà:
PhanSo A, B, C, D, E;
// Không có overloading -> phải viết:
C.Set(A.Cong(B));
E.Set(D.Cong(C));
// Có overloading -> viết tự nhiên như toán học:
E = A + B + C + D;
Operator overloading cho phép định nghĩa lại ý nghĩa của các toán tử có sẵn (+, -, *, <<, [],...) cho các kiểu dữ liệu do người dùng tự định nghĩa. Bản chất vẫn là gọi hàm, nhưng trình biên dịch tự động dịch biểu thức toán tử thành lời gọi hàm tương ứng.
Lợi ích: - Gần với cách diễn đạt toán học tự nhiên mà con người quen dùng. - Đơn giản hóa mã chương trình, tăng tính đọc được. - Tạo ra các kiểu dữ liệu "đóng gói hoàn chỉnh" (fully encapsulated), tích hợp tự nhiên với ngôn ngữ như các kiểu dữ liệu có sẵn.
SoPhuc z1(1, 3), z2(2, 3.4), z3(5.1, 4), z;
z = z1 + z2 * z3 + SoPhuc(3, 1); // Rõ ràng, tự nhiên
2. Phân loại toán tử trong C++¤
Toán tử (Operators)
├── Toán tử đơn (Unary Operator)
│ ├── Tiếp đầu ngữ (Prefix): !a, ++a, --a, -a
│ └── Tiếp vị ngữ (Postfix): a++, a--
└── Toán tử đôi (Binary Operator): a + b, a == b, a[i]
Một số lưu ý quan trọng:
- Toán tử
++và--có thể dùng cả prefix lẫn postfix. - Toán tử
*có thể dùng làm cả toán tử đơn (dereference:*ptr) lẫn toán tử đôi (nhân:a * b). - Toán tử chỉ mục
[]là toán tử đôi (đối tượng và chỉ số). newvàdeletecũng là toán tử và có thể định nghĩa lại.
3. Danh sách toán tử có thể overload¤
Các toán tử CÓ THỂ overload:
+ - * / % ^ & | ~ !
= < > += -= *= /= %= ^= &=
|= << >> >>= <<= == != <= >= &&
|| ++ -- ->* , -> [] () new delete
new[] delete[]
Các toán tử KHÔNG THỂ overload: ::, ., .*, ?:
4. Cú pháp Operator Overloading¤
Tên hàm cho toán tử @ là operator@. Ví dụ: operator+, operator==, operator[].
Số lượng tham số phụ thuộc vào: 1. Toán tử là đơn hay đôi. 2. Định nghĩa là phương thức của lớp hay hàm toàn cục.
4.1 So sánh phương thức lớp và hàm toàn cục¤
| Biểu thức | Phương thức của lớp | Hàm toàn cục |
|---|---|---|
aa @ bb (nhị phân) |
aa.operator@(bb) — 1 tham số |
operator@(aa, bb) — 2 tham số |
@aa (đơn, prefix) |
aa.operator@() — 0 tham số |
operator@(aa) — 1 tham số |
aa@ (đơn, postfix) |
aa.operator@(int) — 1 tham số giả |
operator@(aa, int) — 2 tham số |
Giải thích: Khi là phương thức của lớp, đối tượng gọi (
this) đã ngầm là toán hạng thứ nhất, nên cần ít hơn 1 tham số so với hàm toàn cục.
5. Ví dụ: Lớp PhanSo¤
5.1 Hàm hỗ trợ tính USCLN¤
long USCLN(long x, long y) {
long r;
x = abs(x);
y = abs(y);
if (x == 0 || y == 0) return 1;
while ((r = x % y) != 0) {
x = y;
y = r;
}
return y;
}
5.2 Khai báo lớp¤
class PhanSo {
long tu, mau;
public:
PhanSo() {}
PhanSo(long t, long m) { Set(t, m); }
void UocLuoc();
void Set(long t, long m);
long LayTu() const { return tu; }
long LayMau() const { return mau; }
PhanSo Cong(PhanSo b) const; // Cách cũ: gọi hàm
PhanSo operator+(PhanSo b) const; // Cách mới: dùng toán tử +
PhanSo operator-() const { // Toán tử đơn: đảo dấu
return PhanSo(-tu, mau);
}
bool operator==(PhanSo b) const;
bool operator!=(PhanSo b) const;
void Xuat() const;
};
5.3 Cài đặt các phương thức¤
void PhanSo::UocLuoc() {
long usc = USCLN(tu, mau);
tu /= usc;
mau /= usc;
if (mau < 0) { mau = -mau; tu = -tu; } // Đảm bảo mẫu luôn dương
if (tu == 0) mau = 1;
}
void PhanSo::Set(long t, long m) {
if (m) { // Kiểm tra mẫu khác 0
tu = t;
mau = m;
UocLuoc();
}
}
// Cách cũ (gọi hàm)
PhanSo PhanSo::Cong(PhanSo b) const {
return PhanSo(tu * b.mau + mau * b.tu, mau * b.mau);
}
// Overload toán tử + (kết quả giống hệt Cong)
PhanSo PhanSo::operator+(PhanSo b) const {
return PhanSo(tu * b.mau + mau * b.tu, mau * b.mau);
}
bool PhanSo::operator==(PhanSo b) const {
return tu * b.mau == mau * b.tu;
}
void PhanSo::Xuat() const {
cout << tu;
if (tu != 0 && mau != 1)
cout << "/" << mau;
}
6. Hạn chế của Operator Overloading¤
Không thể: - Tạo toán tử hoàn toàn mới (ví dụ:
**cho lũy thừa). - Thay đổi thứ tự ưu tiên (precedence) của các toán tử —*vẫn được tính trước+. - Thay đổi số lượng toán hạng (arity) — toán tử đơn vẫn phải là đơn. - Định nghĩa lại toán tử cho các kiểu dữ liệu có sẵn (không thể đổi nghĩaint + int).
7. Hàm thành phần vs Hàm toàn cục — Khi nào dùng cái nào?¤
7.1 Vấn đề khi toán hạng đầu không thuộc lớp¤
class PhanSo {
public:
PhanSo operator+(PhanSo b) const;
PhanSo operator+(long b) const { return PhanSo(tu + b * mau, mau); }
};
PhanSo a(2, 3), b(4, 1);
a + b; // OK: a.operator+(b)
a + 5; // OK: a.operator+(5)
3 + a; // LỖI! int không có operator+(PhanSo)
Giải quyết bằng hàm toàn cục kết hợp friend:
class PhanSo {
long tu, mau;
public:
PhanSo(long t, long m) { Set(t, m); }
PhanSo operator+(PhanSo b) const;
PhanSo operator+(long b) const { return PhanSo(tu + b * mau, mau); }
friend PhanSo operator+(int a, PhanSo b); // Hàm toàn cục, truy cập private
};
PhanSo operator+(int a, PhanSo b) {
return PhanSo(a * b.mau + b.tu, b.mau);
}
// Giờ thì:
PhanSo c;
c = 3 + a; // operator+(3, a): OK
7.2 Bắt buộc dùng hàm thành phần¤
Các toán tử =, [], (), -> bắt buộc phải là phương thức của lớp, không được định nghĩa là hàm toàn cục.
8. Chuyển kiểu (Type Conversions)¤
8.1 Vấn đề khi cần trộn kiểu¤
Nếu muốn cộng PhanSo với int, double,... theo mọi thứ tự, ta cần rất nhiều overload:
// Phải định nghĩa cho tất cả tổ hợp:
PhanSo operator+(PhanSo b) const;
PhanSo operator+(long b) const;
friend PhanSo operator+(int a, PhanSo b);
// ... và tương tự cho -, *, /, <, >, ==, != ...
// Rất mệt mỏi và dễ sai sót!
8.2 Chuyển kiểu bằng Constructor (Constructor Conversion)¤
C++ tự động dùng constructor 1 tham số để chuyển kiểu ngầm định:
class PhanSo {
public:
PhanSo(long t, long m) { Set(t, m); }
PhanSo(long t) { Set(t, 1); } // Constructor chuyển kiểu: int -> PhanSo
friend PhanSo operator+(PhanSo a, PhanSo b);
};
PhanSo a(2, 3), c;
PhanSo d = 5; // tương đương: PhanSo d(5); -> PhanSo(5/1)
c = a + 5; // trình biên dịch tự động: a + PhanSo(5)
c = 3 + a; // operator+(PhanSo(3), a) — cần hàm toàn cục
Nhờ vậy, thay vì 3 hàm cho mỗi phép toán, chỉ cần 1 hàm toàn cục:
class PhanSo {
public:
PhanSo(long t, long m) { Set(t, m); }
PhanSo(long t) { Set(t, 1); } // Chuyển kiểu ngầm: int -> PhanSo
friend PhanSo operator+(PhanSo a, PhanSo b); // 1 hàm dùng cho mọi tổ hợp
friend PhanSo operator-(PhanSo a, PhanSo b);
// ...
};
Điều kiện dùng Constructor Conversion: - Chuyển từ kiểu đã có (int) sang kiểu đang định nghĩa (PhanSo). - Có quan hệ "là một" hợp lý: một số nguyên là một phân số đặc biệt (mẫu = 1).
8.3 Chuyển kiểu bằng Phép toán chuyển kiểu (Conversion Operator)¤
Constructor conversion có nhược điểm: không thể chuyển từ kiểu đang định nghĩa sang kiểu cơ bản có sẵn (như double).
Giải pháp: định nghĩa conversion operator dạng operator T():
class PhanSo {
long tu, mau;
public:
PhanSo(long t = 0, long m = 1) { Set(t, m); }
operator double() const { return double(tu) / mau; } // PhanSo -> double
};
PhanSo a(9, 4);
cout << sqrt(a) << "\n";
// Trình biên dịch tự dịch thành: sqrt(a.operator double()) = sqrt(2.25) = 1.5
Ví dụ khác — lớp NumStr (chuỗi số):
class NumStr {
char *s;
public:
NumStr(char *p) { s = strdup(p); }
operator double() { return atof(s); } // Chuyển chuỗi số sang double
friend ostream& operator<<(ostream &o, NumStr &ns);
};
ostream& operator<<(ostream &o, NumStr &ns) { return o << ns.s; }
// Sử dụng:
NumStr s1("123.45"), s2("34.12");
cout << s1 + s2 << "\n"; // 157.57 (tự chuyển sang double rồi cộng)
cout << s1 * 2 << "\n"; // 246.9
cout << s1 / 2 << "\n"; // 61.725
9. Sự Nhập Nhằng (Ambiguity)¤
Nhập nhằng xảy ra khi trình biên dịch tìm được ít nhất hai cách chuyển kiểu hợp lệ để thực hiện một phép toán, không biết chọn cách nào.
9.1 Ví dụ gây nhập nhằng¤
class PhanSo {
public:
PhanSo(long t = 0, long m = 1) { Set(t, m); } // int -> PhanSo
operator double() const { return double(tu) / mau; } // PhanSo -> double
friend PhanSo operator+(PhanSo a, PhanSo b);
};
PhanSo a(2, 3);
// Khi tính: a + 2.5
// Cách 1: double(a) + 2.5 (dùng operator double())
// Cách 2: a + PhanSo(2.5) (dùng constructor? — nhưng không có constructor double)
// -> Trình biên dịch báo lỗi: ambiguous
double r = 2.5 + a; // Lỗi: nhập nhằng
r = a + 2.5; // Lỗi: nhập nhằng
9.2 Giải quyết nhập nhằng¤
Phải chuyển kiểu tường minh (explicit cast):
cout << double(a) + 2.5 << "\n"; // Rõ ràng: dùng operator double()
cout << a + PhanSo(2) << "\n"; // Rõ ràng: dùng constructor
// Hoặc đơn giản hơn: bỏ operator double() nếu không thực sự cần
Nguyên tắc thực tế: Nếu một lớp có cả constructor conversion (A→B) lẫn conversion operator (B→A), khả năng nhập nhằng rất cao. Thường nên chọn một trong hai cơ chế.
10. Gán và Khởi động — Phép toán =¤
10.1 Vấn đề khi lớp quản lý tài nguyên động¤
class String {
char *p;
public:
String(char *s = "") { p = strdup(s); }
String(const String &s) { p = strdup(s.p); } // Copy constructor
~String() { delete[] p; }
void Output() const { cout << p; }
};
void main() {
String a("Nguyen Van A");
String aa = "Le Van AA";
aa = a; // Phép GÁN (assignment) — NGUY HIỂM!
}
Phép gán mặc định chỉ copy con trỏ, dẫn đến:
- a.p và aa.p cùng trỏ đến một vùng nhớ.
- Khi hủy aa, vùng nhớ bị delete.
- Khi hủy a, vùng nhớ bị delete lần nữa → Null pointer assignment / crash!
Trước gán: Sau gán (mặc định):
a ---> [Nguyen Van A] a ---> [Nguyen Van A]
aa ---> [Le Van AA] aa --+
| Cùng trỏ 1 vùng!
↓
[Nguyen Van A]
(Le Van AA bị rò rỉ bộ nhớ!)
10.2 Định nghĩa phép gán đúng cách¤
class String {
char *p;
public:
String(char *s = "") { p = strdup(s); }
String(const String &s) { p = strdup(s.p); }
~String() { delete[] p; }
String& operator=(const String &s); // Khai báo
void Output() const { cout << p; }
};
String& String::operator=(const String &s) {
if (this != &s) { // Kiểm tra tự gán (a = a)
delete[] p; // Dọn dẹp tài nguyên cũ
p = strdup(s.p); // Sao chép dữ liệu mới
}
return *this; // Trả về tham chiếu đến chính đối tượng
}
Lưu ý
return *this: Cho phép gán liên tiếp nhưa = b = c.Quy tắc "Rule of Three": Nếu lớp cần định nghĩa một trong ba: destructor, copy constructor, copy assignment operator — thì thường cần định nghĩa cả ba.
11. Phép toán << và >>¤
<< và >> với số nguyên là phép dịch bit. C++ overload chúng cho ostream/istream để xuất/nhập dữ liệu.
11.1 Cấu trúc lớp ostream và istream (rút gọn)¤
class ostream : virtual public ios {
public:
ostream& operator<<(int);
ostream& operator<<(double);
ostream& operator<<(const char*);
// ... các kiểu cơ bản khác
};
class istream : virtual public ios {
public:
istream& operator>>(int&);
istream& operator>>(double&);
istream& operator>>(char*);
// ...
};
cout, cerr là đối tượng ostream. cin là đối tượng istream.
11.2 Định nghĩa << và >> cho lớp tự định nghĩa¤
Luôn phải là hàm toàn cục (vì toán hạng thứ nhất không thuộc lớp của ta):
class PhanSo {
long tu, mau;
public:
PhanSo(long t = 0, long m = 1) { Set(t, m); }
// ...
friend istream& operator>>(istream &is, PhanSo &p); // Nhập
friend ostream& operator<<(ostream &os, PhanSo p); // Xuất
};
istream& operator>>(istream &is, PhanSo &p) {
is >> p.tu >> p.mau;
while (!p.mau) {
cout << "Nhap lai mau so: ";
is >> p.mau;
}
p.UocLuoc();
return is; // Quan trọng: trả về is để chain: cin >> a >> b
}
ostream& operator<<(ostream &os, PhanSo p) {
os << p.tu;
if (p.tu != 0 && p.mau != 1)
os << "/" << p.mau;
return os; // Quan trọng: trả về os để chain: cout << a << b
}
// Sử dụng:
PhanSo a, b;
cin >> a >> b;
cout << a << " + " << b << " = " << a + b << "\n";
Tại sao trả về tham chiếu? Để hỗ trợ chuỗi lệnh như
cout << a << b << c(operator chaining). Mỗi<<trả về chínhostream&để lệnh tiếp theo tiếp tục dùng.
12. Phép toán lấy phần tử mảng []¤
Cho phép dùng cú pháp obj[i] để truy cập phần tử bên trong đối tượng.
class String {
char *p;
public:
String(char *s = "") { p = strdup(s); }
String(const String &s) { p = strdup(s.p); }
~String() { delete[] p; }
String& operator=(const String &s);
// Phiên bản cho đối tượng thường — có thể dùng làm lvalue (bên trái =)
char& operator[](int i) {
return (i >= 0 && i < (int)strlen(p)) ? p[i] : c;
}
// Phiên bản cho đối tượng const — chỉ đọc
char operator[](int i) const { return p[i]; }
friend ostream& operator<<(ostream &o, const String &s);
private:
static char c; // Ký tự "rác" để trả về khi chỉ số ngoài phạm vi
};
char String::c = '\0';
void main() {
String a("Nguyen van A");
const String aa("Dai Hoc Tu Nhien");
cout << a[7] << "\n"; // Gọi char& operator[](int): 'a'
a[7] = 'V'; // OK: trả về char& nên có thể gán
cout << a << "\n"; // "Nguyen Van A"
cout << aa[4] << "\n"; // Gọi char operator[](int) const: 'H'
aa[4] = 'L'; // LỖI biên dịch: không thể gán cho giá trị const
}
Lý do cần 2 phiên bản
[]: Đối tượngconstchỉ được gọi các phương thứcconst. Phiên bảnconsttrả vềchar(giá trị) thay vìchar&(tham chiếu) để ngăn gán vào đối tượng hằng.
13. Phép toán gọi hàm ()¤
[] chỉ nhận một tham số, nên không tiện cho ma trận 2 chiều. Phép toán () cho phép nhận nhiều tham số tùy ý:
class MATRIX {
float **M;
int row, col;
public:
MATRIX(int r, int c) {
M = new float*[r];
for (int i = 0; i < r; i++)
M[i] = new float[c];
row = r; col = c;
}
~MATRIX() {
for (int i = 0; i < row; i++)
delete[] M[i];
delete[] M;
}
// Truy cập phần tử [i][j]
float& operator()(int i, int j) { return M[i][j]; }
};
void main() {
MATRIX a(2, 3);
// Nhập
for (int i = 0; i < 2; i++)
for (int j = 0; j < 3; j++)
cin >> a(i, j); // a.operator()(i, j)
// Xuất
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++)
cout << a(i, j) << " ";
cout << endl;
}
}
14. Phép toán tăng ++ và giảm --¤
14.1 Prefix vs Postfix¤
Prefix ++a |
Postfix a++ |
|
|---|---|---|
| Hành động | Tăng a, trả về a sau khi tăng | Tăng a, trả về a trước khi tăng |
| Kiểu trả về | T& (tham chiếu) |
T (giá trị — bản sao) |
| Khai báo (method) | T& operator++() |
T operator++(int) — tham số int giả |
Tham số
inttrong postfix chỉ để phân biệt với prefix, không mang ý nghĩa gì, không cần đặt tên.
14.2 Ví dụ lớp ThoiDiem¤
#define SOGIAY_NGAY 86400 // 24 * 60 * 60
class ThoiDiem {
long tsgiay; // Số giây tính từ 00:00:00
public:
ThoiDiem(int g = 0, int p = 0, int gy = 0);
void Set(int g, int p, int gy);
int LayGio() const { return tsgiay / 3600; }
int LayPhut() const { return (tsgiay % 3600) / 60; }
int LayGiay() const { return tsgiay % 60; }
ThoiDiem& operator++(); // Prefix: ++t
ThoiDiem operator++(int); // Postfix: t++
friend ostream& operator<<(ostream &os, ThoiDiem t);
};
// Prefix: tăng rồi trả về chính đối tượng (tham chiếu)
ThoiDiem& ThoiDiem::operator++() {
tsgiay = ++tsgiay % SOGIAY_NGAY; // Tăng, vòng lại sau 24h
return *this;
}
// Postfix: lưu bản sao, tăng, rồi trả về bản sao cũ
ThoiDiem ThoiDiem::operator++(int) {
ThoiDiem t = *this; // Lưu giá trị trước khi tăng
tsgiay = ++tsgiay % SOGIAY_NGAY;
return t; // Trả về giá trị cũ
}
void main() {
ThoiDiem t(23, 59, 59), t1;
cout << "t = " << t << "\n"; // t = 23:59:59
t1 = ++t; // t tăng lên 0:00:00, t1 = 0:00:00 (cùng giá trị)
cout << "t = " << t << "\tt1 = " << t1 << "\n";
// t = 0:00:00 t1 = 0:00:00
t1 = t++; // t1 nhận giá trị CŨ (0:00:00), t tăng lên 0:00:01
cout << "t = " << t << "\tt1 = " << t1 << "\n";
// t = 0:00:01 t1 = 0:00:00
}
15. Bài tập¤
Bài tập 1: Lớp số phức SoPhuc¤
Xây dựng lớp SoPhuc biểu diễn số phức \(A = a_1 + a_2 i\), cho phép xem số thực là số phức đặc biệt (phần ảo = 0). Định nghĩa các phép toán +, -, *, /, ==, <<, >>.
Công thức:
Gợi ý cài đặt:
class SoPhuc {
double thuc, ao;
public:
SoPhuc(double t = 0, double a = 0) : thuc(t), ao(a) {}
// Constructor 1 tham số: cho phép double -> SoPhuc tự động
friend SoPhuc operator+(SoPhuc a, SoPhuc b);
friend SoPhuc operator-(SoPhuc a, SoPhuc b);
friend SoPhuc operator*(SoPhuc a, SoPhuc b);
friend SoPhuc operator/(SoPhuc a, SoPhuc b);
friend bool operator==(SoPhuc a, SoPhuc b);
friend istream& operator>>(istream &is, SoPhuc &z);
friend ostream& operator<<(ostream &os, SoPhuc z);
};
SoPhuc operator+(SoPhuc a, SoPhuc b) {
return SoPhuc(a.thuc + b.thuc, a.ao + b.ao);
}
SoPhuc operator*(SoPhuc a, SoPhuc b) {
return SoPhuc(a.thuc*b.thuc - a.ao*b.ao,
a.thuc*b.ao + a.ao*b.thuc);
}
SoPhuc operator/(SoPhuc a, SoPhuc b) {
double denom = b.thuc*b.thuc + b.ao*b.ao;
return SoPhuc((a.thuc*b.thuc + a.ao*b.ao) / denom,
(a.ao*b.thuc - a.thuc*b.ao) / denom);
}
ostream& operator<<(ostream &os, SoPhuc z) {
os << z.thuc;
if (z.ao >= 0) os << " + " << z.ao << "i";
else os << " - " << -z.ao << "i";
return os;
}
Bài tập 2: Lớp DSPhanSo¤
Cho lớp PhanSo đã có (với operator> so sánh, operator+ cộng). Xây dựng lớp DSPhanSo chứa mảng các phân số.
Gợi ý:
class DSPhanSo {
PhanSo *ds;
int n;
public:
DSPhanSo(int _n) : n(_n) { ds = new PhanSo[n]; }
~DSPhanSo() { delete[] ds; }
void Nhap() {
for (int i = 0; i < n; i++) {
cout << "Nhap phan so thu " << i+1 << ": ";
cin >> ds[i];
}
}
PhanSo TimMax() const {
PhanSo max = ds[0];
for (int i = 1; i < n; i++)
if (ds[i] > max) max = ds[i];
return max;
}
PhanSo TinhTong() const {
PhanSo tong(0, 1);
for (int i = 0; i < n; i++)
tong = tong + ds[i];
return tong;
}
};
Bài tập 3: Lớp DaThuc¤
Đa thức bậc \(n\): \(P(x) = a_0 x^n + a_1 x^{n-1} + \ldots + a_n\)
Gợi ý:
class DaThuc {
double *heSo; // heSo[i] = hệ số của x^(bac-i)
int bac;
public:
DaThuc() : bac(0) { heSo = new double[1]{0}; }
DaThuc(int n) : bac(n) {
heSo = new double[n+1];
for (int i = 0; i <= n; i++) heSo[i] = 0;
}
double TinhGiaTri(double x) const {
double kq = 0, luyThua = 1;
for (int i = bac; i >= 0; i--) {
kq += heSo[i] * luyThua;
luyThua *= x;
}
return kq;
}
friend DaThuc operator+(DaThuc a, DaThuc b);
friend DaThuc operator-(DaThuc a, DaThuc b);
friend istream& operator>>(istream &is, DaThuc &p);
friend ostream& operator<<(ostream &os, DaThuc p);
};
ostream& operator<<(ostream &os, DaThuc p) {
for (int i = 0; i <= p.bac; i++) {
if (p.heSo[i] == 0) continue;
if (i > 0 && p.heSo[i] > 0) os << " + ";
os << p.heSo[i];
int exp = p.bac - i;
if (exp == 1) os << "x";
else if (exp > 1) os << "x^" << exp;
}
return os;
}
Bài tập 4: Lớp Ma trận¤
Gợi ý:
class Matrix {
float **M;
int rows, cols;
public:
Matrix(int r, int c) : rows(r), cols(c) {
M = new float*[r];
for (int i = 0; i < r; i++) {
M[i] = new float[c];
for (int j = 0; j < c; j++) M[i][j] = 0;
}
}
float& operator()(int i, int j) { return M[i][j]; }
// Tạo ngẫu nhiên
void Random() {
for (int i = 0; i < rows; i++)
for (int j = 0; j < cols; j++)
M[i][j] = rand() % 100;
}
friend Matrix operator+(Matrix a, Matrix b);
friend Matrix operator-(Matrix a, Matrix b);
friend ostream& operator<<(ostream &os, Matrix m);
};
Matrix operator+(Matrix a, Matrix b) {
// Giả sử cùng kích thước
Matrix c(a.rows, a.cols);
for (int i = 0; i < a.rows; i++)
for (int j = 0; j < a.cols; j++)
c(i, j) = a(i, j) + b(i, j);
return c;
}
Tổng kết¤
| Chủ đề | Điểm cần nhớ |
|---|---|
| Cú pháp | Tên hàm operator@, số tham số phụ thuộc method/global và unary/binary |
| Method vs Global | =, [], (), -> bắt buộc là method; global cần khi toán hạng 1 không thuộc lớp |
| Constructor conversion | Chuyển kiểu A→B tự động qua constructor 1 tham số |
| Conversion operator | Chuyển kiểu B→A tự động qua operator T() |
| Nhập nhằng | Tránh định nghĩa cả hai chiều chuyển kiểu cho cùng một cặp kiểu |
Phép gán = |
Luôn kiểm tra tự gán, giải phóng tài nguyên cũ, trả về *this |
<< và >> |
Luôn là hàm toàn cục friend, trả về tham chiếu stream |
Prefix/Postfix ++ |
Prefix trả về T&, Postfix trả về T (bản sao) với tham số giả int |