< Lập trình tân binh | 2.3. Các lớp ( phần 2-2 )

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

Chúng ta sẽ tiếp tục phần 2 của bài học trước vì tôi nghĩ là không ai muốn hoãn lại cái sự sung sướng khi học về OOP.

Trong bài học trước, chúng ta đã tạo ra được 1 lớp cơ bản cũng như tìm hiểu bước đầu về tính đóng gói trong OOP. Trong bài học này, chúng ta sẽ hoàn thiện lớp đã tạo ra với phương thức khởi tạo (constructor) và phương thức hủy (destructor). Thêm vào đó, chúng ta cũng sẽ tạo ra thêm 1 lớp mới VuKhi dùng để mô tả các đối tượng là các loại vũ khí và xem làm sao chúng ta có thể liên hệ cũng như kết hợp các lớp với nhau.

Sẽ có vô vàn thứ mà chúng ta có thể tạo ra với chừng đó kiến thức.

Phương thức khởi tạo và phương thức hủy

Hiện thời, trong dự án của chúng ta có 3 tệp :

  • main.cpp : chứa hàm main(), trong đó chúng ta đã tạo ra 2 đối tượng là davidgoliath.
  • NhanVat.h : Tệp tiêu đề của lớp NhanVat. Trong tệp này khai báo tất cả các thuộc tính cũng như các nguyên mẫu các phương thức của lớp. Chúng ta cũng xác định quyền truy cập cho các thuộc tính và phương thức đó. Để bảo đảm tính đóng gói, tất cả các thuộc tính đều có quyền truy cập private.
  • NhanVat.cpp : Tệp nguồn, chứa mã xử lý của các phương thức được khai báo trong lớp.

Ngoài những phương thức thông thường mà chúng ta đã định nghĩa ở trên, còn tồn tại 2 phương thức đặc biệt mà chúng ta sẽ bắt gặp trong hầu hết các lớp : phương thức khởi tạo và phương thức hủy.

  • Phương thức khởi tạo : phương thức được tự động gọi khi chúng ta tạo ra 1 đối tượng dựa trên khai báo của lớp.
  • Phương thức hủy: phương thức được tự động gọi khi đối tượng bị hủy, ví dụ như khi kết thúc hàm mà trong đó đối tượng được tạo ra hoặc khi thực hiện phép toán delete trên những đối tượng được phân bổ động dựa vào new.

Hãy cũng tìm hiểu về từng phương thức !

Phương thức khởi tạo

Giống như tên gọi, phương thức này được sử dụng để tạo nên đối tượng. Khi chúng ta thực hiện tạo đối tượng, phương thức này sẽ tự động được gọi sử dụng.

Ví dụ như trong hàm main(), chúng ta đã viết :

NhanVat david, goliath;

Phương thức khởi tạo sẽ được gọi 2 lần : lần đầu để tạo ra david và lần thứ 2 để tạo ra goliath.

! Phương thức khởi tạo mặc định sễ được tạo ra bởi trình biên dịch. Nó hoàn toàn trống và không thực hiện xử lý gì cả. Vì thế chúng ta thường xuyên cần tự tạo ra 1 phương thức khởi tạo riêng cho từng lớp để thay thế phương thức mặc định.

Vai trò của phương thức khởi tạo

Phương thức khởi tạo được sử dụng khi tạo ra các đối tượng, dùng để khởi tạo giá trị ban đầu cho các thuộc tính của đối tượng. Lý do cần sử dụng phương thức này là vì khi khai báo các thuộc tính trong tệp tiêu đề, chúng ta hoàn toàn không cung cấp cho chúng giá trị ban đầu.

Hãy xem lại tệp NhanVat.h

#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

Các thuộc tính m_hp, m_mp, m_vuKhi, m_dmgVuKhi không được khởi tạo giá trị ban đầu. Vì sao ? Bởi vì chúng ta không có quyền khởi tạo giá trị thuộc tính ở đây mà phải thực hiện bên trong phương thức khởi tạo.

! Trên thực tế, phương thức khởi tạo là vô cùng cần thiết để khởi tạo các thuộc tính không phải là đối tượng mà là kiểu dữ liệu cơ bản như int hay double . Nếu chúng ta không cung cấp cho chúng giá trị ban đầu thì chúng sẽ nhận 1 giá trị ngẫu nhiên không xác đinh trong bộ nhớ.

Những thuộc tính là đối tượng như m_vuKhi thì khác, chúng sẽ tự động nhận các giá trị khởi tạo mặc định khi không được cung cấp giá trị ban đầu.

Tạo 1 phương thức khởi tạo

Phương thức khởi tạo là 1 phương thức nhưng nó có đôi chút khác biệt với các phương thức mà chúng ta đã nói qua. Khi tạo 1 phương thức khởi tạo, có 2 quy tắc cần tuân thủ :

  • Tên của phương thức phải trùng với tên của lớp. Trong trường hợp ví dụ của chúng ta, tên phương thức này sẽ là NhanVat.
  • Phương thức này không trả về bất cứ giá nào, kể cả void ! Đây là 1 phương thức không có kiểu dữ liệu trả về.

