Operator Overloading in C plus plus

Operator Overloading in C++

Lý thuyết cơ bản về Operator Overloading – Nạp chồng toán tử.

Toán tử ta cũng có thể hiểu là hàm nhưng là dạng hàm có kí hiệu đặc biệt giúp các đoạn code sử dụng nó trở nên dễ hiểu và tiện lợi hơn. Và việc Operator Overloading ta cũng có thể xem là một dạng đặc biệt của Function Overloading.

Ví dụ về Operator Overloading cho một class trong C++ và sử dụng
nó. Trong ví dụ sau sẽ thực hiện nạp chồng toán tử +.

class A
{
    public:
        A(){this->mValue = 0;}
        ~A(){}
        int getValue(){return this->mValue;}
        void setValue(int value){this->mValue = value;}
        A operator + (A a2)
        {
            A a3;
            a3.mValue = this->mValue + a2.mValue;
            return a3;
        }

    protected:
    private:
        int mValue;
};

int main()
{
    A a,a2;
    a.setValue(3);
    a2.setValue(4);
    A a3 = a + a2;
    cout << a3.getValue();
    return 0;
}

Việc Operator Overloading trong một class có thể thực hiện không qua nạp chồng như một member của class hoặc ở dạng không là member của class. Tuy nhiên, có một vài toán tử có ràng buộc đặc biệt về việc là hàm member hay không.

  • Toán tử sử dụng dạng @a: member ((a).operator@()) hoặc non-member (operator@ (a)).
  • Toán tử sử dụng dạng a@b: member ((a).operator@ (b)) hoặc non-member (operator@ (a, b)).
  • Toán tử sử dụng dạng a@: member ((a).operator@ (0)) hoặc non-member (operator@ (a, 0)).
  • Toán tử a=b: luôn là member ((a).operator= (b))
  • Toán tử a[b]: luôn là member ((a).operator[] (b)).
  • Toán tử a->: luôn là member ((a).operator-> ( )).
  • Nội dung được tham khảo từ link.

Trong C++, không phải toán tử nào cũng có thể nạp chồng. Sau đây là một số toán tử có thể nạp và chồng không thể nạp chồng:

  • Các thể nạp chồng: +, -, , /, %, ^, &, |, ~, !, ‘,’, =, <, >, <=, >=, ++, –, <<, >>, ==, !=, &&, ||, +=, -=, /=, %=, ^=, &=, |=, =, <<=, >>=, [], (), ->, ->*, new, new [], delete, delete []
  • Các toán tử không thể nạp chồng: :: , .*, ‘.’, ?:
    Nội dung được tham khảo từ link.

Bài tập

Trong bài tập sẽ tiến hành xây dựng class STRING tương tự class string có sẵn trong C++ với một số yêu cầu sau:

class STRING
{
    public:
    //TODO some methods required
    protected:
    private:
        int mLength;
        char *mContent;
};

int main()
{
    STRING a; //1st requirement
    STRING b("abc"); //2nd requirement
    STRING c('a'); //3rd requirement
    STRING d="def", f='t'; //4th requirement
    STRING e(c); //5th requirement

    c = b + a; //6th requirement
    cout << c[1] << endl; //7th requirement - trả về kí tự có chỉ số 1 trong nội của chuỗi, nếu chỉ số không hợp lệ thì nhận được giá trị NULL.
    cout << c << endl; //8th requirement - In nội dụng của chuỗi
    return 0;
}

1st requirement: để hỗ trợ cách khởi tạo này thì class STRING cần hàm khởi tạo không đối. Nếu ta không khai báo bất kỳ hàm khởi tạo nào thì hàm khởi tạo mặc định không đối số sẽ được cung cấp cho class STRING và hàm khởi tạo mặc định sẽ không làm bất cứ điều gì cả. Ở đây, ta sẽ tạo một hàm khởi tạo không đối số để set một 2 giá trị mặc định cho thuộc tính của STRING.

Trong file STRING.h:

class STRING
{
public:
    STRING();
    ~STRING();//cần thêm hàm hủy để giải phóng bộ nhớ cấp cho mContent
private:
    char* mContent;
    int mLength;
};

Trong file STRING.cpp:

STRING::STRING()
{
    this->mContent = NULL;
    this->mLength = 0;
}
STRING::~STRING()
{
    if (this->mContent != NULL)
        delete this->mContent;
}

2nd requirement: để hỗ trợ cách khởi tạo này thì class STRING cần có hàm tạo có đối số đầu vào là một một chuỗi ký tự (sử dụng con trỏ kiều char để để quản lý chuỗi này)

Trong file STRING.h thêm trong public:

STRING(const char*);

Trong file STRING.cpp:

STRING::STRING(const char* content)
{
    this->mContent = NULL;
    this->mLength = 0;
    if (content != NULL)
    {
        this->mLength = strlen(content);
        this->mContent = new char[this->mLength + 1];//thêm một kí tự để chứa kí tự kết thúc chuỗi.
        strcpy(this->mContent, content);
    }
}

