Bài 38.1: Bài tập thực hành lập trình hướng đối tượng cơ bản trong C++

Chào mừng các bạn đã quay trở lại với hành trình khám phá C++ cùng FullhouseDev!

Sau khi đã cùng nhau tìm hiểu về các khái niệm lý thuyết nền tảng của Lập trình hướng đối tượng (OOP), đã đến lúc chúng ta phải xắn tay áo lên và thực hành thật nhiều để biến lý thuyết thành kỹ năng của mình. Bài viết này sẽ tập trung vào các bài tập thực hành cơ bản nhất, giúp bạn củng cố kiến thức về Class, Object, Attributes, MethodsConstructors.

Hãy nhớ, cách tốt nhất để học lập trình là tự mình viết codethử nghiệm. Đừng ngần ngại gõ lại các ví dụ, chỉnh sửa chúng và xem kết quả thay đổi như thế nào nhé!

1. Nhắc Lại Nhanh Các Khái Niệm Cốt Lõi

Trước khi đi sâu vào code, hãy cùng điểm lại những khái niệm không thể thiếu trong OOP:

  • Class: Coi nó như một bản thiết kế hay một khuôn mẫu. Nó định nghĩa các thuộc tính (dữ liệu) và phương thức (hành động) mà các đối tượng được tạo ra từ nó sẽ có.
  • Object: Là một thể hiện cụ thể (instance) của một Class. Bạn tạo ra Object từ Class và làm việc với chính Object đó. Mỗi Object có tập dữ liệu riêng biệt dựa trên thiết kế của Class.
  • Attributes (Thuộc tính): Còn gọi là biến thành viên (member variables). Đây là các dữ liệu, các đặc điểm mô tả trạng thái của Object (ví dụ: tên, tuổi, màu sắc, kích thước...).
  • Methods (Phương thức): Còn gọi là hàm thành viên (member functions). Đây là các hành động, các chức năng mà Object có thể thực hiện (ví dụ: đi, nói, chạy, tính toán...).
  • Encapsulation (Đóng gói): Nguyên lý nhóm dữ liệu và các phương thức xử lý dữ liệu đó vào cùng một đơn vị (Class). Kết hợp với các chỉ định truy cập (public, private, protected), Đóng gói giúp kiểm soát quyền truy cập vào dữ liệu, bảo vệ dữ liệu khỏi bị thay đổi trái phép từ bên ngoài. Trong thực hành cơ bản, chúng ta thường để thuộc tính là private và phương thức là public (hoặc private cho các phương thức nội bộ).

2. Bài Tập 1: Thiết Kế và Sử Dụng Class Đơn Giản

Bài tập đầu tiên là tạo một Class đơn giản, định nghĩa các thuộc tính và phương thức, sau đó tạo một Object và sử dụng các phương thức của nó.

Chúng ta sẽ tạo một Class tên là Dog (Chú Chó).

Bước 1: Định Nghĩa Class Dog

Một chú chó có thể có tên và giống loài. Nó có thể sủa và hiển thị thông tin của mình.

#include <iostream>
#include <string>

using namespace std;

class Dog {
private:
    string ten;
    string giong;

public:
    void datTen(string t) {
        ten = t;
    }

    void datGiong(string g) {
        giong = g;
    }

    void sua() {
        cout << ten << " nói: Gâu gâu!" << endl;
    }

    void hienThi() {
        cout << "Tên: " << ten << ", Giống loài: " << giong << endl;
    }
};

Giải thích code:

  • Chúng ta khai báo Class Dog bằng từ khóa class.
  • Phần private: chứa các thuộc tính (_name, _breed). Dữ liệu ở đây chỉ có thể được truy cập và thay đổi bởi các phương thức bên trong Class Dog. Đây là một phần của nguyên lý Đóng gói.
  • Phần public: chứa các phương thức (setName, setBreed, bark, displayInfo). Các phương thức này có thể được gọi từ bên ngoài Class, thông qua một Object của Class Dog.
  • Các phương thức setNamesetBreed nhận dữ liệu từ bên ngoài (tham số) và gán vào các thuộc tính private tương ứng.
  • Phương thức barkdisplayInfo sử dụng dữ liệu từ các thuộc tính private để thực hiện hành động của chúng.
Bước 2: Tạo Object và Sử Dụng Class Dog

Bây giờ chúng ta sẽ tạo ra một hoặc nhiều chú chó từ bản thiết kế Dog Class và gọi các phương thức của chúng.

#include <iostream>
#include <string>
// Định nghĩa class Dog đã được sửa đổi ở trên.

using namespace std;

int main() {
    Dog cho1;

    cho1.datTen("Buddy");
    cho1.datGiong("Golden Retriever");

    cho1.hienThi();
    cho1.sua();

    cout << "\n--- Tạo một chú chó khác ---" << endl;

    Dog cho2;
    cho2.datTen("Lucy");
    cho2.datGiong("Poodle");

    cho2.hienThi();
    cho2.sua();

    return 0;
}

