Skip to content

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à:

C++
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.

C++
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++¤

Text Only
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ử ++-- 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ố).
  • newdelete cũ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:

Text Only
+    -    *    /    %    ^    &    |    ~    !
=    <    >    +=   -=   *=   /=   %=   ^=   &=
|=   <<   >>   >>=  <<=  ==   !=   <=   >=   &&
||   ++   --   ->*  ,    ->   []   ()   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ử @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¤

C++
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¤

C++
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¤

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ĩa int + 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¤

C++
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:

C++
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:

C++
// 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:

C++
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:

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():

C++
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ố):

C++
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¤

C++
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):

C++
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¤

C++
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.paa.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ữaNull pointer assignment / crash!

Text Only
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¤

C++
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ớ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)¤

C++
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 <<>> 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):

C++
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ính ostream& để 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.

C++
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';
C++
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ượng const chỉ được gọi các phương thức const. Phiên bản const trả 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 ý:

C++
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ố int trong 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¤

C++
#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ũ
}
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:

\[A + B = (a_1+b_1,\ a_2+b_2)$$ $$A - B = (a_1-b_1,\ a_2-b_2)$$ $$A \times B = (a_1 b_1 - a_2 b_2,\ a_1 b_2 + a_2 b_1)$$ $$A / B = \left(\frac{a_1 b_1 + a_2 b_2}{b_1^2+b_2^2},\ \frac{a_2 b_1 - a_1 b_2}{b_1^2+b_2^2}\right)\]

Gợi ý cài đặt:

C++
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 ý:

C++
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 ý:

C++
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 ý:

C++
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
<<>> 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