[Học OOP] Bài 7: Overload toán tử trong Lập trình hướng đối tượng c++

Toán tử là một ký hiệu được sử dụng trong các biểu thức (Ví dụ: + – * / …). Ngôn ngữ lập trình C++ cho phép người lập trình viên Overload toán tử và hàm để phục vụ riêng cho từng loại dữ liệu tự tạo ra.

1. Giới thiệu về Operator Overloading

Operator Overloading hay gọi là nạp chồng toán tử, được dùng để định nghĩa toán tử cho có sẵn trong c++ phục vụ cho dữ liệu riêng do bạn tạo ra.

Giả sử có lớp PhanSo và có các hàm thành phần như Set, Cong, Tru, Nhan, Chia.

Khi bạn muốn tính biểu thức KQ = A+B với những gì đã hướng dẫn trong các bài viết trước thì bạn phải thực hiện như sau:

PhanSo KQ, A, B, C;
KQ.Set(A.Cong(B));

Ở trên là một biểu thức đơn giản, nếu số lượng phép tính nhiều lên, bạn sẽ rất khó để gọi các hàm và xử lí, dễ gây rối cho người lập trình và thiếu trực quan.

Chính vì vậy trong bài viết này mình sẽ hướng dẫn bạn overloading toán tử để có thể thay KQ.Set(A.Cong(B)); Bên trên thành KQ = A + B;

Việc sử dụng toán tử cũng có bản chất như gọi hàm để xử lí dữ liệu.

2. Các toán tử của c++

– C++ chỉ cho phép người dùng overloading lại các toán tử có sẵn trong c++

– Một toán tử có thể được định nghĩa cho nhiều kiểu dữ liệu khác nhau.

Ví dụ:

PhanSo A(1,2), B(3,4), C;
int D=1, E=2, F;

C = A + B;
F = D + E;

Dù cùng kiểu toán tử + nhưng C++ vẫn cho phép sử dụng cho cả kiểu dữ liệu PhanSo và int.

Các loại toán tử - Overloading operator

Các loại toán tử – Overloading operator

a. Toán tử đơn

Toán tử đơn là toán tử một ngôi (unary operator), có thể được dùng làm toán tử trước (prefix operator) và toán tử sau (postfix operator). Ví dụ phép tăng (++) hay phép giảm (–)

Ví dụ:

  • prefix operator: ++A;
  • postfix operator: A++;

b. Toán tử đôi

Toán tử đôi là toán tử có 2 ngôi (binary operator).

Ví dụ: như A+B, A*B, hay toán tử chỉ mục “[…]” cũng là toán tử đôi.

c. Các toán tử có thể overload

Các loại toán được có thể và không thể overload.

Các loại toán được có thể và không thể overload.

3. Cú pháp Operator Overloading

Dạng toán tửPhương thức của lớpHàm toàn cục
aa@bbaa.operator@(bb)operator@(aa,bb)
@aaaa.operator@()operator@(aa)
aa@aa.operator@(int)operator@(aa,int)

a. khai báo A+B cho class phân số

Chúng ta nhìn vào bảng bên trên, A+B có dạng của aa@bb cho nên nếu bạn khai báo ở phương thức của lớp thì khai báo như sau: aa.operator@(bb)

Ở file PhanSo.h bạn khai báo trước

PhanSo operator+(PhanSo b);

Sau đó viết source cho hàm này:

PhanSo PhanSo::operator+(PhanSo b)
{
    //.........
    // return ....
}

Khai báo cho hàm toàn cục: có dạng operator@(aa,bb)

Trước tiên bạn cũng khai báo như trên, tuy nhiên hàm toàn cục nên bạn phải thiêt lập hàm bạn để nó có thể truy cập vào các thuộc tính

friend PhanSo operator+(PhanSo a, PhanSo b);

Sau đó viết source cho hàm này:

PhanSo operator+(PhanSo a, PhanSo b)
{
    //.........
    // return ....
}

b. khai báo số đối -A cho class phân số

Ví dụ bạn muốn sử dụng phép toán -a để lấy số đối của PhanSo thì nhìn vào bảng bên trên, -a có dạng @aa

Đối với khai báo ở Phương thức của lớp:

Bạn khai báo theo cú pháp aa.operator@()

PhanSo operator-();

Và viết nội dung cho hàm này

PhanSo PhanSo::operator-()
{
    return PhanSo(-tu, mau);
}