Nếu chúng ta khai báo nguyên mẫu của phương thức này trong NhanVat.h, đoạn mã sẽ như sau :

Chúng ta có thể dễ dàng nhận ra ngay phương thức này bởi vì thứ nhất, nó không trả về bất cứ kiểu dữ liệu nào và thứ hai, tên của nó trùng khớp với tên lớp.

Và đây là đoạn mã xử lý của phương thức này trong NhanVat.cpp.

NhanVat::NhanVat(){
    m_hp = 100;
    m_mp = 100;
    m_vuKhi = "Kiem go";
    m_dmgVuKhi = 10;
}

Cần tuyệt đối ghi nhớ là phương thức này không trả về gì cả, thậm chí cả void. Đây là 1 trong những sai lầm dễ mắc phải với những người mới lần đầu học OOP.

Tôi đã khởi tạo 1 nhân vật với lượng hp là 100 và vũ khí là kiếm gỗ với sức sát thương là 10.

Vậy là lớp NhanVat đã có phương thức khởi tạo. Từ bây giờ, mỗi khi chúng ta tạo ra 1 đối tượng NhanVat, đối tượng sẽ có lượng hp là 100 và mp là 100 cũng như là dùng vũ khí kiếm gỗ. Như vậy, khi chúng ta tạo ra 2 đối thủ là david goliath trong hàm main(), chúng ta có 2 nhân vật có đặc điểm tương tự nhau.

NhanVat david, goliath; //Phuong thuc khoi tao cua david va goliath duoc goi

Khởi tạo với danh sách giá trị

Chúng ta còn 1 cách khác để khởi tạo  giá trị cho các thuộc tính của lớp thông qua 1 danh sách khởi tạo. Tôi khuyên các bạn nên dùng kỹ thuật này bất cứ khi nào bạn có thể, có nghĩa là gần như trong mọi trường hợp embarassed. Đây là cách chúng ta sẽ sử dụng trong phần sau của giáo trình.

Đây là phương thức khởi tạo mà chúng ta đã viết.

NhanVat::NhanVat(){
    m_hp = 100;
    m_mp = 100;
    m_vuKhi = "Kiem go";
    m_dmgVuKhi = 10;
}

Đoạn mã dưới đây cũng sẽ thực hiện nhưng xử lý tương tự.

NhanVat::NhanVat(): m_hp(100), m_mp(100), m_vuKhi("Kiem go"), m_dmgVuKhi(10){
  //Khong can them ma xu ly
}

Khác biệt là chúng ta sử dụng dấu : tiếp theo đấy là danh sách các giá trị chúng ta muốn cấp cho các thuộc tính.

Thật hay là chúng ta không cần bất cứ xử lý nào bên trong dấu {} bởi vì tất cả những khởi tạo đã được làm từ trước đó. Lợi ích của cách khởi tạo nào là đoạn mã trông sang sủa hơn. Ngoài ra còn 1 số lợi ích khác sẽ được thể hiện ra trong phần sau của giáo trình.

Sử dụng phương pháp khởi tạo với danh sách giá trị là 1 thói quen tốt các bạn nên tập.

! Nguyên mẫu của phương thức trong tệp tiêu đề không hề thay đổi. Tất cả những thứ chúng ta viết sau dấu : không thuộc về nguyên mẫu của phương thức.

Nạp chồng phương thức khởi tạo

Các bạn có biết là trong C++, chúng ta có quyền nạp chồng (overloading) các phương thức. Và bởi vì phương thức khởi tạo cũng là 1 phương thức, chúng ta hoàn toàn có quyền nạp chồng nó.

Hiện giờ; chúng ta đang có 1 phương thức khởi tạo không nhận thông số.

NhanVat();

Đây là phương thức khởi tạo mặc định.

Thế nếu chúng ta muốn khởi tạo 1 đối tượng với vũ khí ban đầu tốt hơn thì làm thế nào ?

Đây chính là lúc khả năng nạp chồng phương thức thể hiện ra ưu điểm của nó.

Trong NhanVat.h, chúng ta thêm vào nguyên mẫu sau đây.

NhanVat(std::string vuKhi, int dmgVuKhi);

! Nhắc lại thêm lần nữa, bởi vì không thể sử dụng using namespace std; trong tệp tiêu đề, thêm std:: trước string là bắt buộc.

Mã xử lý của phương thức khởi tạo mới này sẽ như sau.

NhanVat::NhanVat(string vuKhi, int dmgVuKhi) : m_hp(100), m_mp(100), m_vuKhi(vuKhi), m_dmgVuKhi(dmgVuKhi){

}

