Bài 29.1: Giới thiệu và sử dụng Stringstream trong C++

Bài 29.1: Giới thiệu và sử dụng Stringstream trong C++
Chào mừng các bạn đến với bài học hôm nay trong chuỗi blog về C++!
Trong thế giới lập trình C++, việc xử lý chuỗi (string manipulation) là một nhiệm vụ cực kỳ phổ biến. Chúng ta thường xuyên phải đối mặt với các tình huống như: đọc dữ liệu từ một dòng văn bản, chuyển đổi một số được biểu diễn dưới dạng chuỗi sang kiểu số thực sự, hoặc xây dựng một chuỗi phức tạp từ nhiều kiểu dữ liệu khác nhau.
Trong C++, ngoài các hàm xử lý chuỗi truyền thống của C (strcpy
, sprintf
, sscanf
, v.v.) hay các phương thức của lớp string
, thư viện chuẩn còn cung cấp một công cụ mạnh mẽ và linh hoạt khác: stringstream
. Đây chính là chủ đề chính của bài viết này.
stringstream
là gì?
Hãy tưởng tượng bạn có một chuỗi (string
) và muốn xử lý nó theo cách giống như bạn làm với luồng nhập chuẩn (cin
) hoặc luồng xuất chuẩn (cout
). Đó chính là ý tưởng đằng sau stringstream
.
stringstream
là một lớp trong thư viện chuẩn C++ (<sstream>
) cho phép bạn coi một đối tượng string
như một luồng (stream). Nghĩa là, bạn có thể sử dụng các toán tử <<
(insertion - chèn) và >>
(extraction - trích xuất) quen thuộc giống như khi làm việc với cin
và cout
, nhưng thay vì nhập/xuất ra console, dữ liệu sẽ được đọc từ hoặc ghi vào chính chuỗi đó trong bộ nhớ.
Các loại Stringstream
Thư viện <sstream>
cung cấp ba lớp chính, kế thừa từ các lớp luồng cơ bản, để phục vụ các mục đích khác nhau:
stringstream
: Hỗ trợ cả việc đọc và ghi dữ liệu vào/từ chuỗi. Đây là loại linh hoạt nhất và chúng ta sẽ tập trung vào nó.istringstream
: Chỉ hỗ trợ việc đọc (input) dữ liệu từ chuỗi. (Giốngcin
).ostringstream
: Chỉ hỗ trợ việc ghi (output) dữ liệu vào chuỗi. (Giốngcout
).
Sử dụng stringstream
để Xây dựng (Ghi vào) Chuỗi
Một trong những ứng dụng phổ biến nhất của stringstream
là xây dựng một chuỗi từ nhiều phần tử có kiểu dữ liệu khác nhau một cách dễ dàng. Thay vì phải gọi nhiều hàm chuyển đổi kiểu dữ liệu riêng lẻ, bạn chỉ cần "chèn" chúng vào stringstream
bằng toán tử <<
.
Hãy xem ví dụ:
#include <iostream>
#include <sstream> // Cần include sstream để sử dụng stringstream
#include <string>
int main() {
stringstream ss; // Tạo một đối tượng stringstream
string ten = "Alice";
int tuoi = 30;
double chieu_cao = 1.65;
// Sử dụng toán tử << để ghi các dữ liệu vào stream
ss << "Tên: " << ten << ", Tuổi: " << tuoi << ", Chiều cao: " << chieu_cao << "m.";
// Lấy chuỗi cuối cùng đã được xây dựng từ stream
string ket_qua = ss.str();
cout << "Chuỗi được tạo: " << ket_qua << endl;
return 0;
}
Giải thích:
- Chúng ta khai báo một đối tượng
stringstream
có tênss
. - Sử dụng toán tử
<<
, chúng ta lần lượt đưa các hằng chuỗi, biến kiểustring
,int
,double
vàoss
.stringstream
tự động biết cách chuyển đổi các kiểu dữ liệu này thành biểu diễn chuỗi tương ứng và nối chúng lại bên trong bộ đệm của nó. - Phương thức
.str()
được gọi để lấy ra chuỗistring
cuối cùng màstringstream
đã xây dựng được. - Kết quả là một chuỗi được tạo ra từ việc kết hợp các phần tử khác nhau một cách liền mạch.
Sử dụng stringstream
để Trích xuất (Đọc từ) Chuỗi
Ngược lại với việc xây dựng chuỗi, stringstream
cũng cho phép chúng ta trích xuất (đọc) dữ liệu từ một chuỗi có sẵn, giống như đọc từ cin
. Điều này rất hữu ích khi bạn cần phân tích cú pháp (parse) một dòng văn bản để lấy ra các thông tin cụ thể.
#include <iostream>
#include <sstream>
#include <string>
int main() {
string du_lieu = "ID: 101 Tên_sản_phẩm: Laptop Giá: 1200.50 Số_lượng: 5";
// Khởi tạo stringstream với chuỗi cần đọc
stringstream ss(du_lieu);
string nhan_id, nhan_ten, nhan_gia, nhan_sl;
int id, so_luong;
string ten_san_pham; // Tên sản phẩm có thể chứa khoảng trắng nếu chúng ta muốn đọc cả cụm, nhưng với >> mặc định sẽ dừng ở khoảng trắng
double gia;
// Sử dụng toán tử >> để đọc từng phần tử từ stream
// Mặc định, >> sẽ đọc cho đến khi gặp khoảng trắng
ss >> nhan_id >> id >> nhan_ten >> ten_san_pham >> nhan_gia >> gia >> nhan_sl >> so_luong;
// Lưu ý: Với cách đọc >>, "Tên_sản_phẩm:" sẽ được đọc vào nhan_ten,
// và "Laptop" sẽ được đọc vào ten_san_pham vì >> dừng lại ở khoảng trắng.
cout << "Đã trích xuất:" << endl;
cout << nhan_id << " " << id << endl;
cout << nhan_ten << " " << ten_san_pham << endl;
cout << nhan_gia << " " << gia << endl;
cout << nhan_sl << " " << so_luong << endl;
return 0;
}
Giải thích:
- Chúng ta khởi tạo
stringstream
bằng cách truyền trực tiếp chuỗidu_lieu
vào constructor. - Sử dụng toán tử
>>
, chúng ta lần lượt đọc các phần tử từ chuỗi. Giống nhưcin
, toán tử>>
trênstringstream
sẽ đọc dữ liệu cho đến khi gặp ký tự phân cách (mặc định là khoảng trắng) hoặc kiểu dữ liệu không phù hợp. stringstream
tự động cố gắng chuyển đổi dữ liệu đọc được sang kiểu của biến đích (ví dụ: từ "101" sangint
, từ "1200.50" sangdouble
).- Việc đọc dừng lại khi stream không còn dữ liệu hoặc gặp lỗi chuyển đổi.
Ứng dụng Phổ biến: Chuyển đổi giữa Chuỗi và Số
Đây là một trong những trường hợp sử dụng phổ biến nhất và an toàn nhất của stringstream
. Mặc dù C++11 đã giới thiệu to_string
, stoi
, v.v., nhưng stringstream
vẫn rất hữu ích, đặc biệt khi bạn cần xử lý nhiều giá trị hoặc cần kiểm tra lỗi chi tiết hơn.
Chuyển từ Chuỗi sang Số
Thay vì dùng các hàm kiểu C như atoi
, atof
(ít an toàn vì không có cách kiểm tra lỗi rõ ràng), stringstream
cung cấp một cơ chế tự nhiên hơn để chuyển đổi chuỗi thành các kiểu số nguyên, số thực, v.v., và cho phép kiểm tra xem việc chuyển đổi có thành công hay không.
#include <iostream>
#include <sstream>
#include <string>
int main() {
string str_so_nguyen = "12345";
string str_so_thuc = "3.14159";
string str_khong_phai_so = "hello";
int so_nguyen;
double so_thuc;
// Chuyển đổi thành công từ chuỗi sang int
stringstream ss_int(str_so_nguyen);
if (ss_int >> so_nguyen) {
cout << "Chuỗi \"" << str_so_nguyen << "\" chuyển thành số nguyên: " << so_nguyen << endl;
} else {
cout << "Không thể chuyển đổi \"" << str_so_nguyen << "\" thành số nguyên." << endl;
}
// Chuyển đổi thành công từ chuỗi sang double
stringstream ss_double(str_so_thuc);
if (ss_double >> so_thuc) {
cout << "Chuỗi \"" << str_so_thuc << "\" chuyển thành số thực: " << so_thuc << endl;
} else {
cout << "Không thể chuyển đổi \"" << str_so_thuc << "\" thành số thực." << endl;
}
// Chuyển đổi thất bại
stringstream ss_bad(str_khong_phai_so);
if (ss_bad >> so_nguyen) {
cout << "Chuỗi \"" << str_khong_phai_so << "\" chuyển thành số nguyên: " << so_nguyen << endl;
} else {
cout << "Không thể chuyển đổi \"" << str_khong_phai_so << "\" thành số nguyên." << endl; // Kết quả sẽ vào đây
}
return 0;
}
Giải thích:
- Chúng ta khởi tạo
stringstream
với chuỗi đầu vào. - Sử dụng
ss >> bien_so
, chúng ta cố gắng trích xuất dữ liệu từ stream và chuyển đổi nó sang kiểu củabien_so
. - Điểm quan trọng là chúng ta có thể kiểm tra trạng thái của stream ngay sau thao tác trích xuất. Nếu thao tác thành công (đọc được dữ liệu và chuyển đổi đúng kiểu), stream sẽ ở trạng thái tốt (
good()
) và biểu thứcss >> bien_so
khi dùng trong điều kiệnif
sẽ cho kết quảtrue
. Nếu thao tác thất bại (ví dụ: cố gắng đọc một chuỗi không phải số vào biến số), stream sẽ đặt cờ lỗi (failbit
), và biểu thức sẽ cho kết quảfalse
. Điều này giúp chúng ta xử lý các trường hợp nhập liệu không hợp lệ một cách rõ ràng và an toàn.
Chuyển từ Số sang Chuỗi
Việc chuyển đổi ngược lại, từ số sang chuỗi, cũng đơn giản không kém và rất hữu ích khi bạn muốn kết hợp số vào một chuỗi lớn hơn hoặc cần định dạng đặc biệt cho số.
#include <iostream>
#include <sstream>
#include <string>
#include <iomanip> // Cần cho fixed, setprecision
int main() {
int so = 98765;
double gia_tri_thuc = 123.456789;
// Chuyển đổi int sang chuỗi
stringstream ss_int;
ss_int << so;
string str_tu_int = ss_int.str();
cout << "Số " << so << " chuyển thành chuỗi: \"" << str_tu_int << "\"" << endl;
// Chuyển đổi double sang chuỗi với định dạng mặc định
stringstream ss_double_mac_dinh;
ss_double_mac_dinh << gia_tri_thuc;
string str_tu_double_mac_dinh = ss_double_mac_dinh.str();
cout << "Số thực " << gia_tri_thuc << " (mặc định) chuyển thành chuỗi: \"" << str_tu_double_mac_dinh << "\"" << endl;
// Chuyển đổi double sang chuỗi với định dạng cụ thể (ví dụ: 2 chữ số thập phân)
stringstream ss_double_dinh_dang;
ss_double_dinh_dang << fixed << setprecision(2) << gia_tri_thuc;
string str_tu_double_dinh_dang = ss_double_dinh_dang.str();
cout << "Số thực " << gia_tri_thuc << " (định dạng 2 thập phân) chuyển thành chuỗi: \"" << str_tu_double_dinh_dang << "\"" << endl;
return 0;
}
Giải thích:
- Tương tự như việc xây dựng chuỗi, chúng ta chỉ cần sử dụng toán tử
<<
để "chèn" số vàostringstream
. - Phương thức
.str()
sau đó lấy ra chuỗi kết quả. - Điểm mạnh ở đây là
stringstream
hoạt động với các manipulator định dạng luồng chuẩn, giống nhưcout
. Ví dụ, chúng ta có thể sử dụngfixed
vàsetprecision
(từ<iomanip>
) để kiểm soát cách số thực được biểu diễn trong chuỗi.
Xóa và Tái sử dụng stringstream
Nếu bạn cần sử dụng cùng một đối tượng stringstream
cho nhiều thao tác đọc/ghi độc lập khác nhau, bạn cần xóa nội dung của nó và đặt lại trạng thái của luồng.
.str("")
: Xóa nội dung của chuỗi đệm bên trongstringstream
..clear()
: Đặt lại tất cả các cờ trạng thái lỗi của stream (nhưfailbit
,eofbit
,badbit
) về trạng thái tốt (goodbit
). Đây là bước rất quan trọng vì nếu thao tác trước đó gặp lỗi hoặc đọc hết stream, stream sẽ ở trạng thái lỗi và các thao tác tiếp theo sẽ bị bỏ qua cho đến khi bạn gọi.clear()
.
#include <iostream>
#include <sstream>
#include <string>
int main() {
stringstream ss;
// Lần sử dụng 1: Ghi dữ liệu
ss << "Xin chao, ";
ss << 2023;
cout << "Sau lần 1: " << ss.str() << endl; // Output: Xin chao, 2023
// Chuẩn bị cho lần sử dụng mới: Xóa nội dung và đặt lại trạng thái
ss.str(""); // Xóa "Xin chao, 2023"
ss.clear(); // Đặt lại các cờ trạng thái (rất quan trọng!)
// Lần sử dụng 2: Ghi dữ liệu khác
ss << "Nhiệt độ hiện tại: ";
ss << 25.5 << "°C";
cout << "Sau lần 2: " << ss.str() << endl; // Output: Nhiệt độ hiện tại: 25.5°C
// Chuẩn bị cho lần sử dụng mới: Xóa nội dung và đặt lại trạng thái
ss.str("true false 100"); // Khởi tạo với chuỗi mới để đọc
ss.clear();
// Lần sử dụng 3: Đọc dữ liệu
bool b_val;
string s_val;
int i_val;
ss >> boolalpha >> b_val >> s_val >> i_val; // boolalpha để đọc "true"/"false"
cout << "Sau lần 3 (đọc):" << endl;
cout << "Bool: " << boolalpha << b_val << endl; // Output: Bool: true
cout << "String: " << s_val << endl; // Output: String: false
cout << "Int: " << i_val << endl; // Output: Int: 100
return 0;
}
Giải thích:
- Trong ví dụ này, chúng ta sử dụng cùng một đối tượng
ss
ba lần. - Trước lần sử dụng thứ hai,
ss.str("");
xóa nội dung "Xin chao, 2023".ss.clear();
đảm bảo rằng nếu lần ghi trước đó có bất kỳ vấn đề gì (ví dụ: đầy bộ nhớ - ít xảy ra vớistringstream
), stream sẽ trở lại trạng thái hoạt động bình thường. - Trước lần sử dụng thứ ba (để đọc), chúng ta dùng
ss.str("true false 100");
để thay thế toàn bộ nội dung chuỗi đệm bằng chuỗi mới. Lại gọiss.clear();
để đảm bảo stream sẵn sàng cho việc đọc từ đầu chuỗi mới. - Lưu ý cách chúng ta dùng
boolalpha
(từ<iostream>
) để stream có thể đọc giá trị boolean từ các chuỗi "true" hoặc "false". Điều này cho thấystringstream
tương thích tốt với các manipulator luồng khác.
Lợi ích của việc sử dụng stringstream
- Linh hoạt: Xử lý cả việc đọc (parsing) và ghi (formatting) chuỗi với cùng một cú pháp quen thuộc.
- An toàn kiểu dữ liệu: Dễ dàng làm việc với nhiều kiểu dữ liệu khác nhau (
int
,double
,bool
,string
, v.v.) một cách tự nhiên và an toàn. - Cú pháp quen thuộc: Sử dụng toán tử
<<
và>>
giống như làm việc vớicin
vàcout
, giúp mã nguồn dễ đọc và dễ viết hơn đối với những người đã quen với luồng I/O trong C++. - Hỗ trợ định dạng: Tương thích với các manipulator từ
<iomanip>
và<iostream>
để kiểm soát cách dữ liệu được biểu diễn trong chuỗi (ví dụ: số chữ số thập phân, căn lề, hiển thị boolean). - Kiểm tra lỗi dễ dàng: Trạng thái của stream cung cấp một cách tiêu chuẩn và mạnh mẽ để kiểm tra xem các thao tác đọc hoặc chuyển đổi có thành công hay không.
So sánh ngắn gọn với các phương pháp khác
- So với các hàm C-style (
sprintf
,sscanf
):stringstream
an toàn hơn (không bị tràn bộ đệm), dễ sử dụng hơn vớistring
và các kiểu dữ liệu phức tạp của C++, và có cơ chế kiểm tra lỗi tốt hơn. - So với
to_string
,stoi
, v.v. (C++11 trở đi): Đối với các chuyển đổi đơn giản (chỉ một giá trị từ chuỗi sang số hoặc ngược lại), các hàm này thường ngắn gọn và trực tiếp hơn. Tuy nhiên, khi bạn cần phân tích cú pháp một chuỗi phức tạp chứa nhiều loại dữ liệu (ví dụ: "Tên: Alice Tuổi: 30") hoặc xây dựng một chuỗi phức tạp từ nhiều thành phần,stringstream
lại thể hiện sự vượt trội về tính linh hoạt và sức mạnh.
stringstream
là một công cụ không thể thiếu trong hộp công cụ của lập trình viên C++ khi làm việc với chuỗi, đặc biệt là khi cần kết hợp xử lý nhiều kiểu dữ liệu hoặc cần phân tích cú pháp theo cấu trúc phức tạp. Nắm vững cách sử dụng nó sẽ giúp code của bạn sạch sẽ, an toàn và mạnh mẽ hơn.
Bài tập ví dụ: C++ Bài 18.A1: Khúc côn cầu trên không
Khúc côn cầu trên không
FullHouse Dev đang chơi Khúc côn cầu trên không. Người đầu tiên ghi được bảy điểm sẽ thắng trận đấu. Hiện tại, điểm số của FullHouse Dev là A và điểm số của đối thủ là B.
Hãy giúp FullHouse Dev tính toán số điểm tối thiểu cần được ghi thêm trong trận đấu trước khi nó kết thúc.
INPUT FORMAT
- Dòng đầu tiên chứa một số nguyên T - số lượng bộ test.
- Mỗi bộ test gồm một dòng chứa hai số nguyên A và B cách nhau bởi dấu cách, như mô tả trong đề bài.
OUTPUT FORMAT
Với mỗi bộ test, in ra một dòng chứa số điểm tối thiểu cần được ghi thêm trong trận đấu trước khi nó kết thúc.
CONSTRAINTS
- 1 ≤ T ≤ 50
- 0 ≤ A, B ≤ 6
Ví dụ
Input
3
5 0
0 5
3 3
Output
2
2
4
Giải thích:
- Test 1: FullHouse Dev cần ghi thêm 2 điểm để đạt 7 điểm và thắng trận.
- Test 2: FullHouse Dev cần ghi 7 điểm để thắng trận.
- Test 3: FullHouse Dev cần ghi thêm 4 điểm để đạt 7 điểm và thắng trận. Chào bạn, đây là hướng dẫn giải bài tập "Khúc côn cầu trên không" bằng C++ theo yêu cầu, tập trung vào hướng đi và sử dụng các thành phần chuẩn của thư viện C++ (std).
Phân tích bài toán:
- Trận đấu kết thúc khi một người đạt 7 điểm.
- Hiện tại, điểm của FullHouse Dev là A, điểm của đối thủ là B.
- Ta cần tìm số điểm tối thiểu cần ghi thêm trong trận đấu để nó kết thúc. "Ghi thêm trong trận đấu" ở đây có thể hiểu là tổng số điểm mà cả hai người chơi ghi được kể từ thời điểm hiện tại cho đến khi trận đấu kết thúc.
- Để trận đấu kết thúc với số điểm tối thiểu được ghi thêm, một trong hai người chơi phải đạt 7 điểm ngay lập tức mà người kia không ghi thêm bất kỳ điểm nào.
Lộ trình giải:
- Đọc số lượng bộ test: Chương trình cần đọc số nguyên
T
đầu tiên để biết có bao nhiêu bộ dữ liệu cần xử lý. - Lặp qua từng bộ test: Sử dụng vòng lặp (ví dụ:
while
hoặcfor
) để xử lýT
lần. - Đọc điểm số: Trong mỗi lần lặp, đọc hai số nguyên
A
vàB
là điểm số hiện tại của hai người chơi. - Tính điểm cần thiết cho mỗi người chơi:
- Để FullHouse Dev thắng, điểm của anh ấy cần đạt 7. Anh ấy đang có
A
điểm, vậy cần ghi thêm7 - A
điểm nữa. - Để đối thủ thắng, điểm của họ cần đạt 7. Họ đang có
B
điểm, vậy cần ghi thêm7 - B
điểm nữa.
- Để FullHouse Dev thắng, điểm của anh ấy cần đạt 7. Anh ấy đang có
- Tìm số điểm tối thiểu để kết thúc trận đấu: Trận đấu kết thúc ngay khi một trong hai người đạt 7 điểm. Để số điểm ghi thêm tối thiểu, ta xét hai trường hợp kết thúc nhanh nhất:
- Trường hợp 1: FullHouse Dev ghi điểm liên tục cho đến khi đạt 7 điểm, và đối thủ không ghi thêm điểm nào. Tổng điểm ghi thêm trong trận đấu là
7 - A
. - Trường hợp 2: Đối thủ ghi điểm liên tục cho đến khi đạt 7 điểm, và FullHouse Dev không ghi thêm điểm nào. Tổng điểm ghi thêm trong trận đấu là
7 - B
. Số điểm tối thiểu cần ghi thêm trong trận đấu để nó kết thúc chính là giá trị nhỏ nhất giữa hai trường hợp trên.
- Trường hợp 1: FullHouse Dev ghi điểm liên tục cho đến khi đạt 7 điểm, và đối thủ không ghi thêm điểm nào. Tổng điểm ghi thêm trong trận đấu là
- In kết quả: Với mỗi bộ test, in ra kết quả tính được trên một dòng riêng.
Sử dụng các thành phần chuẩn của C++ (std):
- Sử dụng
cin
để đọc dữ liệu đầu vào. - Sử dụng
cout
để in kết quả ra màn hình. - Sử dụng
min
từ thư viện<algorithm>
để tìm giá trị nhỏ nhất giữa hai số7 - A
và7 - B
. - Có thể sử dụng
endl
hoặc'\n'
để xuống dòng sau mỗi kết quả.
Gợi ý cấu trúc code (không phải code hoàn chỉnh):
// Bao gồm các thư viện cần thiết
#include <iostream> // cho cin, cout
#include <algorithm> // cho min
// Hàm main
int main() {
// Tối ưu tốc độ nhập xuất (không bắt buộc nhưng tốt cho các bài lớn hơn)
ios_base::sync_with_stdio(false);
cin.tie(NULL);
// Khai báo biến cho số bộ test
int T;
// Đọc T
// Vòng lặp xử lý T bộ test
// while (...) {
// Khai báo biến cho điểm A và B
int A, B;
// Đọc A và B
// Tính toán điểm cần thiết cho A
int points_needed_A = ... ; // 7 - A
// Tính toán điểm cần thiết cho B
int points_needed_B = ... ; // 7 - B
// Tìm giá trị nhỏ nhất của hai số trên
int min_additional_points = min(..., ...);
// In kết quả
cout << ... << endl; // hoặc '\n'
// }
// Trả về 0 báo hiệu chương trình kết thúc thành công
return 0;
}
Comments