Chapter 35

Developing a Class


Throughout this book, you've used the many classes that Microsoft created for MFC. Creating classes, however, is not a magical trick that only master programmers who work for Microsoft can accomplish. Any C++ programmer including you can create classes and incorporate them into his or her programs. If you've never created your own classes, however, you might find the process to be a bit mysterious. In this final chapter, you learn how to supplement MFC with your own custom-written classes.

A Review of Object-Oriented Programming Techniques

Before you can start writing your own classes, you have to be comfortable with object-oriented programming techniques. Object-oriented programming enables you to think of program elements as objects. In the case of a window object, you don't need to know the details of how it works, nor do you need to know about the window's private data. You need to know only how to call the various functions that make the window operate. Think about a car. To drive a car, you don't have to know the details of how a car works. You need to know only how to drive it. What's going on under the hood is none of your business. (If you try to make it your business, plan to face an amused mechanic who will have to straighten out your mess!)

If this were all there were to object-oriented programming, you wouldn't have gained much over standard structured-programming techniques. After all, with structured programming, you can create "black box" routines, which a programmer could then use without knowing how they work. Obviously, there must be much more to object-oriented programming than just hiding the details of a process. In this section, you'll discover data encapsulation, inheritance, and polymorphism, the features that give object-oriented programming its true power.

Encapsulation

One major difference between conventional procedural programming and object-oriented programming is a handy thing called encapsulation. Encapsulation enables you to hide both the data and the functions that act on that data inside the object. After you do this, you can control access to the data, forcing programs to retrieve or modify data only through the object's interface. In strict object-oriented design, an object's data is always private to the object. Other parts of a program should never have direct access to that data.

How is this data hiding different from a structured-programming approach? After all, you could always hide data inside of functions, just by making that data local to the function. A problem arises, however, when you want to make the data of one function available to other functions. The way to do this in a structured program is to make the data global to the program, which gives any function access to it. It seems that you could use another level of scope one that would make your data global to the functions that need it but still prevent other functions from gaining access. Encapsulation does just that.

The best way to understand object-oriented programming is to compare a structured program to an object-oriented program. Let's extend the car-object metaphor by writing a program that simulates a car trip. The first version of the program (a pseudo-DOS, or console, program), shown in Listing 35.1, uses a typical structured design.

ON THE WEB

http://www.quecorp.com/semfc The complete source code and executable file for the Car1 application can be found in the Chap35\Car1 directory at this book's Web site.

Listing 35.1 Car1.cpp The First Version of the Car Program
#include <iostream.h>
#include <conio.h>
#include <stdlib.h>
#include <time.h>
#define HOME 10
void StartCar(void)
{
    cout << "Car started." << endl;
    getch();
}
int SteerCar(int destination, int &position)
{
    cout << "Driving..." << endl;
    getch();
    if (++position == destination) return 1;
    return 0;
}
void BrakeCar(void)
{
    cout << "Braking." << endl;
    getch();
}
void ReverseCar(int &forward, int &position)
{
    if (forward)
    {
        cout << "Backing up." << endl;
        getch();
        --position;
        forward = 0;
    }
    else forward = 1;
}
void TurnOffCar(void)
{
    cout << "Turning off car." << endl;
    getch();
}
int FindObstacle(void)
{
    int r = rand() % 5;
    if (r) return 0;
    return 1;
}
int position = 0, destination = HOME;
int at_destination = 0;
int obstacle, forward = 1;
void main()
{
    srand((unsigned)time(NULL));
    StartCar();
    while (!at_destination)
    {
        at_destination = SteerCar(destination, position);
        obstacle = FindObstacle();
        if (obstacle && !at_destination)
        {
            cout << "Look out! There's something in the road!" << endl;
            getch();
            BrakeCar();
            ReverseCar(forward, position);
            ReverseCar(forward, position);
        }
    }
    cout << "Ah, home at last." << endl;
    TurnOffCar();
}
Examine this program, starting with main(). The call to srand() initializes the random number generator, which is used to simulate obstacles in the road. Then the function StartCar() simply prints the message Car started, letting the user know that the trip is about to begin.

The program simulates the trip with a while loop that iterates until at_destination becomes TRUE (1). In the loop, the car moves forward by calling the function SteerCar(). This function prints the message Driving and moves the car one unit closer to the destination. When the integer position is equal to the destination, this function returns a 1, indicating that the trip is over. Otherwise, it returns 0.

Of course, the car's driver must always watch for obstacles. The function FindObstacle() acts as the driver's eyes by looking for obstacles and reporting what it finds. In this function, each time the random number generator comes up with a 0, FindObstacle() informs you that something is blocking the route, by returning 1 rather than 0.

If the car reaches an obstacle, the function BrakeCar() puts on the brakes, and the function ReverseCar() backs the car up. Both functions print an appropriate message. However, ReverseCar() also sets the car's position back one unit unless it was already moving backward, in which case it just reverses the direction again, setting the car back in the forward direction. (The variable forward keeps track of the car's current direction.) The second call to ReverseCar() gets the car moving forward again. Finally, when the car reaches its destination, the function TurnOffCar() ends the program. Here is the output from a typical run of the program:
Car started.
Driving...
Driving...
Driving...
Driving...
Look out! There's something in the road!
Braking.
Backing up.
Driving...
Driving...
Driving...
Driving...
Driving...
Look out! There's something in the road!
Braking.
Backing up.
Driving...
Driving...
Driving...
Ah, home at last.
Turning off car.
Listing 35.2 is the object-oriented version of the program. This version includes the same functions and data. However, now everything unique to a car is encapsulated as part of the Car object.

ON THE WEB

http://www.quecorp.com/semfc The complete source code and executable file for the Car2 application can be found in the Chap35\Car2 directory at this book's Web site.

Listing 35.2 Car2.cpp The Object-Oriented Car Program
#include <iostream.h>
#include <conio.h>
#include <stdlib.h>
#include <time.h>
#define HOME 10
class Car
{
    int test, position, forward;
public:
    Car(int destination);
    void StartCar(void) { cout<<"Car started." << endl; getch(); }
    int SteerCar(void);
    void BrakeCar(void) { cout<<"Braking." << endl; getch(); }
    void ReverseCar(void);
    void TurnOffCar(void) { cout<<"Turning off car." << endl; getch();}
};
Car::Car(int destination)
{
    srand((unsigned)time(NULL));
    test = destination;
    forward = 1;
    position = 0;
}
int Car::SteerCar(void)
{
    cout << "Driving..." << endl;
    getch();
    if (++position == test) return 1;
    return 0;
}
void Car::ReverseCar(void)
{
    if (forward)
    {
        cout << "Backing up." << endl;
        getch();
        --position;
        forward = 0;
    }
    else forward = 1;
}
int FindObstacle(void)
{
    int r = rand() % 5;
    if (r) return 0;
    return 1;
}
int obstacle, at_destination = 0;
Car car(HOME);
void main()
{
    srand((unsigned)time(NULL));
    car.StartCar();
    while (!at_destination)
    {
        at_destination = car.SteerCar();
        obstacle = FindObstacle();
        if (obstacle && !at_destination)
        {
            cout << "Look out! There's something in the road!" << endl;
            getch();
            car.BrakeCar();
            car.ReverseCar();
            car.ReverseCar();
        }
    }
    cout << "Ah, home at last." << endl;
    car.TurnOffCar();
}
Because the program encapsulates much of the data into the class Car, rather than using global variables as in the first version, fewer variables are passed to functions that make up the car. This points out a subtle stylistic difference between structured programming and object-oriented programming. The first version of the program passed variables into functions even though those variables were global so the programmer had a clear idea about what data the function used. This is a form of self-documentation; the style of the code says something about what the code does.

