Chapter 35
Developing a Class
-
Encapsulation, inheritance, and polymorphism
To understand classes, you must understand the three main attributes of
object-oriented programming.
-
Class organization
Programmers use specific techniques for organizing the source code that
makes up their classes.
-
Function and operator overloading
The technique of overloading enables you to write multiple functions that
have the same name but differ in their parameters.
-
Virtual functions
When you derive new classes from base classes, you might want the new class
to perform certain tasks differently than they're performed in the base
class. Virtual functions are the key to this important class design task.
-
Class design
When you're designing a new class, you need to follow an important set
of guidelines.
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:
-
String construction and destruction
-
String assignment
-
String concatenation
-
String comparison
-
String searches
-
String insertion
-
String deletion
-
String extraction
-
String retrieval
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!