Tính Đa Hình Trong Lập Trình Hướng Đối Tượng

Tính Đa Hình Trong Lập Trình Hướng Đối Tượng

Trong bài trước, chúng ta hiểu được thế nào là tính kế thừa trong OOP. Ở bài này chúng ta sẽ tìm hiểu thêm một tính chất nữa của lập trình hướng đối tượng đó là tính đa hình nhé.

Tính đa hình là gì ?

Sự kế thừa trong C++ cho phép có sự tương ứng giữa lớp cơ sở và các lớp dẫn xuất trong sơ đồ thừa kế:  

  • Một con trỏ có kiểu lớp cơ sở luôn có thể trỏ đến địa chỉ của một đối tượng của lớp dẫn xuất. 
  • Tuy nhiên, khi thực hiện lời gọi một phương thức của lớp, trình biên dịch sẽ quan tâm đến kiểu của con trỏ chứ không phải đối tượng mà con trỏ đang trỏ tới: phương thức của lớp mà con trỏ có kiểu được gọi chứ không phải phương thức của đối tượng mà con trỏ đang trỏ tới được gọi. 

Ví dụ: Lớp mayAcer kế thừa từ lớp Mayvitinh, cả hai lớp này đều định nghĩa phương thức show()

class Mayvitinh{  
	public: 	 	 	
	void show(){
	    cout << "mayvitinh" << endl;
	}
}; 
class mayAcer: public Mayvitinh{  	
    public: 	 	 	
    void show(){
        cout << "mayAcer" << endl;
    }
}; 

khi đó, nếu ta khai báo một con trỏ lớp mayAcer, nhưng lại trỏ vào địa chỉ của một đối tượng lớp Mayvitinh

mayAcer may1; 
Mayvitinh *tenmay = &may1;
tenmay->show(); 

thì chương trình sẽ gọi đến phương thức show() của lớp Mayvitinh, mà không gọi tới phương thức show() của lớp mayAcer.
Để giải quyết vấn đề này, chúng ta cần sử dụng đến tính đa hình trong Lập trình hướng đối tượng. Mình sẽ giải thích chi tiết hơn ở bên dưới.

Đa hình (polymorphism) nghĩa là có nhiều hình thái khác nhau. Tiêu biểu là, đa hình xuất hiện khi có một cấu trúc cấp bậc của các lớp và chúng liên quan với nhau bởi tính kế thừa.

Mình lấy một ví dụ thực thế nhé:
Một người cùng một lúc có thể có đặc điểm khác nhau. Giống như một người đàn học sinh thời là một người con trai, một người bạn, một người anh. Vì vậy, cùng một người sở hữu những hành vi khác nhau trong các tình huống khác nhau. Điều này được gọi là đa hình.

Đa hình được coi là một trong những tính năng quan trọng của Lập trình hướng đối tượng.

Các loại đa hình:

Tính đa hình chủ yếu được chia thành hai loại:

  • Compile time Polymorphism.
  • Runtime Polymorphism.

1. Compile time Polymorphism:

Tính đa hình này được sử dụng bằng cách nạp chồng hàm hoặc nạp chồng toán tử.

Vậy nạp chồng hàm và nạp chồng toán tử là gì?

Nạp chồng hàm

Nạp chồng hàm (Function Overloading) cho phép sử dụng cùng một tên gọi cho các hàm “giống nhau” (có cùng mục đích). Nhưng khác nhau về kiểu dữ liệu tham số hoặc số lượng tham số. 

Nạp chồng hàm cho phép ta khai báo và định nghĩa các hàm trên cùng với một tên gọi.

Chúng ta lấy ví dụ:

#include <iostream>
using namespace std;
 
class inDuLieu 
{
   public:
      void hamIn(int i) {
        cout << "In so nguyen: " << i << endl;
      }

      void hamIn(double  f) {
        cout << "In so thuc: " << f << endl;
      }

      void hamIn(string s) {
        cout << "In chuoi: " << s << endl;
      }
};

int main(void)
{
   inDuLieu idl;
 
   // Goi ham hamIn de in so nguyen
   idl.hamIn(1235);
   // Goi ham hamIn de in so thuc
   idl.hamIn(67.02);
   // Goi ham hamIn de in chuoi
   idl.hamIn("Codelearn.io");
 
   return 0;
}

Biên dịch chương trình ta có kết quả:

Trong ví dụ trên, ta chỉ dùng một hàm duy nhất có tên là hamIn() nhưng có thể dùng được cho 3 tình huống khác nhau. Đây là một thể hiện của tính đa hình.

Nạp chồng toán tử

Nạp chồng toán tử (Operator Overloading) đượ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 phương thức tính toán như Cong, Tru, Nhan, Chia.
Nếu gặp một biểu thức phức tạp, số lượng phép tính nhiều thì việc sử dụng các phương thức trên khá khó khăn và có thể gây rối cho người lập trình. Vì thế ta sẽ nạp chồng lại các toán tử để có thể tạo một cái nhìn trực quan vào code, giảm thiểu các lỗi sai không đáng có.

Các toán tử có thể nạp chồng

Các toán tử không thể nạp chồng:

. .* :: ?:

Ví dụ:

#include <iostream>
using namespace std;

class Box
{
   public:

      double tinhTheTich(void)
      {
         return chieudai * chieurong * chieucao;
      }
      void setChieuDai( double dai )
      {
          chieudai = dai;
      }

      void setChieuRong( double rong )
      {
          chieurong = rong;
      }

