C++ Special Topics |
Home Class Info Links |
Lectures Newsgroup Assignments |
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.
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.
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...
}
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:
Complex *cPtr = new Complex(r, i);
dynamically allocates memory for a new instance of Complex
and calls to the constructor to initialize that memory, all in one call.delete cPtr first calls
the destructor for Complex, if any, then gives back the memory.
That way, the destructor gets a chance to do additional cleanup, including
other deletions, before the Complex object disappears.
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.
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:
cin
and coutAn 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.
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!
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:
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:
out lt;< x << y will work.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;
}
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:
Class is your class,
use the conventional form Class & operator=( Class &lhs, const Class &rhs)
to define the operator. lhs and rhs
refer to the left-hand side and right-hand side, respectively.const Class &, as shown in the book.
The CalTech notes
explain why.this == &rhs.*this.
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?
Contact the Prof!