What you have is fine. I'm was simply curious if you knew why it was okay, and why it looked bad.
Every input loop should essentially do this:
1 2 3 4 5 6 7
|
while (true)
{
f >> foo; // attempt to read something from stream 'f'
if (!f) break; // if anything went wrong, quit now
// if it gets this far, then foo has a valid value, and you can use it
quux( foo );
}
|
In C++, the extraction operators return the
stream, so the result of an expression like
cin >> foo
is...
cin! That's right, you get back the stream you are using. Hence, you can test it immediately after the
attempt to input something operation, all in one statement:
1 2 3 4 5
|
while (f >> foo) // attempt to read; if anything goes wrong, quit
{
// if it gets into the loop body here, then foo has a valid value...
quux( foo );
}
|
Now, the phrase "if anything went wrong" doesn't just mean EOF. After your loop, you can check to see whether or not that "anything that went wrong" was EOF:
1 2 3 4 5 6 7 8 9 10 11 12 13
|
while (f >> foo)
{
quux( foo );
}
// I am hoping f is at EOF right now, but if it quit
// the loop for some other reason, I want to know.
if (!f.eof())
{
cerr << "Hey! There was something wrong with the data in the file!\n"
"(Because I totally didn't get to EOF!)\n";
return 1; // ...and so I'm gonna quit. Nyaah!
}
|
After all that, let's think through the code you have:
1 2 3 4 5 6 7
|
while ( ! reader.eof() )
{
if ( ( i + 1 ) % 4 == 0 )
getline( reader ,tab[ i++ ], '\n' ) ;
else
getline( reader, tab[ i++ ], '\t' ) ;
}
|
The first time through, we test if at EOF. We're not, so that's fine. Then we try to get input into its final place. That will either succeed or fail. The very next thing we do is... loop! So the test for EOF (or other failure) comes immediately after the attempt to read. That's good!
The danger is that we have allowed other side effects to take place. In this case, the variable
i has been incremented whether or not the attempt to get input succeeded.
Your program doesn't care. It ignores
i's value after the input loop. The only real danger you face is that the input file may contain too many values (and overwrite the
tab[] array's bounds).
It would also not produce a very nice result if the file contained too few items, of course, but in this program it will not matter.
This is the hard part about dealing with I/O that gets everyone by surprise. It is nice when things are what we'd like them to be. But our programs must be designed to handle the (all too often) reality that they are not, and they must handle it in a way that prevents things from breaking.
Had I written the example, without introducing
stringstream or
vector, and by storing records in a linear 'tabs' array, I would have done it this way:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
|
#include <fstream>
#include <iostream>
#include <string>
using namespace std;
int main()
{
const int MAX_TABS = 12;
string tab[ MAX_TABS ];
int num_tabs = 0;
char fs[] = "\t\t\t\n";
ifstream reader( "records.txt" );
if (!reader)
{
cout << "I could not open records.txt" << endl;
return 1;
}
while (getline( reader, tab[ num_tabs ], fs[ num_tabs % 4 ] ))
{
num_tabs++;
if (num_tabs >= MAX_TABS) break;
}
reader.close();
for (int r = 0, i = 0; i < num_tabs; )
{
cout << endl << "Record Number: " << ++r << endl;
cout << "Forename: " << tab[ i++ ] << endl;
cout << "Surname: " << tab[ i++ ] << endl;
cout << "Department: " << tab[ i++ ] << endl;
cout << "Telephone: " << tab[ i++ ] << endl;
}
}
|
In this code, the most difficult thing to follow is how we index the
fs[] array for the proper delimiter.
Had the records been read properly into a 2D array using a nested loop, we could have avoided all that unpleasantness...
In any case, I hope this helps.