Bài 29.5: Bài tập thực hành Stringstream trong C++

Chào mừng quay trở lại với series học lập trình C++! Chúng ta đã cùng nhau đi qua rất nhiều khái niệm cơ bản và nâng cao. Đến với bài hôm nay, chúng ta sẽ khám phá một công cụ cực kỳ hữu ích khi làm việc với chuỗi và dữ liệu trong C++: Stringstream!

Bạn đã quen với việc nhập dữ liệu từ bàn phím bằng cin và xuất dữ liệu ra màn hình bằng cout. Nhưng điều gì sẽ xảy ra nếu bạn muốn "đọc" dữ liệu từ một chuỗi có sẵn thay vì từ bàn phím? Hoặc bạn muốn "ghi" dữ liệu (như số, biến) vào một chuỗi thay vì in ra màn hình? Đây chính là lúc stringstream tỏa sáng rực rỡ!

Stringstream là gì và tại sao nó lại "ngon"?

Stringstream là một lớp được cung cấp trong thư viện chuẩn C++ thông qua header <sstream>. Về cơ bản, nó cho phép bạn coi một đối tượng string như một luồng (stream). Điều này có nghĩa là bạn có thể sử dụng các toán tử >> (extraction) để trích xuất dữ liệu từ chuỗi, và toán tử << (insertion) để chèn dữ liệu vào chuỗi, giống hệt cách bạn làm với cincout.

Lợi ích to lớn của stringstream là khả năng kết hợp sự linh hoạt của luồng I/O với khả năng lưu trữ và thao tác của string. Nó giải quyết hiệu quả hai vấn đề phổ biến:

  1. Parsing (Phân tích/Trích xuất): Lấy các phần dữ liệu có cấu trúc (như số, từ, v.v.) ra khỏi một chuỗi. Ví dụ: trích xuất tuổi và tên từ chuỗi "John 30".
  2. Formatting (Định dạng/Ghép nối): Kết hợp nhiều loại dữ liệu khác nhau (số nguyên, số thực, chuỗi) thành một chuỗi duy nhất theo định dạng mong muốn. Ví dụ: tạo chuỗi "Sản phẩm A - Giá: 150.00 USD" từ các biến riêng lẻ.

Thư viện <sstream> cung cấp ba lớp chính:

  • istringstream: Cho phép đọc/trích xuất dữ liệu từ chuỗi (Input String Stream).
  • ostringstream: Cho phép ghi/chèn dữ liệu vào chuỗi (Output String Stream).
  • stringstream: Kết hợp cả hai chức năng đọc và ghi (Bidirectional String Stream).

Trong thực tế, stringstream là lớp được sử dụng phổ biến nhất vì sự tiện lợi của nó cho cả hai mục đích.

Thực hành: Bắt tay vào code với Stringstream!

Để sử dụng stringstream, bạn chỉ cần include header <sstream>.

#include <iostream>
#include <sstream> // Thêm thư viện này
#include <string>
// Có thể cần thêm các thư viện khác tùy vào dữ liệu bạn xử lý, ví dụ:
// #include <vector>
// #include <iomanip>
Ví dụ 1: Parsing cơ bản - Trích xuất số và từ từ chuỗi

Giả sử bạn có một chuỗi chứa cả số và chữ, và bạn muốn tách chúng ra. stringstream làm điều này rất dễ dàng.

#include <iostream>
#include <sstream>
#include <string>

int main() {
    string du_lieu_text = "So luong: 100, Mat hang: Ao Thun";
    stringstream ss(du_lieu_text); // Khởi tạo stringstream với chuỗi cần xử lý

    string phan_dau;
    int so_luong;
    string phan_giua;
    string mat_hang;

    // Sử dụng toán tử >> để trích xuất dữ liệu
    // >> sẽ dừng lại khi gặp khoảng trắng hoặc dấu phân cách mặc định
    ss >> phan_dau >> so_luong >> phan_giua >> mat_hang;

    cout << "Phan dau: " << phan_dau << endl;     // Output: Phan dau: So
    cout << "So luong: " << so_luong << endl;     // Output: So luong: 100
    cout << "Phan giua: " << phan_giua << endl;   // Output: Phan giua: Mat
    cout << "Mat hang: " << mat_hang << endl;     // Output: Mat hang: Ao

    return 0;
}