Output:

Tên: Buddy, Giống loài: Golden Retriever
Buddy nói: Gâu gâu!

--- Tạo một chú chó khác ---
Tên: Lucy, Giống loài: Poodle
Lucy nói: Gâu gâu!

Giải thích code:

  • Trong hàm main(), dòng Dog myDog; tạo ra một Object có tên myDog thuộc Class Dog. Lúc này, myDog đã có cấu trúc (các thuộc tính và phương thức) theo định nghĩa của Class Dog.
  • Chúng ta sử dụng toán tử dấu chấm (.) để truy cập và gọi các phương thức public của Object myDog (myDog.setName(...), myDog.displayInfo(), myDog.bark()).
  • Khi gọi myDog.setName("Buddy");, phương thức setName bên trong Object myDog được thực thi, và thuộc tính _name của riêng Object myDog được gán giá trị "Buddy".
  • Tương tự, Dog anotherDog; tạo ra một Object hoàn toàn độc lập với myDog. Khi gọi anotherDog.setName("Lucy");, thuộc tính _name của riêng Object anotherDog mới được gán giá trị "Lucy".

Bạn có thể thấy, mỗi Object (myDog, anotherDog) có dữ liệu (tên, giống loài) riêng của nó, nhưng đều có cùng các phương thức (bark, displayInfo) được định nghĩa trong Class Dog. Đây chính là ý tưởng cơ bản của OOP: quản lý dữ liệu theo từng đối tượng cụ thể.

3. Bài Tập 2: Sử Dụng Constructors Để Khởi Tạo Object

Trong Bài tập 1, chúng ta tạo Object và sau đó phải gọi riêng từng phương thức set để thiết lập giá trị ban đầu cho các thuộc tính. Cách này hoạt động, nhưng có thể không hiệu quả hoặc không an toàn nếu bạn quên gọi một setter nào đó.

Constructors ra đời để giải quyết vấn đề này. Constructor là một loại phương thức đặc biệt, tự động được gọi khi một Object được tạo ra. Nó được dùng để khởi tạo trạng thái ban đầu cho Object (thiết lập giá trị cho các thuộc tính).

Đặc điểm của Constructor:

  • Tên của Constructor phải giống hệt tên Class.
  • Constructor không có kiểu dữ liệu trả về, kể cả void.

Chúng ta sẽ thêm Constructors vào Class Dog.

Bước 1: Thêm Constructors vào Class Dog

Có hai loại Constructor phổ biến:

  • Default Constructor: Không có tham số. Được gọi khi tạo Object không có đối số (ví dụ: Dog myDog;). Thường dùng để gán giá trị mặc định.
  • Parameterized Constructor: Có tham số. Được gọi khi tạo Object với các đối số (ví dụ: Dog myDog("Buddy", "Golden Retriever");). Dùng để khởi tạo Object với các giá trị cụ thể ngay lúc tạo.
#include <iostream>
#include <string>

using namespace std;

class Dog {
private:
    string ten;
    string giong;

public:
    Dog() {
        ten = "Chó không tên";
        giong = "Lai";
        cout << "Chó mới được tạo (constructor mặc định)." << endl;
    }

    Dog(string t, string g) {
        ten = t;
        giong = g;
        cout << "Chó mới được tạo (constructor tham số)." << endl;
    }

    void datTen(string t) { ten = t; }
    string layTen() const { return ten; }

    void datGiong(string g) { giong = g; }
    string layGiong() const { return giong; }

    void sua() {
        cout << ten << " nói: Gâu gâu!" << endl;
    }

    void hienThi() const {
        cout << "Tên: " << ten << ", Giống loài: " << giong << endl;
    }
};

Giải thích code:

  • Chúng ta đã thêm hai phương thức mới có tên trùng với tên Class: Dog(), và Dog(string name, string breed).
  • Dog() là default constructor. Khi gọi, nó gán giá trị "Unnamed Dog" và "Mixed Breed" cho các thuộc tính.
  • Dog(string name, string breed) là parameterized constructor. Nó nhận hai tham số và sử dụng chúng để khởi tạo _name_breed.
  • Lưu ý rằng nếu bạn tự định nghĩa bất kỳ constructor nào (default hoặc parameterized), compiler sẽ không tự động tạo default constructor mặc định nữa. Nếu bạn muốn có cả hai, bạn phải định nghĩa rõ cả hai.
  • Đã thêm các phương thức getter (getName, getBreed) để cho phép truy xuất giá trị của thuộc tính private từ bên ngoài một cách có kiểm soát. Từ khóa const sau tên phương thức getter cho biết phương thức này không thay đổi trạng thái (dữ liệu) của Object.
  • Phương thức displayInfo cũng được thêm const vì nó chỉ đọc dữ liệu mà không ghi đè lên chúng.
