Daniel Sigg, May 2001
Books:
Stanley B. Lippman, and Josee Lajoie, "C++ Primer".
Bjarne Stroustrup, "The C++ Programming Language".
Nicolai M. Josuttis, "The C++ Standard Library".
C++ is derived from C and inherits all its basic features but adds features such as classes, templates, function overloading, operator overloading and a standard template library (STL) containing a set of generic containers and algorithms.
Function overloading refers to the fact that in C++ different functions can have the same name as long as their argument list is different. For example
void Print (int n);are all valid. Function overaloading also applies to methods of a class (see next section).
void Print (double x, int precision = 6);
void Print (const char* p);
Introduction
A class (or object) is a combination of data records and functions (called methods). It puts both together in way which should enable the implementation of data independent interfaces (ar access methods). An Example:
class basic_filter { // define a class named basic_filterA class should behave like any ordinary data type, i.e.,
public: // the public interface is accessible from outside
enum filtertype { // this is a public type
FIR = 0,
IIR
};
// method to get the filter type
filtertype getFilterType() const {
// use const for methods which don't change data members!
return fFilterType; }
// method to set the filter type
void setFilterType (filtertype ftype) {
fFilterType = ftype; }
private: // only accessible by class methods
// data member: filter type
filtertype fFilterType;
};
basic_filter myfilter; // DeclarationThe above class is not complete and wouldn't work in practice, since it misses an initialization method--generally called a constructor. On the other hand a class has a default cleanup method (or destuctor) which does nothing, a default copy (or assignment) operator which copies the data memebers one by one and a default copy constructor which initalizes the object by copying the data members from an other object of the same type. To define a constructor we add the following methods:
basic_filter filter2 = myfilter; // Declaration and initialization
filter2 = myfilter; // Assignment/Copy
basic_filter filter3 = new basic_filter; // Creation and initialization
delete filter3; // Destruction
class basic_filter {The above constructor demonstrate several important features:
public:
basic_filter () : fFilterType (FIR) {}
explicit basic_filter (filtertype ftype) : fFilterType (ftype) {}
...
};
~basic_filter ();Inheritance
One key feature of object oriented design is inheritance. In the above example we defined a class basic_filter with the idea to later derive new classes of type IIIRFilter and FIRFilter which inherit both data members and methods of the basic filter type. This has two big advantages: (i) we have to write the code which is common for both filter classes only once, and (ii) we can use the basic_filter class as an abstract class which defines the interface common to all derived filter classes. This way a user which gets passed a filter class only has to know about basic_filter regardless of the actual implementation. Example:
class basic_filter {To see how this all works in practice let's look at a few statements which use these filter classes:
public:
...
// Define basic data type for filter calculation
typedef float datatype;
// Managing a filter history is common
void GetHistory (const datatype*& hist, int& N) const;
protected:
// Only derived classes can set the history
void SetHistory (const datatype* hist, int N);
public:
// Applying a filter is a common task (interface)
virtual TSeries Apply (const TSeries& ts) = 0;
// Just for the fun of it we also overload the apply operator
TSeries operator() (const TSeries& ts) {
return Apply (ts); }
...
private:
// Data members for filter history
int fHistoryLen;
datatype* fHistory;
...
};
// IIRFilter inherits from basic_filter
class IIRFilter : public basic_filter {
public:
// Must define a new constructor
IIRFilter (complex<double>* poles, int pnum,
complex<double>* zeros, int znum, double gain);
// Must define an Apply method
virtual TSeries Apply (const TSeries& ts);
...
};
// FIRFilter inherits from basic filter as well
class FIRFilter : public basic_filter {
public:
// Must define a new constructor and Apply method
...
};
basic_filter bf1; // Invalid: this is an abstract classThe keyword virtual is used to indicate a method which will be overriden by descendents and which will be resolve at run-time. The virtual Apply method in basic_filter is a pure vurtual method--indicated by setting it equal zero. This is not absolutely necessary, we could have just defined a method which was doing nothing. However, any class which has a pure virtual method defined can not be instantinated; this enforces that all descnedents must override this method and that it isn't possible to declare a basic_filter object.
basic_filter* pf; // Valid: pointer to a filter
IIRFilter if (...); // Valid IIR filter
FIRFilter ff (...); // Valid FIR filter
basic_filter& rf(&if); // Valid: reference to an IIRFilter
pf = &if; // Valid
pf->GetHistory(...) // Calls basic_filter::GetHistory(...)
if.GetHistory(...) // Calls basic_filter::GetHistory(...)
TSeries inp, out;
out = ff.Apply(inp); // Calls FIRFilter::Apply(inp)
out = pf->Apply(inp); // Calls IIRFilter::Apply(inp)
out = ff(inp); // Calls basic_filter(inp) which calls FIRFilter::Apply(inp)
out = rf(inp); // Calls basic_filter(inp) which calls IIRFilter::Apply(inp)
Why all the trouble one may ask? Imagine yet another class FilterBank which manages sets of filters and applies them to an incoming data stream. FilterBank only has to know about basic_filter since it does only apply filters (they are created elsewhere). Using the basic_filter class (or better a list of pointers referencing filters which are derived from basic_filter), the filter bank can be written without any knowlegde how a filter may be implemented or how many different filter implementation eventually will be written! For example, one can easily imagine the following FilterBank class:
class FilterBank {Cleanup and copy revised
public:
FilterBank(); // Initalizes the filter bank
void AddFilter (basic_filter* filter); // Adds a filter to the bank
void AddTrigger (basic_trigger* trigger); // Same tick for the trigger!
EventList Apply (const TSeries& ts);
// Applies the filters, evaulates the trigger and returns an event list
...
private:
int fFilterNum; // Number of filters
basic_filter** fFilterBank; // List of filter pointers
basic_trigger* fTrigger; // Pointer to a trigger
...
}
The alerted reader may have noticed that the introduction of the history buffer introduced a memory leak when the object is no longer used and that the default member-wise copy constructor and assignment operator only copy the pointer to the buffer rather than its contents. To make the basic_filter class work again we have to add the following methods:
class basic_filter {Notice the use of the this pointer; this is an implicit argument to all methods and points to the actual object in memory. Also notice that the detsuctor has been declared virtual; this is necessary to guarantee that the correct cleanup method is called when derived classes have been defined.
public:
...
// Copy constructor
basic_filter (const basic_filter& filter) : fHistory (0) {
*this = filter; }
// Cleanup (destructor)
virtual ~basic_filter() {
delete [] fHistory; }
// Assignment operator
basic_filter& operator= (const basic_filte& filter) {
if (this != &filter) {
fFilterType = filter.fFilterType;
fHistoryLen = filter.fHistoryLen;
if (fHistory) delete [] fHistory;
fHistory = 0;
if (fHistoryLen > 0) {
fHistory = new float [fHistoryLen];
for (int i = 0; i < fHistoryLen; ++i) fHistory[i] = filter.fHistory[i];
}
}
return *this;
}
...
};
Templates are classes or functions which use types as arguments. Templates are rarely defined by the user since they are usually part of an object library. However, it is important to know how to use them. In the basic_filter class we had to decide on the datatype. What if we later change our opinion and alos want to have filters which work on doubles? Obviously this is easy done by cpoying the code, renaming all class and definind datatype as double. A lot of work for a straight forward matter one might think. Indeed, templates allow us to anticipate the need and leave the exact type open until the class is needed. Example:
template <typename T> // Templates use angle bracketsSimilary one would define a template for the IIRFilter class by
class basic_filter {
public:
typedef T datatype;
... // rest stays the same
};
template <typename T>The main difference is when a template class is used, one has to specify the type of the template using angle brackets, i.e.,
class IIRFilter : public basic_filter<T> {
... // rest stays the same
};
basic_filter<double>* pf; // declares a pointer to basic_filter with doublesOne of the instances where templates where already used in the previous section were the complex numbers. C++ comes with a complex class which is implemented as a template. As a matter of fact the main importants of templates stems from their heavy use in the standard template library which is part of C++ (see next section).
IIRFilter<float> if; // declares an IIRFilter besed on floats
C++ allows to overload standard arithemtic operators. We have already seen examples of the assignment and apply operator, but the scheme extens to almost all operators. As an example we define the plus operator for time series which concatenates the two series together (if their adjacent), i.e.,
TSeries operator+ (const TSeries& t1, const TSeries& t2) {
...
}
STL is an imortant part of C++ but it is beyond the scope of the introduciton to give a detailed explanation. STL implements template for containers such as doubly linked lists, arrays of objects, binary trees, sets, stacks, etc. Since these are templates the user can define a doubly linked list of its own classes simply by defining
list<myClass> x;These containers come with a full set of access and modify methods, iterators and algorithms. A programmer who needs to manage a set of classes in one form or the other should take a closer look to see if what he needs is already provided rather than reinvent it again.
STL also contains such useful class as string (array of characters), complex (for complex number arithmetic) and IO streams (for input and output).
Namesapces were introduced to prevent common names defined in different libraries from clashing. One places headers and program code into a namespace by surrounding them with
namespace SignalProcessing { // introduce the SignalProcessing namespaceNow using the filters from outside the SignalProcessing namespace requires either a using directive or a fully qualified name
class basic_filter { // filter class definitions
...
} // end of namespace
using namespace SignalProcessing; // Import everything from SignalProcessingThere are two predefined namespaces: standard (std) and general (no name). STL is defined in std.
using SignalProcessing::basic_filter; // Import basic_filter only
SignalProcessing::basic_filter* pf; // Use fully qualified name in declaration
The idea is to provide a mechanism to rise excpetions when something out-of-the-ordinary happens (such as an error) and allow the programmer to catch it over a whole block of code at once. Example:
// Try blockExceptions are a software construct. Hardware excpetions such as memory violations will still terminate the program if not managed by a signal handler. The usefulness of excpetions is debated. They are usually highly regarded by so-called software designers whereas down-to-earth programers ask themselves wouldn't it be better to develop programs that don't fail.
try {
basic_filter* filter = new IIRFilter (...);
out = filter->Apply (inp);
}
// Catch block
catch (bad_alloc) { cout << "Out of Memory" << endl; }
catch (...) { cout << "Unknown Error" << endl; throw; } // rethrow
...