Bài 3.4: Bài tập thực hành nhập xuất nâng cao trong C++

Chào mừng trở lại với series C++ của chúng ta! Ở các bài trước, chúng ta đã làm quen với những thao tác nhập xuất cơ bản nhất sử dụng cincout. Chúng rất hữu ích và là nền tảng cho mọi chương trình tương tác. Tuy nhiên, thế giới lập trình không chỉ dừng lại ở việc nhập một số hay in ra một dòng chữ đơn giản.

Đôi khi, chúng ta cần định dạng output một cách chuyên nghiệp hơn (ví dụ: in tiền tệ, căn lề, độ chính xác thập phân). Đôi khi, chúng ta cần xử lý những loại input phức tạp hơn (ví dụ: đọc cả một dòng văn bản có chứa khoảng trắng). Và đôi khi, với những bài toán có lượng dữ liệu lớn, tốc độ nhập xuất lại trở thành một yếu tố then chốt.

Trong bài thực hành này, chúng ta sẽ cùng nhau đào sâu hơn vào các kỹ thuật nhập xuất "nâng cao" trong C++, biến cincout từ những công cụ thô sơ thành những "nghệ sĩ" thực thụ trong việc xử lý dữ liệu. Hãy chuẩn bị tinh thần và cùng khám phá nhé!

1. Định dạng Output với cout và Thư viện <iomanip>

cout rất linh hoạt. Ngoài việc in các kiểu dữ liệu cơ bản, chúng ta có thể sử dụng các manipulators (bộ điều chỉnh) để kiểm soát cách dữ liệu được hiển thị. Các manipulators này thường nằm trong thư viện <iomanip>.

1.1. Định dạng Số Thực: Độ Chính Xác và Dạng Hiển Thị

Khi làm việc với số thực (float, double), việc kiểm soát số chữ số sau dấu thập phân là rất quan trọng, đặc biệt là khi hiển thị tiền tệ hoặc các giá trị khoa học.

  • fixed: Đặt chế độ hiển thị số thực ở dạng "fixed-point", tức là luôn có một số chữ số cố định sau dấu thập phân.
  • setprecision(n): Kết hợp với fixed, n sẽ là số chữ số hiển thị sau dấu thập phân. Nếu không có fixed, n sẽ là tổng số chữ số có nghĩa (significant digits).

Hãy xem ví dụ:

#include <iostream>
#include <iomanip> // Cần include thư viện này

int main() {
    double pi = 3.1415926535;
    double price = 19.99;
    double large_num = 12345.6789;

    cout << "Pi mac dinh: " << pi << endl; // Mặc định có thể khác nhau tùy compiler
    cout << "--------------------" << endl;

    // Hien thi voi 2 chu so thap phan
    cout << fixed << setprecision(2);
    cout << "Pi 2 thap phan: " << pi << endl;
    cout << "Gia tien: " << price << endl;
    cout << "So lon 2 thap phan: " << large_num << endl;

    cout << "--------------------" << endl;

    // Hien thi voi 4 chu so thap phan
    cout << fixed << setprecision(4);
    cout << "Pi 4 thap phan: " << pi << endl;

    // Quay lai che do mac dinh (optional, fixed/precision keo dai den khi thay doi)
    // cout << defaultfloat; // Quay lai hien thi theo so chu so co nghia
    // cout << setprecision(6); // Dat lai precision mac dinh

    return 0;
}

Giải thích: Chúng ta sử dụng fixedsetprecision(n) ngay trước biến cần in. Điều này thiết lập chế độ hiển thị cho các output sau đó trên cùng luồng cout cho đến khi bạn thay đổi nó hoặc luồng kết thúc.

1.2. Căn Lề và Độ Rộng Trường Hiển Thị