Giải thích: Chúng ta tạo một stringstream tên là ss và khởi tạo nó với du_lieu_text. Sau đó, chúng ta sử dụng toán tử >> để đọc dữ liệu từ ss vào các biến phan_dau, so_luong, phan_giua, mat_hang. Giống như cin, stringstream sẽ tự động bỏ qua khoảng trắng và chuyển đổi kiểu dữ liệu khi cần thiết (ví dụ: từ chuỗi "100" sang số nguyên 100).

Tuy nhiên, bạn thấy ở ví dụ trên, việc trích xuất dựa vào khoảng trắng có thể không đúng ý nếu dữ liệu có cấu trúc phức tạp hơn (ví dụ: "So luong:" bị tách thành "So").

Ví dụ 2: Parsing với Delimiter - Sử dụng getline

Khi dữ liệu được phân cách bởi một ký tự cụ thể (như dấu phẩy trong CSV - Comma Separated Values), chúng ta thường kết hợp stringstream với hàm getline.

#include <iostream>
#include <sstream>
#include <string>
#include <vector> // Để lưu các phần dữ liệu

int main() {
    string du_lieu_csv = "Tran Van A,25,Ha Noi,Ky Su";
    stringstream ss(du_lieu_csv); // Khởi tạo với chuỗi CSV

    string phan_du_lieu;
    vector<string> cac_thong_tin;

    // Sử dụng getline để đọc từng phần, phân cách bởi dấu ','
    // getline(luong, bien_lưu, ky_tu_phan_cach)
    while (getline(ss, phan_du_lieu, ',')) {
        cac_thong_tin.push_back(phan_du_lieu);
    }

    // Xuất các thông tin đã trích xuất
    if (cac_thong_tin.size() >= 4) {
        cout << "Ho Ten: " << cac_thong_tin[0] << endl; // Output: Ho Ten: Tran Van A
        cout << "Tuoi: " << cac_thong_tin[1] << endl;   // Output: Tuoi: 25
        cout << "Dia Chi: " << cac_thong_tin[2] << endl;// Output: Dia Chi: Ha Noi
        cout << "Nghe Nghiep: " << cac_thong_tin[3] << endl; // Output: Nghe Nghiep: Ky Su
    } else {
        cout << "Chuoi CSV khong dung dinh dang." << endl;
    }


    return 0;
}

Giải thích: Ở đây, chúng ta dùng getline(ss, phan_du_lieu, ','). Hàm này sẽ đọc từ luồng ss cho đến khi gặp ký tự , và lưu phần đọc được vào biến phan_du_lieu. Vòng lặp while sẽ tiếp tục cho đến khi getline không đọc được gì nữa (đến cuối chuỗi). Mỗi phần dữ liệu (tên, tuổi, địa chỉ, nghề nghiệp) được đẩy vào một vector.

Ví dụ 3: Formatting cơ bản - Ghép nối dữ liệu thành chuỗi

Bây giờ, hãy xem cách sử dụng stringstream để tạo ra một chuỗi từ các biến có kiểu dữ liệu khác nhau.

#include <iostream>
#include <sstream>
#include <string>

int main() {
    string ten_san_pham = "Laptop XYZ";
    int so_luong_ton = 50;
    double gia_tien = 1250.75;

    stringstream ss; // Khởi tạo stringstream rỗng

    // Sử dụng toán tử << để chèn dữ liệu vào stringstream
    ss << "Thong tin san pham: "
       << ten_san_pham
       << " - So luong: "
       << so_luong_ton
       << " - Gia: "
       << gia_tien
       << " USD.";

    // Lấy chuỗi kết quả từ stringstream
    string chuoi_thong_tin = ss.str();

    cout << chuoi_thong_tin << endl; // Output: Thong tin san pham: Laptop XYZ - So luong: 50 - Gia: 1250.75 USD.

    return 0;
}

Giải thích: Chúng ta tạo một stringstream rỗng. Sau đó, dùng toán tử << để "in" các biến và chuỗi cố định vào ss. stringstream tự động chuyển đổi các kiểu dữ liệu (int, double) thành dạng chuỗi và nối chúng lại. Cuối cùng, hàm ss.str() trả về toàn bộ nội dung của stringstream dưới dạng một string.

Ví dụ 4: Formatting nâng cao với Manipulators

Giống như khi sử dụng cout, bạn có thể sử dụng các manipulator (bộ điều khiển định dạng) từ thư viện <iomanip> với stringstream để kiểm soát cách dữ liệu được chuyển đổi thành chuỗi.