Đối với khai báo ở Hàm toàn cục:

Có dạng operator@(aa)

Đầu tiên bạn khai báo theo cú pháp friend PhanSo operator-(PhanSo A);

Sau đó viết nội dung cho hàm này ở cục bộ

PhanSo operator-(PhanSo A)
{
    return PhanSo(-A.tu,A.mau);
}

4. Chuyển kiểu dữ liệu

Xét ví dụ sau:

Để viết class PhanSo cho phép sử dụng các phép tính như:

  • A + B (Với A, B là một PhanSo);
  • A + 5 (Với 5 là một số nguyên)
  • 3 + A

Ít nhất bạn phải khai báo 3 dạng:

PhanSo operator+(PhanSo b); // Dành cho PhanSo + PhanSo
PhanSo operator+(int b); // Dành cho PhanSo + Int

Còn trường hợp int + PhanSo (ví dụ: 3+A), Có dạng aa@bb Tuy nhiên aa lúc này là một số nguyên, mà khai báo ở phương thức của lớp thì aa chính là đối tương của lớp ( aa.operator@(bb) ). Nên trong trường hợp này bạn phải khai báo ở hàm toàn cục có dạng operator@(aa,bb)

friend PhanSo operator+(int a, PhanSo b); // Dành cho Int + PhanSo

Tuy nhiên, nếu phép toán 2 ngôi, có cả +, -, *, /, các phép toán so sánh thì mỗi loại bạn phải khai báo 3 lần, rất hỗn loạn, dài và khó kiểm soát, cách viết những hàm tương tự nhau như vậy dễ gây cho bạn sự mệt mỏi và thiếu chính xác. Lúc này việc áp dụng chuyển kiểu dữ liệu trong Overload toán tử sẽ làm một giải pháp hiệu quả.

a. Chuyển kiểu bằng constructor

Theo dõi đoạn code dưới đây

class PhanSo
{
private:
    long tu, mau;
public:
    PhanSo (int t, int m) { Set(t,m); }
    PhanSo (int t) { Set(t,1); }
    void Set (long t, long m);
    friend PhanSo operator + (PhanSo a, PhanSo b);
    friend PhanSo operator - (PhanSo a, PhanSo b);
//...
};

Khi bạn viết PhanSo a=1  thì chương trình sẽ gọi constructor PhanSo (int t) { Set(t,1);}  Để khỏi tạo đối tượng, một phần vì số 1 là số int nên chương trình chạy constructor đúng đối số này.

Và khi bạn viết PhanSo a = 1 + PhanSo(1,2)  lúc này chương trình sẽ hiểu PhanSo a = PhanSo(1) + PhanSo(1,2) và bài toán trở về PhanSo + PhanSo.

Đó là cách giải quyết vấn đề bên trên. Cách này giúp bạn tiết kiệm được công sức, và giảm được sai sót, cũng như tổng quát các trường hợp hơn.

b. Chuyển kiểu bằng phép toán chuyển kiểu

Nếu như chuyển kiểu bằng constructor từ kiểu đang có sẵn để phù hợp với dữ liệu của kiểu mới, thì chuyển kiểu bằng phép toán chuyển kiểu cho phép chuyển từ kiểu dữ liệu do mình định nghĩa thành các kiểu có sẵn.

Tóm lại: ví dụ mình có PhanSo a=PhanSo(1,2)  mình có thể lấy số thực của phân số này bằng cách float x = (float)a;

Khai báo

operator float()
{
    return (float)Tu/Mau;
}

Đó là một ví dụ, bạn có thể thay float bằng các kiểu có sẵn trong c++

 

Ngoài ra bạn nên xem thêm về sự nhập nhằng khi chuyển kiểu

5. Overloading toán tử gán (=)

Khai báo: MyClass& MyClass::operator=(const MyClass &rhs)

MyClass& MyClass::operator=(const MyClass &rhs)
{
    if (this == &rhs)      // kiểm tra có cùng đối tượng?
      return *this;        // Nếu trùng thì bỏ qua và return chính nó
    //Xử lí... (Cấp phát vùng nhớ mới, sao chép giá trị, ...)
    return *this;
}

Cú pháp và cách xử lí ở đây khá giống copy constructor.

6. Overloading toán tử nhập xuất cin cout (>> <<)

Việc overloading toán tử nhập xuất cho phép người dùng dùng cin, cout nhập xuất nhanh một đối tượng mà không cần gọi lại cin cout cho từng thuộc tính của dữ liệu dựa trên việc được định nghĩa trước.