Bước 2: Tạo Object Sử Dụng Constructors

Bây giờ, hãy xem cách tạo Object và Constructor nào được gọi.

#include <iostream>
#include <string>
// Định nghĩa class Dog đã được sửa đổi ở trên.

using namespace std;

int main() {
    Dog c1;

    cout << "\nThông tin của c1 (sau khi tạo):" << endl;
    c1.hienThi();
    c1.sua();

    cout << "\n--- Tạo một chú chó khác ---" << endl;

    Dog c2("Max", "German Shepherd");

    cout << "\nThông tin của c2 (sau khi tạo):" << endl;
    c2.hienThi();
    c2.sua();

    cout << "\n--- Cập nhật thông tin sau khi tạo ---" << endl;

    c1.datTen("Rex");
    c1.datGiong("Bulldog");
    cout << "\nThông tin của c1 (sau khi cập nhật):" << endl;
    c1.hienThi();

    cout << "Tên của c2 là: " << c2.layTen() << endl;

    return 0;
}

Output:

Chó mới được tạo (constructor mặc định).

Thông tin của c1 (sau khi tạo):
Tên: Chó không tên, Giống loài: Lai
Chó không tên nói: Gâu gâu!

--- Tạo một chú chó khác ---
Chó mới được tạo (constructor tham số).

Thông tin của c2 (sau khi tạo):
Tên: Max, Giống loài: German Shepherd
Max nói: Gâu gâu!

--- Cập nhật thông tin sau khi tạo ---

Thông tin của c1 (sau khi cập nhật):
Tên: Rex, Giống loài: Bulldog
Tên của c2 là: Max

Giải thích code:

  • Dòng Dog dog1; gọi Default Constructor, gán giá trị mặc định cho _name_breed của dog1.
  • Dòng Dog dog2("Max", "German Shepherd"); gọi Parameterized Constructor, gán giá trị "Max" cho _name và "German Shepherd" cho _breed của dog2.
  • Các thông báo "Một object Dog được tạo..." sẽ xuất hiện ngay khi mỗi object được tạo ra, chứng minh rằng Constructor đã được gọi.
  • Sau khi tạo, bạn vẫn có thể sử dụng các phương thức setter (như setName, setBreed) để thay đổi trạng thái của Object nếu cần.
  • Các phương thức getter (getName, getBreed) cho phép chúng ta lấy dữ liệu từ các thuộc tính private mà không cần truy cập trực tiếp vào chúng, tuân thủ nguyên tắc Đóng gói.

4. Thử Thách Cho Bạn!

Để thực sự nắm vững kiến thức, hãy thử các bài tập mở rộng sau:

  1. Tạo Class Book:

    • Có các thuộc tính private: _title (tiêu đề), _author (tác giả), _isbn (mã ISBN), _isAvailable (boolean, sách còn hay không).
    • Viết Default Constructor và Parameterized Constructor.
    • Viết các phương thức settergetter cho các thuộc tính (đặc biệt với _isAvailable, có thể chỉ cần borrowBook()returnBook()).
    • Viết phương thức displayBookInfo() để in ra tất cả thông tin của sách.
    • Trong main(), tạo vài object Book sử dụng các constructor khác nhau, gọi các phương thức để mượn/trả sách và hiển thị thông tin.
  2. Tạo Class BankAccount:

    • Có các thuộc tính private: _accountNumber (số tài khoản - string), _accountHolderName (tên chủ tài khoản), _balance (số dư - double).
    • Viết Parameterized Constructor để khởi tạo số tài khoản, tên chủ tài khoản và số dư ban đầu.
    • Viết các phương thức public: deposit(double amount) (nạp tiền), withdraw(double amount) (rút tiền - cần kiểm tra số dư), getBalance() (lấy số dư).
    • Trong main(), tạo một object BankAccount, thực hiện vài thao tác nạp/rút tiền và kiểm tra số dư sau mỗi lần.

5. Tại Sao Phải Thực Hành Nhiều Đến Vậy?

Lý thuyết rất quan trọng, nhưng chỉ đọc và hiểu thôi là chưa đủ. Khi bạn tự mình gõ từng dòng code, bạn sẽ:

  • Ghi nhớ tốt hơn: Việc lặp lại các cấu trúc cú pháp giúp bạn quen thuộc với ngôn ngữ.
  • Hiểu sâu sắc hơn: Bạn sẽ thấy rõ cách các khái niệm tương tác với nhau trong thực tế.
  • Phát hiện lỗi: Bạn sẽ gặp lỗi cú pháp, lỗi logic và học cách debug (tìm và sửa lỗi), một kỹ năng thiết yếu của lập trình viên.
  • Xây dựng trực giác: Dần dần, bạn sẽ có cảm giác tự nhiên về cách thiết kế Class, cách đặt tên thuộc tính/phương thức, và cách chia nhỏ vấn đề thành các đối tượng.

Comments

There are no comments at the moment.