Main

March 03, 2004 (Wednesday)

Chapter 9: Defining new types (cont'd)

Implementation file for the 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

Defining methods outside of a class

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 doubles 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.

Member initialization lists

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

Chapter 10: Managing memory and low-level data structures

The first part of this chapter (from § 10.1 — §10.5) contains topics that we have already discussed when we covered C. Topics include:

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:

File I/O using 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.

The 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

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.

Exception hierarchy (not covered in K&M)

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)