Các bạn sẽ nhận ra lợi ích khi chúng ta thêm m_ vào đầu phần tên của thuộc tính. Chúng ta có thể dễ dàng phân biệt thuộc tính của lớp với thông số nhận vào của phương thức khởi tạo. Ở trong ví dụ trên, chúng ta đơn giản là gán giá trị của vũ khí vào thuộc tính của đối tượng, không có gì đặc biệt.

Số lượng hp và mp vẫn là 100 nhưng bây giờ vũ khí ban đầu có thể được quyết định bởi người dùng lúc tạo ra đối tượng.

? Người dùng nào thế ?

Khác với những người tạo ra các lớp, người dùng ở đây là chỉ người tạo ra và sử dụng các đối tượng của lớp này.

Ví dụ như khi trước, trong hàm main(), chúng ta đã viết :

NhanVat david, goliath;

Bởi vì không có thông số nào được đưa ra, phương thức khởi tạo mặc định sẽ được sử dụng.

Giả sử bây giờ chúng ta muốn cho goliath sử dụng vũ khí tốt hơn, chúng ta sẽ cung cấp thông số về vũ khí đó bên trong dấu ().

NhanVat david, goliath("Kiem sat", 20);

Nhân vật goliath được sử dụng kiếm sắt trong khi david sử dụng vũ khí mặc định là kiếm gỗ. Điều này khá dễ hiểu vì khi tạo ra david, chúng ta không cung cấp thông số nên phương thức khởi tạo là phương thức mặc định. Trong khi đó, chúng ta cung cấp thông số về vũ khí khác khi tạo ra goliath nên phương thức nhận vào thông số là 1 string và 1 int sẽ được sử dụng.

Bài tập : Tôi đã không thực hiện ở trên nhưng chúng ta hoàn toàn có thể thực hiện những thay đổi tương tự trên thuộc tính số hp và số mp. Việc này không quá khó khăn và các bạn có thể thử làm nó như 1 bài tập nhỏ. Kết quả tạo ra là chúng ta sẽ có 3 phương thức khởi tạo.

Phương thức khởi tạo sao chép

Ở đầu bài học, tôi đã nói với các bạn là trình biên dịch sẽ tự động tạo ra 1 phương thức khởi tạo mặc định mà không có mã xử lý. Đấy không phải là tất cả, vì nó còn tạo ra cái mà chúng ta gọi là phương thức khởi tạo sao chép (copy constructor). Đây là 1 bản nạp chồng của phương thức khởi tạo, được sử dụng khi chúng ta tạo ra 1 đối tượng dựa trên 1 đối tượng có sẵn.

Ví dụ như khi chúng ta muốn tạo ra david là bản sao của goliath.

NhanVat goliath("Kiem sat", 20);  //Tao ra goliath voi phuong thuc khoi tao thong thuong
NhanVat david(goliath);           //Sao chep tat ca thuoc tinh cua goliath cho david

Phương thức này rất dễ sử dụng. Và bởi vì trình biên dịch đã tự động giúp chúng ta tạo ra phương thức này, chúng ta cần phải cám ơn nó.

Nếu có lúc nào đó mà bạn muốn thay đổi xử lý của phương thức khởi tạo sao chép, khi khai báo lớp, cần khai báo nguyên mẫu hàm như sau :

NhanVat(NhanVat const& nhanVatKhac);

Và đoạn mã xử lý :

NhanVat::NhanVat(NhanVat const& nhanVatKhac) : m_hp(nhanVatKhac.m_hp), m_mp(nhanVatKhac.m_mp), m_vuKhi(nhanVatKhac.m_vuKhi), m_dmgVuKhi(nhanVatKhac.m_dmgVuKhi){

}

Các bạn có thể thấy là tôi có thể truy cập trực tiếp vào các thuộc tính của nhân vật nhanVatKhac. Đơn giản và cụ thể !

Phương thức hủy

Phương thức hủy là phương thức được sử dụng để xóa đối tượng khỏi bộ nhớ. Nhiệm vụ chính của nó là trả lại vùng nhớ đã được phân bổ khi phân bổ động.

Trong trường hợp của lớp NhanVat, chúng ta không có thực hiện phân bổ động bộ nhớ thông qua phép toán new. Vì thế nên phương thức hủy là không cần thiết. Thế nhưng chắc chắn các bạn sẽ cần phải dùng đến nó trong 1 ngày đẹp trời nào đó mà lớp của bạn sử dụng phân bổ động.

Lấy ví dụ như đối tượng string chẳng hạn, các bạn nghĩ nó hoạt động như thế nào ? Có 1 phương thức hủy cho phép đối tượng trước khi đối tượng bị hủy xóa mảng ký tự được lưu trong vùng nhớ mà đối tượng đã mượn. 1 phép toán delete được thực hiện trên mảng động này để giữ cho vùng nhớ gọn gàng và tránh việc rò rỉ bộ nhớ mà chúng ta từng nhắc đến.

