C++/Linking "ifstream ins" to file "booklist2.txt"
Expert: Ralph McArdell - 3/12/2007
QuestionDear C++ expert,
I am having trouble linking the compiler to an outer file in order to load the data from the outer file into an array of a struct I defined.
Here goes the code:
#include <iostream>
#include <string>
#include <fstream>
using namespace std;
struct Books
{
string isbn, title, fn, ln;
};
const int size = 5;
void loadData (ifstream&, Books[]);
void showData (const Books[]);
void main ()
{
Books bookList [size];
ifstream ins;
ins.open("booklist2.txt");
if (ins.fail())
cerr << "ERROR\n";
loadData (ins, bookList);
showData (bookList);
ins.close();
}
void loadData (ifstream& in, Books b[])
{
cout << "Loading";
for (int i = 0; i < size; i++)
{
in >> b[i].isbn;
in.ignore(1);
getline (in, b[i].title, '\t');
in >> b[i].fn >> b[i].ln;
cout << ".";
}
cout << "Done\n";
}
void showData (const Books b[])
{
for (int i = 0; i < size; i++)
cout << b[i].isbn << "\t" << b[i].title
<< "\t" << b[i].fn << "\t"
<< b[i].ln << endl;
}
The program itself doesn't have any errors and compiles.
The .exe screen looks like this.
ERROR
Loading.....Done
Press any key to continue . . .
The program just doesn't seem to be finding "booklist2.txt." I put "booklist2.txt" in the same folder as the program's project folder. How can I make the presence of the file known to the compiler??
I'll be waiting for your kindly reply!!
AnswerFOLLOWUP:
---------------------------------------------------------------------------------
It occurred to me that I missed a few things you could do with regard to locating your data file:
1/ Look into the debug or debugger settings for your project (for example for VC6 under the Project Settings on the Debug tab). You might well be able to set the working directory used when debugging the application.
2/ You could use the full absolute path to your data file in your code.
3/ You could pass the file name (or file path) to the executable as a command line argument. In which case use the:
int main( int argc, char * argv[] )
{
// ...
}
form of main. Again you can probably set command line arguments to use when debugging in the debugging settings.
4/ You could ask the user for the file or file path to use.
3/ and 4/ are not so much direct solutions as ways to allow you to more easily try different paths for your file.
END OF FOLLOWUP
---------------------------------------------------------------------------------
I am guessing you are executing your program from within an integrated development environment (IDE) application such a Visual C++ or DevC++.
Different development environments place executables in different places and execute them from different working directories, with respect to the main project directory. This is the cause of the confusion (and yes it gets me from time to time as well). You might find you have to place your data file in the build directory where the executable is written to by the linker (e.g. in project\Debug for VC++, assuming you have performed a debug build).
The alternative is to open a command prompt (DOS box) window and change directory (cd) into the directory containing your data file and execute the application from within that directory. For example if your project data file was in the directory D:\projects\my_project and the executable in D:\projects\my_project\Debug then you would do something like the following:
D:
cd \projects\my_project
.\Debug\my_project
You should also improve your error checking. Not opening the data file should almost certainly be a fatal error - you cannot sensibly continue without it being open! You can also check for a file being open using the is_open member function of std::fstream (and thereby of std::ifstream and std::ofstream). For example you could write:
ifstream ins("booklist2.txt");
if ( !ins.is_open() )
{
cerr << "ERROR, unable to open data file.\n";
return 1;
}
Note that there is _no_ standard signature of main that returns void, this is a Microsoft perversion. I suppose this implies you are most likely using VC6. The correct signature for main is:
int main()
{
}
Also even though an int is indicated for the return type for main and main only a return statement is not required. If none is provided return 0 is assumed. I stress this is _only_ allowed for main, all other functions have to return a values unless they specify a void return type. Again VC6 gets this wrong so you have to add in a return 0 statement (or use their non-standard void main() signature).
You could also consider using a std::vector (include <vector>) instead of a built in array. In this context a vector is a single dimension array. In fact std::vector is a class template. You provide information on what type is held in the vector to create a specific vector type. The big plus is that std::vector types handle memory management for you and grow as you add more elements. However this means they do not automatically have any elements at all, so you typically call a member function called push_back to append data. In your case you would read the data into a local temporary Books object and then push_back the completed Books, which makes a copy of it at the end of the vector, extending it as necessary. In fact I think your Books type is in fact only a Book type. A collection of Book objects would be implied by the name Books!
#include <vector>
// ...
// Dropped s at end of type name
struct Book
{
string isbn;
string title;
string fn;
string ln;
};
typedef vector<Book> BookCollection;
// ...
int main ()
{
BookCollection bookList;
// ...
return 0;
}
// The Book collection is now passed as a reference to a BookCollection
// type and the parameter renamed from b to books.
void loadData (ifstream & in, BookCollection & books)
{
cout << "Loading";
Book b; // b is now a single temporary Book object
while ( in.good() )
{
in >> b.isbn;
in.ignore(1);
getline (in, b.title, '\t');
in >> b.fn >> b.ln;
// check all is well before adding to collection
if ( in.good() )
{
books.push_back(b);
}
cout << '.';
}
cout << "Done\n";
}
// etc...
Things to note are that:
- I replaced all tabs with spaces. You seem to be mixing them. Don't. Use one or the other. I tend to convert tabs to spaces for indentation purposes as your code can look really badly mis-formatted if viewed or printed by an application which uses a different number of spaces per tab to those you used originally!
- I removed the s at the end of the Books type name. It is now just Book.
- I separated out the attributes of the Book struct one per line as this tends to be clearer, and easier to comment if necessary (you can add short comments next to each attribute if you wish).
- bookList is now a BookCollection type. BookCollection is a type alias for a std::vector<Book>, i.e. a vector of Book records. This means that loadData and showData have to be modified to accept this type: by reference for loadData (as it has to modify the contents) and by constant reference for showData, as you do not need to modify the contents but also do not wish to waste space and time copying the whole vector!
- As vectors grow as required there is no need for the size constant.
- I show a revised version of loadData that implements the scheme I previously mentioned. It also uses a while loop to keep on reading the records in the data file until there are no more (end of file is reached) or there is some other stream error. I'll leave the revised showData to you to ponder. You might wish to consider what to do if there is an error reading in the data in loadData rather than a natural termination by reaching the end of the file.
- You might like to know that std::vector types have a number of useful member functions and operators, such as size, which returns the number of elements in the vector and operator[] which allows element accesses similar to that for built-in arrays. For more information on std::vector and other C++ standard library facilities check out a good book such as "The C++ Standard Library A Tutorial and Reference" by Nicolai M. Josuttis (my copy is very well used!), or look for information online such as that at
http://www.sgi.com/tech/stl/ although some material refers to non-standard facilities, or the papers at
http://www.digilife.be/quickreferences/PT.htm
- Strictly speaking some additional error handling needs to be added. If the program runs out of memory or some other nastiness occurs then types based on the likes of std::vector et al. tend to throw some sort of exception based on std::expception (include <exception>). If nothing else our top level code (i.e. the code in main) should be prepared to catch such exceptions:
#include <exception> // for std::exception
#include <new> // for std::bad_alloc
// ...
int main ()
{
try
{
// ...
}
catch ( bad_alloc & )
{
cerr << "ERROR: out of memory.\n";
}
catch ( exception & e)
{
cerr << "ERROR: C++ standard exception occurred.\n";
<< e.what() << '\n';
}
catch ( ... )
{
cerr << "ERROR: Unanticipated exception occurred.\n";
}
}
std::bad_alloc is derived from std::exception (i.e. std::bad_alloc is a std::exception) so we have to place the catch clause for it before std::exception otherwise the std::exception catch clause will handle std::bad_alloc exceptions as well. You can add further catch clauses as required. Which operations throw which exceptions is part of the documentation for the operation.
Next you may like to consider making Book a fully paid up class rather than just a grouping of data. You should consider what operations would be useful for Book, and whether they should be members or not. Note that data members (i.e. all members of the Book struct at present) should be private rather than public (the default for struct types).
For more on such things check out a good C++ text book / reference book or three. A very well referenced book for effective use of C++ is "Effective C++" by Scott Meyers. However you may not have covered all the material he mentions (or that I am talking about). Do not worry if you do not get it all at once. I just hope you can take away a few pointers (no pun intended!) to help you in the future if not now.