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>

int main() {
    double pi = 3.1415926535;
    double gia = 19.99;
    double soLon = 12345.6789;

    cout << "Pi mac dinh: " << pi << endl;
    cout << "--------------------" << endl;

    cout << fixed << setprecision(2);
    cout << "Pi 2 thap phan: " << pi << endl;
    cout << "Gia tien: " << gia << endl;
    cout << "So lon 2 thap phan: " << soLon << endl;

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

    cout << fixed << setprecision(4);
    cout << "Pi 4 thap phan: " << pi << endl;

    return 0;
}

Output:

Pi mac dinh: 3.14159
--------------------
Pi 2 thap phan: 3.14
Gia tien: 19.99
So lon 2 thap phan: 12345.68
--------------------
Pi 4 thap phan: 3.1416

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>
#include <string>

int main() {
    string sp1 = "Apple";
    double g1 = 1.25;
    string sp2 = "Banana (Organic)";
    double g2 = 2.50;
    string sp3 = "Orange Juice";
    double g3 = 3.75;

    cout << fixed << setprecision(2);

    cout << left << setw(20) << "Item";
    cout << right << setw(10) << "Price" << endl;
    cout << setfill('-') << setw(30) << "" << endl;
    cout << setfill(' ');

    cout << left << setw(20) << sp1;
    cout << right << setw(10) << g1 << endl;

    cout << left << setw(20) << sp2;
    cout << right << setw(10) << g2 << endl;

    cout << left << setw(20) << sp3;
    cout << right << setw(10) << g3 << endl;

    return 0;
}

Output:

Item                          Price
------------------------------
Apple                          1.25
Banana (Organic)               2.50
Orange Juice                   3.75

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 kt1 = true;
    bool kt2 = false;
    int so = 255;

    cout << "Boolean (mac dinh): " << kt1 << ", " << kt2 << endl;

    cout << boolalpha;
    cout << "Boolean (boolalpha): " << kt1 << ", " << kt2 << endl;
    cout << noboolalpha;

    cout << "So " << so << ":" << endl;
    cout << dec << "- He 10 (dec): " << so << endl;
    cout << hex << "- He 16 (hex): " << so << endl;
    cout << oct << "- He 8 (oct): " << so << endl;

    return 0;
}

Output:

Boolean (mac dinh): 1, 0
Boolean (boolalpha): true, false
So 255:
- He 10 (dec): 255
- He 16 (hex): ff
- He 8 (oct): 377

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 ten;
    string diaChi;

    cout << "Nhap ho ten day du cua ban: ";
    getline(cin, ten);

    cout << "Nhap dia chi cua ban: ";
    getline(cin, diaChi);

    cout << "\nHo ten: " << ten << endl;
    cout << "Dia chi: " << diaChi << endl;

    return 0;
}

Ví dụ nhập xuất:

Nhap ho ten day du cua ban: Nguyen Van A
Nhap dia chi cua ban: 123 Duong ABC, Quan 1

Ho ten: Nguyen Van A
Dia chi: 123 Duong ABC, Quan 1

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 tuoi;
    string ten;

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

    cout << "Nhap ten day du cua ban: ";
    getline(cin, ten);

    cout << "\nTuoi: " << tuoi << endl;
    cout << "Ten: " << ten << endl;
    return 0;
}

Ví dụ nhập xuất (cho thấy lỗi):

Nhap tuoi cua ban: 25
Nhap ten day du cua ban: 
Tuoi: 25
Ten:

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>

int main() {
    int tuoi;
    string ten;

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

    cin.ignore(numeric_limits<streamsize>::max(), '\n');

    cout << "Nhap ten day du cua ban: ";
    getline(cin, ten);

    cout << "\nTuoi: " << tuoi << endl;
    cout << "Ten: " << ten << endl;

    return 0;
}

Ví dụ nhập xuất:

Nhap tuoi cua ban: 30
Nhap ten day du cua ban: Tran Van B
Tuoi: 30
Ten: Tran Van B

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>
#include <cctype>

int main() {
    int so;

    while (true) {
        cout << "Nhap mot so nguyen: ";
        cin >> so;

        if (cin.fail()) {
            cout << "Input khong hop le! Vui long nhap lai mot so nguyen." << endl;
            cin.clear();
            cin.ignore(numeric_limits<streamsize>::max(), '\n');
        } else {
            bool duLieuThua = false;
            char c;
            while ((c = cin.peek()) != '\n' && c != EOF) {
                if (!isspace(c)) {
                    duLieuThua = true;
                    break;
                }
                cin.get();
            }

            if (duLieuThua) {
                cout << "Input co ky tu thua sau so! Vui long nhap lai chi mot so." << endl;
                cin.clear();
                cin.ignore(numeric_limits<streamsize>::max(), '\n');
            } else {
                cin.ignore(numeric_limits<streamsize>::max(), '\n');
                break;
            }
        }
    }

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

    return 0;
}

Ví dụ nhập xuất:

Nhap mot so nguyen: abc
Input khong hop le! Vui long nhap lai mot so nguyen.
Nhap mot so nguyen: 123 xyz
Input co ky tu thua sau so! Vui long nhap lai chi mot so.
Nhap mot so nguyen: 45
Ban da nhap so nguyen hop le: 45

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ự.

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() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int n1, n2;
    cin >> n1 >> n2;
    cout << "Tong: " << n1 + n2 << '\n';

    return 0;
}

Ví dụ nhập xuất:

10 20
Tong: 30

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.