Đôi khi, chúng ta muốn in dữ liệu theo cột để bảng biểu gọn gàng hơn. setw(w) và các bộ căn lề left, right, internal giúp chúng ta làm điều đó.

  • setw(w): Thiết lập độ rộng tối thiểu của trường hiển thị tiếp theo là w. Nếu dữ liệu ngắn hơn w, nó sẽ được đệm bằng khoảng trắng. Quan trọng: setw chỉ áp dụng cho output ngay sau nó, không kéo dài như fixed hay setprecision.
  • left: Căn lề dữ liệu sang trái trong trường hiển thị.
  • right: Căn lề dữ liệu sang phải trong trường hiển thị (mặc định).
  • internal: Dấu (cho số âm) hoặc ký hiệu cơ số (0x cho hệ 16) được căn sang trái, còn giá trị số được căn sang phải.

Ví dụ:

#include <iostream>
#include <iomanip> // Cần include thư viện này
#include <string>

int main() {
    string item1 = "Apple";
    double price1 = 1.25;
    string item2 = "Banana (Organic)";
    double price2 = 2.50;
    string item3 = "Orange Juice";
    double price3 = 3.75;

    // Thiết lập định dạng tiền tệ cố định
    cout << fixed << setprecision(2);

    // Tiêu đề
    cout << left << setw(20) << "Item";
    cout << right << setw(10) << "Price" << endl;
    cout << setfill('-') << setw(30) << "" << endl; // In duong gach ngang
    cout << setfill(' '); // Reset fill character ve khoang trang mac dinh

    // In cac dong du lieu
    cout << left << setw(20) << item1;
    cout << right << setw(10) << price1 << endl;

    cout << left << setw(20) << item2;
    cout << right << setw(10) << price2 << endl;

    cout << left << setw(20) << item3;
    cout << right << setw(10) << price3 << endl;

    return 0;
}

Giải thích: Chúng ta dùng setw(20) cho tên mặt hàng và setw(10) cho giá. leftright được dùng để căn lề. Chú ý cách setw phải được đặt trước mỗi lần in item hoặc price để thiết lập độ rộng cho riêng output đó. setfill(char) dùng để thay đổi ký tự đệm khi độ rộng dữ thị lớn hơn dữ liệu (mặc định là khoảng trắng).

1.3. Hiển Thị Các Kiểu Dữ Liệu Khác
  • boolalpha: Hiển thị giá trị boolean (true/false) dưới dạng chữ thay vì 0/1.
  • hex, oct, dec: Hiển thị số nguyên ở hệ cơ số 16 (hexadecimal), 8 (octal), hoặc 10 (decimal - mặc định).

Ví dụ:

#include <iostream>
#include <iomanip>

int main() {
    bool is_active = true;
    bool is_done = false;
    int number = 255;

    cout << "Boolean (mac dinh): " << is_active << ", " << is_done << endl;

    cout << boolalpha; // Bat che do hien thi boolean dang chu
    cout << "Boolean (boolalpha): " << is_active << ", " << is_done << endl;
    cout << noboolalpha; // Tat che do boolalpha

    cout << "So " << number << ":" << endl;
    cout << dec << "- He 10 (dec): " << number << endl; // Mac dinh
    cout << hex << "- He 16 (hex): " << number << endl;
    cout << oct << "- He 8 (oct): " << number << endl;

    return 0;
}

Giải thích: Các manipulators này cũng có hiệu lực cho đến khi bị thay đổi (noboolalpha để tắt boolalpha) hoặc luồng kết thúc.

2. Xử Lý Input Phức Tạp: Đọc Chuỗi Có Khoảng Trắng

Toán tử >> của cin rất tiện lợi, nhưng nó có một hạn chế lớn: nó dừng đọc khi gặp ký tự khoảng trắng (space, tab, newline). Điều này gây khó khăn khi bạn muốn đọc toàn bộ một dòng văn bản, ví dụ như tên đầy đủ của một người hoặc một câu nói.

Để giải quyết vấn đề này, chúng ta sử dụng hàm getline().

2.1. Sử Dụng getline()

getline() là một hàm (không phải manipulator) thường nhận hai đối số: luồng input (thường là cin) và biến string để lưu kết quả. Nó sẽ đọc toàn bộ ký tự cho đến khi gặp ký tự xuống dòng (\n).

#include <iostream>
#include <string>

