March 01 (Monday) March 05 (Friday)
Student_info
class
The corresponding Student_info.cpp
source module for the
Student_info.h
header file presented earlier is given below.
The source module includes the Student_info.h
header file
and defines all the methods that were declared but not defined in the
header file as well as the global compare()
function.
Many of the method bodies should look familiar to you, since we've seen
them before (e.g. read()
and read_hw
).
#include <iostream>
#include <vector>
#include "grade.h"
#include "Student_info.h"
using std::istream;
using std::vector;
double Student_info::grade() const
{
return ::grade(midterm, final, homework);
}
bool compare(const Student_info& x, const Student_info& y)
{
return x.name() < y.name();
}
Student_info::Student_info(): midterm(0), final(0) { }
Student_info::Student_info(istream& is) { read(is); }
// read homework grades from an input stream into a `vector<double>'
istream& read_hw(istream& in, vector<double>& hw)
{
if (in) {
// get rid of previous contents
hw.clear();
// read homework grades
double x;
while (in >> x)
hw.push_back(x);
// clear the stream so that input will work
// for the next student
in.clear();
}
return in;
}
istream& Student_info::read(istream& in)
{
in >> n >> midterm >> final;
read_hw(in, homework);
return in;
}
Student_info.cpp
In order to define a member function (method) outside a class,
we must qualify the method name with its corresponding class name.
Therefore, when we define the grade()
method, we write
Student_info::grade()
. (Remember that the ::
operator is the scope resolution operator.) Note that we must also
repeat the const
qualifier so that the method definition
is consistent with its declaration.
double Student_info::grade() const { return ::grade(midterm, final, homework); }
This function calls the global grade()
function that takes
two double
values and a vector of double
s
representing the homework assignment grades. Note that in order
to access this global function, we must use the scope resolution
operator with no namespace/class prefix. If we didn't do this, the
Student_info::grade()
function would attempt to call itself,
resulting in a compiler error due to the parameter mismatch.
The following code fragment demonstrates how we can call some of the class methods we defined above:
Student_info stu; // calls Student_info::Student_info() constructor stu.read(cin); stu.grade(); ...
Note that both the read()
and the grade()
method take an implicit first parameter which is a pointer to
the student for which the method was invoked (which is stu
in this case). Consequently, whenever the read()
method
modifies the n
, midterm
, final
or homework
data members, it is actually modifying those
members that belong to stu
. Similarly, because we are
calling grade()
on the stu
object, the
grade()
method accesses the data members that belong to
stu
. Because grade()
is a const
method, it is forbidden from actually modifying the data members or from
calling non-const
member functions.
During default initialization of a Student_info
object,
the string n
and vector homework
will be set
to empty since their classes define appropriate default constructors.
(A default constructor is the constructor which can be called without any
parameters.) However, the midterm
and final
data members, being built in types (double
),
will have not be initialized. The default constructor for the
Student_info
class uses a member initialization list in
order set these values to appropriate defaults:
Student_info::Student_info(): midterm(0), final(0) { }
This notation, which looks peculiar at first, initializes the
midterm
and final
data members to 0.
It is typically more efficient to initialize data members this way
in a constructor than assigning values to the data members in the
body of the constructor.
More information on constructors is presented in Chapter 11.
main()
program that uses the
Student_info
type
The following main()
function provides an example of how we
can use the Student_info
class. The spirit of the program
is exactly the same that we saw earlier. The only major difference
is that we are using Student_info
as a full-fledged class
rather than as a structure.
#include <algorithm>
#include <iomanip>
#include <ios>
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>
#include "Student_info.h"
#include "median.h"
using std::cin; using std::cout;
using std::domain_error; using std::endl;
using std::setprecision; using std::setw;
using std::sort; using std::streamsize;
using std::string; using std::vector;
using std::max;
int main()
{
vector<Student_info> students;
Student_info record;
string::size_type maxlen = 0;
// read and store the data
while (record.read(cin)) { // changed
maxlen = max(maxlen, record.name().size()); // changed
students.push_back(record);
}
// alphabetize the student records
sort(students.begin(), students.end(), compare);
// write the names and grades
for (vector<Student_info>::size_type i = 0;
i != students.size(); ++i) {
cout << students[i].name() // this and the next line changed
<< string(maxlen + 1 - students[i].name().size(), ' ');
try {
double final_grade = students[i].grade(); // changed
streamsize prec = cout.precision();
cout << setprecision(3) << final_grade
<< setprecision(prec) << endl;
} catch (domain_error e) {
cout << e.what() << endl;
}
}
return 0;
}
grading_prog.cpp
The first part of this chapter (from § 10.1 — §10.5) contains topics that we have already discussed when we covered C. Topics include:
main()
(i.e.
argc
/argv
)
These sections of the chapter provide an excellent review of these topics. If you were confused by any of these concepts when we covered them in the C part of the course, reading Chapter 10 would be a good idea.
Some of the more interesting points from these sections include:
NULL
pointer macro, C++ programs use
0
to represent a pointer value that does not point to
anywhere. You should never dereference a 0
pointer in
C++ for the same reason that you should dereference a NULL
pointer in C.
#include <iostream>
#include <string>
#include <algorithm>
using std::string;
int
main()
{
char *p = "hello world";
string s1 = string(p + 3, p + 8);
string s2;
copy(p, p + 5, back_inserter(s2));
std::cout << s1 << std::endl; // "lo wo"
std::cout << s2 << std::endl; // "hello"
return 0;
}
pit.cpp
The s1
string is initialized by giving it two character
pointer values which, in this context are used as iterators that
denote a range of characters to be used to initialize s1
.
Because we can treat pointers as if they were iterators, we can also use
pointers as arguments to the <algorithm>
functions.
The copy()
function above will copy the first five characters
(from index 0 upto, but not including, index 5) into the s2
string. Note that we have to use the back_inserter
iterator
adaptor on s2
since copy()
will not create
new elements in the destination.
ptrdiff_t
type which is typically represented by a signed
int
or long
depending upon the compiler.
Values of type size_t
(which is an unsigned
type) can be used to hold the size of any object, including an array.
The sizeof
operator, which is available in both C and C++,
returns a value of type size_t
.
The size_t
and ptrdiff_t
types are also present
in C. In C++, these types are defined in the <cstddef>
header.
ifstream
and ofstream
(K&M § 10.5)
In addition to the ifstream
class, which we saw earlier
for doing file input, there is an analogous ofstream
class
that can used to create objects that do file output. For example,
consider the following program, which also reviews how the
argc
and argv
main()
function
parameters can be used to determine command line parameters.
#include <iostream>
#include <fstream>
#include <string>
using std::clog; using std::cerr;
using std::endl; using std::string;
using std::ifstream; using std::ofstream;
int main(int argc, char **argv)
{
if (argc < 3) {
cerr << "Usage: " << argv[0]
<< " <infile1> <infile2> ... <out>" << endl;
return 1;
}
ofstream output(argv[--argc]);
if (! output) {
cerr << "Cannot open output file" << endl;
return 1;
}
while (--argc) {
ifstream input(*++argv);
if (! input) {
cerr << "Cannot open input file" << endl;
return 1;
}
clog << "Processing " << *argv << endl;
string s;
while (getline(input, s)) {
output << s << endl;
}
}
return 0;
}
concat.cpp
This program will concatenate all the files specified on the command
line into one large file whose name is given last on the command line.
(If the file denoted by the last command line argument exists, the
file will be overwritten.) When we create the ostream
file object, output
, we test it to make sure that the file
was successfully opened for write. Similarly, as we've seen before,
we test each of the input
ifstream
objects
to make sure that the were successfully opened for read. The regular
input/ouput operators and functions (e.g. <<
,
>>
, getline()
, etc.) work on
the ifstream
/ofstream
objects as expected.
cerr
and clog
output streams
Note that this program makes use of the cerr
error stream
as well as the clog
logging stream. Both these streams send
their output to same destination (standard error), but cerr
is unbuffered, meaning that any output sent to it will written
immediately. The clog
stream is buffered, so data written
to it will not appear until the buffer is full or the stream has been
manually flushed.
new
and delete
operators
(K&M § 10.6)In C++, dynamic memory allocation is much cleaner than in C. For example, to allocate memory for an integer, initialize it and deallocate it, we would write the following in C:
int *p = (int *) malloc(sizeof(int)); *p = 42; ... free(p);
However, in C++, the same thing can be accomplished with a much
simpler syntax using the new
keyword.
int *p = new int(42); ... delete p;
You can think of the int(42)
initializer as passing
in the constant 42
to the int
constructor.
An appropriate amount of memory will be allocated for the integer
and the value stored there will be initialized to 42
.
We could have just allocated the memory as follows:
int *p = new int;
In the above case, the value pointed to by p
will be
uninitialized and will likely be garbage. A value will have to be
assigned to this location (e.g. *p = 42
) prior
to using *p
in an expression.
Allocating an array in C++ is also easier than allocating an array
in C. For example, in C, we could use the following to allocate
an array of 100 integers (note that calloc()
could
also be used).
int *p = (int *) malloc(sizeof(int) * 100); ... free(p);
In C++, we can use the following syntax to allocate an array of 100 integers:
int *p = new int[100]; ... delete [] p;
Note that when we construct an array in C++, we use the new
operator and specify the number of items to allocate in square brackets.
When we deallocate an array in C++, we must use empty square brackets
between the delete
operator and the variable that points
to the array that we are deallocating.
Note that in C, the array contents are not initialized to anything in particular — the values in the array are undefined. In the C++ example, the array elements are initialized by calling the default constructor for the type being defined. In the case of integers (and other built-in types) the array elements are uninitialized, as in C.
Like Java, the standard library in C++ has an exception hierarchy.
At the top of this hierarchy is the exception
class.
This class has two major subclasses, logic_error
and
runtime_error
(we'll be talking about subclasses in
subsequent lectures).
The logic_error
class has four subclasses:
length_error
,
domain_error
,
out_of_range
and
invalid_argument
.
The runtime_error
class has three subclasses:
range_error
,
overflow_error
and
underflow_error
.
In the catch
clause of a try { ... } catch
block, the exceptions should be listed in increasing order of generality.
(i.e. subclass exceptions should be listed before their
parent class exceptions). When an exception is generated, the code
specified in the appropriate catch
clause (if one exists)
will be executed. For example, consider the following program that
calls a function that throws a range_error
exception.
Neither of the catch causes explicitly catch a range_error
exception. However, because a range_error
is a type of
runtime_error
error, the block of code that catches the
runtime_error
exception will be executed.
#include <stdexcept>
#include <iostream>
using std::cout; using std::endl;
using std::domain_error; using std::invalid_argument;
using std::logic_error; using std::exception;
using std::runtime_error; using std::out_of_range;
using std::range_error;
int
f()
{
throw range_error("out of range!");
}
int
main()
{
try {
f();
} catch (const invalid_argument &e) {
cout << "invalid_argument " << e.what() << endl;
} catch (const domain_error &e) {
cout << "domain_error " << e.what() << endl;
} catch (const logic_error &e) {
cout << "logic_error " << e.what() << endl;
} catch (const runtime_error &e) {
// This catch clause will be used since 'range_error'
// is a form of 'runtime_error'.
cout << "runtime_error " << e.what() << endl;
} catch (const exception &e) {
// If we didn't have the previous catch clause, then
// this one would have been used since 'range_error'
// is a form of 'runtime_error', which, in turn
// is a form of 'exception'.
cout << "exception " << e.what() << endl;
}
}
exc.cpp
Last modified: March 6, 2004 15:42:46 NST (Saturday)