EECS 211
C++ Special Topics

This is a short review of special topics in C++ especially helpful for various assignments. These notes are a quick summary. They do not replace the longer discussions in the textbook, though they may modify some.

Constants in C++

See Deitel, Sections 6.8, 7.4.3, and 10.7.

In C, especially older C, constants were defined with

#define PI 3.14159

The textbook rightly avoids these. Define constants in C++ with the const modifier, e.g.,

const double PI = 3.14159;

It's a bit more complicated if you want to define class-level, i.e., static, constants. You declare them with static in the class declaration in the header file. You define them (no static) in the implementation file. For example, to get a class constant Circle::PI, you put this in circle.h,

class Circle {
public:
  static const double PI;
  ...
};

This would go in circle.cpp:

#include "circle.h"
const double Circle::PI = 3.14159;
...

Since the compiler doesn't see the value when compiling files that include circle.h, it can't completely optimize some code. Since this mostly affects integers, there's a special exception for them. You can declare their value in the header file. You still need to "define" them in the implementation file, but with no value. For example, to define integer constants Date::JANUARY, Date::FEBRUARY and so on, put this in date.h:

class Date {
public:
  static const int JANUARY = 0;
  static const int FEBRUARY = 1;
  ...
};

and this in date.cpp:

#include "date.h"
const int Date::JANUARY;
const int Date::FEBRUARY;
...

Another kind of constant is the enumeration, e.g.,

enum { CLUB, DIAMOND, HEART, SPADE } CardSuit;
CardSuit suit = DIAMOND;

This defines constants for the values 0, 1, 2 and 3, from left to right.

Enumerations are also useful to label disconnected values, as in

enum { SOCKET_ERROR = 32, ILLEGAL_URL = 45, ... };

In C++, an enumeration is not an int! It can be easily converted to int, as needed, but, like char, it's a distinct type.

Exceptions

See Deitel, Chapter 16, particularly the first 4 sections, for details. Also see the CPlusPlus page on exceptions.

Signaling and handling errors can often make for complicated code. First, there's the signalling problem. If a function normally can return any number, what should it return if there's an error?

Then, there's the handling problem. If there is a value that a function can return for an error, e.g., -1, then every piece of code that uses that function has to check for -1, and, probably, return -1 itself until eventually some outer function is reached that can respond to the problem, e.g., by asking the user for different input.

These problems led to the invention of exceptions, which appear in C++ and other languages.

The two key syntactic forms are throw and try-catch. throw is like a special kind of return. Throwing a value immediately exits every function that is being executed, until control returns to a try-catch block that is waiting for the type of data being thrown.

In C++, you can throw anything. So, an easy thing is to use integers for error codes, and throw them when something bad is discovered somewhere. Most of your functions don't have to worry about error codes at all. They stay nice and simple. A function that discovers a problem just calls throw some integer. It doesn't have to worry about special return values.

To handle the exception, when it occurs, you write top-level code like this:

try {
 ...code that might, somewhere, way deep, throw an integer...
}
catch (int errorCode) {
 ...code to handle the exception; errorCode is the integer thrown...
}

A catch-clause is only executed if the exception is thrown. If the code inside the try doesn't throw an exception, the catch clause is ignored.

In big programs, there will be many different kinds of possible exceptions, and you probably want to throw more information than just an error code. To do this, create an "exception" class. This can be any class, but it's most common to define subclasses of std::exception or one of its subclasses, such as std::runtime_error, which is defined in the header <stdexcept>. That way, you can handle them specially, if desired, or just catch them along with other exceptions, as shown here.

To process different exceptions differently, use separate catch clauses for each type of exception you want to handle, like this:

try {
 ...code can throw various exception objects...
}
catch (BadURLException &bue) {
 ...code for this case...
}
catch (HostNotFoundException &hnfe) {
 ...code for this case...
}

Memory allocation in C++

See Deitel Section 10.6.

C has two functions, malloc and free, which dynamically allocate untyped blocks of memory. C++ uses new and delete, which are better for several reasons:

Be sure you know the difference between delete and delete[] and which one to use. Your program will crash otherwise.

Be careful not to try and delete the same memory twice.

Streams and string streams

See Deitel, Section 18.12.

Early programming languages used one set of functions to read and write to the console, and another set of functions to read and write to files. With the stream model, pioneered in Unix and C, there's a single set of functions that read from input streams and write to output streams. You write all your code to use these stream functions. You can then easily "redirect" your program's I/O as needed:

An istringstream is an input stream that gets characters from a string. For example,

istringstream in("12 3 456");
int a, b, c;
in >> a >> b >> c;
CPPUNIT_ASSERT_EQUAL( 12, a );
CPPUNIT_ASSERT_EQUAL( 3, b ); 
CPPUNIT_ASSERT_EQUAL( 456, c );

defines an input stream with the characters a, space, b, space, c.