int main() {
    string full_name;
    string address;

    cout << "Nhap ho ten day du cua ban: ";
    getline(cin, full_name); // Doc ca dong, bao gom khoang trang

    cout << "Nhap dia chi cua ban: ";
    getline(cin, address); // Doc dong tiep theo

    cout << "\nHo ten: " << full_name << endl;
    cout << "Dia chi: " << address << endl;

    return 0;
}

Giải thích: getline(cin, full_name); đọc hết dòng người dùng nhập vào (bao gồm cả khoảng trắng) và lưu vào biến full_name. Khi người dùng nhấn Enter, ký tự xuống dòng (\n) được đọc bởi getline và bị loại bỏ, không ảnh hưởng đến lần gọi getline tiếp theo.

2.2. Vấn Đề Khi Kết Hợp cin >>getline()

Đây là một cạm bẫy rất phổ biến khi mới học C++. Khi bạn dùng cin >> để đọc một kiểu dữ liệu (số nguyên, số thực, một từ của chuỗi), toán tử >> sẽ đọc các ký tự cần thiết nhưng để lại ký tự xuống dòng (\n) trong bộ đệm nhập. Nếu ngay sau đó bạn gọi getline(), hàm này sẽ thấy ký tự \n còn sót lại trong bộ đệm và coi đó là một dòng trống, đọc ngay lập tức và kết thúc mà không chờ người dùng nhập liệu!

Hãy xem ví dụ về vấn đề này:

#include <iostream>
#include <string>

int main() {
    int age;
    string name;

    cout << "Nhap tuoi cua ban: ";
    cin >> age; // Nguoi dung nhap so, nhan Enter -> '\n' con lai trong bo dem

    cout << "Nhap ten day du cua ban: ";
    // Dong getline() nay se doc ngay ky tu '\n' con sot lai tu lan nhap tuoi
    // va ket thuc luon ma khong cho ban nhap ten!
    getline(cin, name);

    cout << "\nTuoi: " << age << endl;
    cout << "Ten: " << name << endl; // Ten se bi rong
    return 0;
}

Chạy code trên, bạn sẽ thấy chương trình không dừng lại để bạn nhập tên. Biến name sẽ bị rỗng.

2.3. Giải Pháp: Xóa Bộ Đệm Nhập với cin.ignore()

Để khắc phục, chúng ta cần xóa bỏ ký tự xuống dòng (\n) còn sót lại trong bộ đệm sau khi sử dụng cin >>. Hàm cin.ignore() được dùng cho mục đích này.

  • cin.ignore(n, delim_char): Bỏ qua (đọc và loại bỏ) tối đa n ký tự trong bộ đệm nhập, hoặc cho đến khi gặp ký tự delim_char (bao gồm cả ký tự delim_char đó).

Trong trường hợp sau cin >>, chúng ta chỉ cần bỏ qua ký tự xuống dòng duy nhất. Số lượng ký tự tối đa cần bỏ qua có thể là một số rất lớn (đảm bảo bỏ qua hết mọi thứ cho đến ký tự phân cách), và ký tự phân cách là '\n'. Một giá trị lớn thường dùng cho nnumeric_limits<streamsize>::max().

#include <iostream>
#include <string>
#include <limits> // Can cho numeric_limits

int main() {
    int age;
    string name;

    cout << "Nhap tuoi cua ban: ";
    cin >> age;

    // *** Giai phap: Xoa ky tu xuong dong con sot lai ***
    cin.ignore(numeric_limits<streamsize>::max(), '\n');

    cout << "Nhap ten day du cua ban: ";
    getline(cin, name); // Bay gio getline se hoat dong dung

    cout << "\nTuoi: " << age << endl;
    cout << "Ten: " << name << endl; // Ten bay gio se co gia tri dung

    return 0;
}

Giải thích: Dòng cin.ignore(...) đọc và loại bỏ mọi ký tự trong bộ đệm cho đến khi gặp ký tự '\n', bao gồm cả ký tự '\n' đó. Sau khi thực hiện lệnh này, bộ đệm nhập đã sạch ký tự xuống dòng, và lần gọi getline tiếp theo sẽ chờ người dùng nhập liệu như bình thường.

3. Xử Lý Lỗi Nhập Cơ Bản

