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ẽ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 cincout, 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:

  1. stringstream: Hỗ trợ cả việc đọc 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ó.
  2. istringstream: Chỉ hỗ trợ việc đọc (input) dữ liệu từ chuỗi. (Giống cin).
  3. ostringstream: Chỉ hỗ trợ việc ghi (output) dữ liệu vào chuỗi. (Giống cout).
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 stringstreamxâ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ên ss.
  • Sử dụng toán tử <<, chúng ta lần lượt đưa các hằng chuỗi, biến kiểu string, int, double vào ss. 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ỗi string 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ỗi du_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ên stringstream 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" sang int, từ "1200.50" sang double).
  • 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ấtan 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ủa bien_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ức ss >> bien_so khi dùng trong điều kiện if 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àngan 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ào stringstream.
  • 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ụng fixedsetprecision (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 trong stringstream.
  • .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ới stringstream), 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ọi ss.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ấy stringstream 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ử <<>> giống như làm việc với cincout, 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><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ẩnmạ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ới string 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ọntrự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ànmạ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:

  1. Trận đấu kết thúc khi một người đạt 7 điểm.
  2. Hiện tại, điểm của FullHouse Dev là A, điểm của đối thủ là B.
  3. 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.
  4. Để 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:

  1. Đọ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ý.
  2. Lặp qua từng bộ test: Sử dụng vòng lặp (ví dụ: while hoặc for) để xử lý T lần.
  3. Đọc điểm số: Trong mỗi lần lặp, đọc hai số nguyên AB là điểm số hiện tại của hai người chơi.
  4. 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êm 7 - 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êm 7 - B điểm nữa.
  5. 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.
  6. 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 - A7 - 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;
}

Làm thêm nhiều bài tập miễn phí tại đây

Comments

There are no comments at the moment.