In an object, the encapsulated data members are global to the object's function members, yet they are local to the object. They are not global variables. Because objects represent smaller portions of an entire program, there's no need to pass data members into member functions to help document a function's purpose. Objects are usually concise enough that this type of self-documentation is unnecessary. In Listing 35.2, no variables are passed into functions (except into the class's constructor).

Another advantage of the object-oriented approach taken in Listing 35.2 is that the Car object is clearly defined. All of the data and functions required for a car (at least, all that are needed for this simple computer car) are encapsulated into the class. That means there is less clutter in the main program. It also means that the code is more logically organized. In Listing 35.1, you have no clear idea of what makes up a car. The functions and data needed for the entire program are defined on the same level. For example, whereas starting a car is clearly a function of a car, finding an obstacle is not. (If you don't agree, go out to your car, climb in, and press the Find Obstacle button.) Yet the scope of both the StartCar() and FindObstacle() functions are the same. This is also true of the data. Whereas the car's destination, position, and direction all are information that help define a car, obstacles are not. You don't need an obstacle to drive a car; you do need a destination, a position, and a direction.

In Listing 35.2, every element that makes up a car is part of the class. To drive the car, the program doesn't need to deal with the car's data members. The class takes care of them. The only data that the program needs from Car is whether the car has arrived at its destination. The only function left in the main program is FindObstacle(), the one function in the program that has nothing to do with being a car. Finding obstacles is not a car's job. In all of these ways, encapsulation makes the programming task more logical and organized.

Classes as Data Types

Classes are really nothing more than user-defined data types. As with any data type, you can have as many instances of the data type as you need. For example, you can have more than one car in the car program, each with its own destination. This is because a class is really nothing more than a custom data type. After you have created a data type, you can create as many instances of that data as you need.

For example, one standard data type is an integer. It's absurd to think that a program can have only one integer. You can declare many integers, just about all that you want. The same is true of classes. After you define a new class, you can create many instances of the class. Each instance (called an object) normally has full access to the class's member functions and gets its own copy of the data members. In the car simulation, you can create two cars, each with its own destination, as in the following:

Car car1(10), car2(20);

Although these cars derive from the same class, they are completely separate objects. The object car2 has to go twice as far as car1 to reach its destination.

Header Files and Implementation Files

In Listing 35.2, all the program code is in a single file. This makes it easy to compare the first version with the second. When using object-oriented programming techniques, however, it's standard practice to place each class into two files of its own. The first, the header file, contains the class's definition. Usually, the header file contains all of the information that you need to use the class. The header file traditionally has an .H extension. The actual implementation of a class's functions goes into the implementation file, which usually has the extension .CPP.

The header and implementation files for the Car class are shown in listings 35.3 and 35.4, respectively. Note that the class definition has been slightly modified by adding the keyword protected to the data member section. This is done so that derived classes can access these data members. (I discuss inheritance in the next section.)

Listing 35.3 Car.h The Car Class's Header File
#ifndef _CAR_H
#define _CAR_H
class Car
{
protected:
    int test, position, forward;
public:
    Car(int destination);
    void StartCar(void)
        { cout<<"Car started." << endl; getch(); }
    int SteerCar(void);
    void BrakeCar(void)
        { cout<<"Braking." << endl; getch(); }
    void ReverseCar(void);
    void TurnOffCar(void)
        { cout<<"Turning off car." << endl; getch(); }
};
#endif
Listing 35.4 Car.cpp The Car Class's Implementation File
#include <iostream.h>
#include <conio.h>
#include <stdlib.h>
#include <time.h>
#include "car.h"
Car::Car(int destination)
{
    srand((unsigned)time(NULL));
    test = destination;
    forward = 1;
    position = 0;
}
int Car::SteerCar(void)
{
    cout << "Driving..." << endl;
    getch();
    if (++position == test) return 1;
    return 0;
}
void Car::ReverseCar(void)
{
    if (forward)
    {
        cout << "Backing up." << endl;
        getch();
        --position;
        forward = 0;
    }
    else forward = 1;
}

Inheritance

Inheritance enables you to create a class that is similar to a previously defined class but that still has some of its own properties. Consider the car simulation. Suppose you want to create a car that has a high-speed passing gear. In a traditional program, that would require a lot of code modification. As you modified the code, you would probably introduce bugs into a tested program. To avoid these hassles, use the object-oriented approach: Create a new class by inheritance. This new class inherits all of the data and function members from the base class (the class from which the new class is derived). (You can control the level of access with the public, private, and protected keywords.)

Listings 35.5 and 35.6 show the header and implementation files for a new class of car, PassingCar. This car inherits the member functions and data from its base class, Car, and adds two member functions of its own. The constructor, PassingCar(), does nothing but pass parameters to the base class's constructor. The member function Pass(), however, is unique to PassingCar. This is the function that gives the new car its passing gear. (Ignore the keyword virtual for a moment. You'll learn about virtual functions in the next section.)

If you look at Listing 35.6, you see that Pass() is similar to Car's SteerCar() function, the difference being that Pass() increments the car's position by two units rather than one, which simulates a faster speed. Remember that although PassingCar has a new passing gear (implemented in the Pass() function), it still has access to SteerCar().

Listing 35.5 PassingCar.h The PassingCar Class's Header File
#ifndef _PASSCAR_H
#define _PASSCAR_H
#include "car.h"
class PassingCar: public Car
{
public:
    PassingCar(int destination): Car(destination) {}
    virtual int Pass(void);
};
#endif
Listing 35.6 PassingCar.cpp The PassingCar Class's Implementation File
#include <iostream.h>
#include <conio.h>
#include "PassingCar.h"
int PassingCar::Pass(void)
{
    cout << "Passing..." << endl;
    getch();
    position += 2;
    if (position >= test) return 1;
    return 0;
}
Listing 35.7, a new version of the simulation's main program, gives PassingCar a test drive. When you run the program, PassingCar reaches its destination a little faster because after it backs up, it makes up time by going into passing gear. By using inheritance, this program creates a new kind of car with only a few lines of code. And the original class remains unchanged (except for the addition of the protected keyword). Impressed?

ON THE WEB

http://www.quecorp.com/semfc The complete source code and executable file for the Car3 application can be found in the Chap35\Car3 directory at this book's Web site.

Listing 35.7 Car3.cpp The Main Program for Testing the PassingCar Class
#include <iostream.h>
#include <conio.h>
#include <stdlib.h>
#include <time.h>
#include "PassingCar.h"
#define HOME 10
int FindObstacle(void)
{
    int r = rand() % 5;
    if (r) return 0;
    return 1;
}
int obstacle, at_destination = 0;
PassingCar car2(HOME);
void main()
{
    srand((unsigned)time(NULL));
    car2.StartCar();
    while (!at_destination)
    {
        at_destination = car2.SteerCar();
        obstacle = FindObstacle();
        if (obstacle && !at_destination)
        {
            cout << "Look out! There's something in the road!" << endl;
            getch();
            car2.BrakeCar();
            car2.ReverseCar();
            car2.ReverseCar();
            at_destination = car2.Pass();
        }
    }
    cout << "Ah, home at last." << endl;
    car2.TurnOffCar();
}