Điều gì xảy ra nếu người dùng nhập một giá trị không đúng kiểu dữ liệu mà bạn mong đợi? Ví dụ, bạn mong đợi một số nguyên nhưng họ lại gõ chữ. Mặc định, cin sẽ gặp lỗi, đặt luồng nhập vào trạng thái "fail" và dừng mọi thao tác đọc tiếp theo.

Để xử lý tình huống này một cách linh hoạt, chúng ta cần kiểm tra trạng thái của luồng nhập và biết cách phục hồi nó.

  • cin.fail(): Trả về true nếu có lỗi xảy ra trong lần đọc gần nhất (bao gồm cả lỗi định dạng).
  • cin.good(): Trả về true nếu không có lỗi nào (trạng thái tốt).
  • cin.clear(): Xóa bỏ các cờ lỗi (failbit, badbit, eofbit), đưa luồng về trạng thái tốt để có thể tiếp tục đọc.
  • cin.ignore(): Như đã nói ở trên, dùng để loại bỏ dữ liệu không hợp lệ còn sót lại trong bộ đệm sau khi lỗi xảy ra.

Ví dụ: Yêu cầu người dùng nhập một số nguyên hợp lệ.

#include <iostream>
#include <limits> // Can cho numeric_limits

int main() {
    int number;

    while (true) { // Lap lai cho den khi co input hop le
        cout << "Nhap mot so nguyen: ";
        cin >> number;

        if (cin.fail()) { // Kiem tra neu lan doc truoc do bi loi
            cout << "Input khong hop le! Vui long nhap lai mot so nguyen." << endl;
            cin.clear(); // Xoa cac co loi de luong tro ve trang thai tot
            // Xoa cac ky tu khong hop le con lai trong bo dem nhap
            cin.ignore(numeric_limits<streamsize>::max(), '\n');
        } else {
            // Doc thanh cong va khong co ky tu thua nao tren dong (chi co so va Enter)
            // Can kiem tra them de dam bao nguoi dung khong nhap "123 abc"
            char remaining_char;
            // Peeks at the next character without extracting it
            while ((remaining_char = cin.peek()) != '\n' && remaining_char != EOF) {
                 if (!isspace(remaining_char)) { // If it's not whitespace, it's extra garbage
                     cout << "Input co ky tu thua sau so! Vui long nhap lai chi mot so." << endl;
                     cin.clear(); // Van coi day la loi logic, clear fail bit neu co
                     cin.ignore(numeric_limits<streamsize>::max(), '\n');
                     goto continue_loop; // Quay lai dau vong lap
                 }
                 cin.get(); // Extract the whitespace characters
            }
             cin.ignore(numeric_limits<streamsize>::max(), '\n'); // Remove the final newline
            break; // Thoat vong lap vi input hop le
        }
        continue_loop:; // Label for goto
    }

    cout << "Ban da nhap so nguyen hop le: " << number << endl;

    return 0;
}

Giải thích: Vòng lặp while(true) sẽ chạy mãi cho đến khi người dùng nhập đúng. cin >> number; cố gắng đọc số. Nếu thất bại (cin.fail()true), chúng ta thông báo lỗi, dùng cin.clear() để "reset" trạng thái của luồng cin, và dùng cin.ignore() để loại bỏ phần input không hợp lệ mà người dùng đã gõ (ví dụ: chữ thay vì số) cho đến cuối dòng ('\n'). Sau đó, vòng lặp tiếp tục và yêu cầu nhập lại.

