Bài 32.4: Kết hợp Struct với con trỏ trong C++

Bài 32.4: Kết hợp Struct với con trỏ trong C++
Chào mừng bạn quay trở lại với chuỗi bài viết về C++! Chúng ta đã cùng nhau khám phá struct
để nhóm các dữ liệu liên quan lại với nhau và tìm hiểu về con trỏ
để làm việc trực tiếp với địa chỉ bộ nhớ. Hôm nay, chúng ta sẽ kết hợp hai khái niệm mạnh mẽ này để mở ra những cánh cửa mới trong lập trình C++.
Kết hợp struct
và con trỏ
không chỉ giúp chúng ta thao tác với dữ liệu hiệu quả hơn, đặc biệt là với các cấu trúc dữ liệu lớn, mà còn là nền tảng cho việc sử dụng bộ nhớ động và xây dựng các cấu trúc dữ liệu phức tạp như danh sách liên kết, cây nhị phân, v.v.
Hãy cùng đi sâu tìm hiểu!
Nhắc lại nhanh: Struct và Con trỏ
Trước khi kết hợp, hãy ôn lại một chút về từng thành phần:
Struct (Cấu trúc): Là một kiểu dữ liệu do người dùng định nghĩa, cho phép nhóm các biến có kiểu dữ liệu khác nhau (hoặc giống nhau) lại thành một đơn vị duy nhất.
struct Nguoi { string ten; int tuoi; double chieuCao; }; // Khai báo và sử dụng struct Nguoi sinhVien; sinhVien.ten = "Nguyen Van A"; sinhVien.tuoi = 20; cout << "Ten: " << sinhVien.ten << endl;
Chúng ta truy cập các thành viên của
struct
bằng toán tử dấu chấm (.
).Con trỏ (Pointer): Là một biến lưu trữ địa chỉ của một biến khác trong bộ nhớ.
int diem = 10; int* conTroDiem = &diem; // conTroDiem lưu địa chỉ của bien 'diem' cout << "Gia tri cua diem: " << diem << endl; cout << "Dia chi cua diem: " << &diem << endl; cout << "Dia chi luu trong conTroDiem: " << conTroDiem << endl; cout << "Gia tri tai dia chi ma conTroDiem tro toi: " << *conTroDiem << endl; // Toán tử * (dereference)
Chúng ta dùng toán tử
*
(toán tử dereference) để truy cập giá trị tại địa chỉ mà con trỏ đang trỏ tới, và toán tử&
(toán tử address-of) để lấy địa chỉ của một biến.
Con trỏ trỏ đến Struct
Bây giờ, làm thế nào để một con trỏ có thể trỏ đến một biến kiểu struct
? Rất đơn giản, bạn chỉ cần khai báo con trỏ với kiểu dữ liệu là tên của struct
, theo sau là dấu *
.
Ví dụ, với struct Nguoi
ở trên, bạn có thể khai báo con trỏ trỏ tới Nguoi
như sau:
Nguoi* conTroNguoi;
Con trỏ này bây giờ có thể lưu trữ địa chỉ của một biến kiểu Nguoi
.
Trỏ đến Struct trên Stack
Nếu bạn đã khai báo một biến struct
thông thường (trên stack), bạn có thể làm cho con trỏ trỏ tới nó bằng cách sử dụng toán tử &
:
#include <iostream>
#include <string>
struct Nguoi {
string ten;
int tuoi;
};
int main() {
Nguoi nguoi1; // Khai báo struct tren stack
nguoi1.ten = "Tran Thi B";
nguoi1.tuoi = 25;
Nguoi* conTroNguoi1 = &nguoi1; // Con tro tro toi nguoi1
cout << "Thong tin nguoi 1 (truy cap truc tiep): " << nguoi1.ten << ", " << nguoi1.tuoi << endl;
cout << "Dia chi cua nguoi 1: " << &nguoi1 << endl;
cout << "Dia chi luu trong conTroNguoi1: " << conTroNguoi1 << endl;
return 0;
}
Trong ví dụ này, conTroNguoi1
đang giữ địa chỉ của biến nguoi1
.
Truy cập thành viên của Struct qua Con trỏ
Đây là phần quan trọng! Khi bạn có một con trỏ trỏ đến một struct
, làm thế nào để truy cập các thành viên (ten
, tuoi
,...) của struct
đó?
Cách đầu tiên là sử dụng toán tử *
(dereference) để lấy chính biến struct mà con trỏ đang trỏ tới, sau đó sử dụng toán tử .
để truy cập thành viên:
// Tiep tuc tu vi du tren
cout << "Truy cap qua con tro (*conTroNguoi1).ten: " << (*conTroNguoi1).ten << endl;
cout << "Truy cap qua con tro (*conTroNguoi1).tuoi: " << (*conTroNguoi1).tuoi << endl;
Lưu ý: Dấu ngoặc đơn ()
là bắt buộc vì toán tử .
có độ ưu tiên cao hơn toán tử *
. Nếu không có ngoặc, biểu thức *conTroNguoi1.ten
sẽ được hiểu là *(conTroNguoi1.ten)
, điều này là không hợp lệ vì conTroNguoi1.ten
không phải là con trỏ.
Tuy nhiên, cách này hơi dài dòng và khó đọc. May mắn thay, C++ cung cấp một toán tử rất tiện lợi cho mục đích này: toán tử mũi tên ->
.
Toán tử ->
là viết tắt của việc "lấy giá trị tại địa chỉ mà con trỏ trỏ tới và sau đó truy cập thành viên". Nói cách khác, conTroNguoi1->ten
là tương đương với (*conTroNguoi1).ten
.
// Su dung toan tu mui ten ->
cout << "Truy cap qua con tro conTroNguoi1->ten: " << conTroNguoi1->ten << endl;
cout << "Truy cap qua con tro conTroNguoi1->tuoi: " << conTroNguoi1->tuoi << endl;
// Ban co the thay doi gia tri thanh vien qua con tro
conTroNguoi1->tuoi = 26;
cout << "Tuoi sau khi cap nhat qua con tro: " << nguoi1.tuoi << endl; // Gia tri cua nguoi1 thay doi
Toán tử ->
được sử dụng rất phổ biến khi làm việc với con trỏ trỏ đến struct
(hoặc class
), vì nó giúp code ngắn gọn và dễ đọc hơn nhiều.
Kết hợp Struct với Con trỏ và Bộ nhớ động (Heap)
Một trong những ứng dụng quan trọng nhất của việc kết hợp struct
và con trỏ
là làm việc với bộ nhớ động (heap). Thay vì khai báo struct
trên stack với kích thước và vòng đời cố định trong một scope, bạn có thể cấp phát struct
trên heap sử dụng toán tử new
.
Khi bạn cấp phát bộ nhớ cho một struct
bằng new
, toán tử new
sẽ trả về một con trỏ trỏ đến vùng bộ nhớ đã cấp phát đó.
#include <iostream>
#include <string>
struct Sach {
string tieuDe;
int namXuatBan;
};
int main() {
// Cap phat dong mot doi tuong Sach tren heap
Sach* conTroSach = new Sach;
// Kiem tra xem viec cap phat co thanh cong khong
if (conTroSach == nullptr) {
cerr << "Loi: Khong du bo nho de cap phat Sach!" << endl;
return 1; // Thoat voi ma loi
}
// Truy cap va gan gia tri cho cac thanh vien qua con tro su dung ->
conTroSach->tieuDe = "Lap trinh C++";
conTroSach->namXuatBan = 2023;
// In thong tin Sach qua con tro
cout << "Thong tin Sach (cap phat dong):" << endl;
cout << "Tieu de: " << conTroSach->tieuDe << endl;
cout << "Nam xuat ban: " << conTroSach->namXuatBan << endl;
// *** Quan trong: Giai phong bo nho da cap phat dong ***
delete conTroSach;
conTroSach = nullptr; // Gan con tro ve nullptr de tranh loi dang treo (dangling pointer)
// Accessing conTroSach after delete is undefined behavior!
// cout << conTroSach->tieuDe << endl; // DO NOT DO THIS!
return 0;
}
Trong ví dụ này:
Sach* conTroSach = new Sach;
: Chúng ta yêu cầu hệ điều hành cấp phát một vùng nhớ đủ lớn để chứa một đối tượngSach
trên heap. Toán tửnew
trả về địa chỉ của vùng nhớ đó, và chúng ta lưu địa chỉ này vào biến con trỏconTroSach
.- Chúng ta sử dụng
conTroSach->tieuDe
vàconTroSach->namXuatBan
để truy cập và gán giá trị cho các thành viên của đối tượngSach
trên heap thông qua con trỏ. delete conTroSach;
: Đây là bước cực kỳ quan trọng! Khi bạn cấp phát bộ nhớ bằngnew
, bạn phải tự mình giải phóng nó khi không dùng nữa bằng toán tửdelete
. Nếu không, vùng nhớ này sẽ bị chiếm giữ cho đến khi chương trình kết thúc, dẫn đến tình trạng rò rỉ bộ nhớ (memory leak).conTroSach = nullptr;
: Sau khi giải phóng bộ nhớ, con trỏconTroSach
vẫn giữ địa chỉ của vùng nhớ đã được giải phóng. Việc truy cập vào vùng nhớ này sẽ gây ra lỗi nghiêm trọng (Undefined Behavior). Gán con trỏ vềnullptr
là một thói quen tốt giúp bạn tránh vô tình sử dụng lại con trỏ đó.
Tại sao cần cấp phát động Struct?
- Kích thước không xác định trước: Bạn có thể cần tạo một mảng các
struct
mà số lượng chỉ được biết khi chương trình đang chạy. Cấp phát động cho phép bạn tạo mảng có kích thước linh hoạt. - Vòng đời dài hơn scope: Bạn muốn một đối tượng
struct
tồn tại sau khi hàm hoặc khối lệnh hiện tại kết thúc. Cấp phát trên heap cho phép đối tượng tồn tại cho đến khi bạn chủ độngdelete
nó. - Hiệu quả khi truyền qua hàm: Đối với các
struct
có kích thước lớn, việc truyền toàn bộstruct
theo giá trị vào hàm sẽ tốn kém tài nguyên và thời gian do phải sao chép. Truyền con trỏ đếnstruct
thì nhanh hơn nhiều vì chỉ phải sao chép địa chỉ (thường là 4 hoặc 8 byte).
Truyền Struct vào hàm bằng Con trỏ
Như đã đề cập, truyền struct
bằng con trỏ vào hàm là một kỹ thuật hiệu quả. Bạn có thể truyền con trỏ để hàm chỉ đọc dữ liệu hoặc để hàm có thể thay đổi dữ liệu của struct
gốc.
Truyền bằng con trỏ (có thể thay đổi)
#include <iostream>
#include <string>
struct SinhVien {
string maSV;
double diemTB;
};
// Ham thay doi diem trung binh cua sinh vien
void capNhatDiem(SinhVien* svPtr, double diemMoi) {
// Kiem tra con tro khac nullptr truoc khi su dung
if (svPtr != nullptr) {
svPtr->diemTB = diemMoi; // Thay doi gia tri thanh vien goc qua con tro
cout << "Da cap nhat diem cho SV co ma: " << svPtr->maSV << endl;
} else {
cerr << "Loi: Con tro SinhVien la nullptr!" << endl;
}
}
int main() {
SinhVien sv1;
sv1.maSV = "SV001";
sv1.diemTB = 8.5;
cout << "Diem ban dau: " << sv1.diemTB << endl;
// Truyen dia chi cua sv1 vao ham
capNhatDiem(&sv1, 9.0);
cout << "Diem sau khi cap nhat: " << sv1.diemTB << endl; // Diem da thay doi
return 0;
}
Trong hàm capNhatDiem
, tham số là một con trỏ SinhVien*
. Khi gọi hàm capNhatDiem(&sv1, 9.0)
, chúng ta truyền địa chỉ của biến sv1
. Bằng cách sử dụng svPtr->diemTB = diemMoi;
, chúng ta thao tác trực tiếp lên vùng nhớ của sv1
, do đó giá trị của sv1.diemTB
trong main
bị thay đổi.
Truyền bằng con trỏ const
(chỉ đọc)
Nếu bạn chỉ muốn hàm truy cập dữ liệu của struct
thông qua con trỏ mà không cho phép thay đổi dữ liệu đó, hãy sử dụng con trỏ const
:
#include <iostream>
#include <string>
struct SanPham {
string tenSP;
double gia;
};
// Ham chi in thong tin san pham, khong thay doi du lieu
void inThongTinSanPham(const SanPham* spPtr) {
if (spPtr != nullptr) {
// spPtr->gia = 200; // Loi bien dich! Khong the thay doi thanh vien qua con tro const
cout << "Ten san pham: " << spPtr->tenSP << endl;
cout << "Gia: " << spPtr->gia << endl;
} else {
cerr << "Loi: Con tro SanPham la nullptr!" << endl;
}
}
int main() {
SanPham sp1;
sp1.tenSP = "Laptop ABC";
sp1.gia = 1500.0;
// Truyen dia chi cua sp1 vao ham (duoc phep vi ham chi dung const pointer)
inThongTinSanPham(&sp1);
// Co the truyen con tro toi san pham cap phat dong
SanPham* spDong = new SanPham;
spDong->tenSP = "Mouse XYZ";
spDong->gia = 25.0;
inThongTinSanPham(spDong); // Truyen con tro vao ham
delete spDong; // Giai phong bo nho dong
spDong = nullptr;
return 0;
}
Việc sử dụng const SanPham*
là một thực hành tốt (good practice) để cho trình biên dịch và người đọc code biết rằng hàm này không có ý định sửa đổi đối tượng struct
mà con trỏ trỏ tới, giúp tăng tính an toàn và rõ ràng cho code của bạn.
Bài tập ví dụ: C++ Bài 19.B1: Các khu phố còn lại
Các khu phố còn lại
Khi cuộc bầu cử đang đến gần ở đất nước FullHouseLand, chiến dịch vận động của FullHouse Dev đang diễn ra sôi nổi.
Thành phố của FullHouse Dev có chính xác 100 khu phố. FullHouse Dev đã thăm N khu phố để vận động tranh cử, và anh ấy sẽ không dừng lại cho đến khi thăm hết tất cả các khu phố!
Hãy tính xem FullHouse Dev cần thăm thêm bao nhiêu khu phố nữa?
INPUT FORMAT
- Dòng duy nhất của input chứa một số nguyên N, là số lượng khu phố mà FullHouse Dev đã thăm.
OUTPUT FORMAT
- In ra một số nguyên duy nhất: số lượng khu phố mà FullHouse Dev vẫn cần phải thăm.
CONSTRAINTS
- 0 ≤ N ≤ 100
Ví dụ
Input 1
78
Output 1
22
Giải thích 1: FullHouse Dev đã thăm 78 trong số 100 khu phố. Điều đó có nghĩa là còn lại 22 khu phố cần thăm.
Input 2
100
Output 2
0
Giải thích 2:
FullHouse Dev đã thăm mọi khu phố rồi.
Chào bạn, đây là hướng dẫn giải bài "Các khu phố còn lại" bằng C++ theo yêu cầu, không cung cấp code hoàn chỉnh mà chỉ đưa ra các bước và gợi ý sử dụng các thành phần của thư viện chuẩn std
.
Bài toán rất đơn giản: Có tổng cộng 100 khu phố, và bạn biết FullHouse Dev đã thăm N
khu phố. Cần tìm số khu phố còn lại chưa được thăm.
Hướng dẫn giải:
- Hiểu bài toán: Bạn có tổng số lượng là 100 và một phần đã biết là
N
. Cần tìm phần còn lại. - Công thức: Số khu phố còn lại chính là tổng số khu phố trừ đi số khu phố đã thăm.
- Đọc dữ liệu đầu vào:
- Chương trình của bạn cần đọc một số nguyên từ đầu vào chuẩn (standard input). Số nguyên này chính là
N
. - Trong C++, bạn có thể sử dụng đối tượng
cin
để làm việc này. - Bạn cần khai báo một biến kiểu số nguyên (ví dụ
int
) để lưu giá trị đọc được từcin
.
- Chương trình của bạn cần đọc một số nguyên từ đầu vào chuẩn (standard input). Số nguyên này chính là
- Thực hiện tính toán:
- Lấy số tổng (100).
- Trừ đi giá trị
N
mà bạn vừa đọc được. - Kết quả của phép trừ này là số khu phố còn lại.
- In kết quả:
- Sau khi tính toán xong kết quả, bạn cần in nó ra đầu ra chuẩn (standard output).
- Trong C++, bạn có thể sử dụng đối tượng
cout
để làm việc này. - In giá trị của kết quả tính toán ra màn hình.
- Các thành phần C++ cần dùng:
- Để sử dụng
cin
vàcout
, bạn cần include header<iostream>
. - Hàm chính của chương trình là
main()
. - Sử dụng kiểu dữ liệu
int
để lưu số khu phố vìN
và kết quả đều nằm trong phạm vi cho phép củaint
.
- Để sử dụng
Tóm tắt các bước thực hiện trong code:
- Include
<iostream>
. - Viết hàm
main()
. - Khai báo một biến kiểu
int
(ví dụ:n_visited
). - Đọc giá trị
N
từcin
vào biến vừa khai báo. - Tính toán kết quả:
100 - n_visited
. - In kết quả ra
cout
.
Comments