Polymorphism

The last major feature of object-oriented programming is polymorphism. By using polymorphism, you can create new objects that perform the same functions found in the base object but that perform one or more of these functions in a different way. For example, when the previous program used inheritance, it created a new car with a passing gear. This isn't polymorphism, because the original car didn't have a passing gear. Adding the passing gear didn't change the way an inherited function worked; it simply added a new function. Suppose, however, that you want an even faster passing gear, without having to change the existing classes? You can do that easily with polymorphism.

Listings 35.8 and 35.9 show the header and implementation files for a new class, called FastCar. A FastCar is exactly like a PassingCar, except that it uses its passing gear a little differently: A FastCar moves three units forward (rather than two) when passing. To do this, the program takes an already existing member function and changes how it works relative to the derived class. This is polymorphism, which is functions differently depending on the object type, and regardless of the type of the reference to the object. Remember that when you create a polymorphic function, you must preface its definition with the keyword virtual.

Listing 35.8 FastCar.h The FastCar Class's Header File
#ifndef _FASTCAR_H
#define _FASTCAR_H
#include "PassingCar.h"
class FastCar: public PassingCar
{
public:
    FastCar(int destination):
        PassingCar(destination) {}
    virtual int Pass(void);
};
#endif
Listing 35.9 FastCar.cpp The FastCar Class's Implementation File
#include <iostream.h>
#include <conio.h>
#include "FastCar.h"
int FastCar::Pass(void)
{
    cout << "High-speed pass!" << endl;
    getch();
    position += 3;
    if (position >= test) return 1;
    return 0;
}
Look at Listing 35.10, the new main program for the car simulation. To take advantage of polymorphism, the program allocates the new FastCar dynamically that is, it creates a pointer to the base class and then uses the new operator to create the object. Remember that you can use a pointer to a base class to access any derived classes. Note also that the base class for FastCar is not Car, but rather PassingCar, because this is the first class that declares the virtual function Pass(). If you tried to use Car as a base class, the compiler would complain, informing you that Pass() is not a member of Car. One way around this is to give Car a virtual Pass() function, too. This would make all car classes uniform with respect to a base class. (And that would probably be the best program design.)

ON THE WEB

http://www.quecorp.com/semfc The complete source code and executable file for the Car4 application can be found in the Chap35\Car4 directory at this book's Web site.

End On the Web

Listing 35.10 Car4.cpp The New Main Program for the Car Simulation
#include <iostream.h>
#include <conio.h>
#include <stdlib.h>
#include <time.h>
#include "FastCar.h"
#define HOME 10
int FindObstacle(void)
{
    int r = rand() % 5;
    if (r) return 0;
    return 1;
}
int obstacle, at_destination = 0;
PassingCar *car3;
void main()
{
    srand((unsigned)time(NULL));
    car3 = new FastCar(10);
    car3->StartCar();
    while (!at_destination)
    {
        at_destination = car3->SteerCar();
        obstacle = FindObstacle();
        if (obstacle && !at_destination)
        {
            cout << "Look out! There's something in the road!" << endl;
            getch();
            car3->BrakeCar();
            car3->ReverseCar();
            car3->ReverseCar();
            at_destination = car3->Pass();
        }
    }
    cout << "Ah, home at last." << endl;
    car3->TurnOffCar();
}
You must use pointers with polymorphism, because the point of polymorphism is to enable you to access different types of objects through a common pointer to a base class. You might want to do this, for example, to iterate through an array of objects. To see polymorphism work, change the line

car3 = new FastCar(10)

to

car3 = new PassingCar(10)

When you run the new version, you will be back using the slower passing gear, even though both cars use a pointer to the class PassingCar.

Now that you've reviewed the basics of object-oriented programming and have discovered some ways that it makes programming easier, it's time to learn some usage and style considerations unique to the object-oriented paradigm and C++.

Classes: From General to Specific

Starting with object-oriented programming can be a daunting experience; it's unlike other programming methods and requires adherence to a new set of principles. The process of designing a class is rarely as easy as it was with the car simulation, because classes are often based on abstractions rather than physical objects like automobiles. This makes it difficult to know what parts of a program belong in the object and which don't. Moreover, a complex program has many classes, many of which are derived from classes that might have been derived from still other classes. And each class might have many data and function members. Obviously, designing classes requires some thought and the careful application of the object-oriented philosophy.

The first step in designing a class is to determine the most general form of an object in that class. For example, suppose you're writing a graphics program and you need a class to organize the types of shapes it can draw. (In this new class, you'll draw only points and rectangles, to keep things simple.) Determining the most general class means determining what the objects in the class have in common. Two things that come to mind are color and position. These attributes become data members in the base class. Now, what functions must a shape perform? Each shape object needs a constructor and a way to draw itself on-screen. Because drawing a point is different from drawing a square, you'll need to put polymorphism to work and use a virtual function for the drawing task.

Listing 35.11 is the header file for a Shape class. This class needs no implementation file because the class is fully implemented in the header file. The constructor is implemented inline, and the pure virtual function DrawShape() requires no implementation because it is only a placeholder for derived classes.


A pure virtual function, not only requires no implementation, but by definition it cannot have an implementation. The derived class must provide the implementation.

Listing 35.11 Shape.h The Header File for the Shape Class
#ifndef _SHAPE_H
#define _SHAPE_H
class Shape
{
protected:
    int color, sx, sy;
public:
    Shape(int x, int y, int c)
        { sx=x; sy=y; color=c; }
    virtual void DrawShape(void) = 0;
};
#endif
As you can see from Listing 35.11, Shape does nothing but initialize the data members color, sx, and sy, which are the color and X,Y coordinates of the object. To do anything meaningful with the class, you must derive a new class for each shape that you want to draw. Start with the point. Listings 35.12 and 35.13 are the header and implementation files for this new class.

Listing 35.12 Point.h The Header File for the Point Class
#ifndef _POINT_H
#define _POINT_H
#include "shape.h"
class Point: public Shape
{
public:
    Point(int x, int y, int c): Shape(x, y, c) {};
    virtual void DrawShape(void);
};
#endif
Listing 35.13 Point.cpp The Point Class's Implementation File
#include "point.h"
void Point::DrawShape(void)
{
    putpixel(sx, sy, color);
}
The constructor for this class does nothing but pass parameters to the base class's constructor; thus, it is implemented inline. The DrawShape() function, however, must draw the shape in this case, a dot on-screen at the coordinates and in the color found in the sx, sy, and color data members. This function, too, is short and could have been implemented inline. However, to keep the program construction parallel with the next example, there is a separate implementation file for the Point class.


Remember: To keep the program listings as simple as possible, these are not Windows programs. The Windows API has no putpixel() function. 

Listing 35.14 is the test program for the shape classes. Because polymorphism is used to create shape classes and because each class is derived from the Shape base class, the program can test a new shape class simply by changing the type of object created by the new operator. If you were to run the program, a dot would appear in the middle of your screen.

Listing 35.14 TestShape.cpp The Test Program for the Shape Classes
#include "point.h"
//#include "rectngle.h"
//#include "barrec.h"
void main()
{
    Shape* r;
    r = new Point(100, 100, WHITE);
    r->DrawShape();
    delete r;
}
To make things interesting, add a second shape, Rectngle, to the classes. Rectngle is also derived from Shape. Listings 35.15 and 35.16 show the files for this new class.

