< Lập trình tân binh | 2.7. Tính kế thừa

2.7. Tính kế thừa

Lần này, tôi xin phép được giới thiệu thêm 1 khái niệm nền tảng của lập trình hướng đối tượng, cùng với tính đóng gói mà các bạn đã tìm hiểu trong bài trước, đấy là tính kế thừa (inheritance). Đây có thể coi là 1 bộ phận không thể thiếu tạo nên sức mạnh của OOP.

Trong bài học này, tôi sẽ quay lại với ví dụ của lớp NhanVat, nhưng được giản lược đi nhiều để chúng ta có thể tập trung vào trọng điểm.

Thế nhưng, các bạn cũng đừng quá lo lắng vì khái niệm này không hề quá khó hiểu dù lợi ích mà nó mang lại cho chúng ta vô cùng to lớn.

Ví dụ về tính kế thừa

Nhiều người sẽ thấy lạ khi nghe về sự kế thừa ở trong lập trình tin học nhưng thật sự thì khái niệm này không quá rắc rối. Đấy vốn là kỹ thuật cho phép các lập trình viên tạo ra 1 lớp dựa trên 1 lớp khác có sẵn. Lớp ban đầu sẽ trở thành nền tảng của lớp mới mà chúng ta muốn tạo ra. Kỹ thuật này sẽ cho phép chúng ta không cần viết lại nhiều lần đoạn mã xử lý thực hiện cùng 1 công việc.

Làm sao nhận biết sự kế thừa ?

Câu hỏi hay được đặt ra là « Trong OOP thì làm sao để biết lúc nào thì cần sử dụng tính kế thừa ? ». Nhiều người từng đau khổ vì khái niệm này thì nhìn thấy nó ở khắp mọi nơi. Một số người khác, đa phần là những tân binh mới học lập trình, thì mỗi lần luôn phải tự đặt lại câu hỏi. Thế nhưng tôi sẽ cho các bạn 1 bí quyết để có thể nhận biết lúc nào cần đến sự kế thừa trong lập trình hướng đối tượng. Nó rất đơn giản và cực kỳ chính xác.

Các bạn biết là cần sử dụng đến sự kế thừa nếu chúng ta có thể khẳng định : A là B.

Tôi nghĩ là 1 ví dụ cụ thể hơn sẽ rất có ích. Trò chơi của chúng ta có rất nhiều kiểu nhân vật : chiến binh, phù thủy, thích khách, vv… Rất đa dạng với các đặc điểm khác nhau, thế nhưng chúng đều là các nhân vật. Tôi có thể nói « chiến binh là 1 nhân vật » hay « phù thủy là 1 nhân vật ». Vậy nên nếu chúng ta tạo ra 2 lớp ChienBinhPhuThuy thì cả 2 lớp này sẽ đều kế thừa lớp NhanVat mà chúng ta đã tạo ra trước đó.

Sau đây là 1 số ví dụ khác hay được sử dụng để mô tả tính kế thừa :

  • Xe máy là 1 loại phương tiện nên lớp XeMay kế thừa lớp PhuongTien.
  • Xe đạp là 1 loại phương tiện nên lớp XeDap kế thừa lớp PhuongTien.
  • Khủng long bạo chúa là 1 loài khủng long nên lớp KhungLongBaoChua kế thừa lớp KhungLong.
  • Khủng long là 1 loại động vật nên lớp KhungLong kế thừa lớp DongVat.

Ngược lại, chúng ta sẽ không thể nói « xe máy là 1 loại động vật » thế nên sẽ không có sự kế thừa nào ở đây cả.

! Hãy rất chú ý tôn trong quy tắc trên nếu không các bạn có nguy cơ bắt gặp những vấn đề lớn về logic trong đoạn mã nguồn.

Trong phần tiếp theo, chúng ta sẽ xem cách thực hiện sự kế thừa trong C++ qua lớp NhanVat.

Lớp NhanVat

Nhắc lại 1 chút thì đây là lớp mô tả 1 nhân vật trong siêu cấp trò chơi RPG của chúng ta. Các bạn không cần biết chơi để có thể theo dõi tiếp ví dụ. Cá nhân tôi thấy nó đỡ khô khan hơn nhiều so với những ví dụ vẫn hay được sử dụng bởi các thấy dạy lập trình ở trường.

Tôi sẽ đơn giản hóa 1 chút lớp NhanVat mà chúng ta đã tạo ra ở bài trước.

#ifndef DEF_NHANVAT
#define DEF_NHANVAT
#include <iostream>
#include <string>

class NhanVat{
   public:
       NhanVat();
       void nhanSatThuong(int dmg);
       void tanCong(NhanVat &mucTieu) const;

   private:
       int m_hp;
       std::string m_ten;
};
#endif

Nhân vật của chúng ta sẽ có 1 cái tên và 1 số điểm hp. Chúng ta chỉ sử dụng 1 phương thức khởi tạo mặc định cho phép người chơi đặt tên cho nhân vật và bắt đầu với 100 điểm hp.

Nhân vật có thể chịu sát thương thông qua phương thức nhanSatThuong và tạo ra sát thương cho nhân vật khác qua phương thức tanCong.

Dưới đây là ví dụ 1 đoạn mã xử lý nằm trong tệp NhanVat.cpp.

#include "NhanVat.h"
using namespace std;

NhanVat::NhanVat() : m_hp(100), m_ten("BuBu"){
}

void NhanVat::nhanSatThuong(int dmg){
   m_hp -= dmg;
}

void NhanVat::tanCong(NhanVat &mucTieu) const{
   mucTieu.nhanSatThuong(10);
}

Những kiến thức trên đây chúng ta đều đã từng thảo luận qua.

Lớp ChienBinh kế thừa lớp NhanVat

Sau đây sẽ là tiết mục chính : chúng ta muốn tạo ra 1 lớp con của lớp NhanVat. Nói cách khác, lớp mới này sẽ kế thừa lớp NhanVat.

Tôi sẽ tạo ra 1 lớp ChienBinh để mô tả các nhân vật chiến binh. Định nghĩa lớp này trong tệp ChienBinh.h sẽ như sau :

#ifndef DEF_CHIENBINH
#define DEF_CHIENBINH
#include <iostream>
#include <string>
#include "NhanVat.h"
//Dung quen them NhanVat.h neu ban muon ke thua lop NhanVat

class ChienBinh : public NhanVat{
//Y nghia cua dong tren la lop ChienBinh ke thua lop NhanVat
};

#endif

Dựa theo đoạn mã bên trên thì cơ lớp ChienBinh sẽ sở hữu tất cả các thuộc tính cũng như phương thức của lớp NhanVat.

Trong trường hợp đấy thì chúng ta sẽ gọi lớp NhanVat là « lớp mẹ » còn lớp ChienBinh là « lớp con ».

? Tạo ra thêm 1 lớp chứa cùng các thuộc tính và phương thức thì có lợi ích gì ?

Lợi ích là chúng ta có thể thêm các thuộc tính và phương thức đặc biệt chỉ có ở các nhân vật chiến binh vào lớp ChienBinh. Ví dụ như tôi sẽ thêm vào đây phương thức tanCongLienTiepBangRiu.

#ifndef DEF_CHIENBINH
#define DEF_CHIENBINH
#include <iostream>
#include <string>
#include "NhanVat.h"
//Dung quen them NhanVat.h neu ban muon ke thua lop NhanVat

class ChienBinh : public NhanVat{
  public:
    void tanCongLienTiepBangRiu() const;
};

#endif

Chúng ta sẽ có sơ đồ sau :

Ý nghĩa của sơ đồ là : lớp ChienBinh kế thừa lớp NhanVat nên sở hữu tất cả đặc tính của lớp này (có thuộc tính tên, số điểm hp và có phương thức nhận sát thương). Chúng ta nói rằng lớp ChienBinh được chuyên biệt hóa từ lớp NhanVat. Ngoài ra nó sở hữu thêm 1 phương thức của riêng mình là tanCongLienTiepBangRiu().

! Cần chú ý là trong phép kế thừa thì lớp con sẽ kế thừa tất cả, nghĩa là cả phương thức lẫn thuộc tính của lớp mẹ.

Các bạn đã bắt đầu hình dung ra rồi chứ ? Trong C++, nếu 2 đối tượng có thể được diễn tả theo quan hệ « đối tượng này là đối tượng kia » thì chúng ta sẽ sử dụng đến tính kế thừa. Đối tượng ChienBinh có thể được coi là 1 nâng cấp của đối tượng NhanVat với những tính năng mới thêm vào.

Khái niệm này tưởng chúng như đơn giản nhưng sẽ trở nên vô cùng mạnh mẽ khi các bạn đã thấu hiểu nó. Chúng ta sẽ có nhiều cơ hội để thực hành với nó trong phần tiếp theo của giáo trình.

Lớp PhuThuy cũng kế thừa lớp NhanVat

1 sự kế thừa thì không mang lại nhiều khác biệt nhưng khi mà nhiều sự kế thừa được sử dụng kết hợp với nhau thì sức mạnh của khái niệm này dần dần thể hiện ra.

Tiếp theo chúng ta sẽ tạo ra lớp PhuThuy cũng kế thừa lớp NhanVat, bởi vì 1 phù thủy cũng là 1 nhân vật. Vậy nên 1 phù thủy cũng sẽ có tên và số điểm hp thuộc về mình cũng như có thể sử dụng phép tấn công đơn giản.

Khác biệt là 1 phù thủy còn phải thực hiện được phép thuật như tạo phun ra băng hoặc lửa, vv… Để sử dụng những phép này thì phù thủy cần tiêu tốn điểm mana (1 thuộc tính mới mà chúng ta cần thêm vào). Khi nào điểm mana trở về 0 thì phù thủy sẽ không thể làm phép nữa.

#ifndef DEF_PHUTHUY
#define DEF_PHUTHUY
#include <iostream>
#include <string>
#include "NhanVat.h"

class PhuThuy : public NhanVat{
   public:
       void phunLua();
       void phunBang();

   private:
       int m_mp;
};

#endif

Điều quan trọng là các bạn cần hiểu rõ nguyên lý hoạt động của sự kế thừa.

Trong sơ đồ trên thì tôi không đề cập tới các thuộc tính nhưng chúng cũng vẫn được kế thừa không khác gì các phương thức.

Và thậm chí còn thần kỳ hơn là chúng ta có thể tạo ra 1 lớp con kế thừa 1 lớp mẹ mà bản thân lớp mẹ này cũng là lớp con của một lớp khác! Hãy tưởng tượng là chúng ta sẽ có 2 loại phù thủy : phù thủy đen chuyên tấn công và phù thủy trắng chuyên chữa thương. Mọi thứ sẽ trở nên thật hấp dẫn.

Chúng ta có thể cứ tiếp tục mãi như thế…

Các bạn sẽ thấy là trong thư viện Qt mà chúng ta tìm hiểu ở chương sau có thể xuất hiện đến 5, 6 sự kế thừa chồng lên nhau là bình thường. Đừng vội, rồi các bạn sẽ thấy nhiều hơn!

Dẫn xuất kiểu dữ liệu

Chúng ta hãy xem xét đoạn mã sau đây:

NhanVat nhanVat;
ChienBinh chienBinh;

nhanVat.tanCong(chienBinh);
chienBinh. tanCong(nhanVat);

Đoạn mã trên hoạt động tốt. Thế nhưng nếu các bạn chịu chú ý kỹ, nhiều bạn sẽ thắc mắc là làm thế nào mà đoạn mã xử lý trên có thể chạy được bởi vì theo những gì chúng ta đã thảo luận qua, đoạn mã trên sẽ phải báo lỗi.

Nếu vẫn còn có bạn chưa theo kịp thì vấn đề của chúng ta như thế này : hãy xem nguyên mẫu của phương thức tanCong.

void tanCong(NhanVat &mucTieu) const;

Phương thức này là giống nhau dù là trong lớp NhanVat hay lớp ChienBinh bởi vì ChienBinh đã kế thừa từ NhanVat.

Khi chúng ta thực hiện chienBinh.tanCong(nhanVat);, tham số của chúng ta là 1 đối tượng NhanVat đúng như nguyên mẫu.

Thế nhưng trong câu lệnh nhanVat.tanCong(chienBinh);, chúng ta lại truyền 1 tham số ChienBinh cho phương thức này. Vậy làm thế quái nào mà trình biên dịch không trả về cho chúng ta 1 thông báo lỗi như thường lệ? Nó thậm chí vẫn biên dịch ra được 1 đoạn mã máy hoạt động 1 cách bình thường !

Thật ra thì bí mật của điều thần kỳ này đến từ 1 đặc điểm của tính kế thừa trong C++ : nó cho phép chúng ta 1 đối tượng của lớp con thay thế đối tượng của lớp mẹ trong bất cứ xử lý nào có liên quan đến con trỏ hoặc tham chiếu trên lớp mẹ.

Điều này nghĩa là chúng ta hoàn toàn có quyền viết 1 đoạn mã như sau :

NhanVat *nhanVat(0);
ChienBinh *chienBinh = new ChienBinh();
nhanVat = chienBinh; // Qua than ky roi !!!

2 dòng lệnh đầu tiên hoàn toàn không có gì đặc biệt. Chúng ta chỉ đơn thuần là tạo ra 1 con trỏ lên kiểu NhanVat rồi gán cho nó giá trị là 0 và khởi tạo 1 con trỏ khác lên đối tượng kiểu ChienBinh.

Thế nhưng dòng lệnh cuối thì khá kỳ lạ vì chúng ta đã gán lẫn lộn giữa con trỏ lên 2 kiểu dữ liệu khác nhau.

Nếu trong những trường hợp khác thì trình biên dịch sẽ vô cùng không hài lòng và không cho phép chúng ta thực hiện điều này. Thế nhưng ChienBinh vốn là lớp con của NhanVat. Vậy nên mọi thứ trở nên hợp lý bởi vì nói cho cùng thì ChienBinh cũng là 1 NhanVat. Vì thế quy tắc cần nhớ cho các bạn đó là chúng ta có thể gán giá trị 1 phần tử của lớp con cho 1 phần tử của lớp mẹ.

! Tuy nhiên cần chú ý là phép toán theo chiều ngược lại là sai. Chúng ta không thể viết chienBinh = nhanVat; vì trình biên dịch cấm làm thế và sẽ làm treo chương trình. Hãy chú ý đến chiều của phép gán.

Vậy là không có gì ngăn cản chúng ta sử dụng 1 đối tượng vào 1 ví trí đáng ra thuộc về đối tượng khái quát hơn. Việc này giúp ích rất nhiều khi chúng ta thao tác với các tham số của hàm. Ví dụ :

void tanCong(NhanVat &mucTieu) const;

Phương thức tanCong này cho phép chúng ta tấn cống bất cứ kiểu nhân vật nào dù là chiến binh, phù thùy, phù thủy trắng hay phù thủy đen bởi vì suy cho cùng thì chúng đều là 1 nhân vật.

Có thể một số bạn sẽ thấy hơi lạ lẫm lúc ban đầu nhưng rồi sẽ nhận ra là mọi chuyện hoàn toàn hợp lý. Trong thực tế, phương thức tanCong của chúng ta sẽ đơn giản là gọi phương thức nhanSatThuong vẫn tồn tại trong tất cả các lớp.

Nếu các bạn vẫn thấy khó hiểu thì xin hãy đọc lại phần bên trên thêm 1 lần nữa.

? Tôi vẫn không thể hiểu được tại sao phép gán doiTuongMe = doiTuongCon; có thể thực hiện được. Theo tôi thấy thì đối tượng con sở hữu nhiều thuộc tính mà đối tượng mẹ không có. Vậy thì phép gán phải có vấn đề mới đúng chứ ? Theo chiều ngược lại không hợp lý hơn à ?

Thực ra, sai lầm của bạn là ở chỗ cho rằng chúng ta đã thực hiện phép gán giá trị các đối tượng trong khi không phải thế. Chúng ta chỉ đơn giản là thay thế 1 con trỏ. 2 xử lý trên hoàn toàn khác hẳn nhau. Các đối tượng vẫn tồn tại trong bộ nhớ mà không hề chịu bất cứ thay đổi nào. Việc chúng ta làm chỉ là hướng con trỏ về phía đối tượng con. Đối tượng con này sẽ bao gồm 2 thành phần : phần được thừa kế từ lớp mẹ và phần thuộc về riêng nó. Xử lý lệnh doiTuongMe = doiTuongCon; chỉ đơn giản hướng con trỏ doiTuongMe vào phần chứa những thuộc tính và phương thức kế thừa của doiTuongCon mà thôi.

Tôi khó có thể giải thích rõ ràng hơn nữa, hy vọng là các bạn vẫn có thể hiểu được. Nếu không thì các bạn hãy nhớ lấy quy tắc mà tôi đã nhắc đến ở trên. Ít nhất thì nó sẽ giúp các bạn sống sót với C++ trong 1 thời gian dài dù không hiểu rõ cơ chế của xử lý dạng này.

Dù sao thì các bạn nên biết là kỹ thuật này rất hữu dụng trong C++ và được sử dụng khá thường xuyên. Chúng ta sẽ thực hành nhiều hơn về nó trong phần sau khi các bạn học cách sử dụng Qt.

Sự kế thừa và phương thức khởi tạo

Từ đầu bài học đến giờ, tôi vẫn chưa hề nhắc đến phương thức khởi tạo. Vậy nên phần tiếp theo đây sẽ dành để nói về quan hệ giữa phương thức này và sự kế thừa.

Chúng ta đều biết lớp NhanVat có 1 phương thức khởi tạo mặc định.

NhanVat();

… và mã xử lý của nó :

NhanVat::NhanVat() : m_hp(100), m_ten("BuBu"){
}

Khi mà chúng ta muốn tạo ra 1 đối tượng NhanVat thì phương thức này là phương thức sẽ được gọi trước hết.

Vậy chuyện gì sẽ xảy ra khi bây giờ chúng ta muốn tạo ra 1 đối tượng PhuThuy kế thừa lớp NhanVat ?

Lớp PhuThuy bản thân nó cũng có phương thức khởi tạo của riêng mình. Liệu nó sẽ ảnh hưởng gì tới phương thức khởi tạo của NhanVat không ? Ngoài ra, nó cũng cần gọi phương thức khởi tạo của NhanVat nếu không thì sẽ không thể gán giá trị ban đầu cho thuộc tính điểm hp và tên được.

Thực ra, các xử lý sẽ được thực hiện theo thứ tự sau :

  1. Chúng ta yêu cầu tạo ra đối tượng PhuThuy.
  2. Trình biên dịch trước tiên goi ra phương thức khởi tạo của lớp mẹ (NhanVat).
  3. Tiếp đó, trình biên dịch gọi phương thức khởi tạo của lớp con.

Tức là phương thức khởi tạo của lớp mẹ sẽ luôn được gọi trước phương thức của lớp con và rồi tiếp sau lớp con sẽ là lớp cháu nếu có sự xuất hiện của sự kế thừa chồng chất.

Sử dụng phương thức khởi tạo của lớp mẹ

Để có thể trước hết gọi sử dụng phương thức khởi tạo của lớp mẹ, các bạn cần gọi nó từ trong phương thức tạo của lớp PhuThuy. Trong những trường hợp thế này, các bạn sẽ thấy hiệu quả thiết thực của kỹ thuật khởi tạo bằng danh sách giá trị.

PhuThuy::PhuThuy() : PhuThuy(), m_mp(100){
}

Theo như trong danh sách, chúng ta cần trước tiên gọi phương thức khởi tạo của lớp NhanVat rồi mới tới khởi tạo giá trị cho các thuộc tính riêng của lớp PhuThuy (ở đây là số điểm mp).

! Khi chúng ta tạo ra đối tượng PhuThuy thì trình biên dịch sử dụng phương thức khởi tạo mặc định không yêu cầu tham số của lớp mẹ.

Truyền tham số

Một trong những lợi ích lớn nhất của sự kế thừa là cho phép tham số được truyền đi giữa các phương thức tạo của NhanVatPhuThuy. Ví dụ nếu phương thức khởi tạo của NhanVat cho phép nhận vào 1 tham số là tên của nhan vật thì phương thức khởi tạo của lớp PhuThuy cũng phải cho phép nhận vào tham số này để truyền cho phương thức thức của lớp NhanVat.

PhuThuy::PhuThuy(string ten) : NhanVat(ten), m_mana(100){
}

NhanVat::NhanVat(string ten) : m_vie(100), m_ten(ten){
}

Và đây chính là cách mà chúng ta đảm bảo những đối tượng được tạo ra chính xác như mong muốn.

Dưới đây là 1 sơ đồ đơn giản để giúp các bạn hiểu rõ hơn tiến trình của những xử lý chúng ta vừa nhắc đến.

Chúng ta muốn tạo ra 1 nhân vật phù thủy merlin. Nhưng đây là 1 đối tượng, vậy nên trình biên dịch sẽ gọi phương thức khởi tạo của lớp PhuThuy. Phương thức khởi tạo của PhuThuy lại yêu cầu trình biên dịch gọi phương thức khởi tạo của lớp mẹ là lớp NhanVat nên trình biên dịch sẽ bắt đầu từ thực hiện mã xử lý của phương thức này. Sau khi hoàn thành thì nó quay lại và thực hiện mã của phương thức khởi tạo lớp PhuThuy.

Cuối cùng, sau khi mọi xử lý đều được hoàn thành và đối tượng merlin được tạo ra thì chúng ta có thể bắt đầu sử dụng nhân vật này cho mục đích của chúng ta.

Quyền truy cập protected

Chúng ta sẽ không thể nhắc đến sự kế thừa mà bỏ qua không nói tới khái niệm về quyền truy cập protected.

Trước đó chúng ta đã tìm hiểu về 2 loại quyền truy cập :

  • public : dành cho những thành phần có thể được truy cập từ bên ngoài lớp.
  • private : dành cho những thành phần không thể được truy cập từ bên ngoài lớp.

Những quyền truy cập trên cho phép chúng ta tuân thủ quy tắc vàng về tính đóng gói của C++. Quyền truy cập protected là 1 quyền truy cập khác mà tôi xếp vào khoảng giữa của publicprivate. Nó chỉ mang 1 ý nghĩa cụ thể nếu ở trong lớp chủ thể của sự kế thừa (lớp mẹ) nhưng các bạn có thể sử dụng nó trong tất cả các lớp nếu muốn kể cả là trong trường hợp không có sự kế thừa nào.

Ý nghĩa của quyền truy cập này là những thành phần của lớp được bảo vệ bởi nó sẽ không thể được truy cập từ bên ngoài trừ khi là từ 1 lớp con của lớp sử dụng protected.

Nói cách khác, nếu các thành phần của lớp NhanVat được bảo vệ bởi quyền protected thì chúng có thể được truy cập từ các lớp con của lớp NhanVatChienBinhPhuThuy. Chúng ta sẽ không thể truy cập như thể nếu quyền truy cập được sử dụng là quyền private.

! Trong thực tiễn thì tôi luôn bảo vệ các thuộc tính của những lớp mà tôi tạo ra với quyền truy cập protected. Kết quả là tôi vẫn đảm bảo được tính đóng gói trừ khi là trong trường hợp có sử dụng tính kế thừa lên những lớp đó.

Việc này là cần thiết để giảm bớt đáng kể những phương thức lấy và phương thức đặt cần phải tạo ra và giúp đoạn mã của chúng ta bớt nặng nề đi nhiều.

class NhanVat{
   public:
       NhanVat();
       NhanVat(std::string ten);
       void nhanSatThuong(int dmg);
       void tanCong(NhanVat &mucTieu) const;

   protected: //Chi co the duoc truy cap tu cac lop con (ChienBinh, PhuThuy)
     int m_hp;
     std::string m_ten;
};

Chúng ta có thể thoải mái thao tác với các thuộc tính của NhanVat như số điểm hp và tên từ trong các lớp con như ChienBinh hay PhuThuy.

Ghi đè phương thức

Chúng ta sẽ kết thúc bài học này với khái niệm ghi đè (override) các phương thức giữa lớp con và lớp mẹ.

Phương thức của lớp mẹ

Trò chơi của chúng ta đôi khi đòi hỏi nhân vật tự giới thiệu về bản thân mình. Bởi vì đây là 1 hành động chung của tất cả các kiểu nhân vật, bất kể là chiến binh hay phù thùy, nên vị trí thích hợp nhất cho phương thức tuGioiThieu() là trong khai báo của lớp NhanVat.

class NhanVat{
   public:
       NhanVat();
       NhanVat(std::string ten);
       void nhanSatThuong(int dmg);
       void tanCong(NhanVat &mucTieu) const;
       void tuGioiThieu() const;

   protected:
     int m_hp;
     std::string m_ten;
};

! Đến giờ chắc mọi người đều đã quá quen thuộc với từ khóa const. Ở đây nó biểu thị là phương thức sẽ không được phép thay đối đối tượng chủ thể.

Mã xử lý của phương thức này trong tệp .cpp :

void NhanVat::tuGioiThieu() const{
   cout << "Xin chao, toi ten la " << m_ten<< "." << endl;
   cout << "Toi con " << m_hp << " diem hp." << endl;
}

Và gọi sử dụng phương thức này trong hàm main().

int main(){
   NhanVat bubu("BuBu");
   bubu.tuGioiThieu();

   return 0;
}

Kết quả chúng ta sẽ nhận được là :

Phương thức kế thừa trong lớp con

Chiến binh cũng là 1 nhân vật nên cũng có thể tự giới thiệu về bản thân mình.

int main(){
   ChienBinh gauChienBinh("Gau Chien Binh");
   gauChienBinh.tuGioiThieu();
   return 0;
}

Nhân vật Gấu Chiến binh giới thiệu về mình.

Ghi đè

Bây giờ chúng ta muốn rằng các chiến binh sẽ tự giới thiệu khác đi một chút : phải thêm câu giới thiệu thông báo mình là chiến binh. Vậy nên chúng ta cần viết thêm 1 phiên bản khác của phương thức này cho lớp ChienBinh.

void ChienBinh::tuGioiThieu() const{
   cout << " Xin chao, toi ten la " << m_ten<< "." << endl;
   cout << " Toi con " << m_hp << " diem hp." << endl;
   cout << "Toi la 1 chien binh dung manh." << endl;
}

? Vậy là có 2 phương thức với cùng tên và cùng tham số trong 1 lớp à ? Chúng ta đâu được phép làm thế ?

Các bạn có phần đúng, cũng có phần sai. 2 hàm vốn không thể có nguyên mẫu giống nhau (giống tên và giống tham số), thế nhưng lại có ngoại lệ trong trường hợp các lớp kế thừa. Phương thức của lớp ChienBinh sẽ thay thế phương thức được kế thừa từ lớp NhanVat.

Nếu chúng ta sử dụng phương thức này trong main() thì sẽ nhận được kết quả đúng như chúng ta mong đợi.

Khi chúng ta tạo ra 1 phương thức có giống với phương thức kế thừa từ lớp mẹ thì đó là sự ghi đè. Phương thức của lớp mẹ sẽ bị ghi đè và bị che khuất đi.

! Để che khuất 1 phương thức thì chỉ cần sử dụng cùng tên với phương thức được kế thừa là đủ, không quan trọng số và kiểu tham số truyền cho phương thức.

Tính năng này khá là thực dụng ! Khi thực hiện kế thừa, lớp con sẽ tự động nhận được tất cả các phương thức của lớp mẹ. Nếu trong số đó có phương thức mà chúng ta muốn thay đổi, chúng ta sẽ viết lại nó trong lớp con và trình biên dịch sẽ biết là phải gọi phương thức nào. Trong ví dụ thì nếu nhân vật là 1 chiến binh thì trình biên dịch sẽ sử dụng phiên bản trong lớp ChienBinh của tuGioiThieu(), trong những trường hợp còn lại như NhanVat hay PhuThuy thì nó sẽ sử dụng phương thức cơ bản trong lớp NhanVat.

Rút gọn mã nguồn

Chúng ta thậm chí có thể cải tiến xa hơn đoạn mã của phương thức tự giới thiệu của các chiến binh. Nếu các bạn chú ý thì trong phương thức của lớp ChienBinh có 2 dòng lệnh trùng với các lệnh được sử dụng trong phiên bản của phương thức trong lớp NhanVat. Chúng ta có thể tận dụng phương thức đã bị che khuất này để rút ngắn đoạn mã.

! Tận dụng lại những đoạn mã có sẵn là 1 thói quen tốt vì sẽ giúp chúng ta bảo dưỡng mã nguồn tốt hơn. Đôi khi thì lười cũng không phải là 1 tính quá xấu cool.

Chúng ta sẽ rất hạnh phúc nếu có thể viết như sau :

void ChienBinh::tuGioiThieu() const{
   ham_bi_che_khuat();
   // Thuc hien cac xu ly co ban cua lop NhanVat

   cout << "Toi la 1 chien binh dung manh." << endl;
   // Thuc hien xu ly dac biet cua lop ChienBinh
}

Để có thể sử dụng hàm đã bị che khuất, các bạn sẽ phải sử dụng tên đầy đủ của nó, trong trường hợp này là NhanVat::tuGioiThiet().

void ChienBinh::tuGioiThieu() const{
   NhanVat::tuGioiThiet();
   // Thuc hien cac xu ly co ban cua lop NhanVat
   cout << "Toi la 1 chien binh dung manh." << endl;
   // Thuc hien xu ly dac biet cua lop ChienBinh
}

Và kết quả chúng ta nhận được vẫn không hề thay đổi gì cả.

Dấu :: chính là phép toán cho phép trình biên xác định hàm hay biến nào cần được sử dụng khi có nhiều sự lựa chọn

Tóm tắt bài hoc :
  • Sự kế thừa cho phép chuyên biệt hóa 1 lớp.
  • Khi 1 lớp kế thừa 1 lớp khác thì nó sẽ kế thừa tất cả các thuộc tính và phương thức của lớp đó.
  • Chúng ta có thể sử dụng sự kế thừa khi chúng ta có thể nói « vật A là 1 vật B » như « tiếng Việt là 1 ngôn ngữ ».
  • Lớp nền tảng của sự kế thừa là lớp mẹ, còn lớp kế thừa gọi là lớp con.
  • Các phương thức khởi tạo sẽ được gọi theo thứ tự xác định : bắt đầu từ lớp mẹ rồi mới tới lớp con.
  • Ngoài quyên truy cập publicprivate thì còn có quyền truy cập protected. Quyền truy cập này tương tự như private nhưng thoáng hơn 1 chút khi cho phép các lớp con được thao tác trực tiếp với các thành phần của lớp mẹ.
  • Nếu 2 phương thức có cùng tên trong cả lớp mẹ và lớp con thì phương thức trong lớp con sẽ chuyên biệt hơn và được sử dụng trong trường hợp mặc định.