Tạo 1 phương thức hủy

Dù trong trường hợp của chúng ta thì tạo ra 1 phương thức hủy là không cần thiết, tôi vẫn sẽ chỉ cho các bạn làm sao để tạo ra 1 phương thức hủy tiêu chuẩn. Có 1 số quy tắc cần tuân theo.

  • 1 phương thức hủy là 1 phương thức có tên bắt đầu bằng dấu ~, tiếp theo là tên của lớp.
  • Phương thức hủy không trả về bất cứ giá trị nào, kể cả void (giống như phương thức khởi tạo).
  • Phương thức hủy không nhận bất cứ thông số nào. Vậy nên chỉ có duy nhất 1 phương thức hủy mà không có phương thức nạp chồng của nó.

Nguyên mẫu của phương thức hủy trong ví dụ của chúng ta.

~NhanVat();

Và mã xử lý của phương thức này trong tệp nguồn.

NhanVat::~NhanVat(){
    /* Khong can lam gi ca vi khong co phan bo dong. 
       Thong thuong thi doan ma xu ly nay se chua cac phep delete 
       va thuc hien 1 vai xac nhan truoc khi huy doi tuong */
}

Như các bạn đã thấy là phương thức hủy mà tôi vừa tạo ra không thực hiện bất cứ xử lý nào, vậy nên không cần thiết phải tạo ra nó. Tuy nhiên chúng ta đã biết được cách để tạo phương thức hủy khi cần thiết. Các bạn sẽ thấy là các bạn sẽ rất nhanh chóng dùng đến phân bổ động và sẽ cần dùng đến phương thức hủy để giải phóng bộ nhớ.

Các phương thức hằng

Các phương thức hằng là các phương thức “chỉ đọc”. Chúng ta cần thêm từ khóa const vào cuối nguyên mẫu cũng như định nghĩa của phương thức này.

Khi bạn xác định 1 phương thức là phương thức hằng, bạn nói cho trình biên dịch là phương thức của bạn sẽ không thay đổi đối tượng, nghĩa là không thay đổi giá trị của bất cứ thuộc tính nào của đối tượng. Lấy ví dụ 1 phương thức dùng để hiển thị các thông tin về đối tượng ra màn hình thì sẽ là 1 phương thức hằng bởi vì nó chỉ thực hiện đọc giá trị của các thuộc tính. Trong khi đó, 1 phương thức có tác dụng thay đổi giá trị số điểm hp của 1 nhân vật thì không thể là phương thức hằng.

//Nguyen mau cua phuong thuc (trong tep .h)
void phuongThuc(int thongSo) const;
//Dinh nghia cua phuong thuc  (trong tep .cpp)
void TenLop::phuongThuc(int thongSo) const {
}

Chúng ta sử dụng từ khóa const trên những phương thức chỉ dùng để đọc thông tin mà không thay đổi đối tượng. Chúng ta có thể lấy ví dụ phương thức conSong() mà chúng ta đã viết để xác định xem nhân vật còn sống hay đã chết. Nó không thay đổi đối tượng mà chỉ hiển thị giá trị số điểm hp.

bool NhanVat::conSong() const{
    return m_hp > 0;
}   

! Trong khi đó phương thức nhanSatThuong() không thể được khai báo là phương thức hằng do nó thay đổi giá trị thuộc tính số điểm hp của nhân vật khi nhân vật này chịu sát thương.

Chúng ta có thể dễ dàng tìm ra những phương thức hằng khác. Ví dụ như phương thức size() của lớp string bởi vì phương thức này không thay đổi đối tượng mà chỉ trả về thông tin về độ dài của chuỗi.

? Lợi ích cụ thể của việc tạo ra phương thức hằng là gì ?

Có 3 lợi ích chính :

  • Cho các bạn : các bạn biết rằng phương thức chỉ đọc giá trị các thuôc tính và tự nhắc là không thể dùng nó để thay đổi các thuộc tính. Nếu bạn có nhầm lẫn và thay đổi 1 thuộc tính, trình biên dich sẽ báo lỗi cho bạn để kịp thời sửa chữa.
  • Cho người sử dụng lớp của bạn : việc này rất quan trọng với họ vì họ sẽ biết rằng phương thức chỉ trả về kết quả mà không thay đổi đối tượng. 1 từ khóa const sẽ là chỉ dẫn rõ ràng để nhận biết là phương thức không thể thay đổi đối tượng.
  • Cho trình biên dịch : giống như trong bài học về các biến, tôi khuyên các bạn nên thêm từ const vào bất cứ khi nào có thể. Chúng ta sẽ giúp trình biên dịch tạo ra mã nhị phân chất lượng tốt hơn.
Liên kết các lớp nhau

Lập trình hướng đối tượng trở nên rất thú vị và mạnh mẽ khi chúng ta kết hợp nhiều đối tượng lại với nhau. Hiện giờ thì chúng ta mới tạo ra 1 lớp là NhanVat. Thế nhưng trong thực tế, 1 chương trình bình thường được cấu thành từ nhiều đối tượng khác nhau.

