Bài 28.1: Khái niệm và sử dụng chuỗi ký tự trong C++

Bài 28.1: Khái niệm và sử dụng chuỗi ký tự trong C++
Chào mừng trở lại với loạt bài blog về C++! Hôm nay, chúng ta sẽ đào sâu vào một khái niệm cực kỳ quan trọng và được sử dụng rất thường xuyên trong lập trình: chuỗi ký tự (strings). Trong C++, làm việc với chuỗi ký tự trở nên mạnh mẽ và linh hoạt hơn rất nhiều nhờ vào lớp string
được cung cấp trong thư viện chuẩn.
Bài này sẽ giúp bạn hiểu rõ:
- Chuỗi ký tự là gì trong ngữ cảnh C++.
- Tại sao nên sử dụng
string
thay vì mảng ký tự kiểu C. - Cách khai báo và khởi tạo
string
. - Cách nhập và xuất chuỗi sử dụng
cin
,cout
vàgetline
. - Các thao tác cơ bản và phổ biến với
string
.
Hãy cùng bắt đầu nhé!
Chuỗi ký tự là gì?
Về cơ bản, chuỗi ký tự là một dãy các ký tự được sắp xếp theo một thứ tự nhất định. Ví dụ: "Xin chào", "C++ thật tuyệt", "12345".
Trong C++ truyền thống (tức là C-style strings), chuỗi được biểu diễn bằng một mảng các ký tự kết thúc bằng ký tự null (\0
). Ví dụ: char greeting[] = "Hello";
thực chất là một mảng chứa các ký tự 'H', 'e', 'l', 'l', 'o', '\0'.
Tuy nhiên, cách tiếp cận này có một số nhược điểm:
- Quản lý bộ nhớ thủ công, dễ gây ra lỗi tràn bộ đệm (buffer overflow).
- Các thao tác như nối chuỗi, sao chép, tìm kiếm... yêu cầu sử dụng các hàm riêng biệt (
strcpy
,strcat
,strlen
, etc.) và thường phức tạp. - Không có tính năng tự động thay đổi kích thước.
Để giải quyết những vấn đề này, C++ hiện đại cung cấp lớp **string**
như một phần của Thư viện Chuẩn (Standard Library). string
là một đối tượng linh hoạt hơn rất nhiều, nó tự động quản lý bộ nhớ, cung cấp nhiều phương thức tiện lợi để làm việc với chuỗi.
Tại sao sử dụng string
?
Như đã nói ở trên, string
mang lại nhiều lợi ích so với C-style strings:
- Quản lý bộ nhớ tự động: Bạn không cần lo lắng về việc cấp phát hay giải phóng bộ nhớ.
string
sẽ tự động thay đổi kích thước khi cần thiết. - An toàn hơn: Giảm thiểu rủi ro tràn bộ đệm vì đối tượng biết kích thước của nó.
- Dễ sử dụng: Cung cấp các toán tử và phương thức trực quan cho các thao tác phổ biến (nối chuỗi dùng
+
, gán dùng=
, lấy độ dài dùng.length()
hoặc.size()
). - Giàu tính năng: Có sẵn nhiều phương thức để tìm kiếm, trích xuất, chèn, xóa ký tự/chuỗi con...
Hầu hết thời gian làm việc với chuỗi trong C++, bạn sẽ muốn sử dụng string
. Để sử dụng nó, bạn cần bao gồm header <string>
.
#include <iostream>
#include <string>
int main() {
string s = "Chào thế giới!";
cout << s << endl;
return 0;
}
Output:
Chào thế giới!
Giải thích:
- Dòng
**#include <string>**
là bắt buộc để có thể sử dụng lớpstring
. - Chúng ta khai báo một biến kiểu
string
tên làmyString
và gán cho nó giá trị "Chào thế giới!". - Sử dụng
cout
để in chuỗi ra màn màn hình, giống như cách in các kiểu dữ liệu cơ bản khác.
Khai báo và Khởi tạo string
Có nhiều cách để khai báo và khởi tạo một đối tượng string
:
Khai báo rỗng:
#include <iostream> #include <string> int main() { string sRong; cout << "Chuoi rong: '" << sRong << "'" << endl; return 0; }
Output:
Chuoi rong: ''
Giải thích: Biến
emptyString
được tạo ra nhưng chưa chứa ký tự nào.Khởi tạo từ một chuỗi ký tự cố định (string literal):
#include <iostream> #include <string> int main() { string s1 = "Xin chào C++"; string s2 = string("Thật thú vị!"); cout << s1 << endl; cout << s2 << endl; return 0; }
Output:
Xin chào C++ Thật thú vị!
Giải thích: Cách phổ biến nhất, gán trực tiếp một chuỗi literal cho biến
string
.Khởi tạo từ một chuỗi
string
khác:#include <iostream> #include <string> int main() { string sGoc = "Sao chep toi!"; string sChep1 = sGoc; string sChep2(sGoc); cout << "Ban dau: " << sGoc << endl; cout << "Sao chep: " << sChep1 << endl; cout << "Cung sao chep: " << sChep2 << endl; return 0; }
Output:
Ban dau: Sao chep toi! Sao chep: Sao chep toi! Cung sao chep: Sao chep toi!
Giải thích: Tạo một chuỗi mới là bản sao của một chuỗi
string
đã tồn tại.Khởi tạo với N ký tự lặp lại:
#include <iostream> #include <string> int main() { string sSao(10, '*'); string sGach(5, '-'); cout << sSao << endl; cout << sGach << endl; return 0; }
Output:
********** -----
Giải thích: Tạo một chuỗi có độ dài xác định, chứa lặp đi lặp lại một ký tự cụ thể.
Nhập xuất chuỗi (cin
, cout
, getline
)
Tương tác với người dùng là một phần cốt lõi của hầu hết các chương trình. Với string
, việc nhập xuất cũng rất thuận tiện.
cout
: Dùng để in chuỗi ra màn hình. Như bạn đã thấy ở các ví dụ trên.cin
: Dùng để đọc chuỗi từ bàn phím. Tuy nhiên,cin
dừng đọc khi gặp ký tự khoảng trắng (space, tab, newline). Điều này có nghĩa nó chỉ đọc được một từ duy nhất.getline
: Dùng để đọc toàn bộ một dòng từ luồng nhập, bao gồm cả khoảng trắng, cho đến khi gặp ký tự xuống dòng (\n
). Đây là cách phổ biến để đọc các câu hoặc đoạn văn bản có chứa khoảng trắng.
Hãy xem ví dụ:
#include <iostream>
#include <string>
int main() {
string tu;
cout << "Nhap mot tu: ";
cin >> tu;
cout << "Tu ban vua nhap: " << tu << endl;
cin.ignore(1000, '\n');
string dong;
cout << "Nhap mot dong van ban: ";
getline(cin, dong);
cout << "Dong ban vua nhap: " << dong << endl;
return 0;
}
Giả sử người dùng nhập:
lap trinh C++
day la mot dong van ban
Output:
Nhap mot tu: Tu ban vua nhap: lap
Nhap mot dong van ban: Dong ban vua nhap: day la mot dong van ban
Giải thích:
- Ví dụ đầu tiên với
**cin >> word**
chỉ đọc từ đầu tiên bạn gõ. Nếu bạn nhập "lap trinh C++",word
sẽ chỉ là "lap". - Sau khi dùng
cin >>
, ký tự xuống dòng (Enter) bạn nhấn vẫn còn lại trong bộ đệm nhập. Nếu bạn gọigetline
ngay lập tức,getline
sẽ đọc ký tự xuống dòng đó và coi như đã đọc xong một dòng rỗng. **cin.ignore(1000, '\n')**
là một cách để "xóa" hoặc bỏ qua các ký tự còn lại trong bộ đệm nhập, lên đến 1000 ký tự hoặc cho đến khi gặp ký tự\n
. Điều này giúpgetline
tiếp theo hoạt động đúng.**getline(cin, line)**
đọc toàn bộ dòng văn bản bạn nhập cho đến khi bạn nhấn Enter và lưu vào biếnline
.
Mẹo: Khi bạn trộn lẫn giữa cin >>
(đọc số, ký tự, từ) và getline
(đọc dòng), luôn luôn cân nhắc việc sử dụng cin.ignore()
sau cin >>
để tránh lỗi đọc dòng rỗng với getline
. Nếu bạn chỉ cần đọc dòng, hãy dùng getline
một cách nhất quán.
Các Thao tác Cơ bản với string
string
cung cấp rất nhiều phương thức và toán tử để làm việc với chuỗi. Dưới đây là một số thao tác phổ biến nhất:
Lấy độ dài chuỗi
Bạn có thể dùng .length()
hoặc .size()
. Chúng thường trả về cùng một giá trị.
#include <iostream>
#include <string>
int main() {
string s = "Hello C++";
cout << "Do dai cua chuoi: " << s.length() << endl;
cout << "Kich thuoc cua chuoi: " << s.size() << endl;
return 0;
}
Output:
Do dai cua chuoi: 9
Kich thuoc cua chuoi: 9
Giải thích: Cả length()
và size()
đều trả về số lượng ký tự trong chuỗi.
Truy cập ký tự theo chỉ số
Bạn có thể truy cập từng ký tự trong chuỗi bằng toán tử []
hoặc phương thức .at()
. Chỉ số bắt đầu từ 0.
#include <iostream>
#include <string>
#include <stdexcept> // Can cho out_of_range
int main() {
string s = "Lap trinh";
cout << "Ky tu dau tien: " << s[0] << endl;
cout << "Ky tu thu ba: " << s.at(2) << endl;
try {
cout << s.at(100) << endl;
} catch (const out_of_range& oor) {
cerr << "Loi: Truy cap ngoai pham vi: " << oor.what() << endl;
}
return 0;
}
Output:
Ky tu dau tien: L
Ky tu thu ba: p
Loi: Truy cap ngoai pham vi: basic_string::at: __n (which is 100) >= this->size() (which is 9)
Giải thích:
word[0]
truy cập ký tự ở vị trí đầu tiên (chỉ số 0).word.at(2)
truy cập ký tự ở vị trí thứ ba (chỉ số 2).- Sử dụng
[]
nhanh hơn nhưng không kiểm tra xem chỉ số có hợp lệ không. Nếu chỉ số nằm ngoài phạm vi, chương trình có thể bị lỗi không mong muốn. - Sử dụng
.at()
an toàn hơn vì nó kiểm tra chỉ số. Nếu chỉ số không hợp lệ, nó sẽ ném ra một ngoại lệ (out_of_range
), giúp bạn bắt và xử lý lỗi một cách rõ ràng.
Nối chuỗi (Concatenation)
Sử dụng toán tử +
hoặc phương thức .append()
.
#include <iostream>
#include <string>
int main() {
string p1 = "Hello";
string p2 = " World";
string noi = p1 + p2;
cout << "Chuoi sau khi noi (+): " << noi << endl;
string cau = "C++ ";
cau.append("rat ");
cau.append("tuyet!");
cout << "Chuoi sau khi noi (.append()): " << cau << endl;
return 0;
}
Output:
Chuoi sau khi noi (+): Hello World
Chuoi sau khi noi (.append()): C++ rat tuyet!
Giải thích:
- Toán tử
+
tạo ra một chuỗi mới là kết quả nối của hai chuỗi. - Phương thức
.append()
nối thêm nội dung vào cuối chuỗi hiện có, thay đổi trực tiếp chuỗi đó.
So sánh chuỗi
Sử dụng các toán tử so sánh (==
, !=
, <
, >
, <=
, >=
) hoặc phương thức .compare()
. Phép so sánh dựa trên thứ tự từ điển (lexicographical order).
#include <iostream>
#include <string>
int main() {
string s1 = "apple";
string s2 = "banana";
string s3 = "apple";
if (s1 == s3) {
cout << s1 << " bang " << s3 << endl;
}
if (s1 != s2) {
cout << s1 << " khac " << s2 << endl;
}
if (s1 < s2) {
cout << s1 << " nho hon " << s2 << endl;
}
if (s1.compare(s3) == 0) {
cout << s1 << ".compare(" << s3 << ") tra ve 0" << endl;
}
if (s1.compare(s2) < 0) {
cout << s1 << ".compare(" << s2 << ") tra ve < 0" << endl;
}
return 0;
}
Output:
apple bang apple
apple khac banana
apple nho hon banana
apple.compare(apple) tra ve 0
apple.compare(banana) tra ve < 0
Giải thích:
- Các toán tử so sánh rất trực quan và dễ dùng cho các phép so sánh bằng, khác, lớn hơn, nhỏ hơn.
- Phương thức
.compare()
cung cấp kiểm soát chi tiết hơn và có thể so sánh các phần của chuỗi, nhưng thường ít được dùng cho các phép so sánh đơn giản. Kết quả trả về của nó cho biết mối quan hệ thứ tự.
Các Phương thức hữu ích khác
string
có rất nhiều phương thức mạnh mẽ. Dưới đây là một vài cái tên đáng chú ý:
empty()
: Kiểm tra xem chuỗi có rỗng không (không chứa ký tự nào).clear()
: Xóa tất cả ký tự, làm cho chuỗi trở nên rỗng.find(substring)
: Tìm vị trí xuất hiện đầu tiên của một chuỗi con. Trả vềstring::npos
nếu không tìm thấy.substr(pos, len)
: Trích xuất một chuỗi con từ vị trípos
với độ dàilen
.push_back(char)
: Thêm một ký tự vào cuối chuỗi.pop_back()
: Xóa ký tự cuối cùng của chuỗi (C++11 trở lên).
#include <iostream>
#include <string>
int main() {
string s = "Hoc C++ that vui!";
if (!s.empty()) {
cout << "'" << s << "' khong rong." << endl;
s.clear();
cout << "Sau khi clear, chuoi rong? " << (s.empty() ? "Co" : "Khong") << endl;
}
s = "Day la mot chuoi de tim kiem.";
string k = "tim";
size_t pos = s.find(k);
if (pos != string::npos) {
cout << "Tim thay '" << k << "' tai vi tri: " << pos << endl;
} else {
cout << "Khong tim thay '" << k << "'" << endl;
}
string goc = "Lap trinh C++ rat hay";
string con = goc.substr(10, 3);
cout << "Chuoi con trich xuat: " << con << endl;
string ds = "abc";
ds.push_back('d');
cout << "Sau khi push_back('d'): " << ds << endl;
ds.pop_back();
cout << "Sau khi pop_back(): " << ds << endl;
return 0;
}
Output:
'Hoc C++ that vui!' khong rong.
Sau khi clear, chuoi rong? Co
Tim thay 'tim' tai vi tri: 19
Chuoi con trich xuat: C++
Sau khi push_back('d'): abcd
Sau khi pop_back(): abc
Giải thích:
empty()
trả vềtrue
nếu chuỗi không có ký tự nào,false
nếu ngược lại.clear()
làm cho chuỗi trở thành rỗng.find()
tìm kiếm chuỗi con và trả về chỉ số bắt đầu của nó. Nếu không tìm thấy, nó trả về một giá trị đặc biệt làstring::npos
.substr(pos, len)
trả về một chuỗi mới là một phần của chuỗi gốc, bắt đầu từ chỉ sốpos
và có độ dàilen
.push_back()
thêm một ký tự đơn vào cuối chuỗi.pop_back()
xóa ký tự cuối cùng.
C-style Strings vs string
: Tóm tắt
Mặc dù C-style strings vẫn tồn tại trong C++ và đôi khi cần thiết khi làm việc với các thư viện cũ hoặc API cấp thấp, string
là lựa chọn ưu tiên cho hầu hết các tác vụ lập trình hiện đại. Nó cung cấp sự an toàn, tiện lợi và linh hoạt mà C-style strings không có.
Khi cần chuyển đổi một string
sang C-style string (ví dụ để dùng với hàm C như printf
hoặc các hàm cần const char*
), bạn có thể sử dụng phương thức .c_str()
.
#include <iostream>
#include <string>
#include <cstdio>
int main() {
string s = "Day la string";
const char* cs = s.c_str();
printf("Su dung C-style string voi printf: %s\n", cs);
return 0;
}
Output:
Su dung C-style string voi printf: Day la string
Giải thích: Phương thức .c_str()
trả về một con trỏ const char*
trỏ đến dữ liệu ký tự bên trong string
, kết thúc bằng ký tự null \0
. Giá trị trả về này chỉ hợp lệ cho đến khi chuỗi string
bị thay đổi hoặc bị hủy.
Bài tập ví dụ: C++ Bài 16.A1: Wordle phiên bản FullHouse Dev
Wordle phiên bản FullHouse Dev
FullHouse Dev đã phát minh ra một phiên bản Wordle được sửa đổi.
Mô tả bài toán
Có một từ ẩn S và một từ đoán T, cả hai đều có độ dài 5.
FullHouse Dev định nghĩa một chuỗi M để xác định độ chính xác của từ đoán. Đối với chỉ số thứ i:
- Nếu ký tự đoán ở vị trí thứ i đúng, ký tự thứ i của M là G.
- Nếu ký tự đoán ở vị trí thứ i sai, ký tự thứ i của M là B.
Cho từ ẩn S và từ đoán T, hãy xác định chuỗi M.
Input
- Dòng đầu tiên chứa T, số lượng test case. Sau đó là các test case.
- Mỗi test case gồm hai dòng:
- Dòng đầu chứa chuỗi S - từ ẩn.
- Dòng thứ hai chứa chuỗi T - từ đoán.
Output
Với mỗi test case, in ra giá trị của chuỗi M.
Bạn có thể in mỗi ký tự của chuỗi bằng chữ hoa hoặc chữ thường.
Ràng buộc
- 1 ≤ T ≤ 1000
- |S| = |T| = 5
- S, T chỉ chứa các chữ cái tiếng Anh in hoa.
Ví dụ
Input:
3
ABCDE
EDCBA
ROUND
RINGS
START
STUNT
Output:
BBGBB
GBBBB
GGBBG
Giải thích
Test Case 1:
S = ABCDE và T = EDCBA. Chuỗi M là:
- A ≠ E, nên M[1] = B
- B ≠ D, nên M[2] = B
- C = C, nên M[3] = G
- D ≠ B, nên M[4] = B
- E ≠ A, nên M[5] = B
Vậy M = BBGBB.
Tuyệt vời! Đây là hướng dẫn giải bài Wordle phiên bản FullHouse Dev bằng C++, tập trung vào tư duy và sử dụng các công cụ chuẩn của C++ (
std
), không đưa ra code hoàn chỉnh.
Phân tích bài toán:
- Chúng ta cần xử lý nhiều test case (
T
). - Mỗi test case có hai chuỗi input:
S
(ẩn) vàT
(đoán), cả hai đều có độ dài chính xác là 5. - Cần tạo một chuỗi kết quả
M
có độ dài 5. - Quy tắc tạo
M
: So sánh ký tự tại cùng một vị tríi
trongS
vàT
.- Nếu
S[i]
==T[i]
, ký tự thứi
củaM
là 'G'. - Nếu
S[i]
!=T[i]
, ký tự thứi
củaM
là 'B'.
- Nếu
- In chuỗi
M
cho mỗi test case.
Hướng dẫn giải bằng C++:
Bao gồm các thư viện cần thiết:
- Bạn sẽ cần thư viện để xử lý nhập/xuất (
<iostream>
). - Bạn sẽ cần thư viện để làm việc với chuỗi ký tự (
<string>
).
- Bạn sẽ cần thư viện để xử lý nhập/xuất (
Hàm
main
: Đây là điểm bắt đầu của chương trình C++.Đọc số lượng test case:
- Khai báo một biến nguyên để lưu số lượng test case (ví dụ:
int T;
). - Sử dụng
cin >> T;
để đọc giá trị này.
- Khai báo một biến nguyên để lưu số lượng test case (ví dụ:
Vòng lặp xử lý test case:
- Sử dụng một vòng lặp
while
hoặcfor
để chạy đúngT
lần. Một cách phổ biến và ngắn gọn làwhile (T--)
. Mỗi lần lặp, giá trị củaT
sẽ giảm đi 1 và vòng lặp tiếp tục miễn làT
lớn hơn 0.
- Sử dụng một vòng lặp
Bên trong vòng lặp xử lý test case:
- Đọc chuỗi S và T: Khai báo hai đối tượng
string
để lưu chuỗi ẩn và chuỗi đoán (ví dụ:string S, T;
). Sử dụngcin >> S;
vàcin >> T;
để đọc chúng. - Tạo chuỗi kết quả M: Khai báo một đối tượng
string
để xây dựng chuỗi kết quảM
(ví dụ:string M = "";
). Bạn có thể bắt đầu với một chuỗi rỗng. - Vòng lặp so sánh ký tự: Vì độ dài của S và T luôn là 5, sử dụng một vòng lặp
for
chạy từ chỉ số 0 đến 4 (tổng cộng 5 lần). Ví dụ:for (int i = 0; i < 5; ++i)
. - So sánh và xây dựng M: Bên trong vòng lặp
for
:- Sử dụng câu lệnh
if
để so sánh ký tự tại vị tríi
củaS
vàT
.S[i]
sẽ truy cập ký tự thứi+1
(do chỉ số bắt đầu từ 0) của chuỗi S. - Nếu
S[i] == T[i]
, thêm ký tự 'G' vào cuối chuỗiM
. Sử dụng toán tử+=
. - Nếu
S[i] != T[i]
, thêm ký tự 'B' vào cuối chuỗiM
. Sử dụng toán tử+=
.
- Sử dụng câu lệnh
- In chuỗi kết quả M: Sau khi vòng lặp
for
kết thúc (đã xây dựng xong chuỗiM
), sử dụngcout << M << endl;
để in chuỗiM
theo sau là một ký tự xuống dòng.endl
đảm bảo mỗi kết quả test case nằm trên một dòng riêng biệt.
- Đọc chuỗi S và T: Khai báo hai đối tượng
Kết thúc chương trình: Hàm
main
thường trả về 0 để báo hiệu chương trình chạy thành công.
Tóm tắt luồng xử lý:
Đọc T
Trong khi T > 0:
Đọc chuỗi S
Đọc chuỗi T
Khởi tạo chuỗi kết quả M rỗng
Lặp từ i = 0 đến 4:
Nếu S[i] == T[i]:
Thêm 'G' vào M
Ngược lại:
Thêm 'B' vào M
In M
Giảm T đi 1
Lưu ý:
- Sử dụng
cin
vàcout
để nhập xuất. - Sử dụng
string
để làm việc với các chuỗi. - Tận dụng việc độ dài chuỗi là cố định (= 5) để dùng vòng lặp
for
với giới hạn rõ ràng (0 đến 4). - Đảm bảo in ký tự xuống dòng (
endl
) sau mỗi chuỗi kết quảM
.
#include <iostream>
#include <string>
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int t;
cin >> t;
while (t--) {
string sAn, sDoan;
cin >> sAn >> sDoan;
string m = "";
for (int i = 0; i < 5; ++i) {
if (sAn[i] == sDoan[i]) {
m += 'G';
} else {
m += 'B';
}
}
cout << m << endl;
}
return 0;
}
Output (với Input mẫu):
BBGBB
GBBBB
GGBBG
Comments