Cú pháp tổng quát

friend istream &operator>>( istream &input, MyClass &obj)
friend ostream &operator<<( ostream &output, const MyClass &obj )

Lí do chúng ta sử dụng hàm friendkhông phải hàm của phương thức là vì bên trái toán tử cin rồi đến toán tử >> rồi đến class cần xử lí. Loại này có dạng aa@bb Tuy nhiên aa trong bảng ở mục 3, là đối tượng của class hiện tại, mà cin hay cout là không thuộc class hiện tại, nên không thể khai báo hàm của phương thức. Nên buộc tại đây chúng ta sử dụng friend.

a. Overloading toán tử nhập cin >>

Cú pháp khai báo

friend istream &operator >> (istream &in, phanso &ps);

Ở đây mình lấy ví dụ là nhập cho một PhanSo.

Khi đó bên trong hàm bạn nhập bằng in >> ….. do đã khai báo istream &in

istream &operator >> (istream &in, phanso &ps)
{
    in >> ps.tu >> ps.mau;
    return in;
}

b. Overloading toán tử xuất cout <<

Cú pháp khai báo

friend ostream &operator << (ostream &out, const phanso &ps);

Ở đây mình lấy ví dụ là xuất cho một PhanSo.

Khi đó bên trong hàm bạn xuất bằng out << ….. do đã khai báo ostream &out

ostream &operator << (ostream &out, phanso ps)
{
    if (ps.mau == 0)
        out << "-1";
    else
        out << ps.tu << "/" << ps.mau;
    return out;
}

Xem thêm: https://kienthuc24h.com/struct-cong-2-phan-can-ban/

7. Bài tập ứng dụng Operator Overloading

Về cơ bản còn nhiều loại toán tử khác như (), [],… tuy nhiên mình chỉ hướng các bạn cách để học, ngoài ra để tìm hiểu kĩ hơn các bạn có thể có thể tìm trên các diễn đàn, website khác cách khai báo và cài đặt các loại toán tử còn lại.

a. Bài tập viết class PhanSo hoàn chỉnh sử dụng Operator Overloading

Đề bài: Viết class thể hiện Phân số, có thể +, -, *, /, các phép so sánh logic và sử dụng toán tử nhập xuất.

b. Code class PhanSo hoàn chỉnh sử dụng Operator Overloading

Phanso.h

#pragma once
#include <iostream>
using namespace std;
class PhanSo
{
private:
    int tuSo;
    int mauSo;
    int UCLN(int, int);
public:
    PhanSo();
    PhanSo(int t, int m);
    PhanSo(int t);
    ~PhanSo();
    void set(int, int);
    PhanSo rutgon();
    void chuanhoa();
    friend PhanSo operator+(PhanSo a, PhanSo b);
    friend PhanSo operator-(PhanSo a, PhanSo b);
    friend PhanSo operator*(PhanSo a, PhanSo b);
    friend PhanSo operator/(PhanSo a, PhanSo b);

    friend bool operator == (PhanSo a, PhanSo b);
    friend bool operator != (PhanSo a, PhanSo b);
    friend bool operator > (PhanSo a, PhanSo b);
    friend bool operator >= (PhanSo a, PhanSo b);
    friend bool operator < (PhanSo a, PhanSo b);
    friend bool operator <= (PhanSo a, PhanSo b);
    
    PhanSo operator ++();
    PhanSo operator ++(int);
    PhanSo operator --();
    PhanSo operator --(int);

    PhanSo operator-();

    friend ostream& operator << (ostream &os, PhanSo a);
    friend istream& operator >> (istream &is, PhanSo &a);
};

Các bạn lưu ý rằng, đối với phân số, việc khai báo ++ hay — như trên là sai ý nghĩa, tuy nhiên mình vẫn viết để các bạn hiểu cách dùng nó như thế nào.

Về ý nghĩa, ++ hay — cho phép nó thay đổi thành số sau nó hoặc trước nó, như ta có số 5, ++ thì sẽ ra 6. Hoặc char ‘B’, ++ thì sẽ ra C. Còn việc dùng ++, — trong phân số là sai ý nghĩa.

PhanSo.cpp

#include <iostream>
#include <cmath>
#include "PhanSo.h"
using namespace std;

void PhanSo::set(int a, int b)
{
    this->tuSo = a;
    this->mauSo = b;
}


