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++

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, Methods và Constructors.
Hãy nhớ, cách tốt nhất để học lập trình là tự mình viết code và thử 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ặcprivate
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> // Cần include để sử dụng kiểu dữ liệu string
class Dog {
private:
// Các thuộc tính (dữ liệu) của chú chó
string _name; // Tên chú chó (thường dùng _ cho private members)
string _breed; // Giống loài
public:
// Các phương thức (hành động) mà chú chó có thể thực hiện
// Phương thức setter để đặt tên
void setName(string name) {
_name = name; // Gán giá trị từ tham số vào thuộc tính _name
}
// Phương thức setter để đặt giống loài
void setBreed(string breed) {
_breed = breed; // Gán giá trị từ tham số vào thuộc tính _breed
}
// Phương thức để chú chó sủa
void bark() {
cout << _name << " nói: Gâu gâu!" << endl; // Truy cập thuộc tính _name
}
// Phương thức để hiển thị thông tin chú chó
void displayInfo() {
cout << "Tên: " << _name << ", Giống loài: " << _breed << endl; // Truy cập cả _name và _breed
}
}; // Kết thúc định nghĩa class bằng dấu chấm phẩy
Giải thích code:
- Chúng ta khai báo Class
Dog
bằng từ khóaclass
. - 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 ClassDog
. Đâ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 ClassDog
. - Các phương thức
setName
vàsetBreed
nhận dữ liệu từ bên ngoài (tham số) và gán vào các thuộc tínhprivate
tương ứng. - Phương thức
bark
vàdisplayInfo
sử dụng dữ liệu từ các thuộc tínhprivate
để 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>
// Giả định bạn đã có định nghĩa class Dog ở trên, hoặc đặt nó cùng file
int main() {
// *** Tạo một Object của class Dog ***
// Cú pháp: Tên_Class Tên_Object;
Dog myDog;
// *** Sử dụng các phương thức public để tương tác với object ***
// Dùng toán tử . để truy cập các thành viên public
myDog.setName("Buddy"); // Gọi phương thức setName() của object myDog
myDog.setBreed("Golden Retriever"); // Gọi phương thức setBreed()
// Gọi các phương thức hành động
myDog.displayInfo(); // Output: Tên: Buddy, Giống loài: Golden Retriever
myDog.bark(); // Output: Buddy nói: Gâu gâu!
cout << "\n--- Tạo một chú chó khác ---" << endl;
// *** Tạo thêm một Object khác ***
Dog anotherDog;
anotherDog.setName("Lucy");
anotherDog.setBreed("Poodle");
anotherDog.displayInfo(); // Output: Tên: Lucy, Giống loài: Poodle
anotherDog.bark(); // Output: Lucy nói: Gâu gâu!
return 0;
}
Giải thích code:
- Trong hàm
main()
, dòngDog myDog;
tạo ra một Object có tênmyDog
thuộc ClassDog
. 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 ClassDog
. - Chúng ta sử dụng toán tử dấu chấm (
.
) để truy cập và gọi các phương thứcpublic
của ObjectmyDog
(myDog.setName(...)
,myDog.displayInfo()
,myDog.bark()
). - Khi gọi
myDog.setName("Buddy");
, phương thứcsetName
bên trong ObjectmyDog
được thực thi, và thuộc tính_name
của riêng ObjectmyDog
đượ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ớimyDog
. Khi gọianotherDog.setName("Lucy");
, thuộc tính_name
của riêng ObjectanotherDog
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>
class Dog {
private:
string _name;
string _breed;
public:
// *** Default Constructor ***
// Không có tham số. Được gọi khi: Dog myDog;
Dog() {
_name = "Unnamed Dog"; // Gán tên mặc định
_breed = "Mixed Breed"; // Gán giống mặc định
cout << "Một object Dog được tạo (default constructor)." << endl;
}
// *** Parameterized Constructor ***
// Có tham số để nhận tên và giống loài khi tạo object
// Được gọi khi: Dog myDog("Buddy", "Golden Retriever");
Dog(string name, string breed) {
_name = name; // Gán tên từ tham số
_breed = breed; // Gán giống từ tham số
cout << "Một object Dog được tạo (parameterized constructor)." << endl;
}
// Các phương thức setter và getter (vẫn giữ lại vì có thể cần thay đổi thông tin sau này)
void setName(string name) { _name = name; }
string getName() const { return _name; } // Thêm getter để lấy tên
void setBreed(string breed) { _breed = breed; }
string getBreed() const { return _breed; } // Thêm getter để lấy giống
// Phương thức để chú chó sủa
void bark() {
cout << _name << " nói: Gâu gâu!" << endl;
}
// Phương thức để hiển thị thông tin chú chó
void displayInfo() const { // Thêm const vì phương thức này không thay đổi trạng thái object
cout << "Tên: " << _name << ", Giống loài: " << _breed << 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
và_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ínhprivate
từ bên ngoài một cách có kiểm soát. Từ khóaconst
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êmconst
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>
// Sử dụng lại định nghĩa class Dog có constructors từ ví dụ trên
int main() {
// *** Tạo một Object sử dụng Default Constructor ***
// Cú pháp: Tên_Class Tên_Object;
Dog dog1; // Gọi Dog()
cout << "\nThông tin của dog1 (sau khi tạo):" << endl;
dog1.displayInfo(); // Output: Tên: Unnamed Dog, Giống loài: Mixed Breed
dog1.bark();
cout << "\n--- Tạo một chú chó khác ---" << endl;
// *** Tạo một Object sử dụng Parameterized Constructor ***
// Cú pháp: Tên_Class Tên_Object(đối_số, ...);
Dog dog2("Max", "German Shepherd"); // Gọi Dog(string, string)
cout << "\nThông tin của dog2 (sau khi tạo):" << endl;
dog2.displayInfo(); // Output: Tên: Max, Giống loài: German Shepherd
dog2.bark();
cout << "\n--- Cập nhật thông tin sau khi tạo ---" << endl;
// Vẫn có thể sử dụng setters để thay đổi thông tin sau khi object đã được tạo
dog1.setName("Rex");
dog1.setBreed("Bulldog");
cout << "\nThông tin của dog1 (sau khi cập nhật):" << endl;
dog1.displayInfo(); // Output: Tên: Rex, Giống loài: Bulldog
// Sử dụng getter để lấy thông tin
cout << "Tên của dog2 là: " << dog2.getName() << endl; // Output: Tên của dog2 là: Max
return 0;
}
Giải thích code:
- Dòng
Dog dog1;
gọi Default Constructor, gán giá trị mặc định cho_name
và_breed
củadog1
. - Dòng
Dog dog2("Max", "German Shepherd");
gọi Parameterized Constructor, gán giá trị "Max" cho_name
và "German Shepherd" cho_breed
củadog2
. - 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ínhprivate
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:
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
setter
vàgetter
cho các thuộc tính (đặc biệt với_isAvailable
, có thể chỉ cầnborrowBook()
và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 objectBook
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.
- Có các thuộc tính
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 objectBankAccount
, 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.
- Có các thuộc tính
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