      void setChieuCao( double cao )
      {
          chieucao = cao;
      }
      // Nap chong toa tu + de cong hai doi tuong Box.
      Box operator+(const Box& b)
      {
         Box box;
         box.chieudai = this->chieudai + b.chieudai;
         box.chieurong = this->chieurong + b.chieurong;
         box.chieucao = this->chieucao + b.chieucao;
         return box;
      }
   private:
      double chieudai;      // chieu dai cua mot box
      double chieurong;     // Chieu rong cua mot box
      double chieucao;      // Chieu cao cua mot box
};
// ham main cua chuong trinh
int main( )
{
   Box Box1;                // Khai bao Box1 la cua kieu Box
   Box Box2;                // Khai bao Box2 la cua kieu Box
   Box Box3;                // Khai bao Box3 la cua kieu Box
   double thetich = 0.0;     // Luu giu the tich cua mot box tai day
 
   // thong tin chi tiet cua box 1
   Box1.setChieuDai(5); 
   Box1.setChieuRong(2); 
   Box1.setChieuCao(4);
 
   // thong tin chi tiet cua box 2
   Box2.setChieuDai(7); 
   Box2.setChieuRong(6); 
   Box2.setChieuCao(9);
 
   // the tich cua box 1
   thetich = Box1.tinhTheTich();
   cout << "The tich cua Box1 la: " << thetich <<endl;
 
   // the tich cua box 2
   thetich = Box2.tinhTheTich();
   cout << "The tich cua Box2 la: " << thetich <<endl;

   // cong hai doi tuong nhu sau:
   Box3 = Box1 + Box2;

   // the tich cua box 3
   thetich = Box3.tinhTheTich();
   cout << "The tich cua Box3 la: " << thetich <<endl;

   return 0;
}

Sau khi chạy chương trình cho kết quả:

Trong ví dụ trên, ta đã nạp chồng lại toán tử cộng. Tính đa hình được thể hiện qua việc nạp chồng để tính tổng Box1Box2.

2. Runtime Polymorphism:

Các bàn còn nhớ ví dụ đầu tiên của bài không.

#include <iostream>
using namespace std;

class Mayvitinh{  
	public: 	 	 	
	void show(){
	    cout << "mayvitinh" << endl;
	}
}; 
class mayAcer: public Mayvitinh{  	
    public: 	 	 	
    void show(){
        cout << "mayAcer" << endl;
    }
}; 

int main(){
    mayAcer may1; 
    Mayvitinh *tenmay = &may1;
    tenmay->show(); 
}

Có thể thấy chương trình sau khi chạy sẽ gọi đến phương thức show() của lớp Mayvitinh, mà không gọi tới phương thức show() của lớp mayAcer.

Vậy để chương trình gọi tới phương thức show() của lớp mayAcer ta sử dụng hàm ảo virtual như sau:

#include <iostream>
using namespace std;

class Mayvitinh{  
	public: 	 	 	
	virtual void show(){
	    cout << "mayvitinh" << endl;
	}
}; 
class mayAcer: public Mayvitinh{  	
    public: 	 	 	
    void show(){
        cout << "mayAcer" << endl;
    }
}; 

int main(){
    mayAcer may1; 
    Mayvitinh *tenmay = &may1;
    tenmay->show(); 
}

Sau khi biên dịch chương trình sẽ có kết quả:

Trong ví dụ trên mình đã thêm từ khóa virtual vào hàm show() trong lớp cơ sở Mayvitinh.
Từ khóa virtual này dùng để khai báo một hàm là hàm ảo.

Khi khai báo hàm ảo với từ khóa virtual nghĩa là hàm này sẽ được gọi theo loại đối tượng được trỏ (hoặc tham chiếu), chứ không phải theo loại của con trỏ (hoặc tham chiếu). Và điều này dẫn đến kết quả khác nhau:

  • Nếu không khai báo hàm ảo virtual trình biên dịch sẽ gọi hàm tại lớp cở sở
  • Nếu dùng hàm ảo virtual trình biên dịch sẽ gọi hàm tại lớp dẫn xuất

Khi nhận thấy có khai báo virtual trong lớp cơ sở, trình biên dịch sẽ thêm vào mỗi đối tượng của lớp cơ sở và các lớp dẫn xuất của nó một con trỏ chỉ đến bảng phương thức ảo (virtual function table). Con trỏ đó có tên là vptr (virtual pointer). Bảng phương thức ảo là nơi chứa các con trỏ chỉ đến đoạn chương trình đã biên dịch ứng với các phương thức ảo. Mỗi lớp có một bảng phương thức ảo. Trình biên dịch chỉ lập bảng phương thức ảo khi bắt đầu có việc tạo đối tượng của lớp. Đến khi chương trình chạy, phương thức ảo của đối tượng mới được nối kết và thi hành thông qua con trỏ vptr.

Kết

Vậy là series Lập trình hướng đối tượng của mình đến đây là kết thúc, chúng ta đã được tìm hiểu các khái niệm xoay quanh OOP trong C++ và những tính chất quan trọng của nó. Mình hy vọng bài viết của mình sẽ một phần giúp các ban trong việc học tập lập trình, đặc biệt là tìm hiểu về OOP.

Nếu các bạn có thắc mắc hay câu hỏi gì có thể comment và rate bên dưới bài viết. Mình hy vọng sẽ nhận được sự ủng hộ của các bạn trong những bài khác với chủ đề khác.