PhanSo PhanSo::rutgon()
{
    int z = UCLN(tuSo, mauSo);
    return PhanSo(tuSo / z, mauSo / z);
}

int PhanSo::UCLN(int a, int b)
{
    int r;
    while (a%b != 0)
    {
        r = a%b;
        a = b;
        b = r;
    }
    return b;
}


PhanSo PhanSo::operator-()
{
    return PhanSo(-tuSo,mauSo);
}

void PhanSo::chuanhoa()
{
    this->rutgon();
    if (this->tuSo < 0 && this->mauSo < 0)
    {
        this->tuSo = abs(this->tuSo);
        this->mauSo = abs(this->mauSo);
    }
    else
        if (this->mauSo < 0)
        {
            this->tuSo =  -abs(this->tuSo);
            this->mauSo = abs(this->mauSo);
        }    
}

PhanSo PhanSo::operator++()
{
    *this = *this + 1;
    return *this;
}

PhanSo PhanSo::operator++(int)
{
    PhanSo tmp = *this;
    ++(*this);
    return tmp;
}

PhanSo PhanSo::operator--()
{
    *this = *this - 1;
    return *this;
}

PhanSo PhanSo::operator--(int)
{
    PhanSo tmp = *this;
    --(*this);
    return tmp;
}

PhanSo::PhanSo()
{
    tuSo = 0;
    mauSo = 1;
}

PhanSo::PhanSo(int t, int m)
{
    set(t, m);
}

PhanSo::PhanSo(int t)
{
    set(t, 1);
}

PhanSo::~PhanSo()
{
}

Main.cpp

#include <iostream>
#include "PhanSo.h"
using namespace std;


PhanSo operator+(PhanSo a, PhanSo b)
{
    a = a.rutgon();
    b = b.rutgon();
    PhanSo res(a.tuSo*b.mauSo + a.mauSo*b.tuSo, a.mauSo*b.mauSo);
    res = res.rutgon();
    return res;
}

PhanSo operator-(PhanSo a, PhanSo b)
{
    return PhanSo(a.rutgon()+b.rutgon()*-1).rutgon();
}

PhanSo operator*(PhanSo a, PhanSo b)
{
    a = a.rutgon();
    b = b.rutgon();
    PhanSo res(a.tuSo*b.tuSo, a.mauSo*b.mauSo);
    res = res.rutgon();
    return res;
}

PhanSo operator/(PhanSo a, PhanSo b)
{
    b = b.rutgon();
    return PhanSo(a.rutgon() * PhanSo(b.mauSo, b.tuSo)).rutgon();
}

bool operator==(PhanSo a, PhanSo b)
{
    return (a.tuSo*b.mauSo == a.mauSo*b.tuSo);
}

bool operator!=(PhanSo a, PhanSo b)
{
    return !(a == b);
}

bool operator>(PhanSo a, PhanSo b)
{
    return (a.tuSo*b.mauSo>a.mauSo*b.tuSo);
}

bool operator>=(PhanSo a, PhanSo b)
{
    return (a > b || a == b);
}

bool operator<(PhanSo a, PhanSo b)
{
    return (a.tuSo*b.mauSo<a.mauSo*b.tuSo);
}
bool operator<=(PhanSo a, PhanSo b)
{
    return (a < b || a == b);
}
ostream& operator << (ostream &os, PhanSo a)
{
    a.chuanhoa();
    if (a.mauSo == 1)
        os << a.tuSo << endl;
    else
        os << a.tuSo << "/" << a.mauSo << "\n";
    return os;
}

istream& operator >> (istream &is, PhanSo &a)
{
    is >> a.tuSo >> a.mauSo;
    if (a.mauSo == 0)
        throw "Nhap sai, mau so phai khac 0!!\n";
    return is;
}

int main()
{
    PhanSo a, b;
    try
    {
        cin >> a >> b;
        cout << "Cong = " << a + b;
        cout << "Tru = " << a - b;
        cout << "Nhan = " << a*b;
        cout << "Chia = " << a / b;
    }
    catch (const char * msg)
    {
        cerr << msg << endl;
    }

    system("pause");
    return 0;
}

Nếu có thắc mắc gì vui lòng thảo luận tại đây. Mình sẽ giải đáp cho các bạn.

Chúc các bạn học tốt.

One thought on “[Học OOP] Bài 7: Overload toán tử trong Lập trình hướng đối tượng c++

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *