2.2. Các lớp ( phần 1-2 )

Trong bài học trước, chúng ta đã thấy lập trình hướng đối tượng giúp cuộc sống của lập trình viên trở nên dễ chịu hơn nhiều khi che giấu những xử lý phức tạp đằng sau những giao thức sử dụng đơn giản. Nhưng đây không phải là tất cả lợi ích của OOP. Các bạn sẽ thấy rằng, các đối tượng trong OOP rất dễ dàng được thay đổi và tái sử dụng.

Từ bài này, chúng ta sẽ học cách tạo ra các đối tượng. Các bạn sẽ thấy rằng đây là 1 công việc rất tinh tế, đòi hỏi lập trình viên thực hành nhiều lần. Trên thực tế thì có hàng trăm cách khác nhau để tạo ra cùng 1 mẫu đối tượng và chúng ta cần lựa chọn ra cách thức hợp lý nhất trong đó. Việc này không đơn giản và đòi hỏi phải suy xét thật kỹ trước khi bắt tay vào viết các đoạn mã xử lý.

Nếu các bạn đã sẵn sàng, chúng ta sẽ bắt đầu ngay bây giờ.

Tạo ra 1 lớp

? Tôi tưởng rằng chúng ta sẽ học cách tạo ra các đối tượng. Lớp là gì và có liên gì ở đây ?

Không hề có nhầm lẫn gì ở đây cả, để tạo ra 1 đối tượng, trước hết chúng ta phải tạo ra 1 lớp (class).

Ví dụ cho dễ hiểu : nếu coi đối tượng như 1 ngôi nhà cần xây thì lớp chính là bản vẽ kiến trúc của ngôi nhà đó. Khi muốn xây 1 ngôi nhà, các bạn trước hết cần phải có bản vẽ kiến trúc. Vậy nên đầu tiên, chúng ta sẽ xem làm thế nào để tạo ra các lớp.

Một khi đã có bản vẽ rồi, bạn có thể xây nên nhiều ngôi nhà tương tự nhau dựa theo thiết kế có sẵn đó. Với đối tượng cũng vậy, khi các bạn đã xây dựng xong 1 lớp, các bạn muốn tạo ra bao nhiêu đối tượng cùng kiểu cũng được.

! Người ta gọi đối tượng là 1 thực thể (instance) của lớp. Chúng ta sẽ bắt gặp từ này rất nhiều trong OOP. Điều này mang ý nghĩa là 1 đối tượng là hình ảnh cụ thể biểu diễn 1 lớp.

Trước hết, chúng ta cần xác định lớp mà chúng ta muốn tạo ra, giống như trong ví dụ về xây nhà, chúng ta cần biết là chúng ta muốn xây biệt thự có bể bơi hay nhà cao tầng chọc trời vậy.

Câu hỏi hay bắt gặp ở những người mới bắt đầu lập trình là không biết những gì có thể được coi là đối tượng. Câu trả lời của tôi là : hầu hết tất cả mọi thứ ! Ứng với mỗi thứ mà các bạn có thể nghĩ ra, chúng ta đều có khả năng tạo ra 1 lớp cho nó.

Vì mọi người đều mới bắt đầu, tôi nghĩ rằng sẽ dễ dàng hơn nếu để tôi quyết định xem chúng ta sẽ viết về cái gì.

Trong ví dụ của chúng ta, chúng ta sẽ tạo ra lớp NhanVat dùng để mô tả nhân vật trong siêu cấp trò chơi nhập vai của các bạn (RPG).

! Các bạn không cần bất cứ hiểu biết nào về trò chơi nhập vai để có thể hiểu được phần tiếp theo của bài học này. Tôi chọn ví dụ này đơn giản vì nó nghe có vẻ hấp dẫn và biết đâu có thể giúp ai đó nảy ra được ý tưởng về 1 trò chơi thế hệ mới của riêng mình.

Bắt đầu 1 lớp

Để bắt đầu, tôi xin được nhắc lại rằng 1 lớp được cấu thành từ :

  • Các biến : được gọi là các thuộc tính hoặc biến thành viên
  • Các hàm : được gọi là các phương thức hoặc hàm thành viên

Sau đây là đoạn mã tối thiểu của 1 lớp.

class NhanVat{  
}; // Dung quen dau cham phay o cuoi

Như các bạn có thể thấy, chúng ta sử dụng từ khóa class, tiếp theo đó là tên lớp mà chúng ta muốn tạo ra : NhanVat.

! Quy tắc cần ghi nhớ : Tên của lớp luôn luôn bắt đầu bằng chữ in hoa. Dù không bắt buộc nhưng đây là quy tắc đặt tên giúp chúng ta dễ dàng phân biệt tên lớp và tên đối tượng.

Toàn bộ định nghĩa của lớp sẽ nằm giữa dấu {}. Và tuyệt đối đừng quên dấu ; ở cuối cùng sau dấu }.

Thêm các phương thức và thuộc tính cho lớp

Lớp NhanVat mà chúng ta tạo ra dường như là… vẫn chưa có gì. Chúng ta cần thêm vào 1 số thứ :

  • Các thuộc tính : các biến được chứa trong đối tượng.
  • Các phương thức : các hàm được chứa trong đối tượng.

Mục tiêu đầu tiên của chúng ta là phải xác định được những gì mà chúng ta muốn đặt bên trong đối tượng NhanVat. Nhân vật của 1 trò chơi thì cần có những thuộc tính và phương thức gì ? Chúng ta cần phải trả lời được câu hỏi này trước khi bắt tay vào hùng hục viết mã. Giai đoạn phân tíchthiết kế đối tượng luôn là rất quan trọng nhưng thường bị những người mới bắt đầu coi nhẹ dẫn đến những xử lý phức tạp không đáng có về sau.

! Giai đoạn phân tích và thiết kế trước khi lập trình là vô cùng quan trọng. Nhiều lập trình viên kinh nghiệm thường bắt đầu bằng cách dùng bút chì và giấy để ghi lại các thiết kế mà họ sẽ thực hiện. Công đoạn này càng tốn thời gian bao nhiêu thì các bạn sẽ càng tiết kiệm được bấy nhiêu thời gian khi thực hiện lập trình.

1 ngôn ngữ đặc biệt tên là UML đã được tạo ra chuyên môn dùng để thiết kế các lớp trước khi thực sự lập trình.

Chúng ta có thể bắt đầu thêm các thuộc tính rồi tới các phương thức hoặc ngược lại vì không có thứ tự bắt buộc. Cá nhân tôi thấy là bắt đầu với các thuộc tính thì hợp lý hơn.

Các thuộc tính

Các thuộc tính là những nét đặc trưng cho đối tượng, trong ví dụ của chúng ta là 1 nhân vật trong trò chơi. Bởi vì là các biến, giá trị của chúng có thể thay đổi theo thời gian. 1 vài thuộc tính cơ bản mà chúng ta có thể xác định cho nhân vật :

  • Mọi nhân vật đều có 1 mức năng lượng sống, máu hoặc HP (health point). Thuộc tính thứ nhất của chúng ta sẽ là điểm HP, là số int có giá trị giữa 0 và 100.
  • Các nhân vật cũng có 1 mức năng lượng phép, nội lực hoặc MP (mana point). Thuộc tính thứ 2 là MP cũng là 1 số int có giá trị giữa 0 và 100. Nếu giá trị là 0, nhân vật không thể thực hiện phép. Điểm MP có thể hồi phục theo thời gian hoặc sử dụng thuốc.
  • Thuộc tính thứ 3 là có thể là tên của vũ khí mà nhân vật đang sử dụng. Chúng ta sẽ sử dụng kiểu string cho thuộc tính này.
  • Cuối cùng, mỗi loại vũ khí thì có 1 mức sát thương nhất định. Chúng ta sẽ dùng 1 giá trị int để đánh giá mức sát thương của vũ khí mà nhân vật sử dụng.

Chúng ta sẽ thêm các thuộc tính trên vào lớp NhanVat.

class NhanVat{
   int m_hp;
   int m_mp;
   string m_vuKhi;
   int m_dmgVuKhi; // dmg la damage hay sat thuong
};

Một vài điều cần chú ý trong đoạn mã :

Không phải là bắt buộc nhưng rất nhiều lập trình viên có thói quen thêm m_ vào trước tên thuộc tính ( m đại diện cho "member" nghĩa là thành viên, ở đây là biến thành viên của lớp). Thói quen này giúp chúng ta phân biệt các thuộc tính với các biến cổ điển.

Không thể khởi tạo được giá trị các thuộc tính ở đây mà cần làm thông qua các phương thức khởi tạo (constructor) mà chúng ta sẽ tìm hiểu sau.

Vì chúng ta cần sử dụng string, đừng quên thêm #include <string> vào đầu tệp chứa mã.

Kết luận chính có thể rút ra là giữa lớp và các thuộc tính có quan hệ sở hữu, ví dụ nhân vật sở hữu bao nhiêu hp, mp, vũ khí gì, vv… Vậy nên khi các bạn tìm thấy quan hệ sở hữu giữa 1 lớp với 1 biến, biến đó có thể trở thành 1 thuộc tính của lớp.

Các phương thức

Các phương thức có thể được hiểu là những hành động mà lớp có thể thực hiện. Các phương thức sử dụng và thay đổi giá trị của các thuộc tính.

Trong trường hợp của chúng ta, nhân vật có thể thực hiện 1 số việc.

  • nhanSatThuong : nhân vật chịu sát thương và mất điểm hp.
  • tanCong : nhân vật tấn công 1 nhân vật khác với vũ khí của mình, tạo ra mức sát thương là độ sát thương của vũ khí mà nhân vật sử dụng.
  • uongThuocHP : sử dụng thuốc để tang 1 số điểm hp nhất định.
  • doiVuKhi : đổi sang sử dụng vũ khí khác với mức sát thương cũng thay đổi theo.
  • conSong : trả về true nếu nhân vật còn hp, false nếu không (hp = 0)

Nhiều lập trình viên sẽ thêm các phương thức này vào trước các thuộc tính.

class NhanVat{
   void nhanSatThuong (int soDmg) {
   }
   void tanCong(NhanVat &mucTieu){
   }
   void uongThuocHP (int dungLuongThuoc){
   }
   void doiVuKhi(string vuKhiMoi, int dmgVuKhiMoi) {
   }
   bool conSong(){
   }  

   int m_hp;
   int m_mp;
   string m_vuKhi;
   int m_dmgVuKhi; // dmg la damage hay sat thuong
};

! Tôi đã cố tình bỏ trống đoạn mã của các phương thức. Chúng ta sẽ quay lại đây sau.

Các bạn hẳn đã có chút ít ý niệm về những gì chúng ta sẽ viết trong đoạn mã xử lý của mỗi phương thức rồi phải không?

Ví dụ phương thức nhanSatThuong sẽ lấy đi soDmg từ số điểm hp hiện thời của nhân vật.

Hay như phương thức tanCong cũng khá thú vị vì nhận vào thông số là 1 tham chiếu trên 1 nhân vật khác, cũng là 1 thực thể khác của lớp. Và rồi phương thức này sẽ thực hiện xử lý sử dụng nhanSatThuong của nhân vật mục tiêu.

Mọi thứ đều liên quan đến nhau 1 cách bất ngờ, phải không?

Có thể ban đầu bạn sẽ chưa thấy quen với cách suy nghĩ và tiếp cận hướng đối tượng nhưng thông qua rèn luyện, mọi thứ sẽ đến 1 cách khá tự nhiên.

Vẫn còn những phương thức khác mà chúng ta có thể thêm vào lớp bên trên, ví dụ như cho nhân vật thực hiện phép thay vì tấn công bằng vũ khí, vv… Tôi xin để cho mỗi người tự suy nghĩ xem nhân vật trong trò chơi của mình có thể làm những gì.

Tóm lại, đối tượng chính là 1 chỉnh thể cấu thành từ các biến (thuộc tính) và các hàm (phương thức). Trong đa số trường hợp, các phương thức sử dụng và thay đổi các thuộc tính để tác động thay đổi đối tượng. Bản thân 1 đối tượng trở thành 1 hệ thống nhỏ khá thông minh và tự lập, tự quản lý hoạt động của mình.

Quyền truy cập và tính đóng gói

Bây giờ chúng ta sẽ cùng nói đến 1 trong những khái niệm cơ bản của lập trình hướng đối tượng, đó là tính đóng gói (encapsulation).

Đừng quá sợ hãi, khái niệm này không hề khó hiểu như bạn nghĩ !

Nhắc lại 1 chút, trong OOP, có 2 xử lý riêng biệt :

  • Tạo ra các lớp bên trong đó miêu tả các xử lý của đối tượng. Chính là những thứ chúng ta đang học đây.
  • Sử dụng các đối tượng như chúng ta đã tìm hiểu trong bài học trước.

Khả năng phân biệt rõ rang 2 xử lý này sẽ rất quan trọng trong phần tiếp theo đây.

Tạo ra 1 lớp :

class NhanVat{
   void nhanSatThuong (int soDmg) {
   }
   void tanCong(NhanVat &mucTieu){
   }
   void uongThuocHP (int dungLuongThuoc){
   }
   void doiVuKhi(string vuKhiMoi, int dmgVuKhiMoi) {
   }
   bool conSong(){
   }  

   int m_hp;
   int m_mp;
   string m_vuKhi;
   int m_dmgVuKhi; // dmg la damage hay sat thuong
};

Sử dụng 1 đối tượng :

int main(){
   NhanVat david, goliath;
   //Tao ra 2 nhan vat chang ti hon david va nguoi khong lo goliath

   goliath.tanCong(david); //goliath tan cong david
   david.uongThuocHP(20); //david uong thuoc de hoi phuc 20 diem hp
   goliath. tanCong(david); //goliath lai tan cong david
   david.tanCong(goliath); //david tan cong goliath
   goliath.doiVuKhi("Riu cua tu than", 40);
   goliath.tanCong(david);

   return 0;
}

Tại sao chúng ta lại không chạy thử đoạn mã trên nhỉ ? Chỉ cần chú ý định nghĩa lớp trước hàm main().

#include <iostream>
#include <string>
using namespace std;

class NhanVat{
   void nhanSatThuong (int soDmg) {
   }
   void tanCong(NhanVat &mucTieu){
   }
   void uongThuocHP (int dungLuongThuoc){
   }
   void doiVuKhi(string vuKhiMoi, int dmgVuKhiMoi) {
   }
   bool conSong(){
   }  

   int m_hp;
   int m_mp;
   string m_vuKhi;
   int m_dmgVuKhi; // dmg la damage hay sat thuong
};

int main(){
   NhanVat david, goliath;
   //Tao ra 2 nhan vat chang ti hon david va nguoi khong lo goliath

   goliath.tanCong(david); //goliath tan cong david
   david.uongThuocHP(20); //david uong thuoc de hoi phuc 20 diem hp
   goliath. tanCong(david); //goliath lai tan cong david
   david.tanCong(goliath); //david tan cong goliath
   goliath.doiVuKhi("Riu cua tu than", 40);
   goliath.tanCong(david);
   return 0;
}

Và chương trình cho chúng ta kết quả là… 1 lỗi biên dịch mã nguồn.

Đáng buồn thay !

Quyền truy cập

Đúng như nhiều bạn nghi ngờ, tôi đã cố tình gọi ra lỗi biên dịch này để giời thiệu cho các bạn về khái niệm mà chúng ta sẽ xem sau đây : quyền truy cập (access right).

Mỗi thuộc tính cũng như phương thức mà chúng ta tạo ra trong lớp đều có 1 mức độ cho phép truy cập mà chúng ta gọi là quyền truy cập. 2 quyền truy cập thường thấy nhất là :

  • public : thuộc tính hay phương thức cho phép chúng ta gọi chúng từ bên ngoài đối tượng.
  • private : các thuộc tính hay phương thức chỉ cho phép được sử dụng bên trong đối tương. Mặc định là quyền truy cập của các phương thức và thuộc tính đều là private.

! Còn 1 số cấp độ truy cập khác nhưng phức tạp hơn 1 chút. Chúng ta sẽ nói về chúng sau.

Thế nhưng cần phải xác định rõ thế nào là « bên ngoài » của đối tượng.

Trong chương trình ví dụ bên trên của chúng ta thì « bên ngoài » chính là hàm main(), chính là chỗ chúng ta sử dụng đối tượng. Chúng ta đã thử sử dụng các phương thức của đối tượng nhưng bởi vì quyền truy cập mặc định của chúng là private nên không thể được gọi bên trong hàm main()! Và đó là nguyên nhân của lỗi biên dịch.

Để thay đổi quyền truy cập, chẳng hạn thư đổi thành public, cần thêm vào đoạn mã chữ public tiếp theo đấy là dấu : . Tất cả những gì chúng ta viết sao dấu : sẽ có mức truy cập public.

Dưới đây là ví dụ trong đấy tất cả các thuộc tính đều có quyền private còn tất cả các phương thức thì có quyền public.

class NhanVat{
// Nhung thanh phan tiep theo se duoc public ( truy cap duoc tu ben ngoai)
   public:
   void nhanSatThuong (int soDmg) {
   }
   void tanCong(NhanVat &mucTieu){
   }
   void uongThuocHP (int dungLuongThuoc){
   }
   void doiVuKhi(string vuKhiMoi, int dmgVuKhiMoi) {
   }
   bool conSong(){
   }

   // Nhung thanh phan tiep theo se duoc private ( khong truy cap duoc tu ben ngoai)
   private:  
   int m_hp;
   int m_mp;
   string m_vuKhi;
   int m_dmgVuKhi; // dmg la damage hay sat thuong
};

Nếu bây giờ chúng ta biên dịch lại chương trình thì sẽ không gặp phải lỗi biên dịch như lúc trước nữa bởi vì các phương thức đều có quyền truy cập public nên có thể được sử dụng bên trong hàm main().

Thêm vào đấy là những thuộc tính có quyền truy cập private nên nếu viết dòng lệnh sau trong main() sẽ gây ra lỗi biên dịch :

goliath.m_hp = 90;

Các bạn có thể thử lại và tiếp tục nghe trình biên dịch kêu ca về lỗi khi truy cập vào thuộc tính private.

Thế nhưng điều đó có nghĩa là chúng ta không thể thay đổi điểm hp của nhân vật từ trong hàm main() được. Đấy chính là tính đóng gói.

Tính đóng gói

? Vậy tại sao chúng ta không trao quyền “public” cho tất cả các thuộc tính cũng như các phương thức. Bằng cách đó, chúng ta có thể thao tác thay đổi chúng từ trong hàm main(), đúng không ?

Bạn nói không sai. Thế nhưng bạn vừa trở thành kẻ thù của tất cả các lập trình viên OOP trên thế giới. Vì sao ư ? Bởi vì có 1 quy tắc vàng trong OOP là :

Tính đóng gói : Các thuộc tính trong 1 lớp toàn bộ phải dùng quyền private !

Nghe có vẻ hơi ấu trĩ nhưng nguyên lý cơ bản của OOP đều bắt nguồn từ đây. Và tôi không muốn thấy bất cứ ai trao quyền public cho 1 thuộc tính trước mắt tôi cả !

Tôi sẽ giải lý do tại sao tôi đặc biệt nhấn mạnh vào điểm cần phải tuân thủ tính đóng gói giữ cho các thuộc tính không thể bị thay đổi từ bên ngoài.

Các bạn vẫn còn nhớ hình vẽ dưới đây chứ.

Hệ thống xử lý phức tạp bên trong chính là những thuộc tính, còn những chiếc nút và cần gạt trên bề mặt hộp chính là các phương thức.

Chỉ cần gợi ý như vậy là đủ phải không. Mục đích của chúng ta chẳng phải là che giấu những xử lý phức tạp để tránh cho người dùng khỏi những thao tác sai lầm có thể dễ mắc phải sao ?

Lấy ví dụ nếu chúng ta cho người dùng thao tác trực tiếp với số điểm hp m_hp, ai có thể chắc rằng họ không gán cho thuộc tính này giá trị 150, vượt quá hạn mức 100 điểm mà chúng ta đã đề ra, vv… Chính vì thế mà chúng ta yêu cầu người dùng phải thao tác thông qua các phương thức để có thể dễ dàng quản lý và đảm bảo nhũng dữ liệu hợp lê.

Tóm lại, đối tượng cần phải được cất giấu cẩn thận bên trong « hộp kín» của chúng ta.

! Nếu các bạn đã học C và biết từ khóa struct thi tôi cho các bạn biết là từ khóa này cũng có thể sử dụng trong C++ để tạo lớp. Khác biệt duy nhất với khi chúng ta sử dụng từ khóa class là quyên truy cập mặc định sẽ là "public" thay vì là "private".

Tách biệt nguyên mẫu và định nghĩa

Sau đây là những việc chúng ta sẽ làm trong phần tiếp theo :

  • Tách biệt nguyên mẫu và định nghĩa của các phương thức và đặt trong 2 tệp khác nhau.
  • Hoàn thiện mã nguồn định nghĩa còn thiếu bên trong các phương thức của lớp NhanVat.

Cho đến lúc này thì tất cả xử lý của chúng ta cùng với khai báo lớp đều được viết trong main.cpp. Mặc dù chương trình chúng ta viết ra hoạt động tốt nhưng mã nguồn vẫn chưa được sắp xếp tối ưu.

Để cải thiện, chúng ta cần trước tiên tách riêng các lớp khỏi những xử lý của hàm main() mà chúng ta sẽ đặt trong main.cpp.

Với mỗi lớp, cần có :

  • 1 tệp tiêu đề (tệp .h) chứa các thuộc tính và nguyên mẫu phương thức của lớp.
  • 1 tệp nguồn (tệp .cpp) chứa mã xử lý của các phương thức của lớp.

Vậy nên tôi đề nghị thêm vào 2 tệp :

  • NhanVat.h
  • NhanVat.cpp

! Nhớ là chữ cái đầu tiên trong tên các tệp này cũng viết hoa giống như trong tên lớp tương ứng.

Các bạn có thể dễ dàng bổ sung nhưng tệp này vào dự án trong IDE của mình. Trong Code::Block, trên thanh công cụ, chọn File > New File rồi nhập tên tệp bạn muốn tạo, sau đó đồng ý khi IDE muốn xác nhận thay đổi thêm tệp này.

NhanVat.h

Tệp .h chứa khai báo của lớp cùng với các thuộc tính và nguyên mẫu phương thức của lớp đó.

#ifndef DEF_NHANVAT
#define DEF_NHANVAT
#include <string>

class NhanVat{
   public:
   void nhanSatThuong(int soDmg);
   void tanCong(NhanVat &mucTieu);
   void uongThuocHP(int dungLuongThuoc);
   void doiVuKhi(std::string vuKhiMoi, int dmgVuKhiMoi);
   bool conSong();

   private:
   int m_hp;
   int m_mp;
   std::string m_vuKhi;
   int m_dmgVuKhi;
};

#endif

Như các bạn có thể thấy, mọi thứ trở nên dễ theo dõi hơn khi chỉ có nguyên mẫu của phương thức trong tệp .h.

! Như đã đề cập trong chương trước, không nên sử dụng using namespace std; trong tệp tiêu đề .h. Vì thế cần phải thêm std:: trước string nếu không trình biên dịch sẽ báo lỗi về kiểu dữ liệu "string does not name a type".

NhanVat.cpp

Đây sẽ là nơi chúng ta sẽ viết mã xử lý của các phwuong thức.

Nhất thiết không thể quên bao gồm gói <string> và tệp NhanVat.h vì chúng ta sẽ cần thao tác với chúng.

Bởi vì đây là trong tệp .cpp nên chúng ta có thể thêm using namespace std; nhưng tuyệt đối không làm thế trong tệp .h.

#include "NhanVat.h"
using namespace std;

Chú ý, khi viết định nghĩa các phương thức, phần tên phương thức cần có thêm tên lớp đi kèm với dấu :: phía trước. Lấy ví dụ phương thức nhanSatThuong :

void NhanVat::nhanSatThuong (int soDmg){
}

Trong thực tế, định nghĩa của phương thức nằm bên ngoài khai báo của lớp nên trình biên dịch không xác định được là phương thức này thuộc lớp nào. Cú pháp này cho phép trình biên dịch xác lập quan hệ giữa phương thức này với lớp NhanVat.

NhanVat::nhanSatThuong

Các bạn sẽ thấy là phương thức này không có gì quá rắc rối cả.

void NhanVat::nhanSatThuong (int soDmg){
   m_hp -= soDmg;
   //Tru di so diem hp bang dung so sat thuong phai nhan

   if (m_hp < 0){ //Tranh cho so diem hp xuong duoi 0
       m_hp = 0; //Gan cho 0 diem hp (nhan vat da chet)
   }
}

Phương thức đã thay đổi giá trị của thuộc tính. Phương thức được phép làm điều này bởi bản thân nó cũng là 1 phần tử của lớp. Đây chính là nơi chúng ta có quyền tác động đến giá trị của thuộc tính.

Số điểm hp bị trừ đi bằng đúng số điểm sát thương mà nhân vật phải nhận. Về cơ bản thì dòng lệnh đầu tiên là đủ rồi, thế nhưng có thể viết thêm 1 lệnh điều kiện phía sau để chắc chắn rằng giá trị của thuộc tính m_hp không xuống dưới 0. Kiểu gì thì khi giá trị của m_hp bằng 0 thì nhân vật cũng bị coi như đã chết.

Cùng tiếp tục với phương thức tiếp theo nào !

NhanVat::tanCong

void tanCong(NhanVat &mucTieu){
   mucTieu.nhanSatThuong(m_dmgVuKhi);
   //Gay cho muc tieu muc sat thuong bang muc sat thuong cua vu khi nhan vat dang dung
}

Phương thức này càng thêm thú vị. Chúng ta nhận vào là tham chiếu trên 1 đối tượng NhanVat. Chúng ta cũng đã có thể sử dụng con trỏ nhưng do bởi tham chiếu dễ thao tác hơn nên tôi đã chọn tham chiếu.

Để gây sát thương lên mục tiêu, chúng ta sẽ sử dụng phương thức nhanSatThuong của đối tượng mucTieu : mucTieu.nhanSatThuong. Thêm vào đó, mục tiêu sẽ phải nhận số sát thương bằng đúng chỉ số sát thương của vũ khí mà nhân vật đang sử dụng là m_dmgVuKhi.

Nghe thật hợp lý phải không !

NhanVat::uongThuocHP

void NhanVat::uongThuocHP(int dungLuongThuoc){
   m_hp += dungLuongThuoc;

   if (m_hp > 100){ //So luong hp khong the vuot qua 100
       m_hp = 100;
   }
}

Nhân vật sẽ hồi phục số điểm hp đúng bằng dung lượng của thuốc. Lệnh điều kiện tiếp theo dùng để ngăn việc số điểm hp vượt quá mức giới hạn là 100 điểm.

NhanVat::doiVuKhi

void doiVuKhi(string vuKhiMoi, int dmgVuKhiMoi){
   m_vuKhi = vuKhiMoi;
   m_dmgVuKhi = dmgVuKhiMoi;
}

Để thay đổi vũ khí, chỉ đơn giản là gán tên của vũ khí mới cũng như sức sát thương của vũ khí này cho thuộc tính tương ứng. Các giá trị bên trên được truyền qua thông số của phương thức.

NhanVat::conSong

bool NhanVat::conSong(){
   if (m_hp > 0) { //So diem hp co on hon 0 ?
       return true; //Tra ve dung neu nhan vat con song
   }else{
       return false; //Tra ve sai neu nhan vat da chet
   }
}

Phương thức này dùng để xác nhận xem nhân vật còn sống hay đã chết. Nó trả về kết quả đúng nếu số lượng hp của nhân vật lớn hơn 0 và sai nếu không.

Tuy nhiên, chúng ta có thể trả về trực tiếp giá trị của phép so sánh.

