< Lập trình tân binh | 2.4. Ghi đè các phép toán

2.4. Ghi đè các phép toán

Các bạn đã thấy là ngôn ngữ C++ cung cấp rất nhiều tính năng hữu ích nếu sử dụng 1 cách hợp lý.

1 trong những tính năng đó là khả năng ghi đè (override) các phép toán mà chúng ta sẽ cùng tìm hiểu trong bài học này. Đây là kỹ thuật cho phép chúng ta thực hiện những phép toán thông minh mà thành phần là các đối tượng. Hãy tưởng tượng cách mà chúng ta có thể dùng các dấu +, -, *, /, ==, <, vv… trên phép toán của các đối tượng. Đoạn mã của chúng ta sẽ nhờ thế mà càng thêm gọn và rõ ràng, tiện cho việc theo dõi.

Thao tác chuẩn bị

Nguyên lý rất đơn giản. Hãy giả sử chúng ta tạo ra 1 lớp để lưu thông tin về thời gian (ví dụ 4 giờ 21 phút hay 4h21m) rồi chúng ta tạo ra 2 đối tượng của lớp này và muốn cộng chúng lại để ra kết quả là 1 tổng thời gian.

Với những kiến thức đã biết, chúng ta sẽ tạo ra 1 hàm như sau :

ThoiGian ketQua, thoiGian1, thoiGian2 ;
ketQua = cong(thoiGian1, thoiGian2) ;

Hàm cong sẽ thực hiện tính tổng của thoiGian1thoiGian2 và lưu kết quả vào trong biến ketQua.

Hàm này hoạt động tốt nhưng đoạn mã nhận được vẫn không được dễ đọc lắm. Chúng ta sẽ tìm hiểu trong bài học này cách thay đổi lớp ThoiGian để có thể sử dụng đoạn mã tương đương sau đây :

ThoiGian ketQua, thoiGian1, thoiGian2 ;
ketQua = thoiGian1 + thoiGian2 ;

Chính xác hơn là chúng ta muốn sử dụng các đối tượng như các số thông thường. Và bởi vì 1 đối tượng thì phức tạp hơn nhiều 1 con số, chúng ta sẽ phải giải thích cho máy tính cách để thực hiện các phép toán này.

Ví dụ của lớp ThoiGian

Không phải tất cả các lớp đều thích hợp để sử dụng tính năng ghi đè phép toán. Ví dụ như trường hợp của lớp NhanVat trong bài trước, việc thực hiện phép cộng trên 2 đối tượng của lớp này không mang quá nhiều ý nghĩa thực tế. Dù sao thì C++ cũng không phải chỉ dùng để viết trò chơi.

Lớp ThoiGian có khả năng lưu thông tin về thời gian tính theo giờ, phút, giây. Lớp này không quá phức tạp. Các bạn sẽ không gặp bất cứ vấn đề gì khi tự viết mã của lớp này sử dụng các kiến thức chúng ta đã học trong bài trước.

ThoiGian.h

#ifndef DEF_THOIGIAN
#define DEF_THOIGIAN

class ThoiGian{
   public:
     ThoiGian(int gio = 0, int phut = 0, int giay = 0);

   private:
     int m_gio;
     int m_phut;
     int m_giay;
};
#endif

Mỗi đối tượng ThoiGian sẽ có các thuộc tính là giờ, phút và giây.

Các bạn hẳn là đã nhận thấy là tôi sử dụng các giá trị tham số mặc định để phòng trường hợp người sử dụng không chỉ ra các giá trị này. Chúng ta sẽ có nhiều cách khác nhau để tạo ra đối tượng.

ThoiGian thoiGian; //Luu tong thoi luong 0 gio, 0 phut, 0 giay
ThoiGian thoiGian(5); //Luu tong thoi luong 5 gio, 0 phut, 0 giay
ThoiGian thoiGian(5, 30); //Luu tong thoi luong 5 gio, 30 phut, 0 giay
ThoiGian thoiGian(0, 12, 55); //Luu tong thoi luong 0 gio, 12 phut, 55 giay

ThoiGian.cpp

Viết mã xử lý cho phương thức khởi tạo còn dễ hơn ăn kẹo.

#include "ThoiGian.h"
ThoiGian:: ThoiGian(int gio, int phut, int giay) : m_gio(gio), m_phut(phut), m_giay(giay){
}

Thế là chúng ta đã chuẩn bị xong nguyên liệu cho phần tiếp theo của bài học này.

! Phương thức khởi tạo mà chúng ta tạo ra trên đây cho phép chúng ta truyền những giá trị rất lớn cho các tham số như 500 phút hay 1000 giây. Trên thực tế, người dùng phải có thể viết ThoiGian thoiGian(0, 500, 1000); mà không cần lo lắng gì. Nhiệm vụ của phương thức khởi tạo của chúng ta lúc đó là phải kiểm tra và điều chính chúng về các giá trị thích hợp hoặc phải tự động quy đổi giữa giờ, phút, giây để giá trị của phút và giây không vượt quá 60. Tôi đề nghị các bạn có thể tự viết những thay đổi tính năng này để rèn luyện thêm kỹ năng. Chỉ đơn giản là vài lệnh if cùng 1 số phép toán đơn giản, các bạn không cần quá sợ hãi.