An ostringstring is an output stream that sends characters to a string. For example,

ostringstream out;
out << 12 << " " << "abc";

creates an output string stream, and writes 12, space, and "abc" into it. out.str() will return whatever has been written into the output stream.

See Section 18.12 for more examples.

Testing with stringstreams

Stringstreams are especially useful for testing code that reads and writes. It avoids making users enter data and read output, which is basically useless, and it avoids the need for auxiliary text files that are a pain to maintain. For example, if we had a Rational class that represented numbers like 1/2 and 22/7, and it was supposed to support reading and writing values in that form, we could test it like this:

istringstream in("1/2 22/7");
Rational a, b;
in >> a >> b;
CPPUNIT_ASSERT_EQUAL( Rational(1, 2), a );
CPPUNIT_ASSERT_EQUAL( Rational(22, 7), b );

ostringstream out;
out << a << " " << b;
CPPUNIT_ASSERT_EQUAL( "1/2 22/7", out.str() );

Totally automated. No user action needed. Sweet!

Overloading operators

The book gives a fair number of examples of how to overload all the common (and not so common) operators. For a good readable introduction to this topic, see these notes from CalTech.

The CalTech notes implement all operators as member functions. As a general rule, don't do this!. It leads to

Instead, where possible, define operators as global functions in the header of the class, after the class definition, like this:

class Complex
{
  ...
}
 
bool operator<( const Complex &c1, const Complex &c2 );

Declare operators that need access to private class members as friends at the end of the class definition, like this:

class Complex
{
  ...
 
  friend Complex & operator+=( const Complex &c1, const Complex &c2 );
  ...
}

Declare the operators =, [], (), and ->, if needed, as member functions, because they need to return the class instance, like this:

class Complex
{
  ...
 
  Complex & operator=( const Complex &c );
  ...
}

Note that you only need to define assignment (and a copy constructor) if your class contains pointers to other data structures. Also note that member functions take one less argument. There's an implicit first argument in the this special variable.

Different operators have hidden subtleties to be aware of. If you forget them, you'll get very confusing compiler errors:

The iostream operators

You need to overload at least << if you want to be able to use instances of your classes in code like this:

CPPUNIT_ASSERT_EQUAL( Complex(-1, 0), Complex(0, 1) * Complex(0, 1) );

That's because the above expands into code that uses << to print the expected and actual values if the test fails.

Remember:

The arithmetic operators

If your class is going to support mutation operators like +=, then the general but unintuitive recommendation is to define += first, then use it to define +, not vice versa. This is discussed in the CalTech notes. (Again, ignore the fact that they make the operators member functions.)

There is one small non-obvious refinement you can make for non-member definitions. For example, consider this definition of +:

const MyClass operator+( const MyClass &a, const MyClass &b ) {
  MyClass c( a );
  return c += b;
}

This creates a copy of the first argument, increments it by the second argument, and returns it. But since we need a copy of the first argument, just pass it by value to begin with:

const MyClass operator+( MyClass a, const MyClass &b ) {
  return a += b;
}

The assignment operator

If your class dynamically allocates pointers to other structures, you must define a destructor to deallocate them. If you define such a destructor, you must also define a copy constructor and the assignment operator. Otherwise, C++ will use the default definitions that create new instances with the same pointers. When one of those instance is destroyed, its destructor will delete the memory pointed, thereby invalidating the other instances. When they are used, or even just destroyed, your program will crash!

Some key rules for writing assignment operators:

The relational operators

You might think that if you overload == and <, then C++ should be able to figure out how to do the other relational operators, like != and <= automatically. It can, sort of, but it doesn't do a very good job. The following in your header file:

#include <utility>
using namespace std::rel_ops;

will define templated versions of the relational operators !=, <=, > and >=, using the obvious code, e.g., (simplified)

template < class T >
bool operator>( const T &x, const T &y ) { return y < x; }

Unfortunately, these templates only match when the two arguments have the same type. Assume your header includes

#include <utility>
using namespace std::rel_ops;

// Code for rational number class
class Rational { 
public:
  Rational( double n = 0, double d = 1 ) : numerator( n ), denominator( d ) { }
  ...
};

bool operator<( const Rational &r1, const Rational &r2);

Then here's what will and won't work in client code:

Rational r( 8, 3 );
CPPUNIT_ASSERT( Rational( 2 ) < r ); // OK
CPPUNIT_ASSERT( 2 < r ); // OK, Rational( 2 ) called implicitly
CPPUNIT_ASSERT( r > Rational( 2 ) ); // OK
CPPUNIT_ASSERT( r > 2 ); // NOT OK, not same types

So, for the current version of C++, using the standard libraries, if mixed comparisons are desired, you need to overload the 4 operators explicitly.


Comments? comment icon Contact the Prof!

Valid HTML 4.01 Transitional