3rd requirement: requirement này tương tự với 2nd requirement chỉ khác là 2nd requirement nhận đầu vào là một chuỗi kí tự, còn ở 3rd requirement nhận vào một một kí tự.
Trong file STRING.h thêm trong public:

STRING(const char&);

Trong file STRING.cpp:

STRING::STRING(const char& content)
{
    this->mContent = NULL;
    this->mLength = 0;
    if (content != NULL)
    {
        this->mLength = 1;
        this->mContent = new char[this->mLength + 1];
        this->mContent[0] = content;
        this->mContent[1] = '\0';//kí tự nhận biết kết thúc của một chuỗi
    }
}

4th requirement: với requirement này ta không cần phải thêm dòng code nào trong class cả mà vẫn có thể biên dịch thành công. Và nhiều bạn sẽ thấy làm lạ vì sao lại biên dịch thành công trong khi chưa nạp chồng toán tử gán =. Do khi nhìn vào d=”def”, f=’t’ nhiều bạn sẽ nghĩ cần phải nạp chồng toán tử gán =. Nhưng thực chất, với các câu lệnh đó trình biên dịch sẽ hiểu như sau:

STRING d("def"), f('t');

Hai hàm khởi tạo này đã được xây dựng ở 2nd requirement và 3rd requirement.
Do đó, khi lập trình class mà muốn class hỗ trợ các kiểu khởi tạo tương tự d = “def” thì hãy nghĩ về cách biên dịch thực hiện là d(“def”) để xây dựng các phương thức cho hợp lí.

5th requirement: để hỗ trợ kiểu khởi tạo này thì class STRING cần có hàm khởi tạo sao chép (hàm khởi tạo nhận một đối số là class của chính nó). Hàm này sẽ copy nội dung của đầu vào vào đối tượng được tạo ra:

Trong file STRING.h thêm trong public:

STRING(const STRING&);

Lưu ý: không tạo hàm STRING(STRING) vì nếu tạo hàm như thế này thì khi truyền đối số vào thì hàm tạo sao chép sẽ được gọi. Điều này
dẫn đến việc đang tiến hành được nghĩa hàm tạo sao chép lại gọi hàm
tạo sao chép ở đầu vào. Lúc này trình biên dịch sẽ báo lỗi khi biên
dịch.

Trong file STRING.cpp:

STRING::STRING(const STRING& str2)
{
    this->mLength = 0;
    this->mContent = NULL;
    if (str2.mContent != NULL)
    {
        this->mLength = str2.mLength;
        this->mContent = new char[this->mLength + 1];
        strcpy(this->mContent, str2.mContent);
    }
}

6th requirement: để thực hiện câu lệnh này thì class STRING cần phải thêm nạp chồng toán tử +, toán tử gán =.

Trong file STRING.h thêm trong public:

STRING operator + (const STRING& str2) const;
const STRING& operator = (const STRING& str2);

Trong file STRING.cpp:

STRING STRING::operator + (const STRING& str2) const
{
    char* content = NULL;
    if (this->mContent == NULL)
    {
        if (str2.mContent == NULL)
            content = NULL;
        else
        {
            content = new char[str2.mLength + 1];
            strcpy(content, str2.mContent);
        }
    }
    else
    {
        if (str2.mContent == NULL)
        {
            content = new char[this->mLength + 1];
            strcpy(content, this->mContent);
        }
        else
        {
            content = new char[this->mLength + str2.mLength + 1];
            strcpy(content, this->mContent);
            strcat(content, str2.mContent);
        }
    }
    return STRING(content);
}

const STRING& STRING::operator= (const STRING& str2)
{
    //kiểm tra liệu một STRING có được gán bằng chính nó
    if (this != &str2)
    {
        if (this->mContent != NULL)
            delete this->mContent;
        if (str2.mContent != NULL)
        {
            this->mLength = str2.mLength;
            this->mContent = new char[this->mLength + 1];
            strcpy(this->mContent, str2.mContent);
        }
        else
        {
            this->mLength = 0;
            this->mContent = NULL;
        }
    }
    return *this;
}

7th requirement: ta cần phải nạp chồng toán tử [] như sau:
Trong file STRING.h thêm:

char operator[] (int);

Trong file STRING.cpp thêm:

char STRING::operator[](int index)
{
    if (index >= 0 && index < this->mLength)
        return this->mContent[index];
    else
        return NULL;
}

8th requirement: ta sẽ nạp chồng toán tử << theo dạng non-member nhưng là hàm friend như sau:
Trong file STRING.h thêm:

friend ostream& operator<<(ostream&,const STRING&);

Trong file STRING.cpp thêm:

ostream& operator<<(ostream& os, const STRING& str)
{
    os << str.mContent;
    return os;
}

Bài tập này được cài đặt và test tên MS Visual studio 2015 Express và nếu khi biên dịch báo lỗi có thể báo yêu cầu sử dụng strcpy_s thay cho strcpy thì có thể làm theo hướng dẫn trong post này link