Bài 37.2: Đọc file trong C++

Chào mừng quay trở lại với chuỗi bài về C++! Hôm nay, chúng ta sẽ cùng khám phá một kỹ năng cực kỳ quan trọng trong lập trình: đọc dữ liệu từ file. Dữ liệu thường không chỉ tồn tại trong bộ nhớ khi chương trình chạy mà còn cần được lưu trữ lâu dài trên đĩa cứng dưới dạng các file. Việc đọc file cho phép chương trình của bạn truy cập và xử lý thông tin đã được lưu lại.

Trong C++, chúng ta làm việc với file thông qua các luồng (streams). Khái niệm luồng này tương tự như cách chúng ta đã dùng cin để đọc từ bàn phím và cout để ghi ra màn hình. Thư viện chính hỗ trợ việc này là <fstream>.

Thành phần cốt lõi: ifstream

Để đọc dữ liệu từ một file, chúng ta sử dụng lớp ifstream (input file stream) được định nghĩa trong thư viện <fstream>. Đối tượng của lớp này sẽ đóng vai trò là "cánh cổng" để bạn truy cập nội dung bên trong file.

Mở File: Bước đầu tiên và Quan trọng nhất

Trước khi đọc bất cứ thứ gì, bạn phải mở file đó ra. Có hai cách phổ biến để mở file:

  1. Khi khai báo đối tượng ifstream: Bạn có thể truyền tên file (dưới dạng chuỗi C-style const char* hoặc string) vào constructor của ifstream.

    #include <fstream>
    #include <string>
    
    ifstream f("du_lieu.txt");
    
  2. Sử dụng phương thức open(): Bạn khai báo đối tượng ifstream trước, sau đó gọi phương thức open() để mở file. Điều này hữu ích khi bạn muốn sử dụng cùng một đối tượng ifstream để mở nhiều file khác nhau lần lượt.

    #include <fstream>
    #include <string>
    
    ifstream f;
    f.open("du_lieu.txt");
    
Luôn Kiểm tra File Mở Thành Công Hay Không!

Đây là bước cực kỳ quan trọngkhông thể bỏ qua. Việc mở file có thể thất bại vì nhiều lý do (file không tồn tại, không có quyền truy cập, tên file sai...). Nếu bạn cố gắng đọc từ một file chưa được mở thành công, chương trình của bạn sẽ gặp lỗi hoặc hành vi không xác định.

Cách đơn giản và phổ biến nhất để kiểm tra là sử dụng đối tượng ifstream như một giá trị boolean:

#include <fstream>
#include <iostream>

ifstream f("du_lieu.txt");

if (f) {
    cout << "Mo thanh cong!\n";
} else {
    cerr << "Loi mo file!\n";
}

Đối tượng ifstream có thể được chuyển đổi ngầm định sang bool. Giá trị true biểu thị luồng đang ở trạng thái tốt (sẵn sàng đọc), và false biểu thị có lỗi hoặc đã hết file.

Các Phương Pháp Đọc Dữ Liệu

Sau khi file đã được mở thành công, chúng ta có thể bắt đầu đọc dữ liệu. C++ cung cấp nhiều cách đọc khác nhau tùy thuộc vào định dạng dữ liệu của bạn.

1. Đọc theo Định dạng (sử dụng toán tử >>)

Giống như cin, bạn có thể dùng toán tử >> để đọc dữ liệu được phân tách bởi khoảng trắng (khoảng trắng, tab, xuống dòng).

  • Đọc từng từ (chuỗi):

    Giả sử file data.txt có nội dung:

    Lap trinh C++ that thu vi!
    #include <fstream>
    #include <iostream>
    #include <string>
    
    int main() {
        ifstream f("data.txt");
    
        if (f) {
            string s;
            while (f >> s) {
                cout << s << "\n";
            }
            f.close();
        } else {
            cerr << "Loi mo file!\n";
        }
    
        return 0;
    }
    

    Output:

    Lap
    trinh
    C++
    that
    thu
    vi!

    Giải thích: Vòng lặp while (f >> s) sẽ cố gắng đọc một chuỗi (từ) từ luồng f vào biến s. Phép đọc này sẽ bỏ qua khoảng trắng, tab, và xuống dòng trước khi đọc từ. Vòng lặp tiếp tục cho đến khi phép đọc thất bại (ví dụ: hết file hoặc dữ liệu không đúng định dạng).

  • Đọc dữ liệu số:

    Giả sử file numbers.txt có nội dung:

    10 20
    3.14
    -5 99
    #include <fstream>
    #include <iostream>
    
    int main() {
        ifstream f("numbers.txt");
    
        if (f) {
            int a;
            double b;
    
            if (f >> a) {
                cout << "Doc so nguyen: " << a << "\n";
            } else {
                cerr << "Loi doc so nguyen dau tien!\n";
            }
    
            if (f >> b) {
                 cout << "Doc so thuc: " << b << "\n";
            } else {
                 cerr << "Loi doc so thuc!\n";
            }
    
            while (f >> a) {
                cout << "Doc so nguyen tiep theo: " << a << "\n";
            }
    
            f.close();
        } else {
            cerr << "Loi mo file!\n";
        }
    
        return 0;
    }
    

    Output:

    Doc so nguyen: 10
    Doc so thuc: 20
    Doc so nguyen tiep theo: 3

    Giải thích: Toán tử >> tự động chuyển đổi dữ liệu đọc được sang kiểu của biến đích (ở đây là intdouble). Nó cũng bỏ qua khoảng trắng giữa các số. Vòng lặp while (f >> a) là cách hiệu quả để đọc tất cả các số nguyên còn lại trong file cho đến khi hết.