Không có bí mật nào cả, mọi thứ đều được học tập từng tí một thông qua thực tiễn.

Phần tiếp theo không có gì mới cả. Chúng ta sẽ sử dụng những kiến thức mà chúng ta đã học để tạo ra nhiều lớp hơn nhằm hoàn thiện siêu cấp trò chơi RPG của chúng ta.

Lớp VuKhi

Tôi đề nghị là chúng ta sẽ tạo ra 1 lớp VuKhi để mô tả các loại vũ khí khác nhau thay vì trực tiếp lưu thông tin vào trong các thuộc tính của lớp NhanVat. Thế rồi chúng ta sẽ trang bị 1 vũ khí cho 1 nhân vật. Việc chia nhỏ chương trinh ra đã từng chút một có hơi hướng logic của lập trình hướng đối tượng.

! Các bạn có nhớ là tôi đã từng nói với lập trình hướng đối tượng, có cả trăm cách tiếp cận khác nhau để tạo ra cùng 1 chương trình. Điểm khác nhau là giữa chúng chính là sự kết hợp giữa các lớp, cách thức các lớp liên kết với nhau, vv…

Những gì mà chúng ta đã học được từ đầu đến giờ đã là không tệ rồi, nhưng tôi vẫn muốn chúng ta tiến thêm 1 bước vào cách suy nghĩ kiểu hướng đối tương.

Thêm 1 lớp mới đống nghĩa với thêm 2 tệp :

  • VuKhi.h : chứa khai báo của lớp.
  • VuKhi.cpp : chứa mã xử lý các phương thức của lớp.

! Mặc dù việc tách ra thêm 2 tệp này là không bắt buộc vì C++ cho phép chúng ta ghép nhiều lớp lại trong cùng 1 tệp nhưng vì lý do tổ chức mã nguồn cho gọn gàng, tôi đề nghị các bạn hãy làm như vừa rồi.

VuKhi.h

Đây là mã nguồn trong tệp VuKhi.h :

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

class VuKhi{
  public:
    VuKhi();
    VuKhi(std::string ten, int dmg);
    void thayDoi(std::string ten, int dmg);
    void inThongTin() const;

  private:
    std::string m_ten;
    int m_dmg;  //dmg la damage nghia la muc sat thuong
};

#endif

Đoạn mã bên trên khá đơn giản, đều là những kiến thức chúng ta đã nói qua chứ không có gì quá đặc biệt.

Tên và mức sát thương sẽ là các thuộc tính của lớp vũ khí. Các thuộc tính này đều có quyền truy cập private đúng như tính đóng gói. Trong tên của thuộc tính cũng đơn giản hơn lúc trước vì mọi người đều biết là chúng ta đang làm việc trong lớp VuKhi.

Tiếp sau đó, chúng ta sẽ thêm vào 1 hoặc 2 phương thức khởi tạo, rồi 1 phương thức cho phép thay đổi vũ khí, và cuối cùng là 1 phương thức để hiển thị chi tiết vũ khí.

Giờ chỉ cần thêm mã xử lý của từng phương thức là đủ.

VuKhi.cpp

Mã xử lý của những phương thức này cũng không quá phức tạp. Các bạn có thể tự viết mã của mình, sau đấy chúng ta hãy cùng so sánh.

Đây là mã nguồn của tệp VuKhi.cpp của tôi.

#include "VuKhi.h"
using namespace std;
VuKhi::VuKhi() : m_ten("Kiem go"), m_dmg(10){
}

VuKhi:: VuKhi(string ten, int dmg) : m_ten(ten), m_dmg(dmg){
}

void VuKhi::thayDoi(string ten, int dmg){
    m_ten = ten;
    m_dmg = dmg;
}

void VuKhi::inThongTin() const{
    cout << "Vu khi : " << m_ten<< " (Muc sat thuong : " << m_dmg << ")" << endl;
}

Đừng quên bao gồm thêm VuKhi.h ở đầu tệp.

Sau khi đã tạo ra lớp VuKhi, chúng ta cần thay đổi lớp NhanVat để phù hợp với những thay đổi vừa rồi. Thuộc tính của đối tượng NhanVat sẽ không còn là m_vuKhim_dmgVuKhi mà sẽ đổi thành 1 đối tượng của lớp VuKhi.

Thay đổi lớp NhanVat để sử dụng lớp VuKhi

Lớp NhanVat cần phải được thay đổi một chút mới có thể sử dụng được lớp VuKhi. Cần chú ý bởi vì sử dụng 1 đối tượng bên trong 1 đối tượng có đôi chút đặc biêt.

NhanVat.h

Chúng ta sẽ bắt đầu bằng việc bỏ đi 2 thuộc tính m_vuKhim_dmgVuKhi mà chúng ta không cần sử dụng đến nữa.