Các phép toán so sánh

Chúng ta sẽ bắt đầu bằng việc thêm các phép toàn so sánh (==, !=, <, >=, vv…) cho lớp của chúng ta. Nói chung thì đây là những phép toán dễ thực hiện nhất.

Hãy cùng bắt đầu với dấu ==.

Để có thể sử dụng dấu == giữa 2 đối tượng, chúng ta cần tạo ra 1 hàm có tên là operator== và có nguyên mẫu chính xác như sau :

bool operator==(TenLop const& a, TenLop const& b);

! Dù là chúng ta đang làm việc với các lớp, hàm này không phải là 1 phương thức. Đây là 1 hàm bình thường nằm bên ngoài khai báo lớp.

Hàm này nhận vào 2 tham chiếu hằng trên các đối tượng. Bởi vì là tham chiếu hằng nên chúng ta không thể thay đổi giá trị các tham số này. Chúng ta sẽ so sánh 2 đối tượng và trả về kết quả là 1 biến bool chỉ ra là 2 đối tượng có bằng nhau hay không.

Ở bên cạnh phần khai báo của lớp ThoiGian (trong tệp .h), chúng ta có thể thêm nguyên mẫu của hàm này vào.

bool operator==(ThoiGian const& a, ThoiGian const& b);

Đây là lần đầu tiên chúng ta sử dụng đến tham chiếu hằng kể từ khi học về chúng trong các bài học trước. Bởi sử dụng tham chiếu nên đối tượng sẽ không bị sao chép để sử dụng trong hàm. Lớp ThoiGian của chúng ta bao gồm 3 số nguyên nên việc sử dụng tham chiếu sẽ tránh cho phải thực hiện sao chép không cần thiết cả 3 số nguyên này. Trong trường hợp của ví dụ này thì lợi ích không được lớn nhưng hãy tưởng tượng lợi ích khi tránh được sao chép 1 đối tượng string lưu trữ 1 văn bản dài.

Chính vì lý do đó mà khi làm việc với các đối tượng, cách sử dụng tham chiếu rất được các lập trình viên ưa chuộng. Tuy nhiên, chúng ta vẫn không muốn các hàm có thể thay đổi đối tượng của chúng ta, vậy nên cần sử dụng đến tham chiếu hằng. Ví dụ như khi thực hiện phép toán a==b, cả ab đều không cần bị thay đổi, thế nên từ khóa const là rất quan trọng.

Cách sử dụng

Sau khi hoàn thành ghi đè hàm operator==, chúng ta có thể sử dụng nó để so sánh 2 đối tượng của lớp ThoiGian.

if(thoiGian1 == thoiGian2){
     std::cout << "2 khoang thoi gian bang nhau !" << std::endl;
}

Cách sử dụng không khác gì khi chúng ta thao tác với các con số đơn lẻ.

Trên thực tế, đoạn mã bên trên sẽ được trình biên dịch chuyển thành đoạn mã tương đương như sau.

if(operator==(thoiGian1, thoiGian2)){
     std::cout << "2 khoang thoi gian bang nhau !" << std::endl;
}

Đối với trình biên dịch thì đoạn mã được viết theo phong cách cổ điển bên dưới mới là đoạn mã dễ hiểu hơn. Chính là từ khóa operator giúp trình biên dịch hiểu là cần phải biến đổi 1 toán tử thành 1 lệnh gọi hàm. Đương nhiên là vì thế, kết quả trả về của toán tử == cũng sẽ là bool giống như hàm operator==.

Mã xử lý của toán tử ==

Trước mắt thì những gì chúng ta làm chỉ là định nghĩa phép toán so sánh với trình biên dịch. Vẫn còn việc phải giải thích cho nó cách thức vận hành và xử lý phép toán này. Trong trường hợp ví dụ của chúng ta thì khá đơn giản, 2 đối tượng ThoiGian được coi là bằng nhau nếu chúng có số giờ, số phút và số giây tương ứng bằng nhau. Trong 1 số trường hợp khác thì phức tạp hơn, ví dụ như khi chúng ta thao tác với NhanVat. Thế nào là 2 NhanVat bằng nhau ? Cùng tên ? Cùng số điểm hp ? Cùng loại vũ khí ? Người thiết kế ra lớp đó sẽ là người đưa ra quyết định dựa trên tiêu chí nào để đánh giá 2 đối tượng bằng nhau.

Quay trở về với ví dụ hiện tại, chúng ta đơn giản là sử dụng lệnh if để kiểm tra xem các thuộc tính của 2 đối tượng có giá trị giống nhau không.

