Bài 34.2: Bài tập thực hành Struct nâng cao trong C++

Bài 34.2: Bài tập thực hành Struct nâng cao trong C++
Chào mừng trở lại với chuỗi bài viết về C++! Sau khi đã làm quen với những khái niệm cơ bản của Struct, trong bài viết này, chúng ta sẽ tập trung vào việc thực hành các ứng dụng nâng cao hơn của Struct thông qua các ví dụ cụ thể. Đây là cơ hội tuyệt vời để củng cố kiến thức và khám phá sức mạnh thực sự của việc tổ chức dữ liệu bằng Struct trong các tình huống phức tạp hơn.
Chúng ta sẽ cùng nhau đi qua một số bài tập minh họa, bao gồm:
- Sử dụng Struct lồng nhau (Nested Struct).
- Thêm hàm thành viên (member functions) vào Struct.
- Làm việc với con trỏ tới Struct và cấp phát động.
- Sử dụng Struct trong các cấu trúc dữ liệu như mảng hoặc vector.
- Truyền và trả về Struct từ hàm.
Hãy bắt đầu nào!
1. Struct Lồng Nhau (Nested Struct)
Đôi khi, dữ liệu của một đối tượng có thể bao gồm các thông tin chi tiết mà bản thân chúng cũng có cấu trúc riêng. Lúc này, nested struct là một giải pháp thanh lịch.
Bài tập minh họa: Định nghĩa một Struct DiaChi
và sử dụng nó làm thành viên bên trong một Struct NhanVien
.
#include <iostream>
#include <string>
using namespace std;
struct DC {
string so;
string duong;
string tp;
};
struct NV {
int ma;
string ten;
DC dc;
};
int main() {
NV a;
a.ma = 101;
a.ten = "Nguyen Van A";
a.dc.so = "123";
a.dc.duong = "Le Loi";
a.dc.tp = "Ha Noi";
cout << "Thong tin NV:" << endl;
cout << "Ma: " << a.ma << endl;
cout << "Ten: " << a.ten << endl;
cout << "Dia chi: " << a.dc.so << " "
<< a.dc.duong << ", " << a.dc.tp << endl;
return 0;
}
Thong tin NV:
Ma: 101
Ten: Nguyen Van A
Dia chi: 123 Le Loi, Ha Noi
Giải thích:
- Chúng ta định nghĩa
DiaChi
trước. - Bên trong
NhanVien
, chúng ta khai báo một thành viên có kiểu làDiaChi
với têndiaChi
. - Khi truy cập các thành viên của
DiaChi
thông qua đối tượngNhanVien
, chúng ta sử dụng toán tử chấm (.
) hai lần:nv1.diaChi.soNha
. Điều này thể hiện việc truy cập thành viênsoNha
của structdiaChi
, là thành viên của structnv1
. - Cách tiếp cận này giúp tổ chức dữ liệu một cách rõ ràng và có cấu trúc hơn.
2. Thêm Hàm Thành Viên (Member Functions) vào Struct
Trong C++, Struct có thể chứa hàm thành viên giống như Class. Điều này cho phép bạn kết hợp dữ liệu và hành vi liên quan đến dữ liệu đó vào cùng một đơn vị.
Bài tập minh họa: Định nghĩa một Struct SinhVien
có các thông tin cơ bản và thêm một hàm thành viên để in thông tin của sinh viên đó.
#include <iostream>
#include <string>
using namespace std;
struct SV {
int ma;
string ten;
int tuoi;
void in() {
cout << "Ma SV: " << ma << endl;
cout << "Ho ten: " << ten << endl;
cout << "Tuoi: " << tuoi << endl;
}
void tang() {
tuoi++;
cout << ten << " vua them 1 tuoi, hien tai la: " << tuoi << endl;
}
};
int main() {
SV s;
s.ma = 12345;
s.ten = "Tran Thi B";
s.tuoi = 20;
cout << "Lan 1:" << endl;
s.in();
s.tang();
cout << "\nLan 2 sau khi tang tuoi:" << endl;
s.in();
return 0;
}
Lan 1:
Ma SV: 12345
Ho ten: Tran Thi B
Tuoi: 20
Tran Thi B vua them 1 tuoi, hien tai la: 21
Lan 2 sau khi tang tuoi:
Ma SV: 12345
Ho ten: Tran Thi B
Tuoi: 21
Giải thích:
- Bên trong định nghĩa
SinhVien
, chúng ta khai báo các hàminThongTin()
vàtangTuoi()
. - Các hàm thành viên này có thể truy cập trực tiếp các thành viên dữ liệu (
maSV
,hoTen
,tuoi
) của chính đối tượng mà chúng được gọi. - Để gọi hàm thành viên, chúng ta sử dụng toán tử chấm (
.
) trên đối tượng Struct:sv1.inThongTin();
hoặcsv1.tangTuoi();
. - Việc này giúp đóng gói dữ liệu và các thao tác liên quan đến dữ liệu đó, làm cho code trở nên có tổ chức và dễ bảo trì hơn.
3. Làm Việc với Con Trỏ tới Struct và Cấp Phát Động
Sử dụng con trỏ tới Struct cho phép chúng ta làm việc với các đối tượng Struct thông qua địa chỉ bộ nhớ của chúng. Kết hợp với cấp phát động (new
), chúng ta có thể tạo ra các đối tượng Struct trong bộ nhớ Heap, điều này cực kỳ quan trọng khi xử lý dữ liệu có kích thước không xác định trước hoặc cần tồn tại ngoài phạm vi của một hàm.
Bài tập minh họa: Tạo một đối tượng Struct SanPham
bằng cách cấp phát động và truy cập các thành viên của nó bằng con trỏ.
#include <iostream>
#include <string>
using namespace std;
struct SP {
int ma;
string ten;
double gia;
};
int main() {
SP* p = new SP;
p->ma = 201;
p->ten = "Sach Lap Trinh C++";
p->gia = 150000.0;
cout << "Thong tin SP (qua con tro):" << endl;
cout << "Ma: " << p->ma << endl;
cout << "Ten: " << p->ten << endl;
cout << "Gia: " << p->gia << endl;
delete p;
p = nullptr;
return 0;
}
Thong tin SP (qua con tro):
Ma: 201
Ten: Sach Lap Trinh C++
Gia: 150000
Giải thích:
- Chúng ta khai báo
SanPham* conTroSP;
để tạo một con trỏ có thể trỏ tới một đối tượngSanPham
. conTroSP = new SanPham;
yêu cầu hệ điều hành cấp phát đủ bộ nhớ trên Heap để chứa một đối tượngSanPham
và trả về địa chỉ của vùng nhớ đó, gán vàoconTroSP
.- Khi làm việc với con trỏ tới Struct, chúng ta sử dụng toán tử mũi tên (
->
) thay vì toán tử chấm (.
) để truy cập các thành viên:conTroSP->maSP
tương đương với(*conTroSP).maSP
. Toán tử->
tiện lợi hơn. - Sau khi sử dụng xong, việc giải phóng bộ nhớ bằng
delete conTroSP;
là bắt buộc để tránh tình trạng rò rỉ bộ nhớ (memory leak). GánconTroSP = nullptr;
sau khi delete là một thực hành tốt để tránh sử dụng lại con trỏ đã bị giải phóng (dangling pointer).
4. Sử Dụng Struct trong Mảng và Vector
Việc lưu trữ nhiều đối tượng Struct có cùng kiểu trong một tập hợp là nhu cầu rất phổ biến. Mảng và vector là hai lựa chọn hiệu quả cho việc này. Vector thường linh hoạt hơn do khả năng thay đổi kích thước động.
Bài tập minh họa: Tạo một vector
chứa các đối tượng SinhVien
(từ ví dụ 2) và in thông tin của tất cả sinh viên trong vector.
#include <iostream>
#include <string>
#include <vector>
using namespace std;
struct SV {
int ma;
string ten;
int tuoi;
void in() const {
cout << " - Ma SV: " << ma << ", Ho ten: " << ten << ", Tuoi: " << tuoi << endl;
}
};
int main() {
vector<SV> dsSV;
SV s1 = {1001, "Nguyen Van A", 20};
SV s2 = {1002, "Tran Thi B", 21};
SV s3 = {1003, "Pham Van C", 19};
dsSV.push_back(s1);
dsSV.push_back(s2);
dsSV.push_back(s3);
cout << "Danh sach SV:" << endl;
for (const SV& s : dsSV) {
s.in();
}
return 0;
}
Danh sach SV:
- Ma SV: 1001, Ho ten: Nguyen Van A, Tuoi: 20
- Ma SV: 1002, Ho ten: Tran Thi B, Tuoi: 21
- Ma SV: 1003, Ho ten: Pham Van C, Tuoi: 19
Giải thích:
- Chúng ta khai báo
vector<SinhVien> danhSachSinhVien;
. Điều này tạo ra một vector có khả năng lưu trữ các đối tượng có kiểu làSinhVien
. - Sử dụng
push_back()
để thêm các đối tượngSinhVien
vào cuối vector. - Chúng ta sử dụng một vòng lặp
for
dựa trên phạm vi (range-based for loop) để duyệt qua từng đối tượngSinhVien
trong vector. - Đối với mỗi đối tượng
sv
trong vector, chúng ta gọi hàm thành viênsv.inThongTin()
. - Lưu ý
const SinhVien& sv
: Sử dụng tham chiếu const (const &
) khi duyệt qua các phần tử của vector là một thực hành tốt để tránh việc tạo bản sao không cần thiết của từng đối tượngSinhVien
(tiết kiệm hiệu năng) và đảm bảo rằng vòng lặp không làm thay đổi dữ liệu của các đối tượng đó. Thêmconst
vào cuối hàminThongTin()
cũng củng cố điều này, cho biết hàm không thay đổi trạng thái của đối tượng.
5. Truyền và Trả Về Struct từ Hàm
Làm việc với Struct thường bao gồm việc truyền chúng vào các hàm để xử lý hoặc nhận chúng làm giá trị trả về từ hàm. Có nhiều cách để truyền Struct tới hàm, mỗi cách có ưu nhược điểm riêng.
Bài tập minh họa: Viết các hàm để in thông tin Struct, cập nhật thông tin Struct và tạo mới một Struct rồi trả về.
#include <iostream>
#include <string>
using namespace std;
struct HP {
string ma;
string ten;
int tc;
double diem;
};
void inHP(const HP& h) {
cout << "Ma HP: " << h.ma
<< ", Ten HP: " << h.ten
<< ", So TC: " << h.tc
<< ", Diem: " << h.diem << endl;
}
void capNhatDiem(HP& h, double dMoi) {
h.diem = dMoi;
cout << "Da cap nhat diem cho " << h.ten << " thanh " << h.diem << endl;
}
HP taoHP(string ma, string ten, int tc, double diem) {
HP h;
h.ma = ma;
h.ten = ten;
h.tc = tc;
h.diem = diem;
return h;
}
int main() {
HP ds = {"MH001", "Dai So Tuyen Tinh", 3, 7.5};
cout << "Thong tin ban dau:" << endl;
inHP(ds);
capNhatDiem(ds, 8.8);
cout << "\nThong tin sau cap nhat:" << endl;
inHP(ds);
HP lt = taoHP("MH002", "Lap Trinh C++ Nang Cao", 4, 9.0);
cout << "\nHoc Phan moi duoc tao:" << endl;
inHP(lt);
return 0;
}
Thong tin ban dau:
Ma HP: MH001, Ten HP: Dai So Tuyen Tinh, So TC: 3, Diem: 7.5
Da cap nhat diem cho Dai So Tuyen Tinh thanh 8.8
Thong tin sau cap nhat:
Ma HP: MH001, Ten HP: Dai So Tuyen Tinh, So TC: 3, Diem: 8.8
Hoc Phan moi duoc tao:
Ma HP: MH002, Ten HP: Lap Trinh C++ Nang Cao, So TC: 4, Diem: 9
Giải thích:
inThongTinHocPhan(const HocPhan& hp)
: Hàm này nhận một đối tượngHocPhan
bằng tham chiếu const (const &
). Điều này có nghĩa là hàm nhận địa chỉ của đối tượng gốc chứ không tạo bản sao (hiệu quả hơn khi Struct lớn), và từ khóaconst
đảm bảo rằng hàm không thể thay đổi dữ liệu của đối tượnghp
gốc. Đây là cách khuyến khích nhất để truyền Struct vào hàm chỉ để đọc dữ liệu.capNhatDiem(HocPhan& hp, double diemMoi)
: Hàm này nhận một đối tượngHocPhan
bằng tham chiếu (&
). Điều này cho phép hàm truy cập và thay đổi dữ liệu của đối tượnghp
gốc được truyền vào. Bất kỳ thay đổi nào bên trong hàm này sẽ ảnh hưởng đến đối tượng bên ngoài hàm.HocPhan taoHocPhan(...)
: Hàm này tạo một đối tượngHocPhan
cục bộ (HocPhan moi;
) và sau đó sử dụngreturn moi;
để trả về bản sao của đối tượng đó. C++ có các cơ chế tối ưu hóa (như Return Value Optimization - RVO hoặc Named RVO - NRVO) để giảm thiểu hoặc loại bỏ việc sao chép trong nhiều trường hợp, nhưng về mặt logic, hàm trả về một bản sao.
Comments