Bài 28.3: Kết hợp chuỗi ký tự với mảng và vector trong C++

Bài 28.3: Kết hợp chuỗi ký tự với mảng và vector trong C++
Chào mừng trở lại với loạt bài viết về C++! Sau khi đã làm quen với chuỗi ký tự cơ bản và các cấu trúc dữ liệu như mảng và vector, đã đến lúc chúng ta khám phá một chủ đề vô cùng quan trọng và thực tế: làm thế nào để lưu trữ và làm việc với nhiều chuỗi ký tự cùng một lúc. Đây là tình huống thường gặp khi bạn cần quản lý danh sách tên, danh sách sản phẩm, các dòng văn bản từ một tệp, và vô vàn các ứng dụng khác.
Trong C++, chúng ta có thể tận dụng sức mạnh của mảng (arrays) và vector (vector
) để làm điều này. Mỗi cấu trúc này đều có những ưu điểm riêng và phù hợp với các trường hợp sử dụng khác nhau. Hãy cùng đi sâu vào chi tiết nhé!
Lưu trữ Chuỗi ký tự trong Mảng (Arrays of Strings)
Mảng trong C++ 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 tục trong bộ nhớ. Khi làm việc với chuỗi ký tự, chúng ta có hai cách tiếp cận chính với mảng:
1. Mảng các Chuỗi ký tự kiểu C (C-style String Arrays)
Theo truyền thống, chuỗi ký tự trong C/C++ được biểu diễn bằng mảng các ký tự (char
) kết thúc bằng ký tự null (\0
). Do đó, một "mảng chuỗi" có thể được hiểu là một mảng hai chiều của các ký tự, nơi mỗi hàng là một chuỗi ký tự riêng biệt.
Ví dụ:
#include <iostream>
#include <cstring> // Thu vien de lam viec voi chuoi C-style nhu strcpy
int main() {
// Khai bao mot mang chua 3 chuoi, moi chuoi toi da 19 ky tu + 1 ky tu '\0'
char danhSachTen[3][20];
// Gan gia tri cho cac chuoi
// LU Y: Can su dung cac ham an toan nhu strncpy hoac luon kiem tra kich thuoc
// strcpy co the gay tran bo dem (buffer overflow) neu chuoi nguon lon hon kich thuoc mang dich
strcpy(danhSachTen[0], "Alice");
strcpy(danhSachTen[1], "Bob");
strcpy(danhSachTen[2], "Charlie");
// In ra cac chuoi trong mang
cout << "Danh sach ten (C-style array):\n";
for (int i = 0; i < 3; ++i) {
cout << danhSachTen[i] << endl;
}
// Vi du ket hop chuoi (can su dung cac ham cua <cstring>)
char tenDayDu[50]; // Can du bo dem
strcpy(tenDayDu, danhSachTen[0]);
strcat(tenDayDu, " va ");
strcat(tenDayDu, danhSachTen[1]);
cout << "Ket hop (C-style): " << tenDayDu << endl;
return 0;
}
Giải thích code:
- Chúng ta khai báo
char danhSachTen[3][20]
để tạo ra một mảng có 3 hàng và 20 cột. Mỗi hàngdanhSachTen[i]
có thể chứa một chuỗi ký tự C-style có độ dài tối đa 19 ký tự, còn chỗ cho ký tự\0
. - Để gán giá trị cho các chuỗi, chúng ta không thể dùng toán tử
=
. Thay vào đó, chúng ta phải dùng hàmstrcpy
(hoặcstrncpy
an toàn hơn) từ thư viện<cstring>
để sao chép nội dung chuỗi. - Truy cập từng chuỗi con rất đơn giản, chỉ cần dùng
danhSachTen[i]
. - Kết hợp các chuỗi con cũng yêu cầu sử dụng các hàm của
<cstring>
nhưstrcat
.
Hạn chế: Cách tiếp cận này khá cồng kềnh và tiềm ẩn rủi ro tràn bộ đệm (buffer overflow) nếu bạn không cẩn thận với kích thước của mảng và độ dài chuỗi.
2. Mảng các Đối tượng string
Trong C++ hiện đại, cách tốt hơn và an toàn hơn là sử dụng lớp string
. Khi đó, một "mảng chuỗi" đơn giản là một mảng mà mỗi phần tử của nó là một đối tượng string
.
Ví dụ:
#include <iostream>
#include <string> // Thu vien cho string
int main() {
// Khai bao mot mang chua 3 doi tuong string
string danhSachTen[3];
// Gan gia tri cho cac chuoi (don gian hon rat nhieu!)
danhSachTen[0] = "Alice Wonderland"; // string tu dong quan ly bo nho
danhSachTen[1] = "Bob";
danhSachTen[2] = "Charlie Day La Mot Cai Ten Rat Dai Ma Khong Can Lo Lo Tran Bo Dem";
// In ra cac chuoi trong mang
cout << "Danh sach ten (string array):\n";
for (int i = 0; i < 3; ++i) {
cout << danhSachTen[i] << endl;
}
// Vi du ket hop chuoi (dung toan tu + nhu thong thuong)
string tenDayDu = danhSachTen[0] + " va " + danhSachTen[1];
cout << "Ket hop (string): " << tenDayDu << endl;
return 0;
}
Giải thích code:
- Chúng ta khai báo
string danhSachTen[3];
để tạo ra một mảng chứa 3 đối tượngstring
. - Việc gán giá trị trở nên cực kỳ đơn giản bằng cách sử dụng toán tử gán
=
.string
tự động quản lý bộ nhớ cần thiết cho chuỗi, bất kể độ dài của nó (trong giới hạn bộ nhớ hệ thống). - Truy cập và in ra cũng tương tự như mảng thông thường:
danhSachTen[i]
. - Việc kết hợp (nối) các chuỗi cũng rất trực quan, chỉ cần sử dụng toán tử
+
giữa các đối tượngstring
.
Ưu điểm: An toàn hơn, dễ sử dụng hơn, tự động quản lý bộ nhớ.
Hạn chế của cả hai loại mảng: Kích thước của mảng phải được xác định tại thời điểm biên dịch. Bạn không thể thêm hoặc bớt các chuỗi một cách linh hoạt sau khi mảng đã được tạo. Đây là lúc vector
phát huy tác dụng!
Lưu trữ Chuỗi ký tự trong Vector (vector<string>
)
vector
là một container (vùng chứa) động trong Thư viện Chuẩn C++ (STL - Standard Template Library). Nó hoạt động giống như một mảng, nhưng kích thước của nó có thể thay đổi trong quá trình thực thi chương trình. Khi kết hợp với string
, chúng ta có một giải pháp mạnh mẽ và linh hoạt để quản lý bộ sưu tập các chuỗi ký tự.
Khai báo một vector chứa các chuỗi string
có dạng vector<string>
.
Ví dụ:
#include <iostream>
#include <string>
#include <vector> // Thu vien cho vector
int main() {
// Khai bao mot vector rong chua cac doi tuong string
vector<string> danhSachMonHoc;
// Them cac phan tu (chuoi) vao cuoi vector
danhSachMonHoc.push_back("Toan roi rac");
danhSachMonHoc.push_back("Lap trinh C++ nang cao"); // The hien tinh linh hoat
danhSachMonHoc.push_back("Cau truc du lieu va Giai thuat");
// Kich thuoc cua vector thay doi tu dong
cout << "So luong mon hoc ban dau: " << danhSachMonHoc.size() << endl; // size() cho biet so phan tu
// Truy cap va in ra cac phan tu (giong mang, hoac dung at() an toan hon)
cout << "Danh sach mon hoc:\n";
for (size_t i = 0; i < danhSachMonHoc.size(); ++i) { // size() tra ve size_t, nen dung size_t cho i
cout << "- " << danhSachMonHoc[i] << endl;
// Hoac an toan hon: cout << "- " << danhSachMonHoc.at(i) << endl;
}
// Them mot mon nua
danhSachMonHoc.push_back("He dieu hanh");
cout << "\nSo luong mon hoc sau khi them: " << danhSachMonHoc.size() << endl;
// In lai danh sach su dung range-based for loop (cach hien dai va ngan gon)
cout << "Danh sach mon hoc moi:\n";
for (const string& monHoc : danhSachMonHoc) { // Dung const& de tranh sao chep
cout << "- " << monHoc << endl;
}
// Vi du ket hop (noi) chuoi tu vector
if (danhSachMonHoc.size() > 1) {
string monHocChinh = danhSachMonHoc[1] + " va " + danhSachMonHoc[2];
cout << "\nMon hoc chinh trong danh sach: " << monHocChinh << endl;
}
// Mot vi du lam viec voi chuoi trong vector: Tim chuoi co chua "C++"
cout << "\nMon hoc co chua 'C++':\n";
for (const string& monHoc : danhSachMonHoc) {
if (monHoc.find("C++") != string::npos) { // Su dung phuong thuc find cua string
cout << "- " << monHoc << endl;
}
}
return 0;
}
Giải thích code:
vector<string> danhSachMonHoc;
khai báo một vector rỗng có thể chứa các đối tượngstring
.- Chúng ta sử dụng phương thức
push_back()
để thêm các chuỗi mới vào cuối vector. Vector sẽ tự động điều chỉnh kích thước. size()
trả về số lượng phần tử hiện có trong vector.- Truy cập các phần tử tương tự như mảng, sử dụng toán tử
[]
hoặc phương thứcat()
(phương thứcat()
kiểm tra lỗi ngoài phạm vi và ném ngoại lệ, an toàn hơn). - Chúng ta có thể lặp qua vector bằng vòng lặp
for
truyền thống với chỉ số hoặc sử dụng range-based for loop rất tiện lợi cho việc duyệt qua tất cả các phần tử. - Việc kết hợp chuỗi từ các phần tử vector cũng đơn giản như với mảng
string
, sử dụng toán tử+
. - Chúng ta cũng có thể dễ dàng áp dụng các phương thức của
string
lên từng phần tử của vector, ví dụ như sử dụngfind()
để tìm kiếm chuỗi con.
Ưu điểm của vector<string>
:
- Linh hoạt: Kích thước có thể thay đổi dynamically (thêm/bớt phần tử).
- An toàn: Tự động quản lý bộ nhớ.
- Tiện lợi: Cung cấp nhiều phương thức hữu ích như
push_back
,pop_back
,insert
,erase
,size
,empty
, v.v. - Tương thích: Kết hợp tốt với các thuật toán và iterator trong STL.
Khi nào sử dụng Mảng, khi nào sử dụng Vector?
- Sử dụng mảng
string
khi bạn biết chính xác số lượng chuỗi cần lưu trữ tại thời điểm biên dịch và số lượng này sẽ không thay đổi. Nó có thể hơi hiệu quả hơn về mặt bộ nhớ và tốc độ truy cập nếu kích thước rất lớn và cố định, nhưng sự khác biệt thường không đáng kể với kích thước vừa phải. - Sử dụng
vector<string>
trong hầu hết các trường hợp khác. Khi bạn không biết trước số lượng chuỗi, số lượng có thể thay đổi, hoặc bạn cần thêm/bớt các chuỗi một cách linh hoạt trong quá trình chạy chương trình.vector
là lựa chọn linh hoạt và hiện đại hơn.
Nên tránh sử dụng mảng C-style (char[][]
) cho các bộ sưu tập chuỗi trừ khi bạn có lý do đặc biệt (ví dụ: làm việc với mã cũ) do những rủi ro và sự bất tiện mà nó mang lại.
Kết hợp và Thao tác nâng cao hơn
Với cả mảng string
và vector<string>
, bạn có thể áp dụng tất cả các kiến thức đã học về string
lên từng phần tử.
Ví dụ:
- Truy cập và sửa đổi:
danhSachTen[0] = "Alice moi";
hoặcdanhSachMonHoc[1] += " va hon the nua";
- Truy cập ký tự cụ thể trong một chuỗi:
char kyTuDau = danhSachTen[0][0];
hoặcchar kyTuCuoi = danhSachMonHoc[2].back();
- Tìm kiếm:
size_t viTri = danhSachMonHoc[1].find("C++");
- Trích xuất chuỗi con:
string phanDau = danhSachMonHoc[2].substr(0, 10);
Sức mạnh nằm ở việc bạn coi mỗi phần tử của mảng hoặc vector như một đối tượng string
độc lập và áp dụng các phương thức của nó.
#include <iostream>
#include <string>
#include <vector>
int main() {
vector<string> danhSachThanhPho = {"Ha Noi", "TP Ho Chi Minh", "Da Nang", "Hue"};
// In ra chuoi thu 2 va do dai cua no
if (danhSachThanhPho.size() > 1) {
cout << "Thanh pho thu 2: " << danhSachThanhPho[1]
<< " (Do dai: " << danhSachThanhPho[1].length() << ")" << endl;
}
// Thay doi chuoi cuoi cung
if (!danhSachThanhPho.empty()) {
danhSachThanhPho.back() = "Can Tho"; // back() tra ve tham chieu toi phan tu cuoi
}
// In lai danh sach sau khi thay doi
cout << "\nDanh sach thanh pho sau khi thay doi:\n";
for (const string& tp : danhSachThanhPho) {
cout << "- " << tp << endl;
}
// Tim mot chuoi cu the trong vector
string thanhPhoCanTim = "Da Nang";
bool found = false;
for (const string& tp : danhSachThanhPho) {
if (tp == thanhPhoCanTim) { // So sanh hai chuoi string
found = true;
break;
}
}
if (found) {
cout << "\nDa tim thay thanh pho: " << thanhPhoCanTim << endl;
} else {
cout << "\nKhong tim thay thanh pho: " << thanhPhoCanTim << endl;
}
return 0;
}
Giải thích code:
- Ví dụ này minh họa cách truy cập một phần tử cụ thể trong vector (
danhSachThanhPho[1]
), sử dụng phương thức.length()
củastring
để lấy độ dài của chuỗi đó. - Sử dụng
.back()
để truy cập (và ở đây là sửa đổi) phần tử cuối cùng của vector. - Thực hiện một vòng lặp đơn giản để tìm kiếm một chuỗi cụ thể trong vector bằng cách so sánh trực tiếp các đối tượng
string
sử dụng toán tử==
.
Bài tập ví dụ: C++ Bài 16.A3: Lời chào Giáng sinh
Lời chào Giáng sinh
FullHouse Dev đang chuẩn bị cho mùa Giáng sinh. Anh ấy biết rằng Giáng sinh được tổ chức vào ngày 25 tháng 12 hàng năm.
Mô tả
Cho một ngày X trong tháng 12. Hãy xác định xem đó có phải là ngày Giáng sinh hay không.
In ra "CHRISTMAS" nếu đó là ngày Giáng sinh. Ngược lại, in ra "ORDINARY".
Input
- Dòng đầu tiên chứa số nguyên T - số lượng test case.
- T dòng tiếp theo, mỗi dòng chứa một số nguyên X, biểu thị một ngày trong tháng 12.
Output
Với mỗi test case, in ra trên một dòng mới:
- "CHRISTMAS" nếu đó là ngày Giáng sinh
- "ORDINARY" nếu không phải ngày Giáng sinh
Ràng buộc
- 1 ≤ T ≤ 100
- 1 ≤ X ≤ 31
Ví dụ
Input:
2 25 12
Output:
CHRISTMAS ORDINARY
Giải thích:
- Test case 1: Ngày 25 là Giáng sinh, nên in ra "CHRISTMAS".
- Test case 2: Ngày 12 không phải Giáng sinh, nên in ra "ORDINARY".
Hướng dẫn
- Đọc số lượng test case T.
- Với mỗi test case:
- Đọc ngày X
- Kiểm tra xem X có bằng 25 không
- Nếu X = 25, in ra "CHRISTMAS"
- Ngược lại, in ra "ORDINARY"
Hãy giúp FullHouse Dev xác định ngày Giáng sinh! Chào bạn, đây là hướng dẫn giải bài "Lời chào Giáng sinh" bằng C++ một cách ngắn gọn và sử dụng các thành phần chuẩn (std):
Bài toán rất đơn giản: với mỗi ngày X
trong tháng 12, kiểm tra xem nó có phải ngày 25 hay không.
Các bước thực hiện:
- Bao gồm thư viện cần thiết: Bạn cần thư viện cho việc nhập xuất dữ liệu chuẩn (standard input/output).
- Hàm main: Mọi chương trình C++ bắt đầu từ hàm
main
. - Đọc số lượng test case: Khai báo một biến kiểu số nguyên (ví dụ
int
) để lưu số lượng test caseT
. Đọc giá trị củaT
từ đầu vào chuẩn (sử dụngcin
). - Vòng lặp xử lý test case: Sử dụng một cấu trúc lặp (ví dụ:
while
hoặcfor
) để thực hiện các bước xử lý cho mỗi test case. Vòng lặp này sẽ chạy đúngT
lần. Một cách ngắn gọn để làm vòng lặpT
lần là dùngwhile (T--)
. - Đọc ngày trong tháng: Bên trong vòng lặp, khai báo một biến kiểu số nguyên (ví dụ
int
) để lưu ngàyX
. Đọc giá trị củaX
từ đầu vào chuẩn (sử dụngcin
). - Kiểm tra điều kiện: Sử dụng cấu trúc điều kiện
if
để kiểm tra xem giá trị củaX
có bằng 25 hay không. - In kết quả:
- Nếu điều kiện
X == 25
là đúng, sử dụngcout
để in ra chuỗi "CHRISTMAS". - Ngược lại (sử dụng
else
), sử dụngcout
để in ra chuỗi "ORDINARY". - Sau khi in chuỗi, đảm bảo in thêm ký tự xuống dòng (
\n
hoặcendl
) để mỗi kết quả của test case nằm trên một dòng riêng biệt.
- Nếu điều kiện
- Kết thúc hàm main: Trả về 0 để báo hiệu chương trình kết thúc thành công.
Comments