bool operator==(ThoiGian const& a, ThoiGian const& b){
   if (a.m_gio == b.m_gio && a.m_phut == b.m_phut && a.m_giay == b.m_giay)
       return true;
   else
       return false;
}

Vẫn còn 1 khó khăn nhỏ đó là chúng ta cần phải truy cập vào các thuộc tính của 2 đối tượng ab. Bởi vì chúng sẽ có quyền truy cập là private nên chúng ta không thể truy cập từ bên ngoài.

Có 3 giải pháp cho vấn đề này :

  • Tạo ra và sử dụng các phương thức lấy getGio(), getPhut(), getGiay().
  • Sử dụng khái niệm hàm bạn (friend function) mà chúng ta sẽ tìm hiểu trong những bài học sau.
  • Sử dụng kỹ thuật mà chúng ta sẽ nói ngay tiếp sau đây.

Sau đây sẽ là giải pháp chúng ta sử dụng trong bài học này để giải quyết vấn đề bên trên.

Vấn đề là hàm operator== của chúng ta nằm bên ngoài khai báo của lớp nên không thể truy cập tới các thuộc tính. Giải pháp rất dễ dàng, chúng ta sẽ tạo ra 1 phương thức của lớp cho phép thực hiện so sánh và hàm của chúng ta sẽ gọi phương thức đó.

bool ThoiGian::bang(ThoiGian const& b) const{
   if (m_gio == b.m_gio && m_phut == b.m_phut && m_giay == b.m_giay)
       return true;
   else
       return false;
}

Phương thức bang() trả về true nếu đối tượng b được coi là bằng đối tượng chủ thể đã gọi phương thức. Chúng ta cũng có thể viết ngắn gọn hơn.

bool ThoiGian::bang(ThoiGian const& b) const{
   return (m_gio == b.m_gio && m_phut == b.m_phut && m_giay == b.m_giay);
}

Và sử dụng phương thức đó trong hàm so sánh của chúng ta.

bool operator==(ThoiGian const& a, ThoiGian const& b){
   return a.bang(b);
}

Chúng ta sẽ kiểm tra lại xem mọi thứ diễn ra như mong muốn không.

int main(){
   ThoiGian thoiGian1(0, 10, 28), thoiGian2(0, 10, 28);
   if (thoiGian1 == thoiGian2){
       cout << "2 khoang thoi gian bang nhau !";
   }else{
       cout << "2 khoang thoi gian khac nhau !";
   }
   return 0;
}

Đúng như mong đơi ! Bây giờ chúng ta đã có thể so sánh 2 đối tượng như cách vẫn làm với 2 số đơn lẻ. Hãy tiếp tục với các phép toán khác nhé.

Phép toán !=

Chỉ kiểm tra bằng nhau hay không thôi thì chưa đủ, đôi khi chúng ta muốn kiểm tra nếu 2 đối tượng có khác nhau. Mã xử lý càng thêm đơn giản, để kiểm tra xem 2 đối tượng có khác nhau không cũng đồng nghĩa với kiểm tra xem chúng có không bằng nhau không.

bool operator!=(ThoiGian const& a, ThoiGian const& b){
   if(a == b) //Su dung phep toan == da duoc dinh nghia ben tren
       return false;
   else
       return true ;
}

Hoặc càng thêm ngắn gọn.

bool operator!=(ThoiGian const& a, ThoiGian const& b){
   return !(a==b);
}

Thật dễ dàng ! Mã xử lý của phép toán khác nhau sử dụng phép toán bằng nhau. Cùng 1 cách suy luận mà cho ra mã xử lý của 2 phép toán khác nhau.

int main(){
   ThoiGian thoiGian1(0, 10, 28), thoiGian2(0, 10, 29);
   if (thoiGian1 != thoiGian2){
       cout << "2 khoang thoi gian khac nhau !";
   }else{
       cout << "2 khoang thoi gian bang nhau !";
   }
   return 0;
}

! Sử dụng phép toán == để viết mã xử lý cho phép toán != là 1 cách xử lý khôn khéo. Tôi khuyên các bạn nên áp dụng lối suy nghĩ này bất khi nào có thể. Sử dụng lại những xử lý có sẵn giúp đảm bảo tính thống nhất của cả hệ thống và tránh khỏi những sai lầm ngớ ngẩn có thể dễ mắc phải. Ví dụ như ở đây, chúng ta yêu cầu là xử lý của phép toán != phải trái ngược với xử lý của phép toán == và việc sử dụng phép toán == trong mã xử lý của != giúp chúng ta đảm bảo điều này.

Phép toán <

So sánh bằng giữa 2 đối tượng có thể thực hiện được trong hầu hết các trường hợp còn so sánh hơn kém thì không như thế. Không phải lúc nào chúng ta cũng xác định được khái niệm hơn kém giữa 2 đối tượng.

