Thao tác với file là 1 trong những công việc thường gặp trong lập trình, bất kể ngôn ngử nào, từ các assembly cho đến c/c++ và c#, java...bất kể lập trình ứng dụng hay lập trình game đều ít nhiều động đến việc xử lý file(như lưu các thiết đặt, dữ liệu người dùng, lưu điểm...). Để việc thao tác với file trở nên đơn giản, người ta đã xây dựng các lớp file stream cung cấp các phương thức đọc ghi, kiểm soát lỗi...Ngoài ra còn có các lớp trung gian làm nhiệm vụ như những lớp tiện ích, nó cung cấp các phương thức thao tác với từng loại dữ liệu cụ thể của file. Để hiểu rỏ hơn ta sẽ đi xây dựng lại các lớp stream này trong c++.
Bước đầu tiên ta xây dựng 1 lớp abstract Stream để cung cấp 1 giao diện tổng quan về stream như sau:
Đối với 1 stream sẽ luông có các phương thức như đọc, nghi, kiểm tra kết thúc stream, đóng stream...Các phương thức này cần phải được thực thi lại ở các lớp stream cụ thể cho từng loại stream(như file stream, memory stream, stream socket...). Chi tiết về các phương thức như sau:
- canRead()/canWrite() kiểm tra xem stream hiện tại có thể đọc/ghi hay không.
- getLength() sẽ trả về chiều dài hiện tại của stream(tổng số bytes có trong stream).
- read(...) sẽ đọc 1 khối dữ liệu trong stream(nếu canRead() false thì 1 ngoại lệ sẽ được ném ra), hàm trả về tổng số bytes đã đọc, dữ liệu đọc được từ stream sẽ được lưu trong mảng buff bắt đầu tại vị trí offset với tổng bytes lớn nhất cần đọc là length.
- readByte() sẽ đọc 1 byte duy nhất từ stream(nếu canRead() false thì 1 ngoại lệ sẽ được ném ra) và trả về byte đọc được, nếu giá trị âm hoặc 1 ngoại lệ được ném ra nghĩa là quá trình đọc dữ liệu từ stream thất bại.
- write(...) ghi 1 khối dữ liệu vào stream(nếu canWrite() false thì 1 ngoại lệ sẽ được ném ra), hàm sẽ ghi khối bytes từ mảng buff kể từ vị trí offset với tổng số bytes là length vào stream. Nếu việc ghi dữ liệu thất bại thì 1 ngoại lệ sẽ được ném ra.
- writeByte(...) ghi 1 byte vào stream(nếu canWrite() false thì 1 ngoại lệ sẽ được ném ra), nếu việc ghi dữ liệu thất bại thì 1 ngoại lệ sẽ được ném ra.
- endOfStream() sẽ trả về true nếu đã đọc đến cuối stream. Chú ý rằng nếu stream có thể ghi(canWrite() trả về true) thì endOfStream() luông trả về false.
- close() đóng stream và giải phóng tài nguyên.
Ngoài việc thực thi các phương thức từ lớp Stream ta còn định nghĩa thêm 1 số phương thức giành riêng cho file stream và các phương thức phụ trợ khác. Cụ thể như sau:
- opened() kiểm tra xem file đang được mở hay không, thường thì nó sẽ trả về true trừ khi ta close file stream.
- getFileName() trả về đường dẩn đến file hiện tại.
- getMode() trả về mode hiện tại đang thao tác với file. Hiện tại file stream của chúng ta hổ trợ 4 mode là read, write, readwrite và append, các mode này được định nghĩa thông qua các hằng số MODE_READ, MODE_WRITE, MODE_READWRITE và MODE_APPEDN. Với mode read thì file chỉ được đọc, mode write thì file chỉ được ghi, với mode readwrite thì file vừa được đọc và ghi và mode append thì file chỉ được ghi nhưng nội dung trước đó của file sẽ không bị mất(nghĩa là ghi thêm vào file).
- FileStream(...) hàm khởi tạo của file stream, hàm cần đường dẩn đến file, chế độ thao tác với file và nếu đối số overwrite true thì sẽ tự động ghi đè file nếu file đó đã tồn tại(chỉ có tác dụng với mode MODE_WRITE).
- get_file_length() hàm này sẽ trả về tổng kích thước của file.
- check_file_exists() hàm này kiểm tra xem file có tồn tại không.
- create_file() hàm này sẽ tạo file mới, nếu file đã tồn tại thì nó sẽ ghi đè lên file đó.
- check_read() kiểm tra chế độ hiện tại có được đọc không, nếu không thì sẽ ném ra ngoại lệ.
- check_write() kiểm tra chế độ hiện tại có được viết không, nếu không thì sẽ ném ra ngoại lệ.
Hàm int read(byte* buff, uint offset, uint length) đọc 1 khối bytes từ file được thực thi như sau:
Đầu tiên hàm sẽ thực hiện kiểm tra 1 số thông tin trước khi đọc dữ liệu từ file như mode hiện tại có hổ trợ đọc file hay không, đối số có đúng không....Tiếp theo hàm sẽ đọc dữ liệu từ file thông qua hàm fread(...), cuối cùng kiểm tra xem quá trình đọc có thành công không thông qua tổng số bytes đã đọc(lưu trong biến ret), hàm đọc thất bại khi tổng bytes đã đọc nhỏ hơn tổng bytes yêu cầu(length) trong khi vẩn chưa đọc hết file.
Hàm int readByte() đọc 1 byte từ file được thực thi như sau:
Hàm sẽ kiểm tra xem stream hiện tại có hổ trợ việc đọc không, tiếp theo đó hàm sẽ khởi tạo 1 vùng nhớ để lưu tạm giá trị trả về từ hàm fread. Sau khi đọc dữ liệu thành công hàm sẽ kiểm tra tổng số bytes đã đọc xem có bằng 1 không, vì hàm của chúng ta yêu cầu đọc 1 byte vì thế nên nếu bằng 1 thì đọc thành công, hàm trả về -1 nếu thất bại.
Hàm void write(const byte* buff, uint offset, uint length) ghi 1 khối bytes vô file được thực thi như sau:
Hàm sẽ kiểm tra xem stream hiện tại có hổ trợ ghi không. Nếu đối số length bằng 0 thì hàm sẽ không làm gì cả. Bước tiếp theo hàm sẽ sử dụng fwrite để ghi khối bytes từ mảng buff vô file. Cuối cùng hàm kiểm tra tổng bytes đã ghi(lưu trong biến ret) có bằng length không, nếu không bằng nghĩa là việc ghi file đã thất bại. Sau khi ghi vào file thành công hàm sẽ tăng chiều dài của stream lên.
Hàm void writeByte(byte value) ghi 1 byte vào file được thực thi như sau:
Đầu tiên hàm kiểm tra xem stream hiện tại có hổ trợ ghi file không. Tiếp theo hàm sẽ ghi giá trị từ biến value vào file thông qua hàm fwrite, nếu hàm trả về 1 nghĩa là thành công. Sau khi ghi file thì chiều dài stream sẽ được tăng lên 1.
Oke! thế là xong FileStream, giờ ta có thể thao tác với file đơn giản như sau:
FileStream fs("filename.txt");// mở filename.txtTiếp theo ta sẽ xây dựng các lớp tiện ích để làm việc với FileStream đơn giản hơn nữa, ở đây ta xây dựng lớp StreamReader và StreamWriter nhằm đọc và ghi dữ liệu dạng text.
int total = fs.read(obuff, 0, 100);// đọc 100 bytes từ filename.txt, lưu vô obuff
fs.write(ibuff, 0, 100);// ghi 100 bytes từ ibuff vô filename.txt
fs.close();// đóng stream
Lớp đầu tiên là StreamReader dùng để đọc dữ liệu từ file hoặc từ 1 stream bất kỳ dưới dạng text. Ta xây dựng như sau:
StreamReader của chúng ta cần đầu vào là 1 stream bất kỳ có hổ trợ đọc dữ liệu(canRread() trả về true) hoặc đường dẩn đến file. Ta khảo sát các phương thức trong StreamReader:
- read(uint chars) đọc số lượng ký tự nhất định trong stream.
- readLine() đọc 1 dòng trong stream.
- readAll() đọc toàn bộ nội dung của stream.
Đầu tiên hàm kiểm tra tình trạng của stream hiện tại, sau đó khởi tạo 1 bộ đệm để lưu tạm dữ liệu đọc được từ stream dưới dạng bytes. Sau khi đọc xong dữ liệu thì thêm \0 vào cuối mảng bộ đệm để đánh dấu kết thúc xâu. Cuối cùng khởi tạo 1 biến kết quả và chuyển dữ liệu trong bộ đệm sang string để trả về.
Hàm readLine() được thực thi như sau:
Hàm sử dụng 1 vector để làm bộ đệm dữ liệu và đọc từng bytes trong stream cho đến khi kết thúc stream hoặc gặp ký tự xuống dòng.
Hàm readAll() được thực thi như sau:
Hàm này chỉ đơn giản là đọc toàn bộ dữ liệu cho đến khi kết thúc stream.
Oke! code thì dài quá :)) nhưng khi sử dụng thì rất sướng :)) chỉ đơn giản như thế này:
StreamReader reader("filename.txt");//mở filename.txt để đọcGiờ đến StreamWriter, lớp tiện ích để ghi dữ liệu dạng text xuống file. Ta định nghĩa như sau:
std::string line = reader.readLine();// đọc 1 dòng trong filename.txt, lưu vô biến line
reader.close();// đóng stream
Giờ ta khảo sát các phương thức của StreamWriter:
- write(...) hàm dùng để nghi 1 đoạn text vô file.
- writeLine(...) hàm dùng để ghi 1 dòng vô file(như hàm write nhưng có xuống dòng).
Hàm write(const std::string& st)
Đầu tiên kiểm tra tình trạng của stream hiện tại sau đó ghi mảng bytes trong xâu st ra file.
Hàm writeLine(const std::string& st)
Hàm này gọi lại hàm write để ghi đoạn text vô file và ghi thêm \n để xuống dòng.
Oke! lớp này khá đơn giản, về sử dụng thì tương tự như StreamReader
StreamWriter writer("filename.txt");// mở file để ghiVới 3 class FileStream, StreamReader và StreamWriter ta đã có thể thao tác với file dể dàng hơn nhiều. FileStream sẽ thao tác đọc ghi file ở mức bytes, StreamReader và StreamWriter sẽ hổ trợ đọc ghi ở mức text. Dưới đây là 1 đoạn demo về sử dụng 3 lớp này:
writer.writeLine("this is a text in a line");// ghi 1 đoạn text lên 1 dòng
writer.close();// đóng stream
FileStream fs("filename.txt", FileStream::MODE_WRITE);Đoạn demo sẽ tạo filename.txt và ghi vào dòng "hello world", sau đó sử dụng StreamReader để đọc ra chuổi "hello world" đó rồi nối vào với chuổi ", i'm dev" và cuối cùng sử dụng StreamWriter để ghi mới chuổi "hello world, i'm dev" vào filename.txt.
char buff[] = "hello world";
fs.write((byte *)buff, 0, sizeof(buff));// ghi mảng buff vào filename.txt
fs.close();
StreamReader reader("filename.txt");
std::string line = reader.readLine();// đọc 1 dòng trong filename.txt vào line
reader.close();
line += ", i'm dev";
StreamWriter writer("filename.txt");
writer.writeLine(line);// ghi 1 dòng vào filename.txt
writer.close();
Bên trên ta đã xây dựng cơ bản lớp file stream để thao tác với file và 2 lớp tiện ích để hổ trợ đọc ghi file dưới dạng text. Ngoài việc sử dụng stream để thao tác với file lưu trên đĩa ra ta còn có thể thao tác với dữ liệu lưu trong bộ nhớ, điểm khác biệt ở đây chỉ là dữ liệu nằm ở đâu(trên đĩa hay trong bộ nhớ) còn giao diện đọc ghi dữ liệu thì như nhau(đều sử dụng read, readBytes, write, writeByte). Ví dụ ở đây là 1 memory stream dùng để thao tác với 1 luồng dữ liệu đã lưu trong bộ nhớ được định nghĩa như sau:
Khác với FileStream dữ liệu được ghi trong file thì ở lớp MemoryStream dữ liệu sẽ được ghi trong 1 vector trong bộ nhớ, các phương thức đọc ghi trên stream vẩn hoạt động tương tự nhau ở cả 2 loại stream. Ngoài ra mỗi loại stream còn hổ trợ 1 số phương thức khác đặt trưng riêng cho từng loại như ở FileStream ta định nghĩa hàm opened() để kiểm tra tình trạng của file có đang được mở hay không thì ở MemoryStream lại không có...2 lớp tiện ích là StreamReader và StreamWriter cũng có thể sử dụng với MemoryStream như sau:
char buff[] = "this is a text";Vì 2 lớp FileStream và MemoryStream đều kế thừa từ abstract Stream nên ta có thể dể dàng sử dụng nó với 2 lớp tiện ích StreamReader và StreamWriter.
MemoryStream ms((byte *)buff, 0, sizeof(buff));
StreamReader reader(ms);
std::string text = reader.readAll();// đọc toàn bộ text trong stream
Để tăng tốc độ thao tác với file người ta còn sử dụng bộ đệm trong file stream, nói nôm na là người ta sẽ đọc 1 khối bytes rồi lưu trong bộ đệm trong bộ nhớ, sau đó khi có yêu cầu đọc file thực sự thì chỉ cần đọc từ bộ đệm đó ra thôi vì thế nên tốc độ sẽ nhanh hơn. Khi khối bytes trong bộ đệm hết thì chỉ cần đổ tiếp dữ liệu từ file vô bộ đệm. Do sử dụng bộ đệm nên dữ liệu sẽ đi theo đường file <-> buffer <-> program, ở đây mũi tên 2 chiều biểu thị cho việc dữ liệu được đọc hoặc ghi file đều thông qua bộ đệm trung gian. Mình chỉ sơ lượt về bộ đệm như thế, nếu có thời gian thì mình sẽ viết tiếp 1 bài về sử dụng bộ đệm để thao tác với file.
Đây là toàn bộ mã nguồn của bài này(project được viết bằng CodeBlocks), bạn nào quan tâm có thể tải về để nguyên cứu: http://1drv.ms/1QVAgSt
0 nhận xét :
Post a Comment