Ở trong trường hợp bây giờ phải thay đổi lớp NhanVat, chúng ta sẽ có thể thấy được lợi ích của tính đóng gói của lập trình hướng đối tượng. Bởi vì tất cả các thuộc tính đều có quyền truy cập private, chúng ta có thể thoái mái sửa đổi các thuộc tính mà không lo ngại ảnh hưởng tới người sử dụng lớp bởi vì chúng ta biết chắc là họ không thể thao tác trực tiếp với các thuộc tính này.

Trái lại cần chú ý hơn khi thay đổi các phương thức. Các bạn có thể thêm phương thức mới hoặc thay đổi mã xử lý của các phương thức có sẵn nhưng không nên xóa hoặc đổi tên các phương thức nếu không người dùng có thể gặp rắc rối với các thay đổi này.     

Nói chung, chúng ta không cần và cũng không nên thay đổi nguyên mẫu các phương thức có sẵn. Lý do là tại vì có thể ai đó khác đã sử dụng những phương thức đó trong mã nguồn của họ, trong main() chẳng hạn. Nếu chúng ta thay đổi hoặc xóa những phương thức trên, chương trình sẽ không hoạt động nữa.

Các bạn sẽ thấy là những thay đổi có thể không ảnh hưởng nhiều đến những chương trình nhỏ nhưng sẽ trở thành thảm họa với mức độ tỉ lệ thuận với độ lớn của chương trình. Đấy là chưa kể đến trường hợp những thư viện mã mà tất cả mọi người đều dùng như thư viện của C++. Giả sử có 1 phương thức bị loại bỏ sau 1 bản nâng cấp của thư viện này, tất cả những chương trình sử dụng thư viện có thể không hoạt động nữa.

Quay lại với lớp NhanVat, sau khi ghi nhớ những nhắc nhở bên trên, chúng ta có thể thêm vào lớp NhanVat 1 đối tượng của lớp VuKhi.

! Nếu muốn sử dụng đối tượng của lớp VuKhi, đừng quên bao gồm tệp VuKhi.h

Đây là mã nguồn của VuKhi.h mà tôi đã viết.

#ifndef DEF_NHANVAT
#define DEF_NHANVAT
#include <iostream>
#include <string>
#include "VuKhi.h" //Dung quen them dong nay de su dung lop VuKhi

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

  private:
    int m_hp;
    int m_mp;
    VuKhi m_vuKhi; //Nhan vat so huu 1 vu khi
};           

#endif

NhanVat.cpp

Chúng ta cần thay đổi cho phù hợp mã xử lý của tất cả các phương thức có liên quan đến thuộc tính vũ khí.

Hãy bắt đầu với phương thức khởi tạo.

NhanVat::NhanVat() : m_hp(100), m_mp(100){
}

NhanVat::NhanVat(string vuKhi, int dmgVuKhi) : m_hp(100), m_mp(100), m_vuKhi(vuKhi, dmgVuKhi){
}

Đối tượng m_vuKhi được khởi tạo với các thông số được truyền cho phương thức khởi tạo của NhanVat. Lợi ích của việc khởi tạo sử dụng danh sách được thể hiện ra ở đây. Trên thực thế, chúng ta sẽ không thể khởi tạo được m_vuKhi nếu không sử dụng các khởi tạo sử dụng danh sách.

Có thể các bạn vẫn chưa thấy được lý do như tôi nói bên trên. Tuy nhiên đừng quá mất thời gian để cố gắng tìm hiểu. Chỉ cần ghi nhớ là sử dụng danh sách giá trị để khởi tạo sẽ giúp chúng ta tránh được rất nhiều vấn đề không cần thiết.

Quay trở lại với đoạn mã. Phương thức khởi tạo đầu tiên là phương thức khởi tạo mặc định trong khi phương thức khởi tạo thứ 2 cần 2 thông số có kiểu lần lượt là stringint.

Phương thức nhanSatThuong thì không cần phải thay đổi còn phương thức tanCong cần 1 chút sửa đổi nhất định.

Trên thực tế, chúng ta không thể viết như sau :

void NhanVat::tanCong(NhanVat &mucTieu){
    mucTieu.nhanSatThuong(m_vuKhi.m_dmg);
}

Lý do là bởi m_dmg là 1 thuộc tính của VuKhi, và nếu tính đóng gói được đảm bảo thì quyền truy cập của nó sẽ là private. Chúng ta đang sử dụng lớp VuKhi ở bên trong lớp NhanVat. Và như vậy nghĩa là chúng ta chỉ là người dùng bên ngoài của lớp VuKhi nên không thể truy cập trực tiếp vào các thuộc tính của nó.