Thật may mắn là chúng ta có thể dễ dàng đưa ra khái niệm hơn kém giữa các đối tượng của lớp ThoiGian mà chúng ta đang dùng làm ví dụ.

Các bạn hẳn đã đoán ra, nguyên mẫu của phép toán “nhỏ hơn” trông như sau.

bool operator<(ThoiGian const &a, ThoiGian const& b);

Tên của hàm cũng là sự kết hợp của từ khóa operator và sau đó là toán tử của phép toán như trong 2 phép toán trước.

Trong mã xử lý của phép toán này, chúng ta cũng sẽ gặp khó khăn tương tự khi không thể truy cập đến thuộc tính của lớp. Giải pháp được áp dụng vẫn sẽ giống như bên trên : tạo ra 1 phương thức của lớp và hàm sẽ sử dụng phương thức này để thực hiện xử lý.

Sau đây là mã xử lý của operator< :

bool operator<(ThoiGian const &a, ThoiGian const& b) {
   return a.nhoHon(b);
}

Và phương thức nhoHon() của lớp ThoiGian :

bool ThoiGian::nhoHon(ThoiGian const& b) const {
   if (m_gio < b.m_gio)   // Neu so gio khac nhau
       return true;
   else if (m_gio == b.m_gio && m_phut < b.m_phut) //Neu cung so gio va khac so phut
       return true;
   else if (m_gio == b.m_gio && m_phut == b.m_phut && m_giay < b.m_giay) // Cung so gio va so phut thi so sanh so giay
       return true;
   else   // Neu tat ca deu sai, a se lon hon hoac bang b
       return false;
}

Chỉ cần bỏ ra 1 ít thời gian để suy nghĩ là chúng ta có thể viết được đoạn mã bên trên. Chú ý là đây là phép toán < nên kết quả trả về phải là false khi 2 đối tượng bằng nhau. Nếu thay vào đó là phép toán <= thì kết quả trả về phải là true khi 2 đối tượng bằng nhau.

Đoạn mã kiểm tra trong main() cũng khá đơn giản.

int main(){
   ThoiGian thoiGian1(0, 10, 28), thoiGian2(4, 2, 13);
   if (thoiGian1 < thoiGian2)
       cout << "Khoang thoi gian dau tien nho hon.";
   else
       cout << "Khoang thoi gian dau tien khong nho hon.";
   return 0;
}

Các bạn hãy tự mình kiểm tra lại kết quả nhận được nhé.

Những phép toán so sánh khác

Vẫn còn 1 số phép toán so sánh khác là >, <=>=. Tôi sẽ không viết tất cả chúng ra đây mà coi như 1 bài tập nhỏ để các bạn luyện tập. Ít nhất thì tôi cũng sẽ đưa cho các bạn những nguyên mẫu của chúng.

bool operator>(ThoiGian const &a, ThoiGian const& b) ;

bool operator<=( ThoiGian const &a, ThoiGian const& b) ;

bool operator>=( ThoiGian const &a, ThoiGian const& b);

Nếu biết cách sử dụng thông minh phép toán > mà chúng ta vừa viết bên trên thì đoạn mã xử lý của các phép toán này sẽ trở nên dễ dàng hơn nhiều. Tôi sẽ để cho các bạn tự suy nghĩ. Đừng quên kỹ thuật lợi dụng mà chúng ta đã sử dụng bên trên khi tạo ra phép toán != cool.

Nếu có bạn nào thấy vẫn còn chút khó khăn để viết những đoạn mã trên, hãy tham khảo mã nguồn mà tôi đính kèm phía cuối bài để học tập nhé.

Các phép toán số học

Chúng ta sẽ chuyển qua các phép toán cổ điển hơn : các phép toán số học cộng, trừ, nhân, chia và modulo.

Một khi chúng ta có thể thao tác thành tạo với 1 trong số chúng, những phép toán còn lại không còn có nhiều khó khăn nữa. Vậy nên, tôi đề nghị chúng ta tập luyên với phép cộng.

Áp dụng kiến thức cũ

Sau khi đã quen thuộc với các phép toán so sánh, các bạn hẳn sẽ không bất ngờ nếu sau đây nguyên mẫu của phép cộng.

ThoiGian operator+(ThoiGian const& a, ThoiGian const& b);

! Chú ý lần nữa, nguyên mẫu này nằm bên ngoài khai báo lớp. Đây KHÔNG PHẢI là 1 phương thức.

Sau khi khai báo xong hàm này, chúng ta có thể dùng phép cộng áp dụng lên đối tượng ThoiGian như bình thường.

int main(){
   ThoiGian ketQua, thoiGian1, thoiGian2;
   ketQua = thoiGian1 + thoiGian2;
   return 0;
}

Hàm mà chúng ta đang viết sẽ cần tạo ra 1 đối tượng ThoiGian thứ 3 mà các thuộc tính là tổng của các thuộc tính của ab và trả về kết quả là đối tượng này.

