Basic File HandlingAt some point in your programming existence, if you want to make things easier on yourself, you are going to end up writing utilities to make simple file handling easier. That is, unless you have found substitutes to your own, which is fine also. There are some options to choose from when implementing such utilities. Some are more standard than others, with certain platform specific options as well. Two of the more standard options are the stdio FILE, and the fstream. Streams always make things very intuitive, but the stdio FILE handle isn't that bad either. In this case I will go with the stdio approach, since I already have the stdio implementation completed. If you already have your own file handling utilities, feel free to use them instead of the ones I provide. I will be using a very generic interface that can easily be substituted for. To save some (actually, a large amount of) space, the snippets I include for file handling will only illustrate the interface. The downloadable code will contain everything, of course. First, there is a base file class: class File { public: bool Close() { assert(Good()); return (fclose(_fp) == 0); } bool Good() const { return (_fp != 0); } bool Bad() const { return (_fp == 0); } // file detection static bool Exists(const std::string& fileName) { . . . } // file length (only use before reading/writing) long Length() { /* use fseek() to determine file length */ } protected: File() : _fp(0) {} // this class should not be directly instantiated ~File() { if (_fp) fclose(_fp); } . . . FILE* _fp; }; This purpose of this class is to wrap a stdio FILE, and provide basic utilities that should be accessible for most/all file processing. Since it provides no actual I/O functionality, as it's mainly a stepping stone class of sorts to reduce copy/paste, its construction is protected, to prevent it from being instantiated by outside code. The static function File::Exists() is there for conveniently detecting whether a file with said name already exists on disk. Good() and Bad() are used to determine whether a file is usable for reading/writing. A File will also handle closing itself (if it's valid) when it leaves scope, although the option remains to Close() it manually. You will notice that I separate the functionality for file reading from that of file writing. In our case, we do not have a real need to mix reading and writing on the same file simultaneously, plus the separation is also beneficial for two reasons. One is that the interface becomes much more concise, and easier to read. The second reason is that it becomes impossible to accidentally read from a file you should be writing to, and vice versa. The compiler will not allow you to do this, as the Reading class only has access to a Read method, and likewise for the Writer. It's somewhat like the safety that a strictly typed language gives. The next two types of files are for reading and writing respectively. They also open in binary mode. class ReaderFile : public File { public: ReaderFile() {} ReaderFile(const std::string& fileName) { Open(fileName); } bool Open(const std::string& fileName) { . . . } // reading int Read(char* dstBuf, size_t len) { /* return an fread() call here */ } // for many basic variable types int Read(particular_type& val) { /* typecast particular_type and call Read() with proper size*/ } }; class WriterFile : public File { public: WriterFile() {} WriterFile(const std::string& fileName) { Open(fileName); } WriterFile(const std::string& fileName, bool append) { Open(fileName, append); } bool Open(const std::string& fileName) { /* Truncates an existing file, rather than appending. */ } bool Open(const std::string& fileName, bool append) { . . . } // writing int Write(const char* srcBuf, size_t len) { /* return an fwrite() call here */ } // for many basic variable types int Write(particular_type val) { /* typecast particular_type and call Write() with proper size*/ } }; These two classes are very useful for reading or writing binary data without having to include convoluted typecasting in your own handling code. Calls to Read() and Write() shield you from such details, and allow you to work directly with most data types you will be using. The WriterFile has the option to Open() a file for writing, and if it already exists, append the written data to the end. By default, however, it will overwrite an existing file. To deal with text-based files, there are two more related classes: class TextReaderFile : public File { public: TextReaderFile() {} TextReaderFile(const std::string& fileName) { Open(fileName); } bool Open(const std::string& fileName) { . . . } // reading int Read(char* buffer, size_t len) { /* call fread() */ } int Read(std::string& str, size_t len) { /* call fread() */ } }; class TextWriterFile : public File { public: TextWriterFile() {} TextWriterFile(const std::string& fileName) { Open(fileName); } TextWriterFile(const std::string& fileName, bool append) { Open(fileName, append); } bool Open(const std::string &fileName) { /* Truncates an existing file. */ } bool Open(const std::string& fileName, bool append) { . . . } // writing int Write(const char* str) { /* call fwrite() */ } int Write(const std::string& str) { /* call fwrite() */ } }; The interface for these text-based versions of the ReaderFile and WriterFile are similar to the binary versions. However, to keep things simple with their text-based nature, they only deal with strings in std::string or char array form. So we have some file utilities. Why make these anyway? Would it not have been much easier to simply use file streams much like the test using console I/O? The short answer is yes. The longer answer is that these utilities will be very handy once it's necessary to work with binary files. Eventually we would have had to create these anyway. May as well make use of them now!
|