Bài 3.3: Bài tập thực hành kiểu dữ liệu phức tạp trong C++

Bài 3.3: Bài tập thực hành kiểu dữ liệu phức tạp trong C++
Chào mừng các bạn đã quay trở lại với series blog C++ của FullhouseDev! Sau khi làm quen với các kiểu dữ liệu cơ bản như số nguyên, số thực, ký tự,... chúng ta nhanh chóng nhận ra rằng chúng không đủ để biểu diễn các thông tin phức tạp trong thế giới thực. Làm sao để lưu trữ danh sách điểm của học sinh? Làm sao để mô tả một đối tượng bao gồm nhiều loại thông tin khác nhau (ví dụ: một cuốn sách có tên, tác giả, năm xuất bản)?
Đó chính là lúc các kiểu dữ liệu phức tạp (complex data types) hoặc còn gọi là kiểu dữ liệu có cấu trúc (structured data types) phát huy sức mạnh. Trong bài thực hành này, chúng ta sẽ cùng nhau khám phá và làm quen với một số kiểu dữ liệu phức tạp thông dụng nhất trong C++, bao gồm: Mảng (Arrays), Cấu trúc (Structs), và hai người bạn cực kỳ hữu ích từ Thư viện chuẩn C++ (Standard Library) là vector
và string
.
Nắm vững các kiểu dữ liệu này sẽ là nền tảng vững chắc để bạn xây dựng các chương trình phức tạp và mạnh mẽ hơn rất nhiều!
1. Mảng (Arrays): Ngôi nhà chung của các phần tử cùng loại
Hãy tưởng tượng bạn cần lưu trữ điểm số của 5 môn học. Việc tạo 5 biến riêng biệt (diemMon1, diemMon2, ...) có vẻ không hiệu quả. Đây là lúc mảng tỏa sáng. Mảng là một tập hợp các phần tử cùng kiểu dữ liệu được lưu trữ liên tiếp trong bộ nhớ. Chúng ta có thể truy cập từng phần tử thông qua chỉ số (index).
Đặc điểm chính của mảng trong C++ (mảng C-style truyền thống):
- Kích thước cố định khi khai báo.
- Các phần tử được đánh số từ 0 đến kích thước - 1.
- Truy cập phần tử bằng toán tử
[]
.
Ví dụ 1: Khai báo và sử dụng mảng cơ bản
#include <iostream>
int main() {
// Khai báo một mảng số nguyên có kích thước 5
int diemSo[5];
// Gán giá trị cho các phần tử
diemSo[0] = 85;
diemSo[1] = 92;
diemSo[2] = 78;
diemSo[3] = 95;
diemSo[4] = 88;
// In giá trị của một phần tử cụ thể
cout << "Diem mon thu 3 (chi so 2): " << diemSo[2] << endl;
// Khai báo và khởi tạo ngay lập tức
double nhietDo[] = {25.5, 27.0, 26.3, 28.1}; // Kich thuoc se tu dong la 4
// In kich thuoc (khong phai la cach tot nhat, chi mang tinh minh hoa voi mang C-style)
cout << "So luong ngay trong mang nhiet do: " << sizeof(nhietDo) / sizeof(nhietDo[0]) << endl;
return 0;
}
Giải thích:
int diemSo[5];
khai báo một mảng tên làdiemSo
có thể chứa 5 số nguyên.diemSo[index] = value;
gán giá trịvalue
cho phần tử tại vị tríindex
. Nhớ là chỉ số bắt đầu từ 0!cout << diemSo[2];
in ra giá trị của phần tử thứ 3 (tại chỉ số 2).double nhietDo[] = { ... };
là cách khai báo và khởi tạo mảng ngay lập tức. Trình biên dịch sẽ tự động tính kích thước dựa trên số lượng phần tử bạn cung cấp.sizeof(nhietDo) / sizeof(nhietDo[0])
là một kỹ thuật (hơi "thủ công") để tính số lượng phần tử trong mảng C-style khi nó được khai báo trong cùng một scope.
Ví dụ 2: Duyệt qua mảng bằng vòng lặp
Để làm việc với tất cả (hoặc nhiều) phần tử trong mảng, chúng ta thường dùng vòng lặp.
#include <iostream>
int main() {
int soLuongSp[4] = {100, 150, 75, 200}; // So luong san pham theo quy
// Duyet mang bang vong lap for truyen thong
cout << "So luong san pham theo tung quy:" << endl;
for (int i = 0; i < 4; ++i) {
cout << "- Quy " << (i + 1) << ": " << soLuongSp[i] << endl;
}
// Duyet mang bang range-based for loop (tu C++11) - don gian hon!
cout << "\nSo luong san pham (dung range-based for):" << endl;
for (int soLuong : soLuongSp) {
cout << soLuong << " ";
}
cout << endl;
return 0;
}
Giải thích:
- Vòng lặp
for (int i = 0; i < 4; ++i)
là cách phổ biến để duyệt mảng dựa trên chỉ số.i
đi từ 0 đến 3, tương ứng với các chỉ số của mảngsoLuongSp
. - Vòng lặp
for (int soLuong : soLuongSp)
(range-based for loop) là cách hiện đại và ngắn gọn hơn. Trong mỗi lần lặp, biếnsoLuong
sẽ tự động nhận giá trị của từng phần tử trong mảngsoLuongSp
theo thứ tự.
Ví dụ 3: Bài tập nhỏ với mảng - Tính tổng và trung bình
Hãy tính tổng điểm và điểm trung bình từ mảng diemSo
ở Ví dụ 1.
#include <iostream>
#include <numeric> // Can cho accumulate (trong cac bai sau co the dung)
int main() {
int diemSo[5] = {85, 92, 78, 95, 88};
int tongDiem = 0;
// Tinh tong diem
for (int i = 0; i < 5; ++i) {
tongDiem += diemSo[i];
}
// double trungBinh = static_cast<double>(tongDiem) / 5; // Can ep kieu de chia lay so thuc
// Hoac dung range-based for
int tongDiem2 = 0;
for (int diem : diemSo) {
tongDiem2 += diem;
}
double trungBinh = static_cast<double>(tongDiem2) / sizeof(diemSo)/sizeof(diemSo[0]); // Linh hoat hon mot chut
cout << "Tong diem: " << tongDiem2 << endl;
cout << "Diem trung binh: " << trungBinh << endl;
return 0;
}
Giải thích:
- Chúng ta dùng vòng lặp để cộng dồn giá trị của từng phần tử vào biến
tongDiem
. - Để tính trung bình, chúng ta chia
tongDiem
cho số lượng phần tử (là 5). Việc sử dụngstatic_cast<double>
là quan trọng để đảm bảo phép chia là phép chia số thực, tránh kết quả bị cắt cụt (ví dụ: 440 / 5 = 88, nhưng nếu không ép kiểu và một trong hai là số nguyên, kết quả sẽ là số nguyên). Cách dùngsizeof(diemSo)/sizeof(diemSo[0])
giúp code linh hoạt hơn nếu bạn thay đổi kích thước mảng.
2. Cấu trúc (Structs): Gói gọn dữ liệu khác loại
Mảng rất tốt cho dữ liệu cùng loại, nhưng làm sao để biểu diễn một thứ gì đó có nhiều thuộc tính với các kiểu dữ liệu khác nhau? Ví dụ, thông tin về một người có thể bao gồm Tên (chuỗi), Tuổi (số nguyên), Chiều cao (số thực), Tình trạng hôn nhân (boolean),...
Cấu trúc (struct) trong C++ cho phép chúng ta nhóm các biến thành viên (member variables) lại với nhau dưới một tên duy nhất, tạo thành một kiểu dữ liệu "mới" do người dùng định nghĩa.
Đặc điểm chính của Struct:
- Tập hợp các thành viên với kiểu dữ liệu bất kỳ.
- Truy cập các thành viên bằng toán tử
.
(dot operator). - Mặc định, các thành viên trong struct là public (có thể truy cập từ bên ngoài).
Ví dụ 4: Định nghĩa và sử dụng một Struct đơn giản
Hãy tạo một cấu trúc để biểu diễn một điểm trong mặt phẳng 2D.
#include <iostream>
// Dinh nghia cau truc Diem (Point)
struct Point {
int x; // Thanh vien x (hoanh do)
int y; // Thanh vien y (tung do)
}; // Ket thuc dinh nghia bang dau cham phay
int main() {
// Tao mot bien (doi tuong) thuoc kieu Point
Point p1;
// Truy cap va gan gia tri cho cac thanh vien
p1.x = 10;
p1.y = 20;
// Truy cap va in gia tri cac thanh vien
cout << "Toa do cua diem p1: (" << p1.x << ", " << p1.y << ")" << endl;
// Tao va khoi tao ngay lap tuc
Point p2 = {30, 40};
cout << "Toa do cua diem p2: (" << p2.x << ", " << p2.y << ")" << endl;
return 0;
}
Giải thích:
struct Point { ... };
định nghĩa một kiểu dữ liệu mới tên làPoint
.- Bên trong
{}
là các biến thành viên:int x;
vàint y;
. Point p1;
tạo một biến (một instance) có kiểuPoint
.p1.x
vàp1.y
dùng toán tử.
để truy cập các thành viênx
vày
của biếnp1
.Point p2 = {30, 40};
là cách khởi tạo nhanh các thành viên khi tạo biến struct.
Ví dụ 5: Struct phức tạp hơn
Hãy tạo một struct để lưu thông tin cơ bản về một sinh viên.
#include <iostream>
#include <string> // Can cho string
// Dinh nghia cau truc SinhVien
struct SinhVien {
string maSinhVien; // Ma sinh vien (chuoi)
string hoTen; // Ho va ten (chuoi)
int tuoi; // Tuoi (so nguyen)
double diemTrungBinh; // Diem trung binh (so thuc)
};
int main() {
// Tao mot bien SinhVien
SinhVien sv1;
// Gan gia tri cho cac thanh vien
sv1.maSinhVien = "SV001";
sv1.hoTen = "Nguyen Van A";
sv1.tuoi = 20;
sv1.diemTrungBinh = 8.5;
// In thong tin sinh vien
cout << "Thong tin sinh vien:" << endl;
cout << " Ma SV: " << sv1.maSinhVien << endl;
cout << " Ho Ten: " << sv1.hoTen << endl;
cout << " Tuoi: " << sv1.tuoi << endl;
cout << " Diem TB: " << sv1.diemTrungBinh << endl;
return 0;
}
Giải thích:
- Struct
SinhVien
nhóm 4 thông tin khác loại:string
,int
,double
. - Chúng ta tạo biến
sv1
kiểuSinhVien
và gán giá trị cho từng thành viên bằng toán tử.
.
Ví dụ 6: Mảng của Structs
Chúng ta hoàn toàn có thể kết hợp các kiểu dữ liệu phức tạp với nhau. Một trường hợp phổ biến là tạo một mảng chứa các biến struct.
#include <iostream>
#include <string>
struct SanPham {
string ten;
double gia;
int soLuongTonKho;
};
int main() {
// Tao mot mang chua 3 san pham
SanPham danhSachSP[3];
// Gan du lieu cho cac san pham trong mang
danhSachSP[0] = {"Laptop", 1200.0, 15}; // Khoi tao nhanh phan tu dau tien
danhSachSP[1].ten = "Chuot";
danhSachSP[1].gia = 25.5;
danhSachSP[1].soLuongTonKho = 50;
danhSachSP[2] = {"Ban Phim", 75.0, 30};
// Duyet qua mang san pham va in thong tin
cout << "Danh sach san pham:" << endl;
for (int i = 0; i < 3; ++i) {
cout << "- Ten: " << danhSachSP[i].ten
<< ", Gia: " << danhSachSP[i].gia
<< ", Ton kho: " << danhSachSP[i].soLuongTonKho << endl;
}
return 0;
}
Giải thích:
SanPham danhSachSP[3];
khai báo một mảng có 3 phần tử, mỗi phần tử là một biến kiểuSanPham
.- Để truy cập thành viên của một struct nằm trong mảng, chúng ta sử dụng kết hợp toán tử
[]
(truy cập phần tử mảng) và toán tử.
(truy cập thành viên struct):danhSachSP[i].ten
.
3. vector
: Mảng động linh hoạt
Nhược điểm lớn nhất của mảng C-style là kích thước cố định. Bạn cần biết trước mình sẽ lưu bao nhiêu phần tử. Trong nhiều trường hợp, số lượng dữ liệu có thể thay đổi trong quá trình chạy chương trình. Đây là lúc vector
từ Thư viện chuẩn C++ trở thành giải pháp lý tưởng.
vector
là một container (bộ chứa) kiểu mảng động. Nó có thể tự động thay đổi kích thước khi bạn thêm hoặc bớt phần tử.
Đặc điểm chính của vector
:
- Kích thước động, có thể thay đổi lúc runtime.
- Cung cấp nhiều hàm tiện ích (thêm, xóa, lấy kích thước,...).
- Các phần tử cũng được lưu trữ liền kề trong bộ nhớ (giống mảng).
- Yêu cầu
#include <vector>
.
Ví dụ 7: Tạo và thêm phần tử vào vector
#include <iostream>
#include <vector> // Can cho vector
int main() {
// Khai bao mot vector chua so nguyen rong
vector<int> soNguyen;
// Them phan tu vao cuoi vector
soNguyen.push_back(10);
soNguyen.push_back(25);
soNguyen.push_back(5);
// In kich thuoc hien tai cua vector
cout << "Kich thuoc vector sau khi them: " << soNguyen.size() << endl;
// Them mot phan tu nua
soNguyen.push_back(42);
cout << "Kich thuoc vector sau khi them nua: " << soNguyen.size() << endl;
return 0;
}
Giải thích:
vector<int> soNguyen;
khai báo một vector có thể chứa các số nguyên. Ban đầu nó rỗng..push_back(value)
là hàm để thêmvalue
vào cuối vector. Vector sẽ tự động "lớn lên" để chứa phần tử mới..size()
là hàm trả về số lượng phần tử hiện có trong vector.
Ví dụ 8: Truy cập và duyệt vector
Bạn có thể truy cập phần tử của vector giống như mảng truyền thống bằng toán tử []
, hoặc dùng hàm .at()
(an toàn hơn vì nó kiểm tra giới hạn).
#include <iostream>
#include <vector>
int main() {
vector<double> nhietDo = {22.1, 23.5, 21.9, 24.0, 25.2};
// Truy cap phan tu bang chi so (giong mang)
cout << "Nhiet do ngay thu 2 (chi so 1): " << nhietDo[1] << endl;
// Truy cap phan tu bang .at() (an toan hon)
cout << "Nhiet do ngay thu 4 (chi so 3): " << nhietDo.at(3) << endl;
// Duyet vector bang vong lap for truyen thong
cout << "\nDanh sach nhiet do (for):" << endl;
for (int i = 0; i < nhietDo.size(); ++i) { // Dung .size() de lay kich thuoc dong
cout << nhietDo[i] << " ";
}
cout << endl;
// Duyet vector bang range-based for loop
cout << "Danh sach nhiet do (range-based for):" << endl;
for (double temp : nhietDo) {
cout << temp << " ";
}
cout << endl;
return 0;
}
Giải thích:
nhietDo[1]
vànhietDo.at(3)
đều truy cập phần tử..at(index)
sẽ ném ra một ngoại lệ nếuindex
nằm ngoài phạm vi hợp lệ, giúp phát hiện lỗi sớm hơn là chỉ truy cập bằng[]
(có thể gây ra lỗi runtime không mong muốn).- Vòng lặp
for
truyền thống dùngnhietDo.size()
để biết khi nào dừng. - Range-based for loop hoạt động rất tốt với
vector
.
4. string
: Làm việc với văn bản dễ dàng
Làm việc với chuỗi ký tự là một tác vụ rất phổ biến trong lập trình. C-style strings (mảng ký tự kết thúc bằng '\0') có thể hơi phức tạp và dễ gây lỗi (ví dụ: tràn bộ đệm). string
từ Thư viện chuẩn C++ cung cấp một cách an toàn và tiện lợi để xử lý văn bản.
Đặc điểm chính của string
:
- Quản lý bộ nhớ tự động.
- Cung cấp nhiều hàm tiện ích (nối chuỗi, tìm kiếm, lấy độ dài, so sánh,...).
- Có thể thay đổi kích thước.
- Yêu cầu
#include <string>
.
Ví dụ 9: Khai báo và thao tác với string
#include <iostream>
#include <string> // Can cho string
int main() {
// Khai bao va khoi tao string
string ten = "FullhouseDev";
string loiChao = "Xin chao, ";
// Noi chuoi (concatenation)
string thongDiep = loiChao + ten + "!";
// In chuoi
cout << thongDiep << endl;
// Lay do dai chuoi
cout << "Do dai cua chuoi thongDiep: " << thongDiep.length() << endl; // hoac .size()
// Truy cap ky tu tai vi tri cu the (giong mang)
cout << "Ky tu dau tien: " << thongDiep[0] << endl;
cout << "Ky tu cuoi cung: " << thongDiep[thongDiep.length() - 1] << endl;
return 0;
}
Giải thích:
string ten = "FullhouseDev";
tạo và khởi tạo một chuỗi.- Toán tử
+
được nạp chồng (overload) chostring
để nối chuỗi. .length()
(hoặc.size()
) trả về số ký tự trong chuỗi.- Bạn có thể truy cập từng ký tự bằng toán tử
[]
, giống như mảng ký tự.
Ví dụ 10: Đọc chuỗi từ nhập liệu
Khi đọc chuỗi từ bàn phím, cần lưu ý cách cin >>
hoạt động và khi nào nên dùng getline
.
#include <iostream>
#include <string>
#include <limits> // Can cho numeric_limits
int main() {
string tenRieng;
string tenDayDu;
cout << "Nhap ten rieng (chi mot tu): ";
cin >> tenRieng; // Doc den khoang trang hoac Enter
cout << "Ten rieng ban vua nhap: " << tenRieng << endl;
// Can phai "xoa" ky tu Enter con sot lai trong bo dem nhap
cin.ignore(numeric_limits<streamsize>::max(), '\n');
cout << "Nhap ho va ten day du: ";
getline(cin, tenDayDu); // Doc toan bo dong den khi gap Enter
cout << "Ho ten day du ban vua nhap: " << tenDayDu << endl;
return 0;
}
Giải thích:
cin >> tenRieng;
chỉ đọc một từ duy nhất, dừng lại khi gặp khoảng trắng, tab hoặc Enter.getline(cin, tenDayDu);
đọc toàn bộ dòng văn bản cho đến khi gặp ký tự Enter.- Sau khi dùng
cin >>
, ký tự Enter vẫn còn lại trong bộ đệm nhập. Nếu không "xóa" nó (bằngcin.ignore(...)
), lệnhgetline
tiếp theo sẽ đọc ngay ký tự Enter đó và kết thúc ngay lập tức mà không chờ bạn nhập gì cả. Lệnhcin.ignore(...)
dùng để loại bỏ phần còn lại của dòng trước đó khỏi bộ đệm.
5. Kết hợp các kiểu dữ liệu phức tạp
Sức mạnh thực sự đến khi chúng ta kết hợp các kiểu dữ liệu này. Ví dụ, bạn có thể tạo một vector
chứa các đối tượng SinhVien
(đã dùng struct ở trên), mỗi đối tượng SinhVien
lại chứa string
cho tên và mã số.
Ví dụ 11: vector
chứa Struct
#include <iostream>
#include <vector>
#include <string>
// Dinh nghia lai struct SinhVien
struct SinhVien {
string maSinhVien;
string hoTen;
int tuoi;
double diemTrungBinh;
};
int main() {
// Tao mot vector chua cac doi tuong SinhVien
vector<SinhVien> danhSachSinhVien;
// Tao va them sinh vien vao vector
SinhVien sv1 = {"SV001", "Nguyen Van A", 20, 8.5};
danhSachSinhVien.push_back(sv1);
danhSachSinhVien.push_back({"SV002", "Tran Thi B", 19, 9.0}); // Them truc tiep doi tuong khoi tao nhanh
SinhVien sv3;
sv3.maSinhVien = "SV003";
sv3.hoTen = "Le Van C";
sv3.tuoi = 21;
sv3.diemTrungBinh = 7.8;
danhSachSinhVien.push_back(sv3);
// Duyet qua vector va in thong tin tung sinh vien
cout << "Danh sach sinh vien trong vector:" << endl;
for (const SinhVien& sv : danhSachSinhVien) { // Nen dung tham chieu const de hieu qua hon
cout << " Ma SV: " << sv.maSinhVien
<< ", Ho Ten: " << sv.hoTen
<< ", Tuoi: " << sv.tuoi
<< ", Diem TB: " << sv.diemTrungBinh << endl;
}
return 0;
}
Giải thích:
vector<SinhVien> danhSachSinhVien;
khai báo một vector mà mỗi phần tử của nó là một structSinhVien
.- Chúng ta có thể tạo một biến
SinhVien
riêng rồi dùngpush_back
để thêm vào vector, hoặc khởi tạo struct ngay trong lời gọipush_back
. - Khi duyệt vector, mỗi biến
sv
trong vòng lặpfor (const SinhVien& sv : danhSachSinhVien)
là một tham chiếu đến một đối tượngSinhVien
trong vector. Việc sử dụngconst SinhVien&
là một practice tốt:&
(tham chiếu) giúp tránh việc copy toàn bộ struct lớn trong mỗi lần lặp (tiết kiệm hiệu năng), vàconst
đảm bảo chúng ta không vô tình thay đổi dữ liệu của sinh viên khi duyệt.
Bài tập thực hành này đã giới thiệu cho bạn những kiểu dữ liệu phức tạp quan trọng nhất trong C++: Mảng (cố định, cùng loại), Cấu trúc (Struct) (gom nhóm khác loại), vector
(mảng động, linh hoạt) và string
(chuỗi ký tự tiện lợi).
Comments