ThoiGian operator+(ThoiGian const& a, ThoiGian const& b){
   ThoiGian ketQua;
   // Tinh gia tri cac thuoc tinh cua ketQua
   return ketQua;
}

Lại 1 lần nữa chúng ta gặp vấn đề về truy cập đến thuộc tính của lớp. Và cũng 1 lần nữa tôi đề nghị dùng giải pháp cũ để giải quyết vấn đề giống như trong trường hợp của phép toán ==<.

ThoiGian operator+(ThoiGian const& a, ThoiGian const& b){
   ThoiGian ketQua;
   ketQua = a.cong(b);
   return ketQua;
}

Nếu các bạn có thể tự mình viết được tới đây thì chứng tỏ rằng các bạn đã bắt đầu nắm bắt được các nghĩ theo hướng đối tượng cũng như làm chủ các khái niệm về phương thức và tính đóng gói.

Đây là cách làm đúng nhưng vẫn chưa phải cách thức tối ưu.

Cách thức tôi giới thiệu với các bạn tiếp theo đây có hiệu quả hơn và cũng giúp chúng ta đảm bảo ý nghĩa và tính liên kết giữa hệ thống phép toán của chúng ta.

Các phép toán rút gọn

Trước khi giải quyết vấn đề về truy cập thuộc tính, chúng ta hãy dừng lại 1 chút và cùng nhìn lại những phép toán khác nhau trong C++. Nếu đọc lại bài học về các phép tính, các bạn sẽ nhớ ra là chúng ta có từng nhắc về các phép toán được gọi là các phép toán rút gọn. Và bên cạnh phép cộng thông thường, còn có phép toán sử dụng toán tử += cũng dùng để thực hiện phép cộng. Nhắc lại 1 chút, chúng ta có thể viết như sau :

int a(2);
a += 4; // Bay gio a = 6

Khác biệt là phép toán này thay đổi giá trị của toán hạng bên trái trong khi phép cộng thông thường thì không thay đổi toán hạng nào. Giả sử là chúng ta muốn viết lại phép toán này để áp dụng cho lớp ThoiGian, vậy thì phép toán này phải là 1 phương thức của lớp mà không phải là 1 hàm bên ngoài bởi vì theo tính đóng gói, chỉ có các phương thức của lớp mới có quyền thay đổi đối tượng.

Nguyên mẫu của nó sẽ như sau :

class ThoiGian{
  // ...
  void operator+=(ThoiGian const& a);
};

Phương thức này không có kết quả trả về và nhận vào 1 tham số là 1 đối tượng ThoiGian. Nó không phải là phương thức hằng vì nó làm thay đổi đối tượng bản thể.

? Làm sao để sử dụng phương thức này ?

Cách sử dụng giống như các phép toán == hay < mà chúng ta sử dụng lúc trước.

ThoiGian a(2, 12, 43), b(0, 34, 17);
a += b;

Và trình biên dịch sẽ chuyển đổi thành :

ThoiGian a(2, 12, 43), b(0, 34, 17);
a.operator+=(b);

Thật thần kỳ phải không !

Hãy thử viết mã xử lý của phương thức này. Nó không quá phức tạp nhưng đòi hỏi phải suy nghĩ ký trước khi viết. Chỉ là thực hiện các phép cộng đơn giản nhưng cần chú ý khi giá trị của số phút và số giây vượt quá 60.

Lại thêm 1 bài tập giải thuật nhỏ để cho các bạn luyên tập. Lập trình viên giỏi là những người thường xuyên luyện tập.

Sau khi đã viết xong, các bạn có thể thử so sánh với đoạn mã của tôi.

void ThoiGian::operator+=(ThoiGian const& a){
   //1 : Cong so giay
   m_giay += a.m_giay;    
   //Neu so giay vuot qua 60, cong them vao so phut
   m_phut+= m_giay / 60;
   m_giay %= 60;

   //2 : Cong so phut
   m_phut += a.m_phut;
   //Neu so phut vuot qua 60, cong them vao so gio
   m_gio += m_phut / 60;
   m_phut %= 60;

   //3 : Con so gio
   m_gio += a.m_gio;
}

Tôi nhắc lại là thuật toán không quá phức tạp nhưng đòi hỏi suy nghĩ trước khi viết.

Chúng ta có quyền thay đổi trực tiếp giá trị các thuộc tính bởi vì chúng ta đang thao tác bên trong 1 phương thức của lớp. Các thuộc tính được cộng thêm giá trị số giờ, phút, giây của tham số a.

Có 1 số xử lý đặc biệt đã được áp dụng để thực hiện quy đổi giữa giờ, phút, giây nhằm tránh cho giá trị của số phút và giây vượt quá 60. Tôi sẽ để các bạn tự nghiên cứu và nghiền ngẫm để rút ra kết luận cho mình. Tin tưởng là lúc hiểu ra được, các bạn sẽ hoàn toàn đồng ý với tôi là đoạn xử lý này không có gì quá rắc rối cả.