#include <iostream>
#include <sstream>
#include <string>
#include <iomanip> // Cần thư viện này cho các manipulator

int main() {
    double diem_trung_binh = 8.56789;
    int ma_san_pham_hex = 255; // Tương đương FF trong hex

    stringstream ss;

    // Định dạng số thực với số chữ số thập phân cố định
    ss << "Diem trung binh (2 chu so thap phan): "
       << fixed << setprecision(2) // Sử dụng manipulator
       << diem_trung_binh << endl; // Output: Diem trung binh (2 chu so thap phan): 8.57

    // Định dạng số nguyên sang hệ thập lục phân (hexadecimal)
    ss << "Ma san pham (Hex): "
       << hex << uppercase // Sử dụng manipulator
       << ma_san_pham_hex << endl; // Output: Ma san pham (Hex): FF

    // Lấy chuỗi kết quả
    string chuoi_dinh_dang = ss.str();

    cout << chuoi_dinh_dang;
    /* Output:
    Diem trung binh (2 chu so thap phan): 8.57
    Ma san pham (Hex): FF
    */

    return 0;
}

Giải thích: Chúng ta include <iomanip> để sử dụng fixedsetprecision(2) (để định dạng số thực với 2 chữ số sau dấu thập phân), cùng với hexuppercase (để hiển thị số nguyên ở dạng hệ 16 và chữ hoa). Các manipulator này hoạt động với stringstream theo cách tương tự như với cout, cho phép bạn kiểm soát chi tiết hơn quá trình chuyển đổi dữ liệu sang chuỗi.

Ví dụ 5: Xóa và Tái sử dụng Stringstream

Nếu bạn cần sử dụng lại cùng một đối tượng stringstream cho một thao tác khác, bạn cần "dọn dẹp" nó. Có hai việc cần làm: xóa nội dung buffer và xóa các cờ trạng thái (ví dụ: cờ EOF - End Of File).

#include <iostream>
#include <sstream>
#include <string>

int main() {
    stringstream ss;

    // Lần dùng 1: Ghi dữ liệu vào
    ss << "Hello " << "World!";
    cout << "Noi dung lan 1: " << ss.str() << endl; // Output: Noi dung lan 1: Hello World!

    // Chuẩn bị tái sử dụng: Xóa buffer và cờ trạng thái
    ss.str("");    // Gán chuỗi rỗng để xóa nội dung
    ss.clear();  // Xóa các cờ trạng thái (như EOF, failbit)

    // Lần dùng 2: Ghi dữ liệu khác vào
    ss << "So luong: " << 99;
    cout << "Noi dung lan 2: " << ss.str() << endl; // Output: Noi dung lan 2: So luong: 99

    // Chuẩn bị tái sử dụng lần nữa: Xóa buffer và cờ trạng thái
    ss.str("123 456"); // Gán chuỗi mới để đọc
    ss.clear();        // Xóa cờ trạng thái (rất quan trọng khi đọc lại sau khi đã đọc hết)

    // Lần dùng 3: Đọc dữ liệu từ chuỗi mới
    int x, y;
    ss >> x >> y;
    cout << "Doc du lieu lan 3: x = " << x << ", y = " << y << endl; // Output: Doc du lieu lan 3: x = 123, y = 456

    return 0;
}

Giải thích: ss.str("") sẽ thiết lập nội dung bên trong stringstream thành một chuỗi rỗng, hiệu quả là xóa buffer. Tuy nhiên, nếu trước đó bạn đã đọc hết dữ liệu từ stringstream, cờ trạng thái eofbit (end-of-file bit) sẽ được bật lên, khiến các thao tác đọc hoặc ghi tiếp theo bị lỗi. ss.clear() sẽ xóa tất cả các cờ trạng thái (bao gồm eofbit, failbit, badbit), đưa stringstream về trạng thái tốt (goodbit) và sẵn sàng cho các thao tác mới. Khi bạn muốn đọc từ một chuỗi mới, bạn dùng ss.str("chuoi moi") để gán chuỗi đó vào buffer, và vẫn cần ss.clear() để đảm bảo trạng thái luồng là sạch.