Làm thế nào để giải quyết được vấn đề này ? Thật ra chúng ta không có quá nhiều lựa chọn. Các bạn hẳn sẽ rất ngạc nhiên nhưng chúng ta cần tạo thêm 1 phương thức trong VuKhi để cho phép lấy ra giá trị của thuộc tính này. Phương thức này được gọi là phương thức lấy (getter) dùng để lấy ra giá trị của 1 thuộc tính. Tên của phương thức này thường bắt đầu bằng chữ « get », như trong trường hợp của chúng ta là getDmg.

Người ta cũng thường thêm từ khóa const vào phương thức này vì nó không thay đổi đối tượng.

int VuKhi::getDmg() const{
    return m_dmg;
}

Đừng quên thêm nguyên mẫu của hàm này vào VuKhi.h.

int getDmg() const ;

Nghe có vẻ ngớ ngẩn nhưng đây lại là những bảo mật cần thiết. Đôi khi chúng ta cần phải thêm vào 1 phương thức chỉ có mỗi 1 lệnh return để có thể truy cập gián tiếp tới thuộc tính.

! Bên cạnh các phương thức lấy, còn có các phương thức đặt (setter) dùng để thay đổi giá trị các thuộc tính. Tên của phương thức này thường bắt đầu bằng chữ « set ».

Các bạn sẽ có cảm giác là chúng ta vi phạm tính đóng gói của OOP nhưng thật ra không phải thế. Các phương thức cho phép thực hiện các kiểm tra cần thiết để chắc rằng chúng ta không gán 1 giá trị không hợp lệ cho thuộc tính. Vậy nên đó được coi là 1 cách an toàn để thay đổi 1 thuộc tính.

Quay lại với tệp NhanVat.cpp của chúng ta.

void NhanVat::tanCong(NhanVat &mucTieu){
    mucTieu.nhanSatThuong(m_vuKhi.getDmg());
}

getDmg trả về kết quả là mức sát thương của vũ khí và trở thành thông số cho hàm nhanSatThuong của mục tiêu.

Các phương thức còn lại không cần phải thay đổi gì cả trừ phương thức doiVuKhi của lớp NhanVat.

void NhanVat::doiVuKhi(string vuKhiMoi, int dmgVuKhiMoi){
    m_vuKhi.thayDoi(vuKhiMoi, dmgVuKhiMoi);
}

NhanVat sử dụng phương thức thayDoi của thuộc tính m_vuKhi khi nhận được yêu cầu đổi vũ khí từ người dùng.

Như các bạn đã thấy, nếu được tổ chức tốt chúng ta có thể khiến các đối tượng liên hệ với nhau một cách dễ dàng. Một điều duy nhất cần luôn luôn chú ý là xem chúng ta có quyền truy cập vào thành phần mà chúng ta muốn thao tác không. Đừng ngại tạo ra thêm các phương thức lấy và phương thức đặt để thao tác với các thuộc tính. Có thể các bạn sẽ nghĩ là cấu trúc chương trình sẽ trở nên nặng nề, thế nhưng đây là bảo mật cần thiết. ĐỪNG bao giờ trao quyền truy cập public cho 1 thuộc tính dù bạn nghĩ rằng việc đó đơn giản hóa vấn đề của bạn vì nếu làm như thế, các bạn đã đánh mất tinh thần của lập trình hướng đối tượng, cũng đồng nghĩa với việc không cần thiết phải tiếp tục học C++ làm gì nữa.

Chạy thử nghiệm chương trình

Các nhân vật của chúng ta đang chiến đầu với nhau trong hàm main(), thế nhưng chúng ta lại chẳng quan sát được gì cả. Tốt nhất là nên có 1 phương thức hiển thị trạng thái hiện thời của từng nhân vật.

Tôi đề nghị chúng ta thêm phương thức inTrangThai cho lớp NhanVat. Phương thức này sẽ phụ trách việc in ra console trạng thái hiện tại của đối tượng.

Như vậy, trong tệp .h, chúng ta cần thêm nguyên mẫu:

void inTrangThai() const;

Mã xử lý của phương thức này càng thêm đơn giản, chúng ta chỉ cần sử dụng cout. Trạng thái của nhân vật chính là giá trị của các thuộc tính của nhân vật đó.

void NhanVat::inTrangThai() const {
    cout << "HP : " << m_hp << endl;
    cout << "MP : " << m_mp << endl;
    m_vuKhi.inThongTin();
}  

Như các bạn thấy, thông tin về vũ khí được yêu cầu thông qua phương thức inThongTin() của đối tượng m_vuKhi. Lại 1 lần nữa các đối tượng giao tiếp với nhau để trao đổi thông tin.

Phương thức để hiển thị thông tin đã hoàn thành, chỉ còn cần sử dụng nó trong hàm main() và chúng ta có thể quan sát được diễn biến cuộc chiến. Tôi đề nghị chúng ta sẽ cùng thêm đoạn mã gọi phương thức này vào để hoàn thành hàm main().