Kiểm tra thử nghiệm

Trước khi tiếp tục, hãy thực hiện vài phép kiểm tra để chắc chắn rằng chúng ta vẫn đi đúng hướng. Để làm việc đó, tôi cần thêm vào phương thức hiển thị cho lớp ThoiGian.

#include <iostream>
#include "ThoiGian.h"

using namespace std;

int main(){
   ThoiGian thoiGian1(0, 10, 28), thoiGian2(0, 15, 2);
   ThoiGian ketQua;

   thoiGian1.hienThi();
   cout << " + " << endl;
   thoiGian2.hienThi();
   thoiGian1 += thoiGian2;
   cout << " = " << endl;
   thoiGian1.hienThi();

   return 0;
}

Đoạn mã hoạt động ngon lành. Hãy thử thay đổi và sử dụng những giá trị lớn hơn cho tham số.

Tôi đã thử với những giá trị lớn để xem thuật toán bên trên có thành công không và kết quả cho ra chứng tỏ rằng chương trình đã hoạt động tốt.

Có 1 chút chi tiết kỹ thuật mà tôi vẫn chưa đề cập đến từ trước đến giờ. Để cho phương thức của chúng ta thêm hoàn hảo, nó cần trả về 1 tham chiếu trên 1 đối tượng ThoiGian thay vì void. Lý do của việc này khá là phức tạp và nằm ngoài mức độ mà tôi muốn truyền đạt trong giáo trình này nên chúng ta sẽ không nhắc đến ở đây. Cuối cùng, phương thức của chúng ta sẽ như sau :

ThoiGian& ThoiGian::operator+=(ThoiGian const& a){
   // Xu ly nhu tren khong co gi thay doi
   return *this;
}

Chúng ta sẽ nói về lệnh return *this; sau. Trong lúc này, các bạn chỉ cần biết là nó đi cùng, kết hợp với kiểu trả về ThoiGian& và là 1 quy tắc cần áp dụng cho các phép toán rút gọn như +=, -=, /=, vv...

Được rồi, bây giờ hãy quay lại với mục đích chính của chúng ta trong phần này, đó là có thể biểu diễn phép cộng 2 đối tượng dưới dạng sau :

ketQua = thoiGian1 + thoiGian2;
Trở lại với phép toán cộng

Quay trở lại với mục đích ban đầu, chúng ta cần tạo ra 1 phương thức để hàm operator+ có thể gọi để thực hiện phép cộng.

ThoiGian operator+(ThoiGian const& a, ThoiGian const& b){
   ThoiGian ketQua;
   ketQua = a.cong(b);
   return ketQua;
}

Chúng ta không thể trực tiếp viết a+=b bởi vì phép toán này thay đổi a, là điều mà chúng ta không muốn. Thêm vào đấy, chúng ta đã khai báo a là tham số hằng nên trình biên dịch sẽ không cho phép chúng ta thay đổi nó. Tuy nhiên chúng ta vẫn có thể tạo ra 1 bản sao của a và thực hiện phép toán rút gọn trên bản sao đó.

ThoiGian operator+(ThoiGian const& a, ThoiGian const& b){
   ThoiGian saoChep(a); //Su dung phuong thuc khoi tao sao chep
   saoChep += b; //Su dung phep toan rut gon tren ban sao
   return saoChep; //Tra ve ket qua. Ca a va b deu khong thay doi
}

Đoạn mã này có đôi chút hơi khó hiểu. Các bạn hãy thoải mái xem lại nhiều lần nếu thấy cần thiết. Quan trọng không phải là phải hiểu rõ từng chi tiết mà là nắm được những quy tắc quan trọng và áp dụng chúng vào trong các dự án thực tiễn.

Mã nguồn

Dành cho những bạn không theo dõi kịp bài học, tôi đã chuẩn bị mã nguồn của phần ví dụ bên trên. Các bạn có thể sẽ thấy dễ hiểu hơn nếu nhìn đoạn mã trong tổng thể cấu trúc chương trình. Trong tệp nén này bao gồm :

  • main.cpp
  • ThoiGian.h
  • ThoiGian.cpp
  • Tệp .cbp cho bạn nào sử dụng Code::Block

Tải mã nguồn

Bonus #1

Lợi ích của phép toán mà chúng ta vừa tạo ra là chúng ta có thể thực hiện với nhiều toán hạng cùng lúc mà không xảy ra vấn đề gì.

Ví dụ như trong main(), tôi muốn thêm 1 đối tượng thứ 3 và thực hiện phép cộng 3 số hạng.

int main(){
   ThoiGian thoiGian1(1, 45, 50), thoiGian2(1, 15, 50), thoiGian3(0, 8, 20);
   ThoiGian ketQua;

   thoiGian1.hienThi();
   cout << "+" << endl;
   thoiGian2.hienThi();
   cout << "+" << endl;
   thoiGian3.hienThi();
   ketQua = thoiGian1 + thoiGian2 + thoiGian3;
   cout << "=" << endl;
   ketQua.hienThi();

   return 0;
}