Một vài lưu ý nhỏ khi dùng Stringstream

  • Kiểm tra lỗi: Khi thực hiện các thao tác đọc (>>) từ stringstream, hãy luôn kiểm tra xem việc đọc có thành công không. Bạn có thể làm điều này giống như với cin, ví dụ: if (ss >> bien) { ... } hoặc kiểm tra trạng thái của luồng sau khi đọc: if (!ss.fail()) { ... }.
  • Hiệu quả: Đối với việc nối chuỗi đơn giản, việc sử dụng toán tử + hoặc phương thức append() của string có thể nhanh hơn và đơn giản hơn. stringstream thực sự phát huy thế mạnh khi bạn cần định dạng phức tạp, chuyển đổi kiểu dữ liệu giữa số/kiểu khác và chuỗi, hoặc phân tích chuỗi dựa trên cấu trúc phức tạp hơn là chỉ nối đơn thuần.
  • Chọn đúng lớp: Mặc dù stringstream dùng được cho cả hai mục đích, đôi khi sử dụng istringstream hoặc ostringstream có thể rõ ràng hơn về ý định của bạn (chỉ đọc hoặc chỉ ghi).

Bài tập ví dụ: C++ Bài 18.B2: Kiểm tra nhu cầu đăng ký

Kiểm tra nhu cầu đăng ký

FullHouse Dev cần tổ chức một buổi giảng dạy trực tuyến và cần thiết lập một cuộc họp kéo dài chính xác X phút.

Nền tảng họp trực tuyến hỗ trợ cuộc họp tối đa 30 phút mà không cần đăng ký, và cuộc họp không giới hạn thời gian nếu có đăng ký.

Hãy xác định xem FullHouse Dev có cần đăng ký hay không để thiết lập cuộc họp.

INPUT FORMAT

  • Dòng đầu tiên chứa số nguyên T — số lượng bộ test.
  • Mỗi bộ test chứa một số nguyên X — thời lượng của buổi giảng dạy.

OUTPUT FORMAT

  • Với mỗi bộ test, in ra một dòng duy nhất, YES nếu FullHouse Dev cần đăng ký, ngược lại in NO.

CONSTRAINTS

  • 1 ≤ T ≤ 100
  • 1 ≤ X ≤ 100
Ví dụ

Input

4
50
3
30
80

Output

YES
NO
NO
YES
Giải thích:
  • Test 1: Không có đăng ký, nền tảng chỉ cho phép thời lượng 30 phút. Vì FullHouse Dev cần tổ chức buổi giảng dạy 50 phút, nên cần phải đăng ký.
  • Test 2: Không cần đăng ký vì buổi giảng chỉ kéo dài 3 phút.
  • Test 3: Không cần đăng ký vì buổi giảng kéo dài đúng 30 phút.
  • Test 4: Cần đăng ký vì buổi giảng kéo dài 80 phút, vượt quá giới hạn 30 phút của phiên bản không đăng ký. Chào bạn, đây là hướng dẫn để giải bài toán này bằng C++ theo yêu cầu:
  1. Đọc số lượng bộ test: Đầu tiên, bạn cần đọc số nguyên T từ đầu vào. Đây là số lần bạn sẽ phải xử lý logic kiểm tra.

  2. Vòng lặp xử lý test case: Sử dụng một vòng lặp (ví dụ: for hoặc while) để lặp lại T lần. Mỗi lần lặp tương ứng với một bộ test.

  3. Trong mỗi lần lặp:

    • Đọc thời lượng cuộc họp: Đọc số nguyên X (thời lượng buổi giảng dạy) cho bộ test hiện tại.
    • Kiểm tra điều kiện: Nền tảng cho phép cuộc họp tối đa 30 phút mà không cần đăng ký. Điều này có nghĩa là nếu thời lượng X lớn hơn 30 phút, bạn sẽ cần đăng ký. Ngược lại (nếu X nhỏ hơn hoặc bằng 30 phút), bạn không cần đăng ký.
    • In kết quả:
      • Nếu điều kiện cần đăng ký đúng (nghĩa là X lớn hơn 30), in ra "YES".
      • Ngược lại, in ra "NO".
  4. Xuống dòng: Sau khi in "YES" hoặc "NO" cho mỗi bộ test, hãy đảm bảo in ký tự xuống dòng (\n hoặc sử dụng endl) để kết quả của các bộ test không bị dính vào nhau.

Gợi ý kỹ thuật C++:

  • Sử dụng thư viện iostream cho việc nhập (cin) và xuất (cout).
  • Sử dụng câu lệnh điều kiện if/else để kiểm tra X > 30.
  • Vòng lặp while (T--) là một cách ngắn gọn để lặp lại T lần sau khi đã đọc giá trị T.

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

Comments

There are no comments at the moment.