Day 1: Getting Started

Welcome to Teach Yourself More C++ Programming in 21 Days! This book assumes that you already have read an introductory book on C++ and are comfortable writing simple C++ programs. If this is not so, please take the time to read one of the many available C++ primers, such as Teach Yourself C++ in 21 Days by Jesse Liberty (ISBN: 0-672-30541-0), published by Sams Publishing, 1994.

Today you will review the fundamentals of C++. If the topics are new to you, read a comprehensive primer on the subject before proceeding. Take the time to answer the quiz questions and to do the review exercises at the end of this chapter. These will ensure that you are ready for the material presented in the rest of the book.

This review is different from the rest of the book in that very few examples are provided, and the explanations are quite terse. The idea is to double check that you are comfortable with the fundamentals of C++, rather than to learn anything new.

Understanding Object-Oriented Programming

C++ programs are concerned with the creation, management, and manipulation of objects. An object is an encapsulation of data and the methods, or functions, used to manipulate that data.

Traditional programming languages separated data from functionality. Typically, data was aggregated into structures that then were passed among various functions that created, read, altered, and otherwise managed that data.

Object-oriented programming focuses on the creation and manipulation of "things," such as people, cars, animals, employees, factories, televisions, cities, air traffic control systems, and so on. This type of programming gives you a greater level of abstraction; the programmer can concentrate on how the employee objects interact with the city objects without having to focus on the details of the implementation of either type.

Encapsulation

Encapsulation is the desirable trait of being able to treat an object as an entity without worrying about, or even knowing, how it works. Thus, in object-oriented programming, you can create, use, and destroy dialog boxes, for example, without knowing how they work. You can create and use strings, arrays, collections, or even more complex, user-defined types such as employee and municipal worker, without knowing anything about how they are implemented.

In C++, there is a strong distinction made between a class's interface and its implementation. The class interface tells you how objects of that class are used; the implementation tells you how they work. Clients (users) of the class do not need to know about the implementation; they care only about the interface, which tells what the objects can do, and how they are created and destroyed.

Polymorphism

Polymorphism refers to the capability to treat different types of objects in a common way by using only the abstract interface. You might write a function, for example, that manipulates different types of phones, dial phones, touch-tone phones, and ISDN (digital) phones, for example without knowing or caring about the implementation differences between the different phone types.

Each phone is passed in as a parameter to your function, and you can tell the phone to ring by calling thePhone->ring(). The old-fashioned phone rings, the modern phone trills, and the picture phone flash a message. Your program doesn't need to know which type of phone it is working with; it calls ring(), and the right thing happens based on the real type of the phone.

Inheritance

The traditional object-oriented programming use of the term inheritance refers to the capability to create a new type based on an existing type, specifying only the ways in which the two types differ. The benefit of inheritance is that a well-tested type can be used as the basis of new types, taking advantage of all the work that went into the existing type.

In C++, inheritance is tightly tied to polymorphism; in C++, the principal way to invoke polymorphism is through inheritance.

Note: You should be familiar with the following terms:

Encapsulation: Bundling an entire process with its data into a single class.

Polymorphism: The capability to treat many types as if they were all of one (base) type, or the capability for one object or function to change behavior based on context.

Inheritance: Extending the behavior of an existing type in a new, derived type.

Object oriented: Paying attention to types and their behavior, rather than to functions and the data on which they act.

The Fundamentals

C++ programs consist of classes each can include member data and member functions. Additionally, C++ programs call upon non-member (global) functions. You must be comfortable writing and using member functions and global functions, and you must understand in some depth the differences between passing by reference and passing by value.

The built-in data types such as int and double are augmented in C++ by user-defined types: classes. An essential principle of C++ is that user-defined types are at least as powerful and integrated into the language as the built-in types. Thus, virtually anything you can do with a built-in type such as passing it to a function, getting it as a return type, using operators such as plus and indirection on it, and so on, you can do with a user-defined type.

Understanding Memory

Although it is possible to write simple C++ programs without understanding the stack and the heap, a full and rich understanding of the language requires a deep understanding of how memory is allocated and used.