Trên thực tế, dòng lệnh thực hiện phép tính

ketQua = thoiGian1 + thoiGian2 + thoiGian3;

sẽ được trình biên dịch thay đổi thành

ketQua = operator+(operator+(thoiGian1, thoiGian2), thoiGian3);

! Chú ý là C++ không cho phép thay đổi thứ tự ưu tiên của phép toán.

Bonus #2

Thực tế là phép cộng của chúng ta không bắt buộc 2 số hạng đều phải là đối tượng ThoiGian. Chúng ta hoàn toàn có thể chấp nhận phép cộng giữa 2 đối tượng khác kiểu nếu thấy hợp lý. Ví dụ phép cộng giữa 1 đối tượng ThoiGian và 1 biến kiểu int là hoàn toàn hợp lệ nếu ta coi số số hạng thứ 2 là số giây chúng ta muốn thêm vào giá trị khoảng thời gian.

ThoiGian operator+(ThoiGian const& thoiGian, int giay){
   ThoiGian saoChep(thoiGian); //Su dung phuong thuc khoi tao sao chep
   saoChep += b; //Su dung phep toan rut gon tren ban sao
   return saoChep; //Tra ve ket qua. Ca a va b deu khong thay doi
}

Tất cả phép tính được thực hiện trong xử lý của phương thức operator+= giống như trong phần trước đã trình bày.

ThoiGian& operator+=(int giay);

Lần này tôi thật sự sẽ để các bạn tự viết mã xử lý của phương thức này.

Các phép toán số học khác

Sau khi cùng nhau thao tác với phép cộng, khi làm việc với các phép toán còn lại, bạn sẽ thấy là không có thêm vấn đề gì mới nảy sinh. Chỉ cần biết lợi dụng hợp lý những phép toán mà chúng ta đã tạo ra.

Những phép toán cùng loại với phép cộng là :

  • Phép trừ
  • Phép nhân
  • Phép chia
  • Phép modulo

Để ghi đè các phép toán này, chúng ta vẫn sử dụng các hàm với tên bắt đầu là từ khóa operator và tiếp theo là toán tử của phép toán chúng ta muốn ghi đè.

  • operator-();
  • operator*();
  • operator/();
  • operator%() ;

Chúng ta cũng sẽ cần thêm các phép toán rút gọn tương ứng dưới dạng các phương thức của lớp.

  • operator-=();
  • operator*=();
  • operator/=();
  • operator%=();

Với lớp ThoiGian của chúng ta thì phép trừ có vẻ là sẽ hữu ích. Các bạn hãy dành thời gian để suy nghĩ và tập luyện dựa trên những gì chúng ta đã học về phép cộng.

Những phép toán khác như nhân, chia với modulo thì không mang quá nhiều ý nghĩa khi áp dụng cho lớp ThoiGian. Bởi vì chúng không hữu dụng, chúng ta có thể không cần định nghĩa chúng bởi việc này hoàn toàn không phải bắt buộc.

Phép toán với luồng

Trong số những thứ lạ lẫm mà chúng ta bắt đầu làm quen khi mới học C++, phép toán với các luồng hẳn là 1 trong những thứ gây ấn tượng nhất. Phép toán này sử dụng các toán tử >><<.

cout << "Xin chao!";
cin >> bien;

Bởi vì >><< cũng là các toán tử, vậy nên chúng ta cũng có thể ghi đè phép toán sử dụng chúng bằng cách sử dụng các hàm operator<<operator>>.

Trong bài học, chúng ta sẽ tập trung vào ví dụ toán tử << sử dụng cùng với cout.

Trong thực tế các phép toán với luồng được định nghĩa mặc định cho các kiểu dữ liệu cơ bản như int, double, char và 1 số đối tượng như string. Vấn đề là các phép toán này không được định nghĩa để hoạt động với các lớp mà chúng ta tự tạo ra như ThoiGian. Vậy nên đoạn mã dưới đây sẽ không thể hoạt động.

ThoiGian thoiGian(0, 2, 30);
cout << thoiGian;
//Loi : khong ton tai ham operator<<(cout, ThoiGian &thoiGian)

Để có thể sử dụng được phép toán này, chúng ta sẽ phải tự mình định nghĩa hàm của phép toán.

? Thế nhưng chúng ta không thể sửa mã của thư viện chuẩn cơ mà ?

Không cần làm thế vì thực ra đây chỉ là 1 hàm sử dụng 1 đối tượng của lớp ofstream (cout thật ra là 1 thực thể của lớp này) mà chúng ta cần phải định nghĩa lại.

! Khi chúng ta bao gồm gói <iostream>, 1 đối tượng cout sẽ được tự động khai báo như sau : ofstream cout;. ofstream là lớp còn cout là đối tượng.

