Cpp Chapter 17: Input, Output, and Files Part2
17.3 Input with cin
Typically, you use cin
as follows:
cin >> value_holder;
C++ redefines >>
for handling input with cin
. It is overloaded for the basic types of C++, as well as the char pointer type which is used to store strings. These are referred to formatting input functions because they convert input data to the format indicated by the target. A typical operator function has a prototype like this:
istream & operator>>(int &);
The fact that it returns a istream &
makes it possible to concatenate cin >>
statements. The fact that it takes a reference to int makes it possible to altering the value directly, rather than working with a copy of it, which is not what we want in the input case.
) How cin >> views input
cin skip over white spaces(blanks, newlines, and tabs) in the input stream until they encounter a non-white-space character. It reads everything from the initial non-white-space character up to the first character that doesn't match the desired type.
If input doesn't match the type requirement, cin
would leave the input in the stream and return 0(false), which indicates error in input. Here comes code illustrating these features:
// check_it.cpp -- chekcing for valid input
#include <iostream>
int main()
{
using namespace std;
cout << "Enter numbers: ";
int sum = 0;
int input;
while (cin >> input)
{
sum += input;
}
cout << "Last value entered = " << input << endl;
cout << "Sum = " << sum << endl;
return 0;
}
The fact that cin
returns false on wrong type makes it possible to be put in while
conditions and test for end of input.
) Stream states
A cin
or cout
object contains a data member that describes the stream state. It consists of three ios_base
elements: eofbit
badbit
failbit
.
1 eofbit
is set to 1 if end-of-file reached
2 badbit
is set to 1 if the stream is corrupted
3 failbit
is set to 1 if input failer to read the expected characters or don't match the type
There are also methods to test or set those states:
1 good()
returns true if everything 0
2 eof()
bad()
fail()
if the corresponding flag is set
3 rdstate()
returns the stream state
4 exceptions()
returns a bit mask that identifies which flags causes an exception to be thrown
5 exceptions(iostate ex)
set which states would lead to thrown exception
6 clear(iostate s)
set the stream state to s
7 setstate(iostate s)
sets stream state bits corresponding to those bit set in s, other stream state bits are left unchanged
) I/O and exceptions
You could use the exceptions()
method to control how exceptions are thrown and handled.
The default is that clear()
doesn't throw exceptions. You could use this code to make badbit
throws exceptions when clear()
is called:
cin.exceptions(badbit);
You could also combine it with the bitwise OR operator to specify two bits at one time:
cin.exceptions(badbit | eofbit);
When clear()
encounters a match, it throwns an ios_base::failure
exception. Here comes code throwing exceptions when encountered failure and handle it:
// check_it.cpp -- chekcing for valid input
#include <iostream>
int main()
{
using namespace std;
cout << "Enter numbers: ";
int sum = 0;
int input;
while (cin >> input)
{
sum += input;
}
cout << "Last value entered = " << input << endl;
cout << "Sum = " << sum << endl;
return 0;
}
) Stream state effects
You could nest cin
in while or if conditions to test whether it works properly:
while (cin >> input)
{
...
}
You could also get rid of the rest of the line and reset the stream so that you could use it again after encounter failure of input:
cin.clear();
while (!isspace(cin.get()))
continue;
You could also get rid of mistakes according to the error type. For example, if failbit
and eofbit
encountered, you could clear and reset like listed above. But if badbit
encountered, you could do nothing but quit the input process immediately.
) Other istream class methods
1 get(char &)
provide single-character input that doesn't skip over white space
2 get(char *, int, char)
and getline(char *, int, char)
functions by default read entire lines rather than single words
These are termed unformatted input functions because they read whatever it inputs, without skipping over white space and performing data conversions.
) Single-character input
1 get(char &)
member function
You call it by cin.get(c)
. It reads and displays white space as other printing characters, including blank newline and so on.
The get()
function also returns a reference to the istream
object that invokes the method, which makes the following concatenation possible:
char c1, c2, c3;
cin.get(c1).get(c2) >> c3;
If encounters end of file, it assigns nothing to the variable and calls setstate(failbit)
which makes cin
to test as false
2 getchar()
member function
You could also use the return value of get()
to indicate input, which works the same as get(char &)
:
char ch;
ch = cin.get();
In this way you could not concatenate get()
like this:
cin.get().get() >> c3; // invalid
cin.get(c1).get(); // valid
Upon reaching the end of file, cin.get(void)
returns the value EOF, which is a symbolic constant provided by the iostream
header file. This allows reading input like this:
int ch;
while ((ch = cin.get()) != EOF)
{
...
}
3 Which to choose from >> get(char &) and get(void)?
If skip white space, choose >>
If examine all characters, choose from get(char &) and get(void). The first one is more like class implementations while the second one simulates the getchar()
in C.
) string input: getline() get() and ignore()
getline()
and get()
have the same signatures:
istream & get(char *, int, char);
istream & get(char *, int);
istream & getline(char *, int, char);
istream & getline(char *, int);
The first argument marks where the input goes, the second is 1 plus the maximum number of characters it is going to accept and third(if exists) marks the delimiter of input, which terminates input when encountered. The two-argument version uses newline character as delimiter by default.
The chief difference between get()
and getline()
is that **get()
leaves the newline character in the input stream, while getline()
extracts the newline character and discards it. If the delimiter is given by the user, both functions also act as the newline character case, whether leaves it or discards it.
ignore()
takes two arguments, first marking the maximum number of characters to be read and the second marks the delimiter. It silently reads the characters and stores it nowhere, it also returns istream &
, which enables concatenation.
Here comes code illustrating the features of functions:
// get_fun.cpp -- using get() and getline()
#include <iostream>
const int Limit = 255;
int main()
{
using std::cout;
using std::endl;
using std::cin;
char input[Limit];
cout << "Enter a string for getline() processing:\n";
cin.getline(input, Limit, '#');
cout << "Here is your input:\n";
cout << input << "\nDone with phase 1\n";
char ch;
cin.get(ch);
cout << "The next input character is " << ch << endl;
if (ch != '\n')
cin.ignore(Limit, '\n');
cout << "Enter a string for get() processing:\n";
cin.get(input, Limit, '#');
cout << "Here is your input:\n";
cout << input << "\nDone with phase 2\n";
cin.get(ch);
cout << "The next input character is " << ch << endl;
return 0;
}
) unexpected string input
When faced with unexpected string input, get()
and getline()
have different behaviours:
1 getline(char *, int) sets failbit
if no characters read or characters exceed limit
2 get(char *, int) sets failbit
if no characters read
) other istream methods
1 read()
It takes 2 arguments, with first one marks the location that the input goes to, and the second marks the number of types to be read:
char gross[144];
cin.read(gross, 144);
2 peek()
The peek()
function returns the next character from input without extracting from the input stream, that is reading the next character while leaving it in the input stream:
char ch;
ch = cin.peek();
3 gcount()
The gcount()
method returns the number of characters read by the last unformatted extraction method.
char a[100];
cin.getline(a, 100);
cout << cin.gcount() << endl;
4 putback()
It inserts a character back to the input stream, then the input character becomes the first to be read next time.
Here comes code illustrating how we implement these features originally:
// peeker.cpp -- some istream methods
#include <iostream>
int main()
{
using std::cout;
using std::cin;
using std::endl;
char ch;
while(cin.get(ch))
{
if (ch != '#')
cout << ch;
else
{
cin.putback(ch);
break;
}
}
if (!cin.eof())
{
cin.get(ch);
cout << endl << ch << " is next input character.\n";
}
else
{
cout << "End of file reached.\n";
std::exit(0);
}
while (cin.peek() != '#')
{
cin.get(ch);
cout << ch;
}
if (!cin.eof())
{
cin.get(ch);
cout << endl << ch << " is next input character.\n";
}
else
cout << "End of file reached.\n";
return 0;
}
Now comes code illustrating how we could implement these features with the method listed above:
// truncate.cpp -- using get() to truncate input line, if necessary
#include <iostream>
const int SLEN = 10;
inline void eatline() {while (std::cin.get() != '\n') continue;}
int main()
{
using std::cin;
using std::cout;
using std::endl;
char name[SLEN];
char title[SLEN];
cout << "Enter your name: ";
cin.get(name, SLEN);
if (cin.peek() != '\n')
cout << "Sorrt, we only have enough room for " << name << endl;
eatline();
cout << "Dear " << name << ", enter your title: \n";
cin.get(title, SLEN);
if (cin.peek() != '\n')
cout << "We were forced to truncate your title.\n";
eatline();
cout << " Name: " << name << "\nTitle: " << title << endl;
return 0;
}
17.4 File Input and Output
The C++ I/O class package handles file input and output much as it handles standard input and output, but there's more to care about.
) Simple File I/O
You do the following to write into a file:
1 Create an ofstream
object
2 Associate the object with a file
3 Use the object the same way you use cout
ofstream fout; // 1
fout.open("haha.txt"); // 2
fout << "Dull data"; // 3
The ofstream
class uses buffered output, so the program allocates space for an output buffer when it creates an ofstream
object such as fout
. The object reads from the program byte-by-byte, and transfer it to the file when the buffer is filled. This way boosts the efficiency of transferring data. Also noteworthy that opening a file which doesn't exist would create a clean file and write into it. Opening a file in default mode also truncates it to 0, which wipes out prior contents. Later we would discuss approach to retain previous contents and append to it.
Similar requirements are for writing to a file:
1 Create an istream
object
2 Associate the object with a file
3 Use the object the same way you use cin
istream fin;
fin.open("jellyjar.txt");
char ch;
fin >> ch;
It also uses the buffered approach to speed up the process.
The connections with a file is closed automatically when the object expires, or you could explicitly close a connection:
fout.close();
fin.close();
Closing the file doesn't mean that the stream is eliminated, it still could be reconnected to other files afterwards. Here comes a program illustrating file input and output:
// fileio.cpp -- saving to a file
#include <iostream>
#include <fstream>
#include <string>
int main()
{
using namespace std;
string filename;
cout << "Enter name for new file: ";
cin >> filename;
ofstream fout(filename.c_str());
fout << "For your eyes only!\n";
cout << "Enter your secret number: ";
float secret;
cin >> secret;
fout << "Your secret number is " << secret << endl;
fout.close();
ifstream fin(filename.c_str());
cout << "Here are the contents of " << filename << ":\n";
char ch;
while (fin.get(ch))
cout << ch;
cout << "Done\n";
fin.close();
return 0;
}
) Stream checking and is_open()
You could use is_open()
to indicate whether a file has been opened correctly or not:
if (!fin.is_open())
{
...
}
This could not only detect the failure of opening, but also detect whether you are opening the file with appropriate modes. So it is far better than other choices such as if(fin.fail())
or if(!fin)
.
) Opening multiple files
There are two ways for you to open multiple files:
1 If you would like to simultaneously use them, you must create two objects thus creating two streams to handle the files.
2 If you only use those files sequentially, you could use only one stream and associate it with different files every time.
) Command-line processing
Command-line arguments are arguments that follow the command name. For example:
wc report1 report2 report3
Here wc
is command, report1
report2
report3
are command-line arguments.
C++ could also run programs in a command-line environment, turning your main()
into this:
int main(int argc, char *argv[])
Here argc
is the number of command-line arguments, which is 4 in the previous example. argv
is pointer to pointer to char, where argv[0]
is the pointer to the first thing in the command line, which is wc
, the command name, in the previous example. Here comes code illustrating the usage of command-line environment:
/ count.cpp -- counting characters in a list of files
#include <iostream>
#include <fstream>
#include <cstdlib>
int main(int argc, char * argv[])
{
using namespace std;
if (argc == 1)
{
cerr << "Usage: " << argv[0] << " filename[s]\n";
exit(EXIT_FAILURE);
}
ifstream fin;
long count;
long total = 0;
char ch;
for (int file = 1; file < argc; file++)
{
fin.open(argv[file]);
if (!fin.is_open())
{
cerr << "Cound not open " << argv[file] << endl;
fin.clear();
continue;
}
count = 0;
while (fin.get(ch))
count++;
cout << count << " characters in " << argv[file] << endl;
total += count;
fin.clear();
fin.close();
}
cout << total << " characters in all files\n";
return 0;
}
) File modes
While using the open()
method, you could provide a second argument that specifies the file mode:
ifstream fin("hi.txt", mode1);
Here are some file mode constants defined in the ios_base
class:
Constant | Meaning |
---|---|
ios_base::in | open file for reading |
ios_base::out | open file for writing |
ios_base::ate | seek to end-of-file upon opening file |
ios_base::app | append to end-of-file |
ios_base::trunc | truncates file it if exists |
ios_base::binary | binary file |
In previous examples, the ifstream open()
method has the default value of ios_base::in
for the second argument while ofstream open()
has the default of ios_base::out | ios_base::trunc
, which causes prior contents to be deleted. However, if you would like to preserver previous contents and append to the end of it, you could do it with:
ofstream fout("bagels", ios_base::out | ios_base::app);
The code uses |
to combine modes.
Let's look at a program that appends data to the end of a file, which maintains a guest list:
// append.cpp -- appending information to a file
#include <iostream>
#include <fstream>
#include <string>
#include <cstdlib>
const char * file = "guests.txt";
int main()
{
using namespace std;
char ch;
ifstream fin;
fin.open(file);
if (fin.is_open())
{
cout << "Here are the current contents of the " << file << " file:\n";
while (fin.get(ch))
cout << ch;
fin.close();
}
ofstream fout(file, ios::out | ios::app);
if (!fout.is_open())
{
cerr << "Can't open " << file << " file for output.\n";
exit(EXIT_FAILURE);
}
cout << "Enter guest names (enter a blank line to quit):\n";
string name;
while (getline(cin, name) && name.size() > 0)
{
fout << name << endl;
}
fout.close();
fin.clear();
fin.open(file);
if (fin.is_open())
{
cout << "Here are the new contents of the " << file << " file:\n";
while (fin.get(ch))
cout << ch;
fin.close();
}
cout << "Done.\n";
return 0;
}
) Binary files
When you store data in a file, you could store the data in text form or in binary format. For characters, the binary and text form are the same, just the binary representation of the character's ASCII code. But for numbers, their binary representation and text form could be quite different.
Each representations has advantages. The text form is easy to read, but the binary form is more accurate, takes less space and faster in transferring. To save the contents of a structure in a file, you could do with the binary opening mode and the write()
method:
struct planet
{
...
}
planet p1;
ofstream fout("planets.dat", ios_base::out | ios_base::app | ios_base::app);
fout.write( (char *) &p1, sizeof p1);
This approach uses a binary file mode and uses the write()
member function.
To save data in binary form instead of text form, you could use the write()
member function. It reads any type of data byte-by-byte with no conversion. Here comes file using the write()
function to save data in binary mode into files.
// binary.cpp -- binary file I/O
#include <iostream>
#include <fstream>
#include <iomanip>
#include <cstdlib>
inline void eatline() {while (std::cin.get() != '\n') continue;}
struct planet
{
char name[20];
double population;
double g;
};
const char * file = "planets.dat";
int main()
{
using namespace std;
planet pl;
cout << fixed << right;
ifstream fin;
fin.open(file, ios_base::in | ios_base::binary);
if (fin.is_open())
{
cout << "Here are the current contents of the " << file << " file:\n";
while (fin.read((char *) &pl, sizeof pl))
{
cout << setw(20) << pl.name << ": " << setprecision(0) << setw(12) << pl.population << setprecision(2) << setw(6) << pl.g << endl;
}
fin.close();
}
ofstream fout(file, ios_base::out | ios_base::app | ios_base::binary);
if (!fout.is_open())
{
cerr << "Can't open " << file << "file for output:\n";
exit(EXIT_FAILURE);
}
cout << "Enter planet name (enter a blank line to quit):\n";
cin.get(pl.name, 20);
while (pl.name[0] != '\0')
{
eatline();
cout << "Enter planetary population: ";
cin >> pl.population;
cout << "Enter planet's acceleration of gravity: ";
cin >> pl.g;
eatline();
fout.write((char *) &pl, sizeof pl);
cout << "Enter planet name (enter a blank line to quit):\n";
cin.get(pl.name, 20);
}
fout.close();
fin.clear();
fin.open(file, ios_base::in | ios_base::binary);
if (fin.is_open())
{
cout << "Here are the new contents of the " << file << " file:\n";
while (fin.read((char *) &pl, sizeof pl))
{
cout << setw(20) << pl.name << ": " << setprecision(0) << setw(12) << pl.population << setprecision(2) << setw(6) << pl.g << endl;
}
fin.close();
}
cout << "Done.\n";
return 0;
}
) Random Access
Random Access means moving directly to any location in the file instead of moving through it sequentially. You could create a fstream
object which could both handle input and output. In order to satisfy the requirements of input and output simultaneously, you declare the second argument of open()
in this way:
finout.open(file, ios_base::in | ios_base::out | ios_base::binary);
Next, you have seekg()
to move the input pointer to a given location, and seekp()
to move the output pointer to a given location:
istream & seekg(streamoff, ios_base::seekdir);
istream & seekg(streampos);
The first one accepts two arguments, with the first representing a file position, and the second as an offset from an file location. The second one represents a file position measured from the beginning of the file in bytes. Here are some sample calls:
fin.seekg(30, ios_base::beg); // 30 bytes beyond beginning
fin.seekg(-1, ios_base::cur); // back up one type
fin.seekg(0, ios_base::end); // go to the end of the file
A streampos
argument represents the absolute location from the beginning of a file in bytes. For example:
fin.seekg(112);
You could use tellg()
and tellp()
to check the current position of input and output pointers in absolute distance from the beginning of a file.
Here comes the code to read and revise planet information via random access:
// random.cpp -- random access to a binary file
#include <iostream>
#include <fstream>
#include <iomanip>
#include <cstdlib>
const int LIM = 20;
struct planet
{
char name[LIM];
double population;
double g;
};
const char * file = "planets.dat";
inline void eatline() { while (std::cin.get() != '\n') continue; }
int main()
{
using namespace std;
planet pl;
cout << fixed;
fstream finout;
finout.open(file, ios_base::in | ios_base::out | ios_base::binary);
int ct = 0;
if (finout.is_open())
{
finout.seekg(0); // beginning
cout << "Here are the current contents of the " << file << " file:\n";
while (finout.read((char *) &pl, sizeof pl))
{
cout << ct++ << ": " << setw(LIM) << pl.name << ": " << setprecision(0) << setw(12)
<< pl.population << setprecision(2) << setw(6) << pl.g << endl;
}
if (finout.eof())
finout.clear();
else
{
cerr << "Error in reading " << file << ".\n";
exit(EXIT_FAILURE);
}
}
else
{
cerr << file << " could not be opened -- bye.\n";
exit(EXIT_FAILURE);
}
cout << "Enter the record number you wish to change: ";
long rec;
cin >> rec;
eatline();
if (rec < 0 || rec >= ct)
{
cerr << "Invalid record number -- bye\n";
exit(EXIT_FAILURE);
}
streampos place = rec * sizeof pl;
finout.seekg(place);
if (finout.fail())
{
cerr << "Error on attempted seek\n";
exit(EXIT_FAILURE);
}
finout.read((char *) &pl, sizeof pl);
cout << "Your selection:\n";
cout << rec << ": " << setw(LIM) << pl.name << ": " << setprecision(0) << setw(12) << pl.population
<< setprecision(2) << setw(6) << pl.g << endl;
if (finout.eof())
finout.clear();
cout << "Enter planet name: ";
cin.get(pl.name, LIM);
eatline();
cout << "Enter planetary population: ";
cin >> pl.population;
cout << "Enter planet's acceleration of gravity: ";
cin >> pl.g;
finout.seekp(place);
finout.write((char *) &pl, sizeof pl) << flush;
if (finout.fail())
{
cerr << "Error on attempted write\n";
exit(EXIT_FAILURE);
}
ct = 0;
finout.seekg(0);
cout << "Here are the new contents of the " << file << " file:\n";
while (finout.read((char *) &pl, sizeof pl))
{
cout << ct++ << ": " << setw(LIM) << pl.name << ": " << setprecision(0) << setw(12) << pl.population
<< setprecision(2) << setw(6) << pl.g << endl;
}
finout.close();
cout << "Done.\n";
return 0;
}
) Incore Formatting
The C++ library also provides an sstream
family, which uses the same interface to provide I/O between a program and a string object. You could write formatted data into a string object, which is called incore formatting, then you could do the following:
ostringstream outstr;
double price = 380.0;
char * ps = " for a copy of the ISO/EIC C++ standard!";
outstr.precision(2);
outstr << fixed;
outstr << "Pay only CHF " << price << ps << endl;
The object uses dynamic memory allocation to handle saving the string. In fact, you could use the str()
method to get the string stored inside the object:
string mesg = outstr.str();
Using the str()
method freezes the object and you could no longer write to it. Here comes code illustrating the feature of str()
:
// strout.cpp -- incore formatting (output)
#include <iostream>
#include <sstream>
#include <string>
int main()
{
using namespace std;
ostringstream outstr;
string hdisk;
cout << "What's the name of your hard disk? ";
getline(cin, hdisk);
int cap;
cout << "What's its capacity in GB? ";
cin >> cap;
outstr << "The hard disk " << hdisk << " has a capacity of " << cap << " gigabytes.\n";
string result = outstr.str();
cout << result;
return 0;
}
Similarly, the istringstream
class lets you use methods to read data from an istringstream
object, which could be initialized by a string object:
string facts;
...
istringstream instr(facts);
Then you could use the >>
method to read data from instr. Here comes code using the overloaded >>
operator to read contents of a string:
// strin.cpp -- formatted reading from a char array
#include <iostream>
#include <sstream>
#include <string>
int main()
{
using namespace std;
string lit = "It was a dark and stormy day, and the full moon glowed brilliantly. ";
istringstream instr(lit);
string word;
while (instr >> word)
cout << word << endl;
return 0;
}
至此完成了本学期C++ primer plus书籍的学习,来日方长!