Thêm: Đoạn kiểm tra cin.peek() phức tạp hơn một chút, nó kiểm tra xem sau số nguyên có còn ký tự không phải khoảng trắng nào trên cùng dòng hay không (ví dụ người dùng nhập "123abc"). Nếu có, đó cũng coi là input không hợp lệ. isspace (cần #include <cctype>) dùng để kiểm tra ký tự khoảng trắng. peek nhìn mà không lấy, get lấy 1 ký tự. goto continue_loop được dùng để dễ dàng quay lại đầu vòng lặp nếu phát hiện ký tự thừa. Đây là cách xử lý input khá kỹ lưỡng, trong nhiều trường hợp đơn giản hơn, chỉ cần cin.fail(), clear(), ignore() là đủ.

4. Tăng Tốc Độ Nhập Xuất (Đối với Các Bài Toán Lớn)

Trong các bài toán lập trình thi đấu hoặc khi xử lý lượng dữ liệu cực lớn, tốc độ của cincout đôi khi có thể trở thành điểm nghẽn. Điều này là do mặc định, các luồng C++ (iostream) được đồng bộ hóa với các hàm nhập xuất của C (cstdio, như printf, scanf) để bạn có thể sử dụng lẫn lộn chúng. Tuy nhiên, việc đồng bộ này có chi phí.

Nếu bạn chỉ sử dụng cincout trong chương trình của mình (không dùng printf, scanf, ...), bạn có thể tắt đồng bộ hóa này để tăng tốc.

  • ios_base::sync_with_stdio(false);: Tắt đồng bộ hóa giữa C++ streams và C stdio.
  • cin.tie(NULL);: Bỏ liên kết giữa cincout. Mặc định, cin được "buộc" (tied) với cout, nghĩa là mỗi khi bạn thực hiện thao tác nhập bằng cin, cout sẽ tự động "flush" (đẩy) bộ đệm của nó ra màn hình để đảm bảo các lời nhắc (prompts) hiển thị trước khi chờ nhập. Việc gỡ liên kết này có thể tăng tốc nhưng bạn cần chú ý tự thêm endl hoặc cout << flush; để đảm bảo output hiển thị kịp thời trước khi nhập.

Bạn nên đặt hai dòng này ở đầu hàm main của mình nếu cần tăng tốc I/O:

#include <iostream>

int main() {
    // Hai dong nay giup tang toc I/O, dat o dau ham main
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    // ... phan con lai cua chuong trinh su dung cin/cout toc do cao ...

    int a, b;
    cin >> a >> b; // Nhanh hon
    cout << "Tong: " << a + b << '\n'; // Nen dung '\n' thay vi endl khi tang toc,
                                           // vi endl cung tu dong flush

    return 0;
}

Giải thích: Việc tắt đồng bộ hóa và gỡ liên kết cin/cout có thể làm I/O nhanh hơn đáng kể, đặc biệt với các bài toán đọc ghi lượng lớn dữ liệu. Tuy nhiên, hãy nhớ rằng sau khi gọi sync_with_stdio(false), bạn không nên trộn lẫn cin/cout với printf/scanf trong cùng một chương trình, vì hành vi có thể không dự đoán được. Đồng thời, khi sử dụng cin.tie(NULL), hãy dùng '\n' thay cho endl để xuống dòng nếu không cần flush bộ đệm ngay lập tức, hoặc tự gọi cout << flush; khi thực sự cần đảm bảo output đã in ra.

5. Bài Tập Thực Hành

Bây giờ là lúc áp dụng những kiến thức vừa học vào thực tế. Hãy thử giải quyết các bài tập nhỏ sau:

  1. Định dạng Hóa Đơn Giả:

    • Yêu cầu người dùng nhập tên 3 sản phẩm và giá của chúng (có thể là số thực).
    • In ra một "hóa đơn" đơn giản có định dạng: tên sản phẩm căn lề trái trong 25 ký tự, giá sản phẩm căn lề phải trong 10 ký tự với 2 chữ số thập phân. In một đường gạch ngang để phân cách.
  2. Đọc Thông Tin Cá Nhân:

    • Yêu cầu người dùng nhập tuổi (số nguyên) và sau đó là tên đầy đủ (có thể có khoảng trắng).
    • In lại thông tin vừa nhập, đảm bảo đọc đúng tên đầy đủ.
  3. Nhập Số Nguyên An Toàn:

    • Viết một đoạn code yêu cầu người dùng nhập một số nguyên dương. Nếu họ nhập không phải số nguyên, hoặc nhập số âm, hoặc nhập thêm ký tự rác sau số (ví dụ: "12abc"), hãy báo lỗi và yêu cầu nhập lại cho đến khi nhận được một số nguyên dương hợp lệ.

Comments

There are no comments at the moment.