int main(){
    NhanVat david, goliath("Kiem sat",20);
    //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);

    //Quan sat trang thai cua tung nhan vat
    cout << "David" << endl;
    david.inTrangThai();
    cout << endl << "Goliath" << endl;
    goliath.inTrangThai();
    return 0;
}

Hãy cùng chạy thử chương trình và quan sát kết quả trong console.

Để có thể thấy rõ hơn tổng thể cấu trúc của cả chương trình, tôi đề nghị các bạn hãy tải mã nguồn trong tệp nén dưới đây. Nội dụng tệp nén bao gồm :

  • main.cpp
  • NhanVat.cpp
  • NhanVat.h
  • VuKhi.cpp
  • VuKhi.h

Tải mã nguồn 

Để tiếp tục luyện tập, tôi đề nghị các bạn có thể tự mình bổ sung 1 vài cải tiến tính năng cho chương trình của chúng ta. Như thường lệ, tôi đưa ra 1 vài ý tưởng của bản thân để các bạn tham khảo, nhưng đừng để chúng trở thành trói buộc cho ý tưởng của chính các bạn.

  • Tiếp tục cuộc chiến của davidgoliath, hiển thị dần dần diễn biến cuộc chiến ra màn hình.
  • Thêm 1 đối thủ vào trường đấu để khiến cuộc chiến thêm phần thú vị.
  • Thêm vào lớp NhanVat thuộc tính m_ten để lưu thông tin về tên nhân vật. Thật vui là từ trước đến giờ, các nhân vật của chúng ta thậm chí còn không biết mình tên là gì cool. Chuẩn bị tâm lý phải sửa lại phương thức khởi tạo để yêu cầu người dùng nhập tên cho nhân vật.
  • Thêm vào các lệnh cout để hiển thị trạng thái nhân vật mỗi khi có 1 hành động được diễn ra.
  • Thêm các phương thức mô tả các tấn công bằng phép và sử dụng MP.
  • Cuối cùng, cho phép người chơi điều khiển nhân vât, chọn nhân vật mục tiêu và cách thức tấn công người chơi muốn sử dụng thông qua cin.

Đừng coi nhẹ bài tập này. Nó có thể là nền tảng cho siêu cấp trò chơi RPG của bạn trong tương lai.

! Cần chú ý là ở đây, tôi không hướng dẫn bạn thực hiện 1 trò chơi MMORPG (Massively Multiplayer Online Role Playing Games – trò chơi nhập vai trực tuyến nhiều người chơi) hoàn chỉnh bởi vì nó khá phức tạp và đòi hỏi 1 lượng công việc khổng lồ. Hãy bắt đầu với những dự án nhỏ và khả thi hơn như trò chơi RPG của chúng ta. Quan trọng là các bạn nắm vững được nguyên lý của OOP. Nếu bạn muốn, có thể thử bắt đầu quan sát và phân tích tất cả các thành phần của mỗi trò chơi mà bạn đang chơi. Ví dụ, 1 tòa nhà trong Warcraft có những thuộc tính như 1 số lượng HP nhất định, 1 cái tên và khả năng tạo ra các nhân vật lính thông qua 1 phương thức, vv…

Nếu bạn bắt đầu thấy đối tượng ở khắp mọi nơi, điều đó có nghĩa là bạn đã nắm bắt được tinh thần của lập trình hướng đối tượng.

Sơ đồ tóm tắt

Thật ra tôi không yêu cầu các bạn phải ngay lập tức tự viết được những đoạn mã như chúng ta đã cùng thảo luận ở trên. Cái quan trọng là các bạn nắm bắt được nguyên lý cốt lõi và cách tiếp cận vấn đề của lập trình hương đối tượng.

Để giúp các bạn ghi nhớ tốt hơn, tôi đã thực hiện 1 sơ đồ tóm tắt những thứ chúng ta đã thực hiện trong 2 bài học gần đây. Hãy nhớ kỹ nó trong đầu nếu có thể.

Tóm tắt bài hoc :
  • Phương thức khởi tạo là phương thức được tự động gọi khi 1 đối tượng được tạo ra. Còn phương thức hủy là phương thức được gọi khi xóa đối tượng.
  • Chúng ta có thể nạp chồng phương thức khởi tạo, nhờ đó tạo ra nhiều phương thức khởi tạo khác nhau, cho phép chúng ta tạo ra đối tượng theo nhiều cách khác nhau.
  • Phương thức hằng là phương thức không làm thay đổi đối tượng, nghĩa là các thuộc tính của đối tượng không bị thay đổi bởi phương thức.
  • Bởi vì tính đóng gói bảo vệ các thuộc tính, chúng ta cần sử dụng các phương thức lấy và phương thức đặt để truy cập gián tiếp tới thuộc tính.
  • Đối tượng có thể có thuộc tính là 1 đối tượng khác.
  • Lập trình hướng đối tượng giúp đoạn mã được sắp xếp có hệ thông hơn và dễ dàng được tái sử dụng.