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> // Quan trọng: cần include header này!
int main() {
// Bây giờ bạn có thể sử dụng string
string myString = "Chào thế giới!";
cout << myString << endl;
return 0;
}
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 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 emptyString; // Một chuỗi rỗng cout << "Chuoi rong: '" << emptyString << "'" << endl; return 0; }
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 greeting = "Xin chào C++"; // Khởi tạo trực tiếp string another = string("Thật thú vị!"); // Có thể dùng constructor rõ ràng cout << greeting << endl; cout << another << endl; return 0; }
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 original = "Sao chep toi!"; string copied = original; // Sao chép nội dung string alsoCopied(original); // Cũng là sao chép cout << "Ban dau: " << original << endl; cout << "Sao chep: " << copied << endl; cout << "Cung sao chep: " << alsoCopied << endl; return 0; }
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 stars(10, '*'); // Chuỗi gồm 10 ký tự '*' string dashes(5, '-'); cout << stars << endl; cout << dashes << endl; return 0; }
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 word;
cout << "Nhap mot tu: ";
cin >> word; // Chi doc mot tu
cout << "Tu ban vua nhap: " << word << endl;
// Problem: cin de lai ky tu newline trong bo dem
// Neu dung cin >> tiep theo se bi loi.
// Neu dung getline() ngay sau cin >> se bi loi.
// Can xoa bo dem nhap
cin.ignore(1000, '\n'); // Xoa toi da 1000 ky tu hoac den khi gap newline
string line;
cout << "Nhap mot dong van ban: ";
getline(cin, line); // Doc ca dong
cout << "Dong ban vua nhap: " << line << endl;
return 0;
}
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 text = "Hello C++";
cout << "Do dai cua chuoi: " << text.length() << endl;
cout << "Kich thuoc cua chuoi: " << text.size() << endl; // size() cung tuong tu
return 0;
}
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>
int main() {
string word = "Lap trinh";
cout << "Ky tu dau tien: " << word[0] << endl; // 'L'
cout << "Ky tu thu ba: " << word.at(2) << endl; // 'p'
// Can than: truy cap ngoai chi so bang [] co the gay loi chuong trinh
// cout << word[100] << endl; // Danger!
// truy cap ngoai chi so bang .at() se throw exception
try {
cout << word.at(100) << endl; // An toan hon, se bao loi
} catch (const out_of_range& oor) {
cerr << "Loi: Truy cap ngoai pham vi: " << oor.what() << endl;
}
return 0;
}
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 part1 = "Hello";
string part2 = " World";
string combined = part1 + part2; // Dung toan tu +
cout << "Chuoi sau khi noi (+): " << combined << endl;
string sentence = "C++ ";
sentence.append("rat "); // Noi them chuoi
sentence.append("tuyet!");
cout << "Chuoi sau khi noi (.append()): " << sentence << endl;
return 0;
}
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) { // So sanh bang nhau
cout << s1 << " bang " << s3 << endl;
}
if (s1 != s2) { // So sanh khac nhau
cout << s1 << " khac " << s2 << endl;
}
if (s1 < s2) { // So sanh nho hon (theo tu dien)
cout << s1 << " nho hon " << s2 << endl;
}
// Su dung .compare()
// Tra ve 0 neu bang nhau
// Tra ve < 0 neu chuoi goi nho hon chuoi doi so
// Tra ve > 0 neu chuoi goi lon hon chuoi doi so
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;
}
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!";
// empty() va clear()
if (!s.empty()) { // Kiem tra khong rong
cout << "'" << s << "' khong rong." << endl;
s.clear(); // Xoa chuoi
cout << "Sau khi clear, chuoi rong? " << (s.empty() ? "Co" : "Khong") << endl;
}
s = "Day la mot chuoi de tim kiem.";
// find()
string keyword = "tim";
size_t pos = s.find(keyword); // Tim vi tri cua "tim"
if (pos != string::npos) { // string::npos la gia tri khi khong tim thay
cout << "Tim thay '" << keyword << "' tai vi tri: " << pos << endl;
} else {
cout << "Khong tim thay '" << keyword << "'" << endl;
}
// substr()
string original = "Lap trinh C++ rat hay";
// trich xuat tu vi tri 10 (chu 'C') voi do dai 3 ky tu
string sub = original.substr(10, 3);
cout << "Chuoi con trich xuat: " << sub << endl; // Ket qua: C++
// push_back() va pop_back()
string dynamic_str = "abc";
dynamic_str.push_back('d'); // Them 'd' vao cuoi
cout << "Sau khi push_back('d'): " << dynamic_str << endl; // abcd
dynamic_str.pop_back(); // Xoa ky tu cuoi 'd'
cout << "Sau khi pop_back(): " << dynamic_str << endl; // abc
return 0;
}
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> // Can cho printf
int main() {
string cppString = "Day la string";
// Chuyen doi sang C-style string de su dung voi printf
const char* cString = cppString.c_str();
printf("Su dung C-style string voi printf: %s\n", cString);
return 0;
}
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
.
Comments