Typically when I'm reading and writing data from files, especially when reading them;
I like to create a Data Structure that will represent the information that I'm pulling from the file. You will have to know how the file is structured in order to read the contents from it. Typically there are 3 different ways; you can read a single line at a time, you can read line by line until all lines have been read, or you can read everything from the file all in one go. There are ways to read different amount of bytes but that's a little more complicated and beyond the scope of this design process. What I normally do is; I'll read the contents from the file and store them into either a string, or a set of strings. Then after I retrieved the information from the file; I can then close it and be done with it. Once I have that information stored; then I will parse the string data, and from there I will then populate my Data Structures on the parsed data. I like breaking things down into individual functions to separate their logic and responsibility.
Your code structure may look something like this:
struct MyDataType {
// the contents that you will store from a file.
};
// A basic method to split a string based on a single delimiter
std::vector<std::string> splitString( const std::string& s, char delimiter ) {
std::vector<std::string> tokens;
std::string token;
std::istringstream tokenStream( s );
while( std::getline( tokenStream, token, delimiter ) ) {
tokens.push_back( token );
}
return tokens;
}
// Similar to above but with the ability to use a string as a delimiter as opposed to just a single char
std::vector<std::string> splitString( const std::string& strStringToSplit, const std::string& strDelimiter, const bool keepEmpty = true ) {
std::vector<std::string> tokens;
if( strDelimiter.empty() ) {
tokens.push_back( strStringToSplit );
return tokens;
}
std::string::const_iterator itSubStrStart = strStringToSplit.begin(), itSubStrEnd;
while( true ) {
itSubStrEnd = search( itSubStrStart, strStringToSplit.end(), strDelimiter.begin(), strDelimiter.end() );
std::string strTemp( itSubStrStart, itSubStrEnd );
if( keepEmpty || !strTemp.empty() ) {
tokens.push_back( strTemp );
}
if( itSubStrEnd == strStringToSplit.end() ) {
break;
}
itSubStrStart = itSubStrEnd + strDelimiter.size();
}
return tokens;
}
// This function will open a file, read a single line
// closes the file handle and returns that line as a std::string
std::string getLineFromFile( const char* filename ) {
std::ifstream file( filename );
if( !file ) {
std::stringstream stream;
stream << "failed to open file " << filename << '\n';
throw std::runtime_error( stream.str() );
}
std::string line;
std::getline( file, line );
file.close();
return line;
}
// This function will open a file and read the file line by line
// storing each line as a string and closes the file then returns
// the contents as a std::vector<std::string>
void getAllLinesFromFile( const char* filename, std::vector<std::string>& output ) {
std::ifstream file( filename );
if( !file ) {
std::stringstream stream;
stream << "failed to open file " << filename << '\n';
throw std::runtime_error( stream.str() );
}
std::string line;
while( std::getline( file, line ) ) {
if( line.size() > 0 )
output.push_back( line );
}
file.close();
}
// This function will open a file and read all of the file's contents and store it into
// a large buffer or a single string.
void getDataFromFile( const char* filename, std::string& output ) {
std::ifstream file( filename );
if( !file ) {
std::stringstream stream;
stream << "failed to open file " << filename << '\n';
throw std::runtime_error( stream.str() );
}
std::stringstream buf;
buf << file.rdbuf();
output.clear();
output.reserve( buf.str().length() );
output = buf.str();
}
// The declaration of this can vary too; depending on if you are doing a single line
// from the file, doing single line at a time for the entire file, or reading all
// of the contents from a large buffer.
void parseDataFromFile( const std::string& fileContents, std::vector<std::string>& output, std::vector<MyDataStructure>& data ) {
// This will vary on the file's data structure,
// but this is where you will call either of the `splitString` functions
// to tokenize the data.
// You will also use the `std::string's` conversion utilities such as
// std::stoi(...)... to convert to your basic types
// then you will create an instance of your data type structure
// and push that into the vector passed in.
}
Then your main would look something like this: I'll use the line by line version
int main() {
try {
std::string fileContents;
getAllinesFromFile( "test.txt", fileContents );
std::vector<std::string> tokens;
std::vector<MyDataStructure> data;
parseDataFromFile( fileContents, tokens, data );
} catch( std::runtime_error& e ) {
std::cerr << e.what() << std::endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
This allows the code to be readable, more modular, reusable, and in some ways even generic. It also helps to keep minimize the amount of debugging, and lessens the code management.
-Note- Also if you looked carefully at my functions where I'm reading in the data from the file; you will not see while( !file.eof() )
! This is bad code practice! The best way is to either use std::getline(...)
or the stream <<
operators within a while loop to read in the data.