Listing 35.15 Rectngle.h The Header File for the Rectngle Class
#ifndef _RECTNGLE_H
#define _RECTNGLE_H
#include "shape.h"
class Rectngle: public Shape
{
protected:
    int x2, y2;
public:
    Rectngle(int x1, int y1, int w, int h, int c);
    virtual void DrawShape(void);
};
#endif
Listing 35.16 Rectngle.cpp The Implementation File for the Rectngle Class
#include "rectngle.h"
Rectngle::Rectngle(int x1, int y1, int w, int h, int c):
         Shape(x1, y1, c)
{
    x2 = sx + w;
    y2 =  sy + h;
}
void Rectngle::DrawShape(void)
{
    setcolor(color);
    rectangle(sx, sy, x2, y2);
}
If you want to test this new class, in the main program, change the line

r = new Point(100, 100, WHITE);

to something like

r = new Rectngle(200, 200, 100, 100, WHITE);

Thanks to polymorphism, this is the only change (outside of uncommenting the line #include "rectngle.h") that you need in the main program to draw a rectangle.

The class Rectngle is more complicated than the Point class. To draw a rectangle, the program needs in addition to the rectangle's X,Y coordinates the rectangle's width and height. This means that Rectngle's constructor does more than send parameters to the base class. It also initializes two extra data members, x2 and y2. Rectngle's DrawShape() function, too, is more complicated than Point's, because drawing a rectangle takes more work than drawing a dot.

So far, you've gone from an abstract shape, which did nothing but initialize a couple of data members, to drawing two simple shapes on-screen. You can now move down another level, from the general shape of a rectangle to a more specific type: a rectangle with a colored bar at the top. This type of rectangle might, for example, be the starting point for a labeled window. Listings 35.17 and 35.18 are the source code for the BarRec class.

Listing 35.17 BarRec.h The Header File for the BarRec Class
#ifndef _BARREC_H
#define _BARREC_H
#include "rectngle.h"
class BarRec: public Rectngle
{
public:
    BarRec(int x1, int y1, int w, int h, int c):
        Rectngle(x1, y1, w, h, c) {}
    virtual void DrawShape(void);
};
#endif
Listing 35.18 BarRec.cpp The Implementation File for the BarRec Class
#include "barrec.h"
void BarRec::DrawShape(void)
{
    setcolor(color);
    rectangle(sx, sy, x2, y2);
    setfillstyle(SOLID_FILL, RED);
    bar(sx+2, sy+2, x2+-2, sy+15);
}
If this were a real program, you could test the new shape by changing the new statement in the main program to

r = new BarRec(200, 200, 100, 100, WHITE);

Then, when you ran the program, the new type of rectangle object would appear on-screen.

You could easily continue creating new types of rectangles. For example, if you want a rectangle with both a bar at the top and a double-line border, you can derive a new type from BarRec, overriding its virtual DrawShape() with one of its own. (This new function would probably need to call its base's DrawShape() function to draw the bar at the top and then do the extra drawing required for the double border.)


The need to call a base class's version of an overridden function should be a familiar concept to you as an MFC programmer. Often, when you override member functions of an MFC class, you, not only provide your own specialized code, but also call the base class's version of the function to ensure that the function performs all of the tasks it was designed to perform for the class.



By using the general-to-specific method of creating classes, you end up with extremely flexible code. You'll have many classes from which to choose when it comes time to derive a new one. Moreover, classes will be less complex than they would be if you tried to cram a lot of extra functionality into them. Remember that the more general you make your classes, the more flexible they are.

Single-Instance Classes

Object-oriented programming means power. When programmers first experience this power, they find it irresistible. Suddenly, they're using objects for everything in their programs, without thinking about whether each use is appropriate. Remember that C++ is both an object-oriented language and a procedural language. In other words, C++ programmers get the best of both worlds and can develop a strategy for a particular programming problem that best suits the current task. That strategy might or might not include an object-oriented approach.

Classes are most powerful when used as the basis for many instances. For example, soon you'll delve more deeply into object-oriented programming techniques by putting together a string class (in the section "Developing a String Class."). After developing the class, you're likely to have many instances of strings in your programs, each inheriting all the functionality of its class.

Nothing comes free, however. There is always a price. For example, to call an object's member functions, you must use a more complicated syntax than you need for ordinary function calls; you must supply the object and function name. Moreover, creating classes is a lot of work. Why go through all of the extra effort if the advantages don't outweigh the disadvantages?

Although classes are most appropriate when used to define a set of objects, there are times when creating a single-instance class is a reasonable strategy. For example, many DOS programmers created classes to handle the mouse. Although you'll never have more than one mouse operating simultaneously, writing mouse functions into a single-instance class enables a programmer to conveniently package and organize routines that he or she will need often.

Generally, a single-instance class is okay for wrapping up a big idea, like a screen display, a mouse driver, or a graphics library. It might not, however, be appropriate for smaller uses that would suffer from the overhead inherent in using classes. Remember that although you're programming in C++, you still can use simpler data structures like structures and arrays. When you need to create a new data type, don't automatically assume that the object-oriented approach is best. Often, it's not.

Responsible Overloading

One of the things that differentiates C from C++ is function and operator overloading. Overloading is the capability to create several versions of a function or operator, each version of which has an identical name but which requires different arguments. For example, in C++, you can have two functions named Sum(), one that adds integers and another that adds floating-point numbers. When you call Sum() in a program, the compiler can tell which function you mean by checking the function's parameters.

The capability of C++ to overload functions and operators offers immense flexibility. You no longer have to come up with different names for functions that, although they take different parameters, perform virtually identical operations. You can simply write several versions of the function, using the same name, each version with its own set of arguments. As you've already learned, however, powerful techniques are often misused. In this section, you'll examine function- and operator-overloading etiquette.

Overloading versus Default Arguments

There's no question that function overloading is a great feature of C++ programming. However, when overused, it can make code more difficult to understand. If nothing else, having several versions of a function considerably increases program maintenance. The solution? Default arguments also enable you to call functions with different parameters, but without resorting to overloading. For example, consider the following overloaded function:
int Example(int x);
int Example(int x, int y);
Because of overloading, you can call the function Example() with one or two integer arguments:
Example(1);
Example(1,2);
This adds much to the function's flexibility. However, do you really need two copies of the function to get this flexibility? Not really. By using default arguments, you can create one version of Example() that accepts either one or two integer arguments:

int Example(int x, int y = 0);

This new function retains the flexibility of the overloaded function, but without the extra baggage. Of course, you can't always replace overloaded functions with default arguments. For example, if the parameter types of overloaded functions are different, the default-argument technique won't work. The following overloaded function cannot be written using default arguments:
int Example(int x);
float Example(float x);
You can't have a default type, only a default value. When you get the urge to overload a function, first consider whether it would be more expedient to use default arguments.

Using Operator Overloading Logically

You've seen how function overloading can be both bounty and bane. Operator overloading, too, requires thought before you use it. Although the use of default arguments doesn't apply here, there are still important considerations. The most important is using overloaded operators logically in other words, using them as they were originally designed to be used.

Using operator overloading, you can make any of C++'s operators perform whatever task you want. For example, the + operator sums two values. Without operator overloading, this operator can be used only on C++'s built-in data types in other words, types like int, float, and long. Suppose, however, that you want to add two arrays and assign the result to a third array. You can then overload the + and = operators in an array class so that they can take arrays as arguments. Assuming you've done this, what do you suppose the following line would do (where a, b, and c are objects of your array class)?

c = a + b;

You'd expect that the equal sign acts as an assignment operator because that is normally its purpose. Similarly, you'd expect that the + operator summed the elements of each array. (You can find the code that performs this overloading in Listing 35.19.) What you wouldn't expect is for the sum operator to take, for example, two two-element arrays and combine them into a four-element array. This type of operation would not be consistent with the operator's conventional usage.

ON THE WEB

http://www.quecorp.com/semfc The complete source code and executable file for the Array application can be found in the Chap35\Array directory at this book's Web site.

Listing 35.19 Array.cpp Defining Array Operators
#include <iostream.h>
#include <conio.h>
class Array
{
    int a[2];
public:
    Array(int x=0, int y=0);
    void Print(void);
    Array operator=(Array b);
    Array operator+(Array b);
};
Array::Array(int x, int y)
{
    a[0] = x;
    a[1] = y;
}
void Array::Print(void)
{
    cout << a[0] << ' ' << a[1] << endl;
}
Array Array::operator=(Array b)
{
    a[0] = b.a[0];
    a[1] = b.a[1];
    return *this;
}
Array Array::operator+(Array b)
{
    Array c;
    c.a[0] = a[0] + b.a[0];
    c.a[1] = a[1] + b.a[1];
    return c;
}
void main()
{
    Array a(10, 15);
    Array b(20, 30);
    Array c;
    a.Print();
    b.Print();
    c.Print();
    c = a + b;
    c.Print();
    getch();
}
Operators should perform as expected. This means more than simply using them for the expected operation. It also means that they should perform that operation in a way that is consistent with the language's implementation. For example, look at the code for the + operator in the array class (Listing 35.19). Notice that the source arrays are unchanged by the operation. Instead, a third array is used to hold the results of the addition. This third array is returned from the function. This is how you expect the addition operator to work in C++. Contrast this with the way an addition instruction works in assembly language, by storing the result of the operation into one of the two operands. In most assembly languages, one of the operands is changed by the operation. In C++, it is not.


Overloading functions and operators is a powerful technique. Like all powerful features of a language, however, this one must be used with thought and style. Don't use overloading when a simpler method will do, and ensure that overloaded operators perform in the expected way.

When to Use Virtual Functions

Using virtual functions, you can create classes that, like the simple graphics demonstration in a previous section ("Classes: From General to Specific"), perform the same general functions but perform those functions differently for each derived class. Like overloading, however, virtual functions are often misused.

Before using a virtual function, consider how the classes in the hierarchy differ. Do they need to perform different actions? Or do the classes require only different values? For example, in the shapes demonstration, the program used virtual functions so that each class could draw its shape properly. Every shape object must know how to draw itself; however, every object needs to do it differently. Drawing a shape is an action. It's inappropriate, though, to use a virtual function to assign a color to an object. Although each shape object has its own color attribute, the color attribute is a value rather than an action, and so it is best represented by a data member in the base class. Using polymorphism to set an object's color is like trying to kill a mosquito with a machine gun.


Make sure that when you use virtual functions you are creating classes that differ in action rather than value.

Developing a String Class

In the preceding sections, you reviewed object-oriented program design and some C++ style considerations. In this section, you will apply much of what you learned before to create a string class for your C++ programs. (Yes, MFC provides the CString class, which you can use to do your string handling. However, designing a similar string class enables you to create a class that better suits your needs and tastes not to mention that it's a great exercise in class design.)

Handling strings in C has always been tougher than pulling meat from a lion's mouth, especially when compared with the excellent string-handling capabilities of many other high-level languages. Unfortunately, in C, you can't create classes and overload operators, so good string handling can't be incorporated into the language, even by user-written routines. For example, in C, strings cannot be assigned by the simple expression A = B.

Thankfully, you're a C++ programmer. By using C++'s overloading capabilities both for functions and operators as well as taking advantage of its object-oriented programming features, you can create a string class that provides all the string-manipulation features of languages like Pascal.

Choosing a Storage Method

To design your own string class, ask yourself a couple of questions. First, how should you represent the string? There are two approaches you can take: a standard character array or dynamically allocated memory. Both approaches have strong and weak points. For example, the character-array approach is the simplest, enabling you to use C++'s array-handling capabilities without having to worry about the details of memory allocation.

On the other hand, using a character array is the least flexible of the choices, because you must choose a maximum string size and stick with it. Moreover, your character array will take up the same amount of memory regardless of the actual length of the string. For example, suppose you choose an 81-element character array, which has enough space for 80 characters plus a null. Then each string that you create will take up 81 bytes of memory, although the string data might be only a few bytes.

By dynamically allocating space for a string and by grabbing only the memory that you actually need to contain the string, you can use memory more efficiently. This method, however, requires a lot of program overhead. You must write code to handle memory allocation and deallocation, check for allocation errors and null strings, keep track of a string's size, and take care of other messy details. In fact, the extra code required for a dynamically allocated string class would probably use as much memory as you'd waste with the character-array approach (depending on the number of strings a program uses, of course). To keep things simple and clean, then, the class presented in this chapter uses the character-array method, with an 81-element array.

Determining the Class's Functionality

Now that you've chosen a type of storage, consider how your programs will use strings. To be as flexible as possible, your programs must be able to handle two types of strings: standard character arrays and String objects (instances of the String class). For example, you must allow string assignments such as str1 = str2 (in which str1 and str2 are String objects) and str1 = "STRING" (in which str1 is a String object and "STRING" is a standard C character array). This means that you're going to have to overload functions to accept either type of parameter.

You now have a general strategy for string storage and string usage. Next you need to decide what functions will give you the string-handling power that you want. The basic functions required in a string class vary with the needs of each programmer; everyone programs differently. Moreover, each program has its own requirements. The string class in this chapter contains the most-often-used functions. When the string class is complete and you've used it in your own programs, you might find that you need additional functions. No problem! Add them by modifying the original class. When you understand how the basic class was created, you should have no difficulty modifying it to meet specific needs.

What are the basic functions that a string class requires? You can answer this question easily by examining a popular, high-level language with good string handling, such as Pascal. Examining Borland's Turbo Pascal yields a list of important string-handling functions. These functions are listed here: Each function in the string class will be covered in its own section. Before you get started, however, look at Listing 35.20, the header file for the String class. Compare it with Table 35.1, which lists each function and its usage. Obviously, if you understand how to use the class, you'll better understand the programming involved. You might also want to look over Listing 35.34, near the end of this chapter, to get a general idea of how the string functions are used in a program.

Listing 35.20 STRNG.H The Header File for the String Class
#ifndef _STRNG_H
#define _STRNG_H
#include <string.h>
#include <conio.h>
#include <iostream.h>
class String
{
    char s[81];
public:
    String(char *ch);
    String(String &str) { strcpy(s, str.s); }
    void GetStr(char *ch, int size);
    String GetSubStr(unsigned index, int count);
    void Delete(unsigned index, int count);
    void Insert(String str, int index);
    void Insert(char *ch, int index);
    int Length() { return strlen(s); }
    int Pos(String str);
    int Pos(char *ch);
    String operator=(String str);
    String operator=(char *ch);
    String operator+(String str);
    String operator+(char *ch);
    int operator==(String str);
    int operator==(char *ch);
    int operator!=(String str);
    int operator!=(char *ch);
    int operator<(String str);
    int operator<(char *ch);
    int operator>(String str);
    int operator>(char *ch);
    int operator>=(String str);
    int operator>=(char *ch);
    int operator<=(String str);
    int operator<=(char *ch);
};
#endif
Add() Adds an image to the image list

Attach() Attaches an existing image list to an object of the CImageList class

BeginDrag() Starts an image-dragging operation

Create() Creates an image list control

DeleteImageList() Deletes an image list

Detach() Detaches an image list from an object of the CImageList class

DragEnter() Locks a window for updates and shows the drag image

Table 35.1 String Class Description
String Function  Description 
String(char *ch) 
String(String &str)  These are the class's constructors. The constructor accepts as a parameter either a character array or a String object. 
void GetStr(char *ch, int size)  This function retrieves a String and places it into a character array. The parameter ch is a pointer to the destination character array, and size is the length of the destination array. 
String GetSubStr(unsigned index, int count)  This function returns a String made up of count characters. The characters are extracted from the String starting with the character at position index
void Delete(unsigned index, int count)  This function deletes count characters from the String object, starting with the character at position index
void Insert(String str, int index)  This function inserts str into a String, at position index
void Insert(char *ch, int index)  This function inserts a character array pointed to by ch into a String, starting at string character position index
int Length()  This function returns the length of a String
int Pos(String str)  This function returns the character position of the first occurrence of str within a String
int Pos(char *ch)  This function returns the character position of the first occurrence of ch (a character array) within a String
String operator=(String str)  Assigns str to a String
String operator=(char *ch)  Assigns ch (a character array) to a String
String operator+(String str)  Concatenates a String and str
String operator+(char *ch)  Concatenates a String and ch (a character array). 
int operator==(String str)  Compares a String with str, returning 1 if they are equal or 0 if they are not equal. 
int operator==(char *ch)  Compares a String to ch (a character array), returning 1 if they are equal or 0 if they are not equal. 
int operator<(String str)  Returns 1 if String is less than str, or else returns 0. 
int operator<(char *ch)  Returns 1 if String is less than ch, or else returns 0. 
int operator>(String str)  Returns 1 if String is greater than str, or else returns 0. 
int operator>(char *ch)  Returns 1 if String is greater than ch, or else returns 0. 
int operator<=(String str)  Returns 1 if String is less than or equal to str, or else returns 0. 
int operator<=(char *ch)  Returns 1 if String is less than or equal to ch, or else returns 0. 
int operator>=(String str)  Returns 1 if String is greater than or equal to str, or else returns 0. 
int operator>=(char *ch)  Returns 1 if String is greater than or equal to ch, or else returns 0.

String Construction and Destruction

Thanks to object-oriented programming, string initialization can be handled by the class's constructor. This means that you can create and initialize a new String with a single declaration for example, String str("TEST STRING") or you can create an empty string; for example, String str("").

By using conventional character arrays, rather than dynamically allocated memory, the class needs no string destructor. The class creates nothing that can't be handled automatically by C++. If, however, the string class used dynamically allocated memory, its destructor would have been responsible for releasing memory allocated to a string.

Finally, as mentioned previously, the String class must deal with both String objects and standard C character arrays. Therefore, it needs to overload the constructor. One version constructs a String from an existing String and another constructs a String from a character array. The former is implemented inline:

String(String &str) { strcpy(s, str.s); }

The constructor doesn't need to worry about the length of str.s. It's already a String object; thus, it is guaranteed to be 80 characters or less. To construct the new String, the function simply copies one string into the other.

Listing 35.21 is the source code for the character array version of the constructor.

Listing 35.21 lst35_21.cpp The String Class's Constructor for Character Arrays
String::String(char *ch)
{
    strncpy(s, ch, 80);
    s[80] = 0;
}
Here, the constructor uses strncpy() to ensure that no more than 80 characters will be copied from the source array to the string object's storage. The constructor then places a null in ch[80], because strncpy() isn't guaranteed to place the null.

String Assignments

To conveniently handle string assignments, the string class overloads C++'s assignment operator (=). In fact, it overloads it twice: once for character arrays and once for String objects. An assignment operator would be crippled if it couldn't accept string constants, which are represented in C++ by character arrays. Listing 35.22 is the source code for the String version of the = operator.

Listing 35.22 lst35_22.cpp The Assignment Operator for String Objects
String String::operator=(String str)
{
    strcpy(s, str.s);
    return *this;
}
As with the string constructor, the source String is already in the acceptable format, so the function can just copy it directly into the destination String. Note the use of the pointer this, which is a pointer to the object that called the function. Every call to a class's function gets this as a hidden parameter. So the function in Listing 35.22 returns a pointer to the object. This makes it possible to use the new assignment operator in such expressions as str1 = str2 = "TEST STRING". Also, this is the way that programmers expect the C++ assignment operator to work. You should avoid giving programmers nasty surprises. Surprises make them cranky. Listing 35.23 is the character array version of the function.

Listing 35.23 lst35_23.cpp The Assignment Operator for a Character Array
String String::operator=(char *ch)
{
    strncpy(s, ch, 80);
    s[80] = 0;
    return *this;
}
This version works much like the first, except that the function can no longer assume that the source character array is 80 characters or less. As with the character array version of the constructor, therefore, the function checks the length of ch and truncates it if necessary. Then it uses strcpy() to copy the array into s.

String Concatenation

There probably aren't too many string-intensive programs that couldn't benefit from a string-concatenation function. For example, a program might need to combine a person's first and last names, build a complete path name out of directory and file name strings, or assemble phrases into sentences.

Concatenating strings is trickier than making simple string assignments. First you must be sure that the final String is no longer than the allowable 80 characters. Also, as discussed previously ( in the section "Using Operator Overloading Logically"), you must use the + operator in the expected way. Specifically, you must not change either of the source strings, but rather return a third string that is the concatenation of the source strings. And you need two versions of the function, one for Strings and one for character arrays. Listing 35.24 is the String version.

Listing 35.24 lst35_24.cpp The Concatenation Operator for String Objects
String String::operator+(String str)
{
    char ch[161];
    String str1("");
    strcpy(ch, s);
    strcpy(&ch[strlen(s)], str.s);
    ch[80] = 0;
    strcpy(str1.s, ch);
    return str1;
}
Although the function concatenates two strings, you might wonder why only one string is listed in the function's parameters. This is because the other source string is the String object that called the function. Which object calls the operator function? With operators, the object on the left is always the one that makes the function call. For example, in the statement str2 = str1 + "TEST STRING", the object str1 calls the concatenation function. You don't need to pass str1 as a parameter, because you already have access to it from within the class.

The function in Listing 35.24 uses a 161-element character array as temporary storage for the strings being concatenated. By using this double-sized character array, the function can concatenate the two Strings (which are 80 characters or less) without worrying about overrunning the destination array. To return a String in the proper format, the function simply places a null in ch[80], which truncates ch if it's larger than 80 characters. After concatenating the Strings, the function copies the resulting character string into a new String object, str1, which is the String returned.

The character array version of the concatenation function is much simpler than the String version, as shown in Listing 35.25.

Listing 35.25 lst35_25.cpp The Concatenation Operator for a Character Array
String String::operator+(char *ch)
{
    String str(ch);
    return *this + str;
}
Rather than duplicate a lot of code, it's much easier to convert the character array to a String object and then use the String version of the concatenation function to do the dirty work. Notice that the function uses a dereferenced this pointer to access the String object that called the function.

String Comparison

Comparing strings is a particularly handy function. Often, for example, in an interactive program, you need to check a user's input against some expected response. C++ already provides string-comparison functions, but those functions can be improved by hiding their somewhat clumsy implementation inside of the String class. By overloading C++'s == operator, you can compare strings in a more natural way. Listing 35.26 is the implementation for both versions of this function.

Listing 35.26 lst35_26.cpp The Comparison Operator
int String::operator==(String str)
{
    if (strcmp(s, str.s) == 0) return 1;
    return 0;
}
int String::operator==(char *ch)
{
    if (strcmp(s, ch) == 0) return 1;
    return 0;
}
These functions differ only in the type of parameter that they accept. Both use the C++ function strcmp() to compare two character arrays. Unlike the strcmp() function, however, which returns a false (0) value when the strings match, the string class's comparison function returns true (1) for a match and false (0) otherwise. This is the way you would expect the == comparison operator to work.

The string class also includes overloaded functions for all other types of comparisons, as shown in Table 35.1. Note that the comparison functions provided there are case-sensitive. You might want to develop comparison operators that are not case-sensitive.

String Searches

Sometimes you might want to locate a series of characters within a string. Again, as with string comparisons, C++'s string library already provides a function for locating substrings. The strncmp() function works like strcmp(), except that it limits its comparison to the number of characters specified in the last parameter. You can easily use strncmp() to locate a substring and return its position. The string class's Pos() function uses strncmp() for just this task, as shown in Listing 35.27.

Listing 35.27 lst35_27.cpp The Pos() Function
int String::Pos(String str)
{
    int found = 0;
    if ((str == "") || (str.Length() > Length())) 
        return 0;
    int i = 0;
    while ((!found) && (i<Length()))
    {
        if (strncmp(&s[i], str.s, str.Length()) == 0)
            found = 1;
        else ++i;
    }
    if (found) return i+1;
    return 0;
}
Here, the function first checks whether the passed String is null or is longer than the String that called the function. In either case, there can't possibly be a match; therefore, the function returns a 0. If the function gets past this first check, it enters a while loop that uses the index i to cycle through each character of the String object. In the call to strncmp(), the index is used to calculate the address of the character with which to start the compare (&s[i]) with the search String. If strncmp() finds a match, the flag found is set, which causes the loop to end. Then the value i+1 the position of the character that begins the substring is returned from the function.

The character array version of Pos(), like the character array version of the concatenation function, simply converts the character array to a String object and then calls the String version of Pos(). This trick makes adding a character array version of most functions easier than toasting marshmallows in a forest fire. Listing 35.28 is the character array version of the function.

Listing 35.28 lst35_28.cpp The Character Array Version of the Pos() Function
int String::Pos(char *ch)
{
    String str(ch);
    return Pos(str);
}

String Insertion

Another handy string operation one that's similar to string concatenation is string insertion (placing one string into another). The String class accomplishes this task with the function Insert(). The String version is shown in Listing 35.29.

Listing 35.29 lst35_29.cpp The String Version of Insert()
void String::Insert(String str, int index)
{
    char ch[161];
    if ((index <= Length()) && (index > 0))
    {
        strncpy(ch, s, index-1);
        strcpy(&ch[index-1], str.s);
        strcpy(&ch[strlen(ch)], &s[index-1]);
        ch[80] = 0;
        strcpy(s, ch);
    }
}
This function first checks for a valid index. If the index is okay, it uses strncpy() to copy all of the characters, up to the index, into a temporary character array. Then it adds the string that you want to insert to the array. Finally, it copies the remaining characters in the original String into the temporary array, placing a null in ch[80] to ensure that the returned string is 80 characters or less, as required by the String class. Note that this function returns no value; it operates directly on the String object that calls the function.

The character array version of Insert(), again, does nothing more than convert the array to a String object and then call the String version of the function. Listing 35.30 is that version of the function.

Listing 35.30 lst35_30.cpp The Character Array Version of Insert()
void String::Insert(char *ch, int index)
{
    String s1(ch);
    Insert(s1, index);
}

String Deletion

The opposite of insertion is, of course, deletion. A string deletion function enables you to remove a substring from a String object. In the String class, the function Delete() does the job, as shown in Listing 35.31.

Listing 35.31 lst35_31.cpp The Delete() Function
void String::Delete(unsigned index, int count)
{
    String s1("");
    if ((index <= strlen(s)) && (index > 0) && (count > 0))
    {
        strncpy(s1.s, s, index-1);
        if ((index+count-1) <= strlen(s))
            strcpy(&s1.s[index-1], &s[index+count-1]);
        else s1.s[index-1] = 0;
        *this = s1;
    }
}
This function works similarly to the insertion function. It first checks that the index is valid. It also checks that count is greater than 0. If the index or the count is invalid, the function does nothing. If the index is okay (greater than 0 and less than or equal to the length of the string) and count is greater than 0, index-1 characters are copied from the beginning of the source String (the one that called the function) into a temporary String. Then the function checks whether the source String, starting at index, contains at least count characters. If it does, the characters starting at index+count-1 are added to the temporary String. Otherwise, if count is larger than the number of remaining characters in the source String, the function just adds a null to the temporary String, which effectively deletes all remaining characters in the String. Note that in the last line, *this = s1, the assignment operator is the one defined for the String class, not the usual C++ assignment operator.

String Extraction

A string extraction function is much like a string deletion function, except that the extraction function returns a new string containing the requested characters without deleting the characters from the original string. In the String class, the function GetSubStr() takes on this chore. Because the function takes only integer parameters, only one version is needed, as shown in Listing 35.32.

Listing 35.32 lst35_32.cpp The GetSubStr() Function
String String::GetSubStr(unsigned index, int count)
{
    String s1("");
    if ((index <= strlen(s)) && (index > 0) && (count > 0))
    {
        int c = Length() - index + 1;
        if (count > c) count = c;
        strncpy(s1.s, &s[index-1], count);
        s1.s[count] = 0;
    }
    return s1;
}
As always, the function first checks for a valid index and count. If the index is valid (less than or equal to the length of the string and greater than zero) or count is less than 1, it returns a null String from the function. If the index and count are okay, the number of characters in the String starting at index are calculated, and count is adjusted if necessary. (You don't want to try to copy more characters than exist in the String.) Finally, strncpy() copies the requested characters into the new String object, and that String is returned from the function.

String Retrieval

The last function in the String class enables you to convert the contents of a String back to a character array. You might need to do this, for example, to manipulate the string in a manner that is not supported by the String class. The GetStr() function, shown in Listing 35.33, handles the conversion task.

Listing 35.33 lst35_33.cpp The GetStr() Function
void String::GetStr(char *ch, int size)
{
    strncpy(ch, s, size-1);
    ch[size-1] = 0;
}
Here, the function simply copies the String object's character array into the array pointed to by ch. Note that it's imperative that the size parameter, which tells the function the size of ch, be correct. To be sure of this, you should always use sizeof() as the second parameter in a call to this function, as in the following:

str.GetStr(ch, sizeof(ch));

Why can't you use the sizeof() function inside GetStr() and avoid having to pass it off as a parameter? Because all GetStr() knows about ch is that it's a pointer to char; the size of a pointer is four bytes. De-referencing the pointer won't work either, because then you'd be asking for the size of the data to which ch pointed. What does ch point to? Characters, of course, which are actually integers. GetStr() has no way of knowing that ch actually points to an array of characters.

Testing the String Class

That's it! You now know how to design and write a class. Listing 32.34 is the complete source code for the String class's implementation. Listings 32.35 through 32.37 make up a Windows program that tests the new class and show how each function is called. You can find the source code, as well as the executable file, for this program in the Chap32\strg folder at this book's Web site.

Listing 35.34 STRGAPP.H Header File for the CStrgApp Class
///////////////////////////////////////////////////////////
// STRGAPP.H: Header file for the CStrgApp class, which
//            represents the application object.
///////////////////////////////////////////////////////////
class CStrgApp : public CWinApp
{
public:
    CStrgApp();
    // Virtual function overrides.
    BOOL InitInstance();
};
Listing 35.35 STRGAPP.CPP Implementation File for the CStrgApp Class
///////////////////////////////////////////////////////////
// STRGAPP.CPP: Implementation file for the CStrgApp,
//              class, which represents the application
//              object.
///////////////////////////////////////////////////////////
#include <afxwin.h>
#include "strgapp.h"
#include "mainfrm.h"
// Global application object.
CStrgApp StrgApp;
///////////////////////////////////////////////////////////
// Construction/Destruction.
///////////////////////////////////////////////////////////
CStrgApp::CStrgApp()
{
}
///////////////////////////////////////////////////////////
// Overrides
///////////////////////////////////////////////////////////
BOOL CStrgApp::InitInstance()
{
    m_pMainWnd = new CMainFrame();
    m_pMainWnd->ShowWindow(m_nCmdShow);
    m_pMainWnd->UpdateWindow();
    return TRUE;
}
Listing 35.36 MAINFRM.H Header File for the CMainFrame Class
///////////////////////////////////////////////////////////
// MAINFRM.H: Header file for the CMainFrame class, which
//            represents the application's main window.
///////////////////////////////////////////////////////////
class CMainFrame : public CFrameWnd
{
// Constructor and destructor.
public:
    CMainFrame();
    ~CMainFrame();
// Overrides.
protected:
    virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
// Message map functions.
public:
    afx_msg void OnPaint();
  
// Protected member functions.
protected:
    void ShowStrings(CPaintDC* paintDC);
    DECLARE_MESSAGE_MAP()
};
Listing 35.37 MAINFRM.CPP Implementation of the CMainFrame Class
///////////////////////////////////////////////////////////
// MAINFRM.CPP: Implementation file for the CMainFrame
//              class, which represents the application's
//              main window.
///////////////////////////////////////////////////////////
#include <afxwin.h>
#include "mainfrm.h"
#include "strng.h"
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
    ON_WM_PAINT()
END_MESSAGE_MAP()
///////////////////////////////////////////////////////////
// CMainFrame: Construction and destruction.
///////////////////////////////////////////////////////////
CMainFrame::CMainFrame()
{
    Create(NULL, "String App", WS_OVERLAPPED | WS_SYSMENU);
}
CMainFrame::~CMainFrame()
{
}
///////////////////////////////////////////////////////////
// Overrides.
///////////////////////////////////////////////////////////
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
    // Set size of the main window.
    cs.cx = 240;
    cs.cy = 360;
    // Call the base class's version.
    BOOL returnCode = CFrameWnd::PreCreateWindow(cs);
    return returnCode;
}
///////////////////////////////////////////////////////////
// Message map functions.
///////////////////////////////////////////////////////////
void CMainFrame::OnPaint()
{
    CPaintDC* paintDC = new CPaintDC(this);
    ShowStrings(paintDC);
    delete paintDC;
}
///////////////////////////////////////////////////////////
// Protected member functions.
///////////////////////////////////////////////////////////
void CMainFrame::ShowStrings(CPaintDC* paintDC)
{
    String s1("THE HAT");
    String s2(s1);
    String s3("");
    char ch[81];
    TEXTMETRIC tm;
    paintDC->GetTextMetrics(&tm);
    UINT position = tm.tmHeight;
    s1.GetStr(ch, sizeof(ch));
    paintDC->TextOut(10, position, ch);
    position += tm.tmHeight;
    s2.GetStr(ch, sizeof(ch));
    paintDC->TextOut(10, position, ch);
    position += tm.tmHeight;
    s2.Insert("CAT ", 5);
    s2.GetStr(ch, sizeof(ch));
    paintDC->TextOut(10, position, ch);
    position += tm.tmHeight;
    s3 = "IN THE THE";
    s3.GetStr(ch, sizeof(ch));
    paintDC->TextOut(10, position, ch);
    position += tm.tmHeight;
    s2.Insert(s3, 9);
    s2.GetStr(ch, sizeof(ch));
    paintDC->TextOut(10, position, ch);
    position += tm.tmHeight;
    s2.Delete(16, 3);
    s2.GetStr(ch, sizeof(ch));
    paintDC->TextOut(10, position, ch);
    position += tm.tmHeight;
    s3 = s2;
    s2.GetStr(ch, sizeof(ch));
    paintDC->TextOut(10, position, ch);
    position += tm.tmHeight;
    wsprintf(ch, "S3 is %d characters long.", s3.Length());
    paintDC->TextOut(10, position, ch);
    position += tm.tmHeight;
    wsprintf(ch, "'CAT' is at position %d.", s3.Pos("CAT"));
    paintDC->TextOut(10, position, ch);
    position += tm.tmHeight;
    s2 = "HAT";
    wsprintf(ch, "'HAT' is at position %d.", s3.Pos(s2));
    paintDC->TextOut(10, position, ch);
    position += tm.tmHeight;
    s3 = s2 + " TRICKS";
    s3.GetStr(ch, sizeof(ch));
    paintDC->TextOut(10, position, ch);
    position += tm.tmHeight;
    s3 = s3 + " " + s3;
    s3.GetStr(ch, sizeof(ch));
    paintDC->TextOut(10, position, ch);
    position += tm.tmHeight;
    s1 = s3.GetSubStr(5, 6);
    s1.GetStr(ch, sizeof(ch));
    paintDC->TextOut(10, position, ch);
    position += tm.tmHeight;
    s3 = s2;
    if ((s2 == "HAT") && (s2 == s3))
        paintDC->TextOut(10, position, "The strings are equal.");
}
The output from the test program should look like Figure 35.1.


FIG. 35.1 

The STRGAPP application gives the String class a chance to perform its tricks.

Now not only do you have a handy programming tool, but also you have reinforced some of what you learned earlier in this chapter specifically, what you learned about the proper use of function and operator overloading. This String class overloads functions that vary in parameter type, not parameter count. Also, it overloads operators in a way that is consistent with their intended use except in one instance. Do you see a problem with the concatenation function? The concatenation function will allow an expression like

str3 = str1 + str2

or

str3 = str1 + "TEST"

It won't, however, allow an expression like

str3 = "TEST" + str1

Why? Because, if you recall, it's the object on the left of the operator that calls the overloaded operator function. Because "TEST" is a character array and not a String object, the previous expression is invalid. It won't even compile. To perform the operation in question, "TEST" must first be converted to a String object.

Conclusion

Over the course of this book, you've learned a great deal about Microsoft Foundation Classes, everything from creating an AppWizard application to writing MFC programs from scratch including designing your own classes. You've learned about OLE and database applications. You've even dipped into technical subjects like linked lists and recursion. At this point, you should be well armed to get started on your own MFC projects. Whether you opt for the AppWizard approach to MFC programming, or you like the direct approach of writing all your code by hand, MFC gives you the tools that you need to create sensational Windows programs. So what are you waiting for? Get to it!