Dù là chúng ta không thể thay đổi lớp ofstream nhưng không sao vì chúng ta chỉ muốn viết 1 hàm nhận vào tham số là 1 đối tượng của lớp này. Sau đây là cách thức mà chúng ta sẽ tiến hành thực hiện.

Mã xử lý của operator<<

Bắt đầu với khung của hàm.

ostream& operator<<(ostream &luong, ThoiGian const& thoiGian){
   //Hien thi cac thuoc tinh
   return luong;
}

Giống như chúng ta đã làm khi thao tác với operator+, kết quả trả về của hàm cũng là 1 tham chiếu chứ không phải là đối tượng.

Tham số thứ nhất chính là 1 tham chiếu trên đối tượng của lớp ofstream được truyền tự động cho hàm. Trên thực tế thì đây chính là đối tượng cout nhưng ở đây, chúng ta đặt là luong để tránh xảy ra xung đột về tên đối tượng. Tham số thứ 2 là 1 tham chiếu hằng lên đối tượng của lớp ThoiGian mà chúng ta muốn hiển thị nhờ phép toán <<.

Hàm này cần chọn ra những thuộc tính trong đối tượng và gửi chúng cho đối tượng luong. Sau đó, hàm sẽ trả về kết quả là 1 tham chiếu trên đối tượng. Chính điều này cho phép chúng ta có thể thực hiện phép toán thành chuỗi liên tiếp.

cout << thoiGian1 << thoiGian2;

Tôi nhắc lại lần nữa, hãy coi như đây là 1 quy tắc phải theo. Việc đoán ra kiểu dữ liệu trả về của các phép toán không hề đơn giản nên tốt nhất là các bạn hãy học thuộc lòng nếu có thể.

Tiếp theo, cần tạo ra 1 phương thức của lớp ThoiGian để có thể truy cập đến các thuộc tính và hiển thị chúng ra.

ostream& operator<<(ostream &luong, ThoiGian const& thoiGian){
   thoiGian.hienThi();
   return luong;
}

Thêm nguyên mẫu của phương thức hienThi() vào khai báo của lớp.

void hienThi(std::ostream &luong) const;

Sau đó, chúng ta bổ sung đoạn mã xử lý của phương thức này vào tệp ThoiGian.cpp.

void ThoiGian::hienThi(ostream &luong) const{
   luong << m_gio << "h" << m_phut << "m" << m_giay << "s";
}

Như những lần trước, chúng ta thông qua 1 phương thức bên trong lớp để gián tiếp truy cập đến các thuộc tính. Hàm này nhận vào tham số là tham chiếu trên luồng mà chúng ta muốn gửi thông tin vào. Những xử lý không thể thực hiện bên trong hàm operator<< thì chúng ta chuyển qua cho phương thức bên trong lớp ThoiGian, giống hệt như những gì chúng ta đã làm với phép cộng.

Kiểm nghiệm trong main()

Sau khi trải qua vô số rắc rối để tạo ra được phép toán, chúng ta có thể hưởng thụ thành quả trong những xử lý bên trong hàm main(). Ví dụ dưới đây sẽ hiển thị đối tượng của lớp ThoiGian ra màn hình.

int main(){
   ThoiGian thoiGian1(2, 25, 28), thoiGian2(0, 16, 33);
   cout << thoiGian1 << " va " << thoiGian2 << endl;
   return 0;
}

Công sức phải bỏ ra để tạo ra các hàm thật không nhỏ nhưng cũng rất xứng đáng với thành quả nhận được là cách sử dụng vô cùng dễ dàng.

Chúng ta hãy thử kết hợp nhiều phép toán vào trong cùng 1 dòng lệnh xem sao nhé : thực hiện phép cộng rồi in kết quả ra màn hình.

int main(){
   ThoiGian thoiGian1(2, 25, 28), thoiGian2(0, 16, 33);
   cout << thoiGian1 + thoiGian2 << endl;
   return 0;
}

Cuối cùng thì chúng ta đã có thể dễ dàng sử dụng các phép toán với lớp không khác gì với các biến cổ điển như int hay double. Thật tuyệt vời !

Tóm tắt bài hoc :
  • C++ cho phép chúng ta ghi đè các phép toán, nghĩa là tạo ra các hàm để thay đổi xử lý của các toán tử +, -, *, vv…
  • Để ghi đè 1 phép toán, cần đặt tên hàm gồm từ khóa operator rồi tiếp ngay sau đấy là toán tử của phép toán muốn ghi đè, ví dụ : operator+.
  • Các hàm cần nhận vào các tham số và trả về các giá trị của 1 kiểu xác định tùy thuộc vào phép toán mà chúng ta muốn ghi đè.
  • Không nên lạm dụng việc ghi đè các phép toán nếu không muốn làm mất đi mục đích ban đầu chúng ta hướng đến là giúp cho đoạn mã gọn gàng và dễ đọc hơn.