2. Đọc từng Dòng (sử dụng getline)

Khi bạn cần đọc toàn bộ nội dung của từng dòng văn bản, bao gồm cả khoảng trắng bên trong dòng đó, hàm getline là lựa chọn phù hợp.

Giả sử file lines.txt có nội dung:

Day la dong dau tien.
Day la dong thu hai, co nhieu khoang trang hon.
Dong cuoi cung!
#include <fstream>
#include <iostream>
#include <string>

int main() {
    ifstream f("lines.txt");

    if (f) {
        string s;
        int i = 0;
        while (getline(f, s)) {
            i++;
            cout << "Dong " << i << ": " << s << "\n";
        }
        f.close();
    } else {
        cerr << "Loi mo file!\n";
    }

    return 0;
}

Output:

Dong 1: Day la dong dau tien.
Dong 2: Day la dong thu hai, co nhieu khoang trang hon.
Dong 3: Dong cuoi cung!

Giải thích: Hàm getline(f, s) đọc dữ liệu từ luồng f và lưu vào biến s kiểu string. Nó đọc cho đến khi gặp ký tự xuống dòng \n (và loại bỏ ký tự này), hoặc cho đến khi hết file. Vòng lặp while sẽ dừng khi getline thất bại (thường là do đã đọc hết file).

3. Đọc từng Ký tự

Bạn có thể đọc từng ký tự một bằng cách sử dụng toán tử >> với biến kiểu char, hoặc dùng phương thức get(). Phương thức get() đặc biệt hữu ích vì nó không bỏ qua khoảng trắng, tab hay xuống dòng.

Giả sử file chars.txt có nội dung:

A 1
B 2
#include <fstream>
#include <iostream>

int main() {
    ifstream f("chars.txt");

    if (f) {
        char c;
        while (f.get(c)) {
            cout << "[" << c << "]";
        }
        f.close();
    } else {
        cerr << "Loi mo file!\n";
    }

    return 0;
}

Output:

[A][ ][1][
][B][ ][2]

Giải thích: Phương thức f.get(c) đọc ký tự tiếp theo từ luồng và lưu vào biến c. Nó trả về đối tượng luồng, có thể dùng trong điều kiện while để kiểm tra thành công (tức là chưa hết file hoặc chưa gặp lỗi).

Đóng File (close())

Khi bạn đã hoàn thành việc đọc file, bạn nên đóng nó lại bằng cách gọi phương thức close() trên đối tượng ifstream:

f.close();

Việc đóng file giúp giải phóng tài nguyên hệ thống và đảm bảo dữ liệu được xử lý đúng cách. Tuy nhiên, ifstream tuân thủ nguyên tắc RAII (Resource Acquisition Is Initialization). Điều này có nghĩa là khi đối tượng ifstream bị hủy (ví dụ: khi nó ra khỏi phạm vi - scope - của nó), destructor của lớp ifstream sẽ tự động gọi close() cho bạn. Vì vậy, trong nhiều trường hợp đơn giản, bạn không cần gọi close() một cách tường minh, nhưng việc gọi nó giúp code rõ ràng hơn và bạn kiểm soát được thời điểm đóng file.

Kiểm tra trạng thái luồng và Xử lý lỗi nâng cao

Ngoài việc kiểm tra is_open() hoặc sử dụng đối tượng luồng như boolean, bạn có thể kiểm tra các cờ trạng thái chi tiết hơn:

  • file.good(): Trả về true nếu không có cờ lỗi nào được bật (luồng ở trạng thái tốt).
  • file.eof(): Trả về true nếu đã đọc đến cuối file. Lưu ý: Cờ eofbit chỉ được bật sau khi một thao tác đọc cố gắng đọc qua cuối file.
  • file.fail(): Trả về true nếu cờ failbit hoặc badbit được bật. Điều này xảy ra do lỗi định dạng (ví dụ: cố đọc số vào biến int nhưng gặp chữ) hoặc lỗi đọc chung.
  • file.bad(): Trả về true nếu cờ badbit được bật. Điều này thường chỉ ra lỗi nghiêm trọng không thể phục hồi (ví dụ: lỗi phần cứng).

Thông thường, cách tốt nhất để kiểm tra kết thúc file trong vòng lặp là kiểm tra kết quả của phép đọc (while (file >> data)) thay vì kiểm tra file.eof() trước mỗi lần đọc.

Comments

There are no comments at the moment.