When you begin your program, your operating system (such as DOS or Microsoft Windows) sets up various areas of memory based on the requirements of your compiler. The principle areas of memory covered in this section are global name space, the free store, the registers, the code space, and the stack.

Global variables are available to every function in the program. Registers are a special area of memory built right into the central processing unit (CPU). They take care of internal housekeeping. A great deal goes on in the registers that is beyond the scope of this book; we're concerned with the set of registers responsible for pointing, at any given moment, to the next line of code. We'll call these registers or, as a group, the instruction pointer. The instruction pointer keeps track of which line of code is to be executed next.

The code itself is in code space, which is the part of memory set aside to hold the binary form of the instructions you created in your program. Each line of source code is translated into a series of instructions, and each of these instructions is at a particular address in memory. The instruction pointer has the address of the next instruction to execute.

The stack is a special area of memory allocated for your program to hold the data required by each of the functions in your program. When data is "pushed" onto the stack, the stack grows; as data is "popped" off the stack, the stack shrinks.

A stack of dishes in a cafeteria is the common analogy, and it is fine as far as it goes, but it is wrong in a fundamental way. A closer mental picture is of a series of cubbyholes aligned top to bottom. The top of the stack is whatever cubby the "stack pointer" (which is another register) happens to be pointing to.

Each of the cubbies has a sequential address, and one of those addresses is kept, at any moment, in the stack pointer register. Everything below that magic address, known as the top of the stack, is considered to be on the stack. Everything above the top of the stack is considered off the stack and invalid.

When data is put on the stack, it is placed into a cubby above the stack pointer, and then the stack pointer is moved to the new data. When data is popped off the stack, all that really happens is that the address of the stack pointer changes.

The Stack and Functions

When your code branches to a function, here's what happens:

  1. All the arguments to the function are placed on the stack.

  2. The address in the instruction pointer is incremented to the next instruction past the function call. That address then is placed on the stack, and will be the return address when the function returns.

  3. Room is made on the stack for the return type you've declared. On a system with two-byte integers, if the return type is declared to be an integer, another two bytes are added to the stack, but no value is placed in these bytes. Note that some compilers store small values in registers rather than using the stack.

  4. The address of the called function is loaded into the instruction pointer, so the next instruction executed will be in the called function.

  5. The instruction now in the instruction pointer is executed, thus executing the first instruction in the function.

  6. The current top of the stack is noted and held in a special pointer called the stack frame. Everything added to the stack from now until the function returns will be considered "local" to the function.

  7. Local variables are pushed onto the stack as they are defined.

When the function is ready to return, the return value is placed in the area of the stack reserved at step 2. The stack then is popped all the way up to the stack frame pointer, which effectively throws away all the local variables, calls their destructors, and removes the arguments to the function.

The return value is popped off the stack and assigned as the value of the function call, and the address stashed away in step one is retrieved and put into the instruction pointer. The program thus resumes immediately after the function call, with the value of the function retrieved.

Some of the details of this process are compiler dependent; that is, they change from compiler to compiler, but the essential idea is consistent across computers and compilers. When you call a function, the parameters and the return address are put on the stack. During the life of the function, local variables are added to the stack. When the function returns, these are all removed by popping the stack.

Using Classes and Structures