bool NhanVat::conSong(){
     return m_hp > 0; //Tra ve true neu m_hp > 0 va false neu khong dung
}

Vừa rõ ràng lại vừa cụ thể hơn nhiều ! Có thể các bạn chưa kịp làm quen nhưng đây là cách chúng ta nên dùng để viết nhưng phương thức kiểu này. Các lập trình viên thường không thích sử dụng nhưng lệnh điều kiện mà không mang quá nhiều ý nghĩa.

Mã nguồn hoàn chỉnh của NhanVat.cpp

Để tổng kết lại thì đây là mã nguồn hoàn chỉnh của NhanVat.cpp.

#include "NhanVat.h"
using namespace std;

void NhanVat::nhanSatThuong (int soDmg){
   m_hp -= soDmg;
   //Tru di so diem hp bang dung so sat thuong phai nhan

   if (m_hp < 0){ //Tranh cho so diem hp xuong duoi 0
       m_hp = 0; //Gan cho 0 diem hp (nhan vat da chet)
   }
}

void tanCong(NhanVat &mucTieu){
   mucTieu.nhanSatThuong(m_dmgVuKhi);
   //Gay cho muc tieu muc sat thuong bang muc sat thuong cua vu khi nhan vat dang dung
}

void NhanVat::uongThuocHP(int dungLuongThuoc){
   m_hp += dungLuongThuoc;

   if (m_hp > 100){ //So luong hp khong the vuot qua 100
       m_hp = 100;
   }
}

void doiVuKhi(string vuKhiMoi, int dmgVuKhiMoi){
   m_vuKhi = vuKhiMoi;
   m_dmgVuKhi = dmgVuKhiMoi;
}

bool NhanVat::conSong(){
     return m_hp > 0; //Tra ve true neu m_hp > 0 va false neu khong dung
}
main.cpp

Quay lại với main.cpp, cần thêm vào NhanVat.h để có thể sử dụng lớp vừa tạo. Về phần xử lý bên trong hàm main() thì không cần thay đổi gì cả.

#include <iostream>
#include "NhanVat.h" //Dung quen them NhanVat.h
using namespace std;

int main(){
   NhanVat david, goliath;
   //Tao ra 2 nhan vat chang ti hon david va nguoi khong lo goliath

   goliath.tanCong(david); //goliath tan cong david
   david.uongThuocHP(20); //david uong thuoc de hoi phuc 20 diem hp
   goliath. tanCong(david); //goliath lai tan cong david
   david.tanCong(goliath); //david tan cong goliath
   goliath.doiVuKhi("Riu cua tu than", 40);
   goliath.tanCong(david);
   return 0;
}

! Tạm thời đừng thử chạy chương trình bởi vì chúng ta vẫn chưa học cách làm sao để khởi tạo giá trị cho thuộc tính nên chương trình vẫn chưa thể sử dụng được. Chúng ta sẽ hoàn thiện chương trình trong bài học sau và sẽ sẵn sàng để hoạt động thử.

Trong lúc chờ đợi, các bạn có thể dùng trí tưởng tượng của mình để tự hình dung ra 1 trận chiến tuyệt vời giữa David và Goliath.

Tóm tắt bài hoc :
  • Chúng ta cần tạo ra 1 lớp trước khi tạo ra các đối tượng.
  • Lớp chính là cấu trúc của đối tượng giống như bản vẽ kiến trúc đối với 1 ngôi nhà.
  • Lớp được cấu thành từ các thuộc tính và các phương thức (biến và hàm).
  • Các thành phần của lớp có thể có quyền truy cập public hoặc private. Nếu 1 thành phần có quyền truy cập là public thì nó có thể được sử dụng ở bất cứ chỗ nào trong đoạn mã, trong khi 1 thành phần có quyền private thì chỉ có thể sử dụng từ bên trong đối tượng.
  • Trong lập trình hướng đối tượng, chúng ta cần tuân thủ tính đóng gói, nghĩa là tất cả các thuộc tính phải có quyền truy cập private và các lập trình viên khác chỉ có thể thao tác với chúng thông qua các phương thức.