Classes are user-defined data types. The member data and functions of the class are accessible through objects of the class, with the exception of static members, which are described later. Member functions have an implicit this pointer, which operates as a pointer to the individual instance of the class. (Note that static functions don't have a this pointer.)

Classes differ from structures only in that their default inheritance and access is private, whereas structures inherit publicly by default and, by default, have public access. Other than this, there is no difference between classes and structures.

Classes with virtual functions maintain a virtual function table, known as the vtable. This table is used to allow polymorphism. Virtual functions can be overridden in derived classes. Pointers to base classes can also point to objects of derived classes (e.g., a pointer to Animal can point to a Dog object). When the member function is called, the overridden implementation is invoked by way of the vtable (e.g., when you call speak() on the Animal pointer, the Dog's overridden speak() function will be invoked).

Using Pointers

A pointer is a variable that holds in memory the address of an object. Pointers enable objects to be acted on indirectly; the pointer is used to access the object at the address stored in the pointer.

Pointers are declared by writing the type of object they point to, followed by the indirection operator(*), followed by the pointer name. Pointers should be initialized to point to an object or to point to zero.

Caution: Some programmers, especially old C hackers, use the word NULL, but this is controversial in C++ and is not a keyword in the language. You are better off assigning the value 0 to your pointers.

You access the value at the address stored in a pointer by using the indirection operator (*). You can declare constant pointers, which can't be reassigned to point to other objects, and pointers to constant objects, which can't be used to change the objects to which they point. Using const and nonconst pointers is illustrated in Listing 1.1.

Listing 1.1 Constant and Nonconstant Pointers

    1:     // Listing 1.1 - constant and nonconstant pointers
    2:
    3:     #include <iostream.h>
    4:     int main()
    5:     {
    6:        int x = 5;
    7:        int y = 90;
    8:        int * pInt = &x;
    9:        const int * ptrConstInt = &x;
    10:       int * const ConstPtrInt = &x;
    11:       const int * const ConstPtrConstInt = &x;
    12:
    13:       *pInt = 7;
    14:      //   *ptrConstInt = 8;   // error, can't change value
    15:       *ConstPtrInt = 9;
    16:      //   *ConstPtrConstInt = 10;  // error, can't change value
    17:
    18:       cout << "pInt: " << pInt << " *pInt: " << *pInt << endl;
    19:       cout << "ptrConstInt: " << (int*)ptrConstInt << " *ptrConstInt: "
              << *ptrConstInt << endl;
    20:       cout << "ConstPtrInt: " << ConstPtrInt << " *ConstPtrInt: "
              << *ConstPtrInt << endl;
    21:       cout << "ConstPtrConstInt: " << (int*)ConstPtrConstInt;
    22:       cout << " *ConstPtrConstInt: " << *ConstPtrConstInt << endl;
    23:
    24:       pInt = &y;
    25:       ptrConstInt = &y;
    26:        //   ConstPtrInt = &y;      // error, can't reassign
    27:        //   ConstPtrConstInt = &y;      // error, can't reassign
    28:
    29:       *pInt = 97;
    30:       //   *ptrConstInt = 98;      // error, can't change value
    31:       *ConstPtrInt = 99;
    32:      //   *ConstPtrConstInt = 100;   // error, can't change value
    33:
    34:       cout << "pInt: " << pInt << " *pInt: " << *pInt << endl;
    35:       cout << "ptrConstInt: " << (int*)ptrConstInt << " *ptrConstInt: "
              <<*ptrConstInt << endl;
    36:       cout << "ConstPtrInt: " << ConstPtrInt << " *ConstPtrInt: " <<
              *ConstPtrInt << endl;
    37:       cout << "ConstPtrConstInt: " << (int*)ConstPtrConstInt;
    38:       cout << " *ConstPtrConstInt: " << *ConstPtrConstInt << endl;
    39:
    40:       return 0;
    41:     }

Output:

    pInt: 0x1ee30ffe *pInt: 9
    ptrContInt: 0x1ee30ffe *ptrContInt: 9
    ContPtrInt: 0x1ee30ffe *ContPtrInt: 9
    ContPtrContInt: 0x1ee30ffe *ContPtrContInt: 9
    pInt: 0x1ee30ffc *pInt: 97
    ptrContInt: 0x1ee30ffc *ptrContInt: 97
    ContPtrInt: 0x1ee30ffe *ContPtrInt: 99
    ContPtrContInt: 0x1ee30ffe *ContPtrContInt: 99

Note: The exact values you receive might be different, depending on what segment of memory your program happens to use.

Analysis:

On lines 6 and 7, two local variables, x and y, are declared and initialized. On lines 8 through 10, four pointers are declared and initialized with the address of x. The first pointer, pInt is a pointer to an integer.

The second, ptrConstInt, is a pointer to a constant integer, and thus cannot be used to change the value of the integer to which it points.

The third, ConstPtrInt, is a constant pointer to an integer, and thus cannot be reassigned.

The final pointer, ConstPtrConstInt, is a constant pointer to a constant integer, and thus cannot be used to change the value of the integer to which it points, and also cannot be reassigned to point to a different integer.

Lines 14 and 16, if uncommented, would cause a compile-time error, in each case because you are attempting to change the integer pointed to. Because the pointer is defined to be a pointer to a constant integer, the compiler correctly balks.

Lines 26 and 27 would, if uncommented, create compile-time errors because you are attempting to reassign a constant pointer.

Note that on lines 19, 21, 35, and 37, the constness of the pointer must be cast away so that you can pass the pointer to the cout object.

Using new and delete

You create new objects on the free store by using the new keyword, and assigning the address that is returned to a pointer. You free the memory by calling the delete on that pointer. delete frees the memory but does not destroy the pointer; you must reassign the pointer after its memory is freed.

If you create an array of objects on the heap, you must signal delete that more than one object is to be destroyed; do this by including the brackets in the delete call. For example, if you create an array of integers with

    int *myArray = new int[50];

you must delete this array by writing

    delete[] myArray;

Using References

References are aliases to objects that already exist somewhere in memory. They often are implemented using pointers, but they are quite different in fundamental ways.

References must be initialized to refer to an existing object and cannot be reassigned to refer to anything else. Any action taken on a reference is in fact taken on the reference's target object; taking the address of a reference returns the address of the target. Listing 1.2 presents a simple program that illustrates the use of references.

Listing 1.2 Using References

    1:     // listing 1.2
    2:
    3:     #include <iostream.h>
    4:
    5:     int main()
    6:     {
    7:        int x = 5;
    8:        int y = 7;
    9:        // int &FirstRef; // error, must be initialized
    10:       int &intRef = x;
    11:
    12:       cout << "x: " << x << " y: " << y << " intRef: " << intRef << endl;
    13:
    14:       intRef = 8;
    15:
    16:       cout << "x: " << x << " y: " << y << " intRef: " << intRef << endl;
    17:
    18:       intRef = y;  // not what you might expect!
    19:
    20:       cout << "x: " << x << " y: " << y << " intRef: " << intRef << endl;
    21:
    22:       return 0;
    23:    }

Output:

    x: 5 y: 7 intRef: 5
    x: 8 y: 7 intRef: 8
    x: 7 y: 7 intRef: 7

Analysis:

On lines 7 and 8, two local variables are defined and initialized. If line 9 were uncommented, it would generate a compile-time error because references must be initialized.

Line 10 declares and initializes intRef, making it an alias for x. On line 12, the values of x, y, and intRef are printed, resulting in the first output line.

On line 14, intRef is assigned the value 8, and because intRef is an alias for x, the printout reflects that both have changed their value.

On line 18, the programmer meant to reassign intRef to point to y, but this is not possible with references. What happens instead is that intRef continues to act as an alias for x. Consequently,

intRef = y becomes an alias for x = y, setting x (and therefore intRef) to the value of y, as reflected in the final line of the printout.

Passing objects by reference, either using references or pointers, can be far more efficient than passing by value. Passing by reference allows the called function to change the value in the arguments back in the calling function, unless the reference or pointer is declared to be constant (const).

Using const

As discussed earlier, constant pointers cannot be reassigned, references are always constant in this way and do not need to be declared as such. Pointers or references to constant objects cannot be used to change those objects. Member functions also can be declared constant, which indicates that the function does not change the object, and thus can be used with constant objects.

It is desirable to use the keyword const wherever appropriate because it enlists the compiler in the effort to find programming errors before they become runtime bugs. Bugs found at compile time are cheaper to fix than those found once the product ships.

Using Arrays

An array is a fixed-size collection of objects all of the same type. Arrays do not do bounds checking, so it is legal, even if disastrous to read or write past the end of an array. Arrays count from 0. Thus, a 10-member array will count from offset 0 to offset 9, and more generally an array of n values will be numbered from 0 to n-1. It is a common mistake to write to offset n of an n-member array, which is one past the end of the array.

Arrays can be single dimensional or multi-dimensional. In either case, the members of the array can be initialized, as long as the array contains either built-in types such as int, or objects of a class that has a default constructor.

Arrays and their contents can be on the free store or on the stack. If you delete an array on the free store, remember to use the brackets in the call to delete.

Array names are constant pointers to the first elements of the array. Pointers and arrays use pointer arithmetic to find the next element of an array.

Linked lists can be created to manage collections for which size cannot be known at compile time. From linked lists, any number of more complex data structures can be created.

Strings are arrays of characters (type char). C++ provides some special features for the management of character arrays, including the capability to initialize them with quoted strings.

Note: Character arrays typically are terminated with zero. These strings are called null terminated because old C programs declared the name NULL for the value zero. The traditional character array manipulation functions, such as strcpy() and strlen(), rely on this convention.

Examining Inheritance in Detail

New classes can be created by deriving from existing classes. The class derived from is referred to as the base class.

Derived classes inherit all the public and protected data and functions from their base classes. Protected access is public to derived classes and private to all other objects. Even derived classes cannot access private data or functions in their base classes.

Constructors can be initialized before the body of the constructor. It is at this time that base constructors are invoked and parameters can be passed to the base class.

Functions in the base class can be overridden in the derived class. If the base class functions are virtual, and if the object is accessed by pointer or reference, the derived class's functions will be invoked based on the runtime type of the object pointed to. This is the essence of polymorphism in C++; your function invokes a base class method, and based on the runtime type of the actual object referred to, the right method is called.

Methods in the base class can be invoked by explicitly naming the function with the prefix of the base class name and the scoping operator.

Tip: In classes with virtual methods, the destructor almost always should be made virtual. A virtual destructor ensures that the derived part of the object will be freed when delete is called on the pointer.

Tip: Although constructors cannot be virtual, a virtual copy constructor can be created by making a virtual member function, which then calls the copy constructor.

Using Abstract Data Types

C++ supports the creation of abstract data types (ADT) with pure virtual functions. A virtual function is made pure by initializing it with zero, as in the following:

    virtual void Draw() = 0;

Any class with one or more pure virtual functions is an ADT. Attempting to instantiate an object of a class that is an ADT will cause a compile-time error. Putting a pure virtual function in your class signals two things to clients of your class: (1) don't make an object of this class, instead, derive from it and (2) make sure you override the pure virtual function.

Any class that derives from an ADT inherits the pure virtual function as pure, and so must override every pure virtual function if it wants to instantiate objects.

Using Multiple Inheritance

Classes can inherit from more than one base class: this is called multiple inheritance. When a class is multiply derived, it inherits all the member functions and data from all its base classes. Virtual inheritance can be used to avoid inheriting multiple copies of the same base class when a new class is derived from two classes, each of which shares a common ancestor class.

Understanding Private Inheritance and Containment

Private inheritance can be used to implement one class in terms of another. Thus, the implementation is inherited in whole or in part, but not the interface. Privately inherited methods and data are private in the derived class, regardless of their access status in the base class.

Functionality can be delegated to a second class, or implemented in terms of that class, by private inheritance or containment. When one class contains another, you obtain nearly all the benefits of private inheritance with two substantial differences: you cannot override methods of the contained class, but you can instantiate more than one instance.

Defining Operator Overloading

User-defined classes can override nearly all the built-in operators, such as the mathematical operators, the increment and decrement operators, and so on. Operator overloading enables user-defined classes to be used in much the same way as built-in classes.

Using Static Data and Functions

Class member data and member functions can be declared static. Static data is scoped to the class rather than to the object; only one instance of static data will exist, shared among all instances of the class. Access can be public, protected, or private, just as for any other class data.

Static methods provide access to private static data, and can be invoked without having an actual object of the class type by using the scoping operator. Static data and functions provide the flexibility of global data, while maintaining type safety.

Using Friend Functions and Classes

Classes can declare other classes or member functions of other classes to be friends. This extends the interface of the class to include the friends; the friend functions have access to the public, protected, and private members of the class as if they were member functions of that class.

At times, you will want to override operators so that an object of your class can be on the right side of the operator. To do this, you often must declare the operator to be a friend function, so that it can access private data members of your class.

Using Streams

Streams provide the basic input and output functionality to your program. The iostream libraries provided with every C++ compiler override the insertion and extraction operators for all the built-in types, although you are free to extend this to user-defined types as well. Manipulators and other member functions of this library provide for formatting and manipulating both input and output of text and binary data. The data can come from the keyboard or from the disk and can go out to the monitor or to permanent storage.

Using Exceptions

Exceptions are objects that can be created and "thrown" at points in the program where the executing code cannot handle the error or other exceptional condition that has arisen. Other parts of the program, higher in the call stack, implement catch blocks, which catch the exception and take appropriate action.

Exceptions are normal user-created objects and, as such, can be passed by value or by reference. They can contain data and methods, and the catch block can use that data to decide how to deal with the exception.

It is possible to create multiple catch blocks, but once an exception matches a catch block's signature, it is considered to be handled and is not given to the subsequent catch blocks. It is important to order the catch blocks appropriately, so that more specific catch blocks have first chance, and more general catch blocks handle those not otherwise handled.

Writing Large Programs

The goal of object-oriented programming is to provide the programmer with the tools needed to manage large, complex solutions to difficult programming problems. Although there are thousands of useful tiny utilities available on bulletin board systems nationwide, the truly interesting, commercially feasible programs are getting larger and more complicated every day.

In order to manage this complexity, the programmer must be able to move up and down through the different levels of abstraction and organization with the program. To make this more manageable, most C++ programmers use two files for every class they create: a header file for the interface to the class and a .cp file for the implementation of the class's methods. That is the style this book uses.

Compiling each of these files when they change and linking them together can become a major project. The classic solution to this problem is the make file.

Looking At Style Guidelines

Large programs, especially those worked on by more than one programmer, can become difficult to maintain if consistent programming style rules are not used. One programmer might write the following, for example.

    while (someCondition){
           someAction(int x);
    }

Another programmer might write this:

    while ( some condition )
    {
           someAction( int x );
    };

With enough variation, it becomes difficult for a programmer to read through the code recognizing where conditions end, what variables refer to, and so on. It is important to adopt a consistent coding style, although in many ways it doesn't matter which style you adopt. A consistent style makes it easier to guess what you meant by a particular part of the code, and avoids having to look up whether you spelled the function with an initial cap or a lowercase letter the last time you invoked it.

Note: The following guidelines are arbitrary; they are based on the guidelines used in projects I've worked on in the past, and they've worked well. You can just as easily make up your own, but these will get you started.

Although Emerson said, "A foolish consistency is the hobgoblin of little minds," having some consistency in your code is a good thing. Make up your own, but then treat your code as if it were dispensed by the programming gods.

Indenting

A good size for tab spacing is three or four spaces; this keeps the listings narrow enough to print well. Where the editor allows it, use tabs rather than multiple spaces; this enables other programmers to reformat your code to their liking.

Braces

Matching braces should be aligned vertically. The outermost set of braces in a definition or declaration should be at the left margin. Statements within should be indented. All other sets of braces should be in line with their leading statement. No code should appear on the same line as a brace. An example of the correct use of code follows:

    if ()
    {
          j = k;
          foo();
    }
    m++;

Long Lines

Even though your editor will allow wider lines, keep lines to the width displayable on a single screen. Code that is off to the right easily is overlooked, and scrolling horizontally is annoying. When a single logical line is broken, indent the following lines. Try to break the line at a rational point, and try to leave the intervening operator at the end of the preceding line (rather than the beginning of the following line) so that it is clear that the line does not stand alone and that there is more code coming.

Switch Statements

Indent switches as follows, to conserve horizontal space:

    switch(variable)
    {
    case ValueOne:
           ActionOne();
           break;
    case ValueTwo:
           ActionTwo();
           break;
    default:
           assert("bad Action");
           break;
    }

Program Text

There are several tips you can use to create code that is easy to read. Easy-to-read code is easy to maintain.

Identifier Names

Here are some guidelines for working with identifiers:

Spelling and Capitalization of Names

Spelling and capitalization should not be overlooked when creating your own style. Some tips for these areas include the following:

Comments

Comments can make it much easier to understand a program. Often, you will not work on a program for several days or even months. After this amount of time, you can forget what certain code does or why it was included. Problems in understanding code also can occur when someone else reads your code. Comments that are applied in a consistent, well thought-out style can be well worth the effort. There are several tips to remember concerning comments:

Access

The way you access portions of your program also should be consistent. Some tips for access include the following:

Constants

Use const wherever appropriate: for parameters, variables, and methods. Often, there is a need for both a const and a non-const version of a method; don't use this as an excuse to leave one out. Be very careful when explicitly casting from const to non-const and vice versa. There are times when this is the only way to do something, but be certain that it makes sense, and include a comment.

Note: The use of const in your code enlists the compiler in helping you find bugs in your code. Remember, bugs found at compile time are easier to find and fix than bugs found at runtime!

The Project

This book concentrates on a single project that serves as the departure point for a number of programming issues. Like many programmers, I collect a thousand notes during the course of a week. These include ideas for this book, tips on how to accomplish something with the local area network, programming ideas, bugs to fix, and so on.

I've often wished for a simple utility that would let me type in a note and then get it back quickly when I need it. Such a Personal Information Manager (PIM) makes an ideal project for this book, because it requires all the skills you'll be learning in the next three weeks.

The key idea behind this project is that the user will not have to provide a subject, keywords, or categories to the message until the messages are retrieved! The average PIM forces you to provide sort criteria when you save the message. This is unfortunate, because it slows you down and decreases the likelihood that you'll use the product.

This project, which we'll call ROBIN, does not have that requirement; you just type your note and forget it. When you save it, the note is indexed and stored, and ROBIN finds it when you need it.

ROBIN can be written for a non-graphical environment such as DOS or Unix, and then the heart of the program easily can be ported to a GUI such as Windows or Mac. This book focuses on building the preliminary portable version. When you are done, you will have learned a great deal about databases and the more advanced aspects of C++. You also will have a program that you then can expand and enhance. Along the way, you will learn a lot about writing portable code and the fundamentals of event-driven programming.

The most effective way to work on a project is to design your program, write a bit of functionality, try it out, fix it, adjust your design, and then write some more. The project evolves and builds itself up in stages. But don't be fooled, this approach requires more up-front design--not less.

The ROBIN project anticipates a number of enhancements that you will not implement for version 1, such as a full-screen editor, a GUI interface complete with menus and display windows, and so on.

Prior even to version 1, there will be intermediate preliminary versions. For example, an early version will not sort the notes; later versions will sort them in increasingly efficient ways. Faster search algorithms will be added as you progress, and better ways to organize the data will be tried in turn. Early versions of ROBIN will not bother writing the data to the disk; all the data will be stored in memory. Later versions will explore approaches to writing the data to disk in a way that provides optimal retrieval speed.

You will refine and build the program as new skills and techniques are discussed. Each subsequent iteration of the program will provide real-world experience with new methodologies.

Understanding How ROBIN Works

When you invoke ROBIN version 1.0, you will do so with either the keywords Add or Find. Robin Add can be followed either by text on the command line, or by the switch -F followed by the name of a text file.

If a text file is provided, a new note will be created from that file. If a file is not provided, the text following the keyword Add will be used for the new note.

In either case, the note will be date stamped, numbered, indexed, and saved for later retrieval.

To find a note, you enter Robin Find followed by one or more terms. ROBIN will print a menu of matching notes, displaying the first few words of the message and the date of the note, like this:

    C:>Robin find Using Member Functions of Classes
    [1] Using Member Functions in C++      5/5/94
    [2] Member Functions and Classes       10/1/94
    [3] C++ Functions and Member Data      8/3/93
    [4] Functions and C++ Member Access    7/10/89

In the preliminary version that you will develop in the next few weeks, only a brute force match will be made on all the terms entered; the messages will be ordered first by how many words match and then by date. Enhancements to ROBIN might include the capability to enter phrases rather than individual words, the capability to score results based on proximity (how close the words are to one another), and so on. You might want to add a thesaurus to the program, allowing for searches on synonyms, so that member function would match notes with the word method, and so on.

When and if you port the program to a graphical environment, such as Windows, you will have the option of adding a fancier and more flexible user interface. If a multitasking environment such as UNIX or Windows 95 is available, the indexing and searching can be done in the background.

Reinventing the Wheel

In a real-world project, you would constantly be evaluating the build/buy decision. Often, it makes more sense to buy a class library that does much of what you need, rather than writing your own. For the purposes of this book, however, you will write your own classes, starting with the string class.

Each "note" will have a number of strings of various lengths, and you will depend heavily on a good, robust, String class. You'll write your String class on day 3, "Strings and Lists," and it will be the linchpin of your project, which you will develop over the full three weeks.

Relating ROBIN to Your Own Projects

It is easy, in a programming book, to become overfocused on the demonstration program. The point of this book, however, is not to teach you how to create a Personal Information Manager, or even how to create a database, but how to apply a series of advanced techniques and skills to solve real-world problems. The program written here is a prototype of a design and creation process that you will be able to transfer to your own programs.

Summary

Today you reviewed the fundamentals of C++. This review included a quick survey of the basic object-oriented programming concepts of inheritance, encapsulation, and polymorphism.

You also reviewed how memory is allocated and used in C++ programs, how the stack works, and how pointers and references are manipulated. The core elements of the language were reviewed, including arrays, class constructors and destructors, as well as operator and function overloading.

Advanced topics reviewed included multiple inheritance, abstract data types, static members and friends, and exceptions. Finally, make files were reviewed and a set of somewhat arbitrary style guidelines were offered.

Q&A

Q: What do I do if some of this is new material?

A: It is imperative that you are comfortable with this material before going on. I strongly recommend picking up a good C++ Primer, such as Teach Yourself C++ In 21 Days by Jesse Liberty (ISBN: 0-672-30541-0), published by Sams Publishing, 1994.

Q: What compiler is required for the rest of this book?

A: The code in this book is designed to be platform independent, and should work with any compiler that supports iostreams.

Q: What about Windows, Mac, and X Window programming?

A: The graphical user interfaces (GUIs) are complex development environments. This book does not attempt to teach you how to write programs for the GUIs, but it does equip you with the advanced programming skills you'll need when you do write professional programs in those environments.

Workshop

In future chapters, the Workshop will provide quiz questions to help you solidify your understanding of the material covered and exercises to provide you with experience in using what you've learned. Today, however, use the Workshop quiz and exercises as a preliminary qualifying exam to prove to yourself that you understand all the aspects of C++ that you will need for the work to come. Try to answer the quiz and exercise questions before checking the answers in Appendix A, and make sure that you understand the answers before continuing to the next chapter.

Quiz

  1. Provide the header for a constant member function getText() of the class String, which returns a constant pointer to a constant string of characters and takes no parameters.

  2. What is the difference between const int * ptrOne and int * const ptrTwo?

  3. How does the copy constructor differ from the assignment operator (=)?

  4. What is the this pointer?

  5. What is a v-table?

  6. If you create two classes, Horse and Bird, and they inherit virtual public from the base class Animal, do their constructors initialize the Animal constructor? If you then create a new class, Pegasus, which inherits from both Horse and Bird, how does it initialize Animal's constructor?

  7. What is the difference between containment and delegation?

  8. What is the difference between delegation and implemented in terms of?

  9. What is the difference between public and private inheritance?

  10. What are the three forms of cin.get() and what are their differences?

  11. What is the difference between cin.read() and cin.getline()?

[Click here for Answers]

Exercises

  1. Write two small programs, one with recursion, and one using iteration to print out the nth number in a Fibonnacci series. (The Fibonnacci series is 0,1,1,2,3,5,8... where each number is the sum of the previous two, except the first two, which are zero and one.)

  2. Write a short program declaring a class with three member variables and one static member variable. Have the constructor initialize the member variables and increment the static member variable. Have the destructor decrement the static member variable.

  3. Write a short driver program that makes three objects of your class and then displays their member variables and the static member variable. Then destroy each object and show the effect on the static member variable.

  4. Write a program that takes a file name as a parameter and opens the file for reading. Read every character of the file and display only the letters and punctuation to the screen (ignore all non-printing characters). Then close the file and exit.

  5. Create a try block, a catch statement, and a hierarchy of exceptions. Put data into the exceptions, along with an accessor function, and use the data in the catch block. Test this with a driver program that uses three levels of function calls.

Go to: Table of Contents | Next Page