TOCBACKFORWARD

Charlie Calvert's C++ Builder Unleashed

- 19 -

Inheritance

This chapter focuses on object-oriented programming (OOP) as it applies to the VCL. Specifically, it takes a close look at inheritance, one of the big-three topics in object-oriented code. The other two key topics are encapsulation and polymorphism, which you learn about in the next two chapters. Even if you already know all about OOP, you should still at least skim this chapter so you can learn about the difference between VCL objects and standard C++ objects.

In particular, this chapter covers the following topics:

The text focuses on several programs designed to show how objects are constructed. One of the programs is developed in several stages so that you can see how an object hierarchy emerges out of a set of raw ideas.

After you read the next three chapters on inheritance, encapsulation, and polymorphism, the next big step is to learn how to build components. In fact, the real justification for learning this material is that it gives you the ability to start creating your own components. Building your own components is one of the most important tasks you can tackle in BCB, so I will lay the groundwork for it carefully.

It is important to understand that the next three chapters are aimed at programmers who want to work inside the VCL. I make no attempt to do justice to all the complex features of the C++ object model. Instead, I try to present you with a subset of those features as they apply to the VCL. This means that I make short shrift of interesting topics such as function and operator overloading. My intent, however, is to show you how to create components. You do not have to be an expert in C++ OOP theory to achieve that goal.

For all its wonders, I don't think there is anything in C++Builder that even approaches the significance of components. VCL components are the most amazing technological achievement I have seen in contemporary programming. If you want to do something really fantastic with your computer, then pay attention to the next few chapters so that you can learn how to build great components.

When reading this chapter, you might want to make use of the ClassBrowser sample program that ships with BCB. It allows you to explore the hierarchy of the VCL. This program is found in the Examples/ClassBrw directory. It is far from perfect, but it will serve to give you an overview of the VCL classes. You should also go to www.object-domain.com and see whether they have a version of Snorkle for C++Builder available. The versions of Snorkle for Delphi that I have seen are very nice indeed, and if they can duplicate their efforts in the world of C++, then most readers of this book will want to test their technology.

About Objects

It might seem a little strange to start focusing on objects this late in the book. After all, almost every program I have shown so far uses object-oriented code. So how could I wait this long to begin talking seriously about objects? To answer this question, I need to discuss two different issues:

The developers wanted BCB to be very easy to use. By its very nature, OOP is not always a simple topic. As a result, BCB goes to considerable lengths to hide some of the difficulties of object-oriented programming from the user. The biggest steps in this direction include the automatic construction of Form1 as an object and the existence of the delegation model. The fact that the scaffolding for most methods is produced automatically by the IDE is one of the key ways the product saves time--and one of the key ways it eases the process of producing applications.

The simple fact is that some people would never be able to approach BCB if they had to go through the process of writing all this every time they created a form:

//--------------------------------------------------------------------------

#ifndef Unit1H

#define Unit1H

//--------------------------------------------------------------------------

#include <vcl\Classes.hpp>


#include <vcl\Controls.hpp>

#include <vcl\StdCtrls.hpp>

#include <vcl\Forms.hpp>

//--------------------------------------------------------------------------

class TForm1 : public TForm

{

__published:

private:

public:        
// User declarations

  virtual __fastcall TForm1(TComponent* Owner);

};

//--------------------------------------------------------------------------

extern TForm1 *Form1;


//--------------------------------------------------------------------------

#endif

I'm leaving out the implementation of the constructor, and a few other features, but in a stripped-down form, this code is indeed the basis for most BCB units. It's simple enough to write; nonetheless, it could form a barrier between the product and certain types of programmers.

The next obvious question is, "Why did the developers choose to write object-oriented code if the subject itself can at times become somewhat complex? Why not just use the relatively simpler framework provided by structured programming?" The answer is that although it is simpler to create small structured programs than small object-oriented programs, it's easier to write large object-oriented programs than it is to write large structured programs.

OOP brings discipline and structure to a project. In the long run, this makes coding easier. The problem is the learning curve associated with understanding OOP.

Almost everyone agrees that it's easier to finish a group project if you appoint a leader for the group; it's easier to win at sports if you practice regularly; and, ultimately, it's easier to become a good musician if you sit through some boring lessons with a professional. It also might seem at first as if structured programs are simpler to learn how to write and, therefore, are simpler to write, but this isn't true. Just as it helps to take lessons, practice, and learn discipline if you want to become good at playing a sport or a musical instrument, it helps to learn object- oriented code if you want to write good programs.

Here's another way of stating the same matter. There is nothing you can do with object- oriented code that you can't also do with structured programming. It's just that OOP makes it relatively easy to construct programs that are fundamentally sound and easily maintained. This doesn't mean you can't write structured programs that are every bit as architecturally sound as object-oriented programs. The problem, however, is that it is very difficult to design a structured program that is truly modularized and truly easy to maintain. Object-oriented code, on the other hand, has a natural tendency to move you in the direction of a sound, well-structured design.

The thesis of this chapter is that object-oriented code is basically a technique for designing robust, well-planned programs. The syntax of OOP emerged out of the desire to help programmers design applications that work. It is perhaps arguable as to whether or not OOP by itself succeeded in achieving its goal, though certainly I personally believe that it is a success. However, I think it is undeniable that OOP in conjunction with components is the answer to many core programming problems. If your only experience with components is in creating ActiveX controls, then you haven't yet seen what this technology can do. The combination of OOP and components is something that can make programmers many times more productive than they had ever imagined possible when writing structured code, or when working with either objects or components alone.


NOTE: It's probably worth pointing out that OOP is not a separate subject from structured programming but its natural child. OOP emerged out of the same types of thinking that generated structured code. Much of what is true in structured programs is also true in object-oriented programs, except OOP takes these theories much further. Object-based programmers should know nearly everything that structured programmers know and should then add another layer of information on top of it.

OOP is certainly not the end-all and be-all of programming. Rather, it is an intermediate step in an ongoing process that might never have an end. BCB, with its heavy use of components, already shows part of what the future holds. In particular, the future is about components and visual manipulation of objects.

The Object Inspector enables you to see inside objects and to start to manipulate them visually. You can do this without having to write code. It is quite likely that this trend will continue in the future, and you will start to see programs not as code but as a series of objects depicted as a hierarchy. If programmers want to manipulate these objects, they will be able to do so through tools such as the Object Inspector or through other means currently being used only in experimental languages.

To take this out of the clouds for a moment, here is my list of what's best about BCB:

Here are the same ideas looked at again from a slightly more in-depth perspective:

OOP, then, is part of a theory of design that is moving increasingly in the direction of reusable, visual components that can be manipulated with the mouse. Undoubtedly, this means that some types of programs that are difficult to construct today will become trivial to build in the future. BCB has already performed this magic with databases. A 10-year-old child could use BCB to construct a simple database application. However, creating complex programs will probably always be difficult, simply because it is so hard to design a good program that performs anything more than trivial tasks. First printing presses, then typewriters, and finally word processors have made writing much easier than it used to be, but they have not succeeded in making us all into a race of Shakespeares.


NOTE: One thing that is not built into BCB that can help you create robust programs is a good object-modeling tool. There are some tools, such as the products called WithClass and Snorkle, that are designed to work with the Delphi VCL and should soon appear in a BCB-based format.

It is worth pointing out that it is easy to draw object hierarchies using some form of custom or agreed upon notation. Programs such as Visio or Playground can help with this process. Even if there is no direct code generation involved, there is still an enormous benefit to be derived from this process.

I have worked on projects I thought had gone hopelessly astray and could not ever be salvaged. These "lost causes" were saved by simply drawing out my object hierarchy with a tool that would let me rearrange its elements in several different patterns. C++ is a great language, but it offers no means for providing an overview of your object hierarchy. Drawing the object hierarchy with a simple object notation can help enormously when it is not clear how to design a particular feature or when you need to try to salvage a product that has gone astray.

BCB's object-oriented, component-based architecture makes programming easier than it used to be. That doesn't mean that now everyone will be able to program. It just means that now the best programmers can make better applications. The key terms are reuse, visual design tools, components, and objects. If you can find an object-modeling tool that can aid in program development, you will be even further ahead.

Creating Simple Objects

To start a discussion of objects, it might be a good idea to cut the VCL out of the picture as much as possible. This will eliminate the complex object hierarchy associated with the VCL. In its place, you can construct some very simple objects with a known hierarchy that is easy to define. As the discussion progresses, the VCL can be introduced into the programs in a planned and sensible manner.

1. Start a new project.

2. Bring up the Project Manager from the View menu and remove Form1.cpp and the project resource file.

3. Go to the View menu again and choose Project Source.

4. Go to Options | Project | Linker and choose Console application, as shown in Figure 19.1.


FIGURE 19.1. Creating a console application in BCB.

Edit the main source file for the project so it looks like this:

#include <stdio.h>

int main(void)

{

  printf("Daughters of Time, the hypocritic Days,\n");

  printf("Muffled and 
dumb like barefoot dervishes\n");

  printf("-- Ralph Waldo Emerson");

  return 0;

}

Save this file as Object1.mak. It is now a complete application that circumvents the VCL. If you open up a DOS window and run the program from the DOS prompt, the output looks like Figure 19.2.

FIGURE 19.2. The output from the first take of the OBJECT1 program as it appears when run from the DOS prompt.

It might seem strange to you that I have gone out of my way to eliminate so much of the object hierarchy in a chapter that is about objects. My goal, however, is to clear the boards so that you can view objects in a simplified state, thereby clearly delineating their most salient points.

The program that unfolds through the next few pages is called OBJECT1. This is a very simple object-oriented program that you will build on the console application framework established earlier. I'm not going to start by showing you the code for the whole program, because I want you to build it one step at a time so that its structure emerges little by little.

To begin, you should create a small object at the top of the program:

class TMyObject

{

};



int main(void)

{

  return 0;

}

All I have done here is added a simple class definition and removed the printf()statements.

Delphi programmers should note that this class is not a descendant of TObject, even though it would have been in Object Pascal. One of the fundamental rules of Object Pascal programming is that it is impossible to build an object that is not a descendant of TObject or one of TObject's children. The reason for this rule is that TObject contains some RTTI-based intelligence that is needed by all BCB objects. This same intelligence is present in the metaclass that is part of the BCB version of TObject. However, you can create C++ objects that do not descend from TObject and thus do not include this intelligence.


NOTE: You will find that I use the words class and object almost completely interchangeably. This is technically correct, although there is some merit in using the word class to describe the written declarations that appear in a text file and object to refer to a compiled class that is part of a binary file. In other words, programs are made up of objects, whereas source files show class definitions. However, this distinction is not one that I spend a great deal of time stressing in this book.

To create a true VCL object, you should change TMyObject's definition so that it reads as follows:

#include <vcl\vcl.h>



class TMyObject : 
public TObject

{

public:

  __fastcall TMyObject(void) : TObject() {};

}



int main(void)

{

  return 0;

}

Logically, there is now a considerable difference between this declaration and the one you created earlier. In particular, this is now a VCL object and must be created on the heap. It also supports VCL specific syntax such as the __published directive.

All VCL objects must have a constructor, and it should be declared __fastcall. Methods or functions declared __fastcall can have some of their parameters passed in registers, rather than always being pushed on the stack. This is the calling convention used by VCL constructors, so you should conform to it.

All VCL objects that are descendants of TComponent must have destructors declared __fastcall virtual:

__fastcall virtual TComponent(TComponent* Aowner);

The reason for the __fastcall and virtual restrictions has to do with conformance to the VCL programming model. In particular, the VCL declares the constructor for TComponent as virtual, so C++ objects that descend from TComponent must follow along. This means that all components you create must be declared with virtual constructors, because all components are, at least in practice, descendants of TComponent. I comment on this fact simply because it is very unusual for C++ constructors to be declared virtual.


NOTE: Actually, it is theoretically possible for you to create your own class that performs the same chores that TComponent performs. There is nothing magical about TComponent; it simply contains standard VCL code that makes it possible for an object to live on the Component Palette. It is not practical to duplicate this effort in your own code, and so one could perhaps go so far as to say that "by definition" all components are descendants of TComponent. However, this is not strictly true, as you could create your own class that appears on the Component Palette without descending from TComponent. I personally cannot imagine any set of circumstances that would justify the effort involved in duplicating the work done in TComponent.

The declaration and implementation for a C++ constructor can have two forms. They can appear entirely inside a class declaration, or you can split them up, with the declaration inside the class and the implementation outside:

#include 
<vcl\vcl.h>



class TMyObject : public TObject

{

public:

  __fastcall TMyObject(void);

};



__fastcall TMyObject::TMyObject(void) : TObject()

{

}



int main(void)

{

  return 0;

}

When you implement a constructor, you should follow the header with a colon and a call to the ancestor's constructor:

_fastcall TMyObject::TMyObject(void) : TObject()

Now that you have an overview of a basic VCL object declaration, the next step is to declare a variable of type TMyObject and then instantiate it and dispose of it:

class TMyObject : public TObject

{

public:

  __fastcall TMyObject() : TObject()  {}

};



int main(void)

{

  
TMyObject *MyObject = new TMyObject;

  delete MyObject; 

  return 0;

}

The code shown here doesn't do anything functional. Its only purpose is to teach you how objects work. Specifically, it declares a variable of type TMyObject:

TMyObject *MyObject

Next, it allocates the memory for the object:

new TMyObject;

Put together on one line, the statement looks like this:

TMyObject *MyObject = new TMyObject;

This statement actually creates a pointer variable of type TMyObject. In VCL programming, you have to take this step if you want to use MyObject, and, furthermore, you must dispose of this memory when you are finished with it.

There are two ways to destroy an object. One is to call Free, and the other is to use the delete operator:

MyObject->Free();

delete MyObject;

Both techniques have the same outcome. I believe the majority of people prefer delete, and it is what I use most often in the code found in this book. However, there are reasons you might want to call Free, so I will discuss it in the next few paragraphs.

When you free an object, what you are really doing is calling the object's destructor. The following code shows approximately what takes place in the Free method of TObject:

procedure TObject.Free;

begin

  if Self <> nil then

    Destroy;

end;

The variable Self always points to the current object. It plays the same role in Object Pascal that this plays in C++. If you are inside one of the methods of an object, you can refer to that object by using Self. (Self is passed as an implicit parameter to all BCB methods.) Here is how the VCL Free method would look in C++:

void __fastcall TObject::Free()

{

  if (this != NULL)

   ~TObject();

}


NOTE: Programmers use the words descendant, child object, derived class, and subclass as synonyms. I prefer to use either descendant or child object, because subclass is also used in another context and derived class seems unnecessarily obscure. My feeling is that it's best to stick to one metaphor: parent, child, ancestor, and descendant, where child and descendant are synonymous, and parent and ancestor are synonymous.

Standard C++ does not define a Free method. This is something specific to the VCL. It is added to the VCL to make objects easier to use. It is very bad to call the destructor of an object that no longer exists. As a result, the Free method is there to provide a check that gives you some measure of protection against this error. Despite this, the general consensus is that it is best to call delete. The great virtue of delete is that it looks like standard C++ code, and C++ programmers care a lot about standards.

Now that you know how to declare, allocate, and deallocate a simple object, it's time to narrow the focus and tackle the subject of inheritance. The next two sections are dedicated to this chore--specifically, to explaining the relationship between a parent and child object.

Understanding Inheritance

In general, a child object can use any of its parent's methods. A descendant of an object gets the benefit of its parent's capabilities, plus any new capabilities it might bring to the table. I say that this is true in general, because the private directive can limit the capability of a child to call some of its parent's routines. The private directive is explained in depth later in this chapter.

Except for its constructor, all of TMyObject's methods and fields are inherited:

class TMyObject : public TObject

{

public:

  __fastcall TMyObject() : TObject()  {}

};


This declaration is somewhat deceiving because TObject contains many methods that are available to instances of TMyObject. In other words, TMyObject is not quite as simple an object as it appears at first.

So, what are all these methods associated with TObject? Well, you can see their definitions, as well as their implementations, if you open up the SysDefs.h file from the \BCB\Include\VCL subdirectory:

class   __declspec(delphiclass) TObject

{

  public:

    __fastcall TObject() {}

    __fastcall Free();

    TClass __fastcall ClassType();

    void __fastcall CleanupInstance();

    void * __fastcall FieldAddress(const 
ShortString &Name);

    static TObject * __fastcall InitInstance(TClass cls, void *instance);

    static ShortString __fastcall ClassName(TClass cls);

    static bool __fastcall ClassNameIs(TClass cls, const AnsiString string);

    static 
TClass __fastcall ClassParent(TClass cls);

    static void * __fastcall ClassInfo(TClass cls);

    static long __fastcall InstanceSize(TClass cls);

    static bool __fastcall InheritsFrom(TClass cls, TClass aClass);

    static void * __fastcall 
MethodAddress(TClass cls, const ShortString &Name);

    static ShortString __fastcall MethodName(TClass cls, void *Address);

    ...// Code omitted here

    virtual void __fastcall Dispatch(void *Message);

    virtual void __fastcall 
DefaultHandler(void* Message);

  private:

    virtual TObject* __fastcall NewInstance(TClass cls);

  public:

    virtual void __fastcall FreeInstance();

    virtual __fastcall ~TObject() {}

};

You can find the entire implementation of TObject in the Object Pascal System.pas unit that ships with BCB. Much of it is actually in assembler, but the source is there if you want to study it. You should, of course, also examine the declaration for TObject in SysDefs.h. The calls in SysDefs.h, however, ultimately resolve into calls to the Pascal implementation in System.pas. I should perhaps add that the TObject declaration in SysDefs.h is very hard to understand, but I promise you that it does end up resolving into calls that access the System.pas version of TObject.


NOTE: Although I have mentioned this subject before, it's probably once again time to stress the importance of viewing the Pascal source code to the VCL. Your version of BCB might or might not ship with the source, but if you don't have it and can possibly afford to buy it, you should think seriously about obtaining it. You should peruse the BCB\Include\VCL subdirectory that contains the header files for the imported VCL Pascal units. These files provide the interface for key BCB units. They are not as good as having the source, but they are very valuable. I refer to both the header files and the Pascal source continuously.

You can see that TObject has a few basic functions declared right at the top:

__fastcall TObject() {}

__fastcall Free();

virtual __fastcall ~TObject() {}

The point to grasp here is that TMyObject has a destructor and Free method because it inherits them from TObject.

To understand this point, you can add a line of code to the nascent OBJECT1 program:

#include <conio.h>

#include <stdio.h>

int main(void)

{

  TMyObject *MyObject = new 
TMyObject;

  AnsiString S = MyObject->ClassName();

  printf(S.c_str());

  delete MyObject;

  getch();

  return 0;

}

This code enables the object to write its name to the screen. The output from this program is a single string:

TMyObject

When you run the program, this string might flash by too quickly for leisurely perusal. To remedy the situation, add a getch() at the very end of the code, right before the return statement. To end this program, press Enter. (That's the way it used to be done back in the DOS world.)

If you want to, you can even get this object to say its parent's name:

int main(void)

{

  
TMyObject *MyObject = new TMyObject;

  printf(Format("ClassName: %s\nParent's ClassName: %s",

    OPENARRAY(TVarRec, (

      MyObject->ClassName(),

      MyObject->ClassParent()->ClassName()))).c_str());

  

  delete MyObject;

  
getch();

  return 0;

}

The output from this code is the following:

ClassName: TMyObject

Parent's ClassName: TObject

The point, of course, is that TMyObject inherits quite a bit of functionality from its parent, and, as a result, it has numerous capabilities that might not be obvious from merely viewing its declaration.

The ability to trace an object's ancestry is relatively appealing, so it might be nice to add it to TMyObject as a method:

#include <vcl/vcl.h>

#include <stdio.h>

#include <conio.h>

#include "classrefs.h"

USEUNIT("ClassRefs.cpp");



class TMyObject : public TObject


{

public:

  TMyObject() : TObject()  {}

  void PrintString(AnsiString S);

  void ShowHierarchy();

};



void TMyObject::PrintString(AnsiString S)

{

  printf("%s\n", S.c_str());

}



void TMyObject::ShowHierarchy()

{

  TClass 
AClass;



  AnsiString AClassName = AnsiString(ClassName()).c_str();

  PrintString(AClassName);

  AClass = ClassParent();

  while (AClass)

  {

    AClassName = AnsiString(AClass->ClassName());

    PrintString(AClassName);

    AClass = 
AClass->ClassParent();

  }

}



int main(void)

{

  ShowClassReferences();

  TMyObject *MyObject = new TMyObject;

  MyObject->ShowHierarchy();

delete MyObject;

  getch();

  return 0;

}

This version of the OBJECT1 program includes two methods, listed in the TMyObject class declaration:

class TMyObject : public TObject

{

public:

  TMyObject() : TObject()  {}

  void PrintString(AnsiString S);

  void ShowHierarchy();


};

Take a look at the implementation for ShowHierarchy. Perhaps the first thing you notice in it is the class reference in the first line, which uses the TClass type.

The type TClass is an object reference and is declared in Sysdefs.h as follows:

typedef TMetaClass* TClass;

Because ClassParent returns a variable of type TClass, it is obviously what needs to be used here.


NOTE: An object reference is a special metaclass that can be assigned to an object. Here is a unit that shows some legal uses of an object reference:

///////////////////////////////////////

// ClassRefs.cpp

// Object1

// copyright (c) 1997 by Charlie Calvert

//

#include <vcl\vcl.h>

#include <forms.hpp>

#pragma hdrstop

#include 
"ClassRefs.h"

TClass AClass;

  class TDescendant: public TObject

  {

  public:

    TDescendant(): TObject() {}



  };

void ShowClassReferences()

{

  printf("** Start object references **\n");

  AClass = __classid(TObject);

  printf("%s\n", AnsiString(AClass->ClassName()).c_str());

  AClass = __classid(TDescendant);

  
printf("%s\n", AnsiString(AClass->ClassName()).c_str());

  AClass = __classid(TForm);

  printf("%s\n", AnsiString(AClass->ClassName()).c_str());

  printf("** End object references **\n\n");

}

Notice that you do not have to create an object before you can use it with an object reference. In general, you can call any of the static methods of TObject with a class reference.

You cannot use an object reference to refer to a field that belongs only to the child of the object reference type. For instance, this code does not compile because Caption is not a property of TObject:

ObjectRef =  
__classid(TForm)

ObjectRef.Caption := `Sam';

WriteLn(ObjectRef.Caption);

You will find a version of the CLSREF unit in the same subdirectory as OBJECT1. You can use the Project Manager to add this file to the project, and you can then call it in the second line of the body of the OBJECT1 program. However, you should not leave this unit as part of the project, because it will muddy the view of the object hierarchy that you get in the Browser.

There are not that many times in which you need to use an object reference in day-to-day programming. If you are not totally clear on what they do, you can probably afford to skip the subject. If you really want to know more, you should examine Sysdefs.h; recognize that the TMetaClass you see there is the C++ way of creating a feature that exists in the VCL. The reason the VCL supports this feature is that it needs fairly extensive Run Time Type Information (RTTI) in order to run, and it gets a good portion of that information from the methods in TObject that are part of TMetaClass and are in turn used in an object reference.


When you get past the object reference, the remaining portions of the ShowHierarchy method are fairly straightforward:

void TMyObject::ShowHierarchy()

{

  TClass AClass;

  AnsiString AClassName = AnsiString(ClassName()).c_str();

  
PrintString(AClassName);

  AClass = ClassParent();

  while (AClass)

  {

    AClassName = AnsiString(AClass->ClassName());

    PrintString(AClassName);

    AClass = AClass->ClassParent();

  }

}



This code first writes the ClassName of the current object, which is TMyObject. Then it gets the ClassParent, which is TObject, and writes its name to the screen. The code then tries to get TObject's parent and fails, because TObject has no parent. At this point, AClass is set to NULL and the code exits the while loop. The output for the program is shown in Figure 19.3.

FIGURE 19.3. The output from the OBJECT1 program.

In this section, you have learned about the Create, Destroy, Free, ClassParent, and ClassName methods of TObject. The declaration of TObject shows that several other methods are available to BCB programmers. However, I do not discuss these methods in depth because they are either self-explanatory (InheritsFrom) or beyond the scope of this book. I should mention, however, that some of these routines are used by the compiler itself when dispatching routines or performing other complex tasks that usually require RTTI support. These are advanced programming issues that impact only a very small percentage of BCB programmers.

Virtual Methods

Inheritance, in itself, is an interesting feature, but it would not take on much significance were it not for the presence of virtual methods. Virtual methods can be overridden in a descendant class. As such, they provide the key to polymorphism, which is a trait of OOP programs that enables you to give the same command to two different objects but have them respond in different ways. This chapter introduces polymorphism, but I will leave the more complex aspects of this subject for Chapter 21, titled, appropriately enough, "Polymorphism." Polymorphism is a relatively difficult subject to grasp; therefore, I have stretched out a full explanation of it over several chapters.

Unlike Object Pascal, BCB has only one type of virtual method. This directive tells the compiler to store the address of the function in a virtual method table.


NOTE: Delphi programmers should note that C++ does not support either the dynamic or message directives. In their place, you can use the MESSAGE_MAP macro.

The OBJECT2 program (shown in Listing 19.2) has one virtual method. The virtual method is overridden in a child object. When you are creating the OBJECT2 program, you should start with the source code for the OBJECT1 program. Modify the code by declaring PrintString as virtual and by creating a descendant of TMyObject called THierarchy. Also, don't forget to make sure the program is set to work as a console application. If you don't have this option checked, you can get an EInOutError exception. After changing the setting, you should also rebuild your project so the new option takes effect.


NOTE: When creating one project based on another, you can often copy the code from the directory where the first project is stored into a separate directory made for the second project. After copying the project, it is probably simplest to delete everything but the actual source files from the new directory. For instance, delete the DSK file, the MAK file, and any other extraneous files you will not need. Then create a new project, delete its main form, and add copies of the source files you want to reuse from the previous project. Otherwise, you might find paths hard-coded into your DSK or MAK files that address files stored in the first program's directory. In this particular case, it is probably easiest just to re-create the project entirely from scratch, but the information in this note can be used as a general set of guidelines for use when copying projects from one directory to another.

Listing 19.2. The source code for the main unit in the OBJECT2 program.

///////////////////////////////////////

// Object2.cpp

// Project: Object2

// Copyright 
(c) 1997 by Charlie Calvert

//

#include <vcl/vcl.h>

#include <stdio.h>

#include <conio.h>



class TMyObject :public TObject

{

public:

  TMyObject() : TObject()  {}

  void ShowHierarchy();

  virtual void 
PrintString(AnsiString S);

};





class THierarchy: public TMyObject

{

  int FColor;

public:

  THierarchy() : TMyObject() {}

  virtual void PrintString(AnsiString S);

  __property int Color={read=FColor,write=FColor};

};



void 
TMyObject::PrintString(AnsiString S)

{

  printf("%s\n", S.c_str());

}



void TMyObject::ShowHierarchy()

{

  TClass AClass;



  AnsiString AClassName = AnsiString(ClassName()).c_str();

  PrintString(AClassName);



  AClass = 
ClassParent();

  while (AClass)

  {

    AClassName = AnsiString(AClass->ClassName());

    PrintString(AClassName);

    AClass = AClass->ClassParent();

  }

}



void THierarchy::PrintString(AnsiString S)

{

  char Temp[250];



  
textcolor(FColor);

  sprintf(Temp, "%s\n\r", S.c_str());

  cputs(Temp);

}



int main(void)

{

  TMyObject *MyObject = new TMyObject();

  MyObject->ShowHierarchy();

  MyObject->Free();



  THierarchy *Hierarchy = new 
THierarchy();

  Hierarchy->Color = YELLOW;

  Hierarchy->ShowHierarchy();

  Hierarchy->Free();



  getch();

  return 0;

}

In OBJECT1, the ShowHierarchy method wrote its output to the screen. Suppose that you found this object somewhere and liked the way it worked but wanted to change its behavior so it could also write its output in color. The OBJECT2 program shows a preliminary version of how you might proceed. After completing this first take on creating a descendant of TMyObject, I will revisit the subject and show ways to improve the model shown here. The output from the program is shown in Figure 19.4.

FIGURE 19.4. The output from the OBJECT2 program.

In the old world of structured programming, the most likely step would be to rewrite the original ShowHierarchy method. However, rewriting an existing method can be a problem for two reasons:

A combination of design and maintenance issues might deter the impulse to rewrite the original method. Many projects have been delayed or mothballed because changes in their designs have broken existing code and thrown the entire project into chaos.

OOP has a simple solution to this whole problem. Instead of declaring TMyObject as

class TMyObject :public TObject

{

public:

  TMyObject() : TObject()  {}

  void ShowHierarchy();

  void PrintString(AnsiString S);

};


thoughtful programmers declare it like this:

class TMyObject :public TObject

{

public:

  TMyObject() : TObject()  {}

  void ShowHierarchy();

  virtual void PrintString(AnsiString S);

};


The difference is that in the second example, the PrintString method is declared as virtual.

If PrintString is declared as virtual, you can override it in a descendant object, thereby changing the way the method works without ever changing the original version of the method. This means that all the other code that relies on the first version of the program continues to work, and yet you can rewrite the function for your own purposes. Furthermore, this technique would work even if you didn't have the source code for TMyObject! I will show you how this works in just a moment.

Some readers might have asked themselves earlier why I created the PrintString method in the first place. The answer hinges on the fact that iterating through a hierarchy of VCL objects can always be accomplished by the same algorithm. The same technique works for all VCL objects. But the act of printing information to the screen changes depending on your current circumstances. Are you in DOS? Are you in Windows? Do you want to use colors? Each of these circumstances calls for a different way of printing information to the screen. As a result, I separated the screen IO portion of TMyObject from the portion of the object that iterates through a hierarchy. Furthermore, there is no need to declare ShowHierarchy as virtual, but I must declare PrintString as virtual. The reasoning here is simply that ShowHierarchy does not need to change in descendants of the object, but PrintString will need to change. In particular, it will need to change so that it can write output in color. These types of considerations are part of a subject known as object design.

At this point, you might think that using virtual methods seems like an unnecessarily opaque solution to this problem. Wouldn't it have been simpler to add new methods to the inherited class? Then the user of this second class could call these new methods rather than the ones from the first instance of the class. There are three problems with this technique:

The word virtual is inherited from one class to the next. If a base class declares a method virtual, the descendants need not do so, because they will inherit the virtual declaration for a particular method from the base class. Delphi users take note, as this is the exact opposite of what happens in Object Pascal. I should add that it is generally considered bad form not to repeat the declaration in child objects, as you want to be sure the reader of your code can see at a glance how it is structured.

Here is a look at a stripped-down descendant of TMyObject that overrides the PrintString method:

class THierarchy: public TMyObject

{

  int 
FColor;

public:

  THierarchy() : TMyObject() {}

  virtual void PrintString(AnsiString S);

  __property int Color={read=FColor,write=FColor};

};

This declaration states that class THierarchy is a descendant of class TMyObject and that it overrides PrintString.


NOTE: Delphi programmers beware! The Pascal object model performs the same chore by using the override directive. That's not the way C++ works. This is a major change between the new BCB code and the old Object Pascal techniques.

A first take on the new version of the PrintString method looks like this:

void THierarchy::PrintString(AnsiString S)

{

  char Temp[250];

  textcolor(FColor);

  sprintf(Temp, "%s\n\r", S.c_str());

  cputs(Temp);

}

This code depends on functionality from Conio.h that allows you to print strings to the screen in color.

You can see that a field called FColor has been added to this object: THierarchy now contains not only procedures but also data. One of the key aspects of class declarations is that they can contain both methods and data, so that you can bring all the code related to the THierarchy object together in one place. This is part of a concept called encapsulation, explained in the next chapter.


NOTE: By convention, VCL objects declare private data as starting with the letter F, which stands for field. This technique is helpful, because it highlights the difference between an object's properties and its data. A property is published to the world and does not begin with the letter F. Private data is inaccessible to the rest of the world and begins with the letter F.

When you run the OBJECT2 program, the following code is executed in its main body:

int main(void)

{

  TMyObject *MyObject = new TMyObject();

  MyObject->ShowHierarchy();

  MyObject->Free();

  THierarchy 
*Hierarchy = new THierarchy();

  Hierarchy->Color = GREEN;

  Hierarchy->ShowHierarchy();

  Hierarchy->Free();

  getch();

  return 0;

}

This code creates an object of type THierarchy and then shows you how to use the new functionality of the ShowHierarchy method. I also create an instance of TMyObject so that you can compare the two classes demonstrated so far in this chapter.


NOTE: I should perhaps mention that it is more expensive to declare or call virtual methods than it is to call a static method. As a result, you need to weigh the whole issue of whether you want to declare a method to be virtual.

In my opinion, you should usually create objects that have the best possible design, re-gardless of the amount of overhead they entail. Of course, it is possible to take this theory too far, but the mere fact of adding a few virtual methods is usually not the problem in bloated object hierarchies.

Besides space and performance, a second reason for not declaring an object virtual is a desire to hide its implementation so you can change it later. There is usually no point in declaring a private method virtual, because it can't be seen by other objects, unless they are friends of the original object. (I will talk about friend objects later in this chapter.) The great advantage of private methods is that they can always be changed later to whatever degree you want, because other objects usually cannot see them and cannot access them directly. As a result, you may want to declare methods private, and non-virtual, so you can change their implementation later on. Needless to say, I am referring specifically to the act of changing the number of parameters these methods take.

Your users will, of course, complain if you take a method they occasionally want to override and make it private and non-virtual. However, it is sometimes better to listen to their complaints than to saddle them with a broken object that cannot be fixed without breaking existing code.

In this section, you have learned about the virtual directive. This subject's true significance won't be clear until you read about polymorphism. However, before you tackle that subject, it's best to learn more about inheritance and encapsulation. In particular, the next section of the chapter looks at more issues involving object design.

Searching for the Right Design

It is almost impossible to find the right design for an object the first time you write it. As a rule, the only way you can figure out the design for an object is by creating it, discovering its limitations, and then making improvements to its design. In short, object design is an iterative process.

There is no good way to step you through the process of discovering the correct object design in a book, because the written word is by its nature static, and the process I'm describing is dynamic. Furthermore, it's confusing to the reader to show a series of poorly designed objects that are successively improved in each iteration. The problem with this technique is that the reader keeps seeing the wrong way to create an object and can easily pick up bad habits or fundamental misconceptions about object design. Even worse, the reader tends to get frustrated with having to unlearn the techniques they just acquired in the previous example that are now revealed as being flawed.

To avoid the problems outlined in the preceding paragraph, I will simply show you a second version of the THierarchy object and explain why it contains changes to the original version of the object you saw in the last section. This process does not tell you much about how I discovered the flaws in the object, but it will show you what the flaws are and how I got around them. The main point to grasp is that object design is an iterative process, and that the correct way to find the flaws in an object is to implement them once as best you can, and then look for problems.


NOTE: There is a second school of thought that states that you can find the correct design for an object before implementing it. Proponents of this technique often suggest that a team be split in two, with part of the members designing objects and the other part implementing them. I have to confess that I've never actually discussed the results of this technique with someone who has used it successfully, as all my experience has been with programmers who use the iterative technique I discuss here. (Some of these programmers have also tried the second school of programming, but it did not work for them.)

Of course, you should try to get an object right the first time, and you should use high-level tools that help with design. However, you should also expect to have to refine the objects you create through a perhaps lengthy, iterative process.

Furthermore, you should design your object defensively. That is, you should carefully hide your implementation inside private methods and data because you will surely have to change its design at a later time.

The first problem with the original version of the THierarchy object became clear when I wanted to find a method to clear the screen. When doing so, I needed to first set the text and background color to which I wanted the screen to be cleared. As a result, I added new properties and new set methods to the object. The new property let me add a background color, and the set methods let me change the text and background colors at the same time I assigned them to the private data:

class THierarchy: public TMyObject

{

  int FTextColor;

  int FBackColor;

protected:

  virtual void SetTextColor(int Color)

    { FTextColor = 
Color; textcolor(FTextColor); }

  virtual void SetBackColor(int Color)

    { FBackColor = Color; textbackground(FBackColor); }

public:

  THierarchy() : TMyObject() {}

  virtual void PrintString(AnsiString S);

  virtual void ClrScr();

  
__property int TextColor={read=FTextColor,write=SetTextColor};

  __property int BackColor={read=FBackColor,write=SetBackColor};

};

The implementation of PrintString now looks like this:

void 
THierarchy::PrintString(AnsiString S)

{

  char Temp[250];

  sprintf(Temp, "%s\n\r", S.c_str());

  cputs(Temp);

}

Note that I have removed the code that changed the color of the text. This code is no longer necessary as the color gets changed in the SetTextColor method.

The extremely straightforward implementation of ClrScr looks like this:

void THierarchy::ClrScr()

{

  clrscr();

}


NOTE: C++ allows you to declare methods inline, as shown by the SetTextColor and SetBackColor method declarations. Conversely, you can also declare a method outside of the object, as I do in the case of ClrScr. This latter technique is called an out_of_line declaration, but that has such a ghastly ring in my ear that I refuse to use it. You can, in fact, implement a method outside an object and make it inline by using the inline directive:

inline void SetTextColor(int Color)

{

  FTextColor = Color;

  textcolor(FTextColor);

}

Inline methods usually execute faster than regular methods because they are placed directly in your code and do not require the overhead associated with a function call. Whether you implement them inside or outside of a class declaration, you should declare only very small methods as inline, and they should not contain any loops.

Inline methods are great, and I use them regularly. The only drawback I see to them is that they can make object declarations difficult to read. In particular, the great thing about an object declaration is that it can provide a summary of the functionality of an object without asking you to wade through its implementation. Inline methods implemented inside a class declaration detract from this feature because they clutter up the landscape.

One possible workaround is to declare inline functions separately from the object declaration by using the inline keyword. In fact, this is probably the ideal solution, and it is only laziness that keeps me from using it at all times. I, for one, would have been glad if the compiler enforced this rule.


The next change I made to THierarchy involved a desire to increase the flexibility of the object. In particular, it would be great to be able to use this object even if you do not descend from it directly. One way to do this is via multiple inheritance, but that technology is not supported by VCL objects. I should perhaps add that I am not particularly partial to multiple inheritance as a technology for use in real-world applications.

A much simpler way to make the object more flexible is to pass the ShowHierarchy method a copy of the object whose hierarchy you want to explore:

void TMyObject::ShowHierarchy(TObject *AnObject)

{

  TClass AClass;



  AnsiString AClassName = AnsiString(AnObject->ClassName()).c_str();

  PrintString(AClassName);

  AClass = 
AnObject->ClassParent();

  while (AClass)

  {

    AClassName = AnsiString(AClass->ClassName());

    PrintString(AClassName);

    AClass = AClass->ClassParent();

  }

}

The interesting thing about this change is that it occurs at the level of the TMyObject implementation and declaration. In other words, it represents changes not to THierarchy but to TMyObject. If this object were of more earth-shaking import and if I had released TMyObject to the public, I could not have made this sort of change because I would be breaking the code of those who already called the ShowHierarchy method. There are, I suppose, three things you can learn from this example:

1. Try not to publish any part of an object hierarchy until you are sure you can live with its current public interface.

2. Hide your implementation. Avoid letting consumers of your objects directly call one of the methods that involves a significant part of your implementation.

3. Object design is an art, not a science. There is no such thing as a perfect object. All objects have flaws. Plan them out carefully ahead of time, bring them to fruition by a lengthy iterative process, and then send them out into the world with the understanding that your design is flawed by definition. Recognize that you are going to have to support the objects you send into the world, because your users are going to find flaws in them. It is the mark of an amateur to stonewall the consumer of an object when he or she offers intelligent criticism of an object implementation. Likewise, it is naïve for the consumer of an object to expect it to be perfect. Perfection is too high a goal. Instead, shoot for high quality, and then demand that object producers provide fixes when flaws are discovered. Fixes need not arrive immediately, but they should appear in the next version of the product.

After making these changes, I decided that the object was ready to see the light of day. As a result, I moved it into its own file called MyObject.cpp. I then made a program called OBJECT3 that tests this new arrangement. The results of this effort are shown on the CD-ROM that accompanies this book in a program called OBJECT3. The code for this program is shown in Listings 19.3 through 19.5.

Listing 19.3. TMyObject and THierarchy now reside in their own file, called MyObject.

///////////////////////////////////////

// MyObject.h

// Learning how to use objects

// Copyright (c) 1997 by Charlie Calvert

//

#ifndef 
MyObjectH

#define MyObjectH

#include <conio.h>

#include <vcl\stdctrls.hpp>



class TMyObject :public TObject

{

public:

  TMyObject() : TObject()  {}

  void ShowHierarchy(TObject *AnObject);

  virtual void PrintString(AnsiString 
S);

};



class THierarchy: public TMyObject

{

  int FTextColor;

  int FBackColor;

protected:

  virtual void SetTextColor(int Color)

    { FTextColor = Color; textcolor(FTextColor); }

  virtual void SetBackColor(int Color)

    { FBackColor = 
Color; textbackground(FBackColor); }

public:

  THierarchy() : TMyObject() {}

  virtual void PrintString(AnsiString S);

  virtual void ClrScr();

  __property int TextColor={read=FTextColor,write=SetTextColor};

  __property int 
BackColor={read=FBackColor,write=SetBackColor};

};



#endif

Listing 19.4. The implementation for TMyObject and THierarchy are shown here in MyObject.cpp.

///////////////////////////////////////

// MyObject.cpp

// Learning how to use objects

// Copyright (c) 1997 by Charlie Calvert

//

#include <vcl\vcl.h>

#include <conio.h>

#pragma hdrstop

#include 
"myobject.h"



void TMyObject::PrintString(AnsiString S)

{

  printf("%s\n", S.c_str());

}



void TMyObject::ShowHierarchy(TObject *AnObject)

{

  TClass AClass;



  AnsiString AClassName = 
AnsiString(AnObject->ClassName()).c_str();

  PrintString(AClassName);



  AClass = AnObject->ClassParent();

  while (AClass)

  {

    AClassName = AnsiString(AClass->ClassName());

    PrintString(AClassName);

    AClass = 
AClass->ClassParent();

  }

}



void THierarchy::PrintString(AnsiString S)

{

  char Temp[250];



  sprintf(Temp, "%s\n\r", S.c_str());

  cputs(Temp);

}



void THierarchy::ClrScr()

{

  clrscr();

}

Listing 19.5. The test program for the MyObject unit.

#include <vcl\vcl.h>

#include <conio.h>

#pragma hdrstop

#include "myobject.h"


USEUNIT("MyObject.cpp");

void main()

{

  THierarchy *H = new THierarchy();

  H->TextColor = YELLOW;

  H->BackColor = BLUE;

  H->ClrScr();

  H->ShowHierarchy(H);

  delete H;

  getch();

}

Notice that I include code in the test program that "uses" the MyObject unit. I did not insert this code manually but instead used the program manager to add the MyObject module to the project. This is the best way to proceed, as it allows you to avoid editing the make file.

The program itself simply sets a background and text color for the output, then clears the screen to those colors and shows the hierarchy to the user. The next-to-last line in the program calls delete rather than Free. You can use either technique, depending on the dictates of your taste and background.

Showing the Hierarchy of a VCL Program

To really appreciate the new objects developed in the last section, you need to add them to a regular BCB application. For instance, if you use them in a standard Form1 class, here is the output you get:

TForm1

TForm


TScrollingWinControl

TWinControl

TControl

TComponent

TPersistent

TObject

This shows the whole hierarchy of class TForm1, starting with this, moving to TForm, back to TScrollingWinControl, and so on, all the way back to TObject.

As you know, VCL objects do not support multiple inheritance. As a result, I could not add the functionality of THierarchy to TForm by letting the object inherit it. This is fine with me, because I use multiple inheritance only very reluctantly.

Instead of multiple inheritance, I prefer to use a technique called aggregation. In aggregation, you add the object you want to use as a field of your current object and then expose its methods either by publishing the whole object as a property, or by wrapping the methods of the object inside methods of your current object. This technique allows you to easily take precise control of the functionality from the new object, and it tends to support clean, bug-free programming.

Before I show you how to use aggregation, I need to point out that it is not possible to use THierarchy directly in a Windows program because THierarchy uses Conio.h. The question then becomes, "Is THierarchy designed in such a way that I can descend from it and change its functionality so that it works with a Windows program?"

Well, part of the answer should be obvious. The key method that had to be virtual, the one that had to be overridden to make this work, is PrintString. And indeed, PrintString is declared virtual, so it is possible to change THierarchy's stripes. In fact, you will find that I also override the setters and the ClrScr method.


NOTE: You might feel that having to override so many methods in order to change the output of this program is an excessive amount of work when compared to the relatively simple task of rewriting the ShowHierarchy method from scratch. Indeed, when looked at from this perspective, the whole task of creating an object in this case seems fruitless. I readily confess that it would indeed have been simpler to write three versions of a non-OOP method called ShowHierarchyBlackandWhite, ShowHierarchyColor, and ShowHierarchyWindows. Here is a ShowHierarchy method tailored for use with a VCL form-based program:

void TForm1::ShowHierarchy()

{

  TClass AClass;

  Memo1->Clear();

  Memo1->Lines->Add(AnsiString(ClassName()));

  AClass = 
ClassParent();

  while (True)

  {

    AnsiString S = AnsiString(AClass->ClassName());

    Memo1->Lines->Add(S);

    if (AnsiString(AClass->ClassName()) == "TObject")

      break;

    AClass = AClass->ClassParent();

  }


}

Clearly, this object is easier to write than the objects I have produced here. The key reason I created an object like THierarchy is simply that it illustrates how OOP works. In real life, objects often contain 20, 30, or even 50 methods. When seen in that light, having to override three or four methods does not seem like such a big chore. Furthermore, you can often use delegation to solve these kinds of problems.

Of course, this isn't real life but a book on programming. I have therefore intentionally created a small object with few methods so that I can focus your attention solely on virtual methods that need to be overridden. If I had cluttered up this chapter with a big object, you never could have seen the trees for the forest, because at least half the chapter would have involved an explanation of a huge object.

Furthermore, the ShowHierarchy method is the difficult method in this program. It's easy to write the methods that need to be overridden, while some programmers might have trouble creating ShowHierarchy. In this sense, even the rather simple object I have created here does a good job of hiding complexity and promoting bug-free reuse.

A final point about this subject is that OOP's main strength is not in producing small code, nor is it true that objects are necessarily easier to create than equivalent structured code. Rather, the strength of OOP is in letting you easily and safely reuse premade objects. Now that these objects are completed, they can be reused easily in any WinTel-based C++ program, whether it runs in Windows or from the command line. OOP never makes sense if you think in terms of writing your whole program from scratch. Instead, the advantage of OOP comes when you want to create programs by reusing premade objects. OOP is about code reuse and about writing bug-free programs; it is not about finding the smallest possible implementation of an algorithm. When you add components to OOP, you can also get RAD, which is another major boost in productivity.


In Listings 19.6 through 19.9, you will find the code for the new version of the Object1.cpp module, as well as the code for a standard VCL form-based program called HIERARCHY that uses the object. The output from the program is shown in Figure 19.5.

FIGURE 19.5. The HIERARCHY form sports a TButton and a TMemo component.

Listing 19.6. The new code for the MyObject header file.

///////////////////////////////////////

// MyObject.h

// Learning how to use objects

// Copyright (c) 1997 by Charlie Calvert

//

#ifndef MyObjectH

#define MyObjectH

#include <conio.h>

#include 
<vcl\stdctrls.hpp>



class TMyObject :public TObject

{

protected:

  virtual void PrintString(AnsiString S);

public:

  TMyObject() : TObject()  {}

  void ShowHierarchy(TObject *AnObject);

};



class TListHierarchy: public TMyObject

{


private:

  TStringList *FList;

protected:

  virtual void PrintString(AnsiString S)

    { FList->Add(S); }

public:

  TListHierarchy() { FList = new TStringList(); }

  __fastcall virtual ~TListHierarchy() { delete FList; }

  TStringList 
*GetHierarchy(TObject *AnObject)

    { FList->Clear(); ShowHierarchy(AnObject); return FList; } 

};



class __declspec(delphiclass) TVCLHierarchy;



class THierarchy: public TMyObject

{

  friend TVCLHierarchy;

  int FTextColor;

  int 
FBackColor;

protected:

  virtual __fastcall void SetTextColor(int Color)

    { FTextColor = Color; textcolor(FTextColor); }

  virtual __fastcall void SetBackColor(int Color)

    { FBackColor = Color; textbackground(FBackColor); }

public:

  
THierarchy() : TMyObject() {}

  virtual void PrintString(AnsiString S);

  virtual void ClrScr();

  __property int TextColor={read=FTextColor,write=SetTextColor};

  __property int BackColor={read=FBackColor,write=SetBackColor};

};



class 
TVCLHierarchy : public THierarchy

{

  TMemo *FMemo;

protected:

  virtual __fastcall void SetTextColor(int Color)

    { FTextColor = Color; FMemo->Font->Color = TColor(FTextColor); }

  virtual __fastcall void SetBackColor(int Color);




public:

  TVCLHierarchy(TMemo *AMemo): THierarchy() { FMemo = AMemo; }

  virtual void PrintString(AnsiString S)

    { FMemo->Lines->Add(S); }

  virtual void ClrScr() { FMemo->Clear(); }

};



#endif

Listing 19.7. The new code for the MyObject implementation.

///////////////////////////////////////

// 
MyObject.cpp

// Learning how to use objects

// Copyright (c) 1997 by Charlie Calvert

//

#include <vcl\vcl.h>

#include <conio.h>

#pragma hdrstop

#include "myobject.h"



void TMyObject::PrintString(AnsiString S)

{

  
printf("%s\n", S.c_str());

}



void TMyObject::ShowHierarchy(TObject *AnObject)

{

  TClass AClass;



  AnsiString AClassName = AnsiString(AnObject->ClassName()).c_str();

  PrintString(AClassName);



  AClass = 
AnObject->ClassParent();

  while (True)

  {

    AClassName = AnsiString(AClass->ClassName());

    PrintString(AClassName);

    if (AClassName == "TObject")

      break;

    AClass = AClass->ClassParent();

  }

}



void 
THierarchy::PrintString(AnsiString S)

{

  char Temp[250];



  sprintf(Temp, "%s\n\r", S.c_str());

  cputs(Temp);

}



void THierarchy::ClrScr()

{

  clrscr();

}



void __fastcall TVCLHierarchy::SetBackColor(int Color)

{

  
FBackColor = Color;

  FMemo->Color = TColor(FBackColor);

}

Listing 19.8. The code for the HIERARCHY header file.

///////////////////////////////////////

// Main.h

// Copyright (c) 1997 by Charlie Calvert

//



#ifndef MainH

#define MainH



#include <vcl\Classes.hpp>

#include <vcl\Controls.hpp>


#include <vcl\StdCtrls.hpp>

#include <vcl\Forms.hpp>

#include <vcl\ExtCtrls.hpp>

#include "MyObject.h"





class TForm1 : public TForm

{

__published:

  TButton *ShowHierarchyBtn;

  TMemo *Memo1;

  TPanel *Panel1;

  
void __fastcall ShowHierarchyBtnClick(TObject *Sender);

  void __fastcall FormDestroy(TObject *Sender);

private:

  TVCLHierarchy *FHierarchy;

  void ShowHierarchy(TObject *Object);

public:

  virtual __fastcall TForm1(TComponent* Owner);

  
__property TVCLHierarchy *Hierarchy=

    {read=FHierarchy, write=FHierarchy};

};



extern TForm1 *Form1;



#endif

Listing 19.9. The code for the HIERARCHY program.

///////////////////////////////////////

// Main.cpp

// Copyright (c) 1997 by Charlie Calvert

//

#include <vcl\vcl.h>

#pragma hdrstop

#include "Main.h"

#pragma resource "*.dfm"

TForm1 *Form1;




__fastcall TForm1::TForm1(TComponent* Owner)

  : TForm(Owner)

{

  FHierarchy = new TVCLHierarchy(Memo1);

  Hierarchy->BackColor = clBlue;

  Hierarchy->TextColor = clYellow;

}



void __fastcall TForm1::FormDestroy(TObject *Sender)

{

  
delete Hierarchy;

}



void TForm1::ShowHierarchy(TObject *Object)

{

  Hierarchy->ShowHierarchy(Object);

}



void __fastcall TForm1::ShowHierarchyBtnClick(TObject *Sender)

{

  ShowHierarchy(this);

}

When you run this program, it will display the hierarchy of the TForm object in a memo control. It does so by creating an aggregate of the THierarchy object and the TForm1 object.

Creating Friend Objects

Here is the declaration for the descendant of THierarchy that works in Windows:

class TVCLHierarchy : public THierarchy

{

  TMemo *FMemo;

protected:

  virtual void 
SetTextColor(int Color)

    { FTextColor = Color; FMemo->Font->Color = TColor(FTextColor); }

  virtual void SetBackColor(int Color)

    { FBackColor = Color; FMemo->Color = TColor(FBackColor); }

public:

  TVCLHierarchy(TMemo *AMemo): 
THierarchy() { FMemo = AMemo; }

  virtual void PrintString(AnsiString S)

    { FMemo->Lines->Add(S); }

  virtual void ClrScr() { FMemo->Clear(); }

};

Several changes to this program should jump right out at you. First, notice that the constructor now takes a parameter. This parameter is the memo control that will be used to display text to the user. The SetTextColor, SetBackColor, PrintString, and ClrScr methods have all been rewritten to take advantage of this new control.

As a result, the only parts of the original object that have come through unchanged are as shown in the following pseudocode:

class THierarchy: public TMyObject}

{

  
int FTextColor;

  int FBackColor;

public:

  void ShowHierarchy(TObject *AnObject); // inherited from TMyObject

  __property int TextColor={read=FTextColor,write=SetTextColor};

  __property int BackColor={read=FBackColor,write=SetBackColor};

};


In this particular case, it is not possible to use the TextColor and BackColor properties to access the private FTextColor and FBackColor data stores. Instead, I declare TVCLHierarchy to be a friend of THierarchy:

class TVCLHierarchy;

class THierarchy: public TMyObject

{

  friend TVCLHierarchy;



  int FTextColor;

  int FBackColor;

  ... // Code omitted here

}

Notice that TVCLHierarchy is declared as a friend in the first line of the THierarchy declaration. This means that TVCLHierarchy has access to the private data of THierarchy. Descendants of TVCLHierarchy will not inherit this trait.

Using Aggregation

Rather than use multiple inheritance to access TVCLHierarchy, TForm1 declares a field of this type:

class TForm1 : public TForm

{

  ... // Code omitted here

  TVCLHierarchy *Hierarchy;

  void ShowHierarchy();

public:

  virtual __fastcall TForm1(TComponent* Owner);

  __property TVCLHierarchy *Hierarchy=

    {read=FHierarchy, 
write=FHierarchy};

};

I have also added a method called ShowHierarchy and a new property called Hierarchy so that descendants of TForm1 can have access to the BackColor and TextColor properties of TVCLHierarchy.

A fine point of object design could be debated here. In this particular case, I have created a property of type TVCLHierarchy that is exposed to consumers of TForm1. Alternatively, I could have completely hidden TVCLHierarchy behind a set of properties that provided access to the FTextColor and FBackColor fields of THierarchy:

class TForm1 : public TForm

{

  ... // 
Code omitted here

  TVCLHierarchy *Hierarchy;

  void ShowHierarchy(TObject *Object);

  int GetHierarchyBackColor();

  void SetHierarchyBackColor(int Color);

  int GetHierarchyTextColor();

  void SetHierarchyTextColor(int Color);

public:

  
virtual __fastcall TForm1(TComponent* Owner);

  __property int HierarchyTextColor=

    {read=GetHierarchyTextColor, write=SetHierarchyTextColor};

  __property int HierarchyBackColor=

    {read=GetHierarchyBackColor, write=SetHierarchyBackColor};


};

This technique is very pure from an OOP-based point of view, and it is perhaps a more classic and complete example of aggregation than the one I use. However, it is not strictly necessary to create the HierarchyTextColor and HierarchyBackColor properties. Instead, I can make the Hierarchy object public by making it a property of type TVCLHierarchy. The reason this approach is acceptable is because FHierarchy, as well as the FTextColor and FBackColor fields of THierarchy, are all still hidden behind properties.

It would be wrong, however, to make FHierarchy public or to allow users to directly access the FTextColor or FBackColor fields of THierarchy. The reason this is wrong is because it exposes your data to the public, thereby limiting your choices when it comes time to maintain or improve your program.

The technique shown here of exposing TVCLHierarchy via a property shows up everywhere in the VCL. Most notably, it is present in the Items or Lines fields of TListBox, TComboBox, and TMemo. The point here is that you want to use properties to protect your data and your implementation, but you don't want to take things so far that your code becomes needlessly bloated.

However, I do not feel that it would be incorrect or wrong to completely hide FHierarchy from consumers of TForm1 and to instead expose its properties via properties of TForm1. The great advantage of this technique would be that it would allow a seamless aggregation of TForm1 and TVCLHierarchy.


NOTE: If you sense me waffling a bit here, that is because I do not believe there is a hard and fast answer as to what is best in a situation like this. It happens that there are good arguments on both sides. Component design is an art, not a science. The moment it becomes a science, most programmers are going to be out of a job.

In the constructor for TForm1, you should create the TVCLHierarchy object:

__fastcall TForm1::TForm1(TComponent* Owner)

  : TForm(Owner)

{

  FHierarchy = new TVCLHierarchy(Memo1);

  Hierarchy->BackColor = clBlue;

  Hierarchy->TextColor = 
clYellow;

}

As you can see, I have chosen to hard code default values for the BackColor and TextColor properties of TVCLHierarchy into this constructor. This is not necessarily the best thing to do from the point of view of a descendant of TForm1, but it will not cause any problems in the simple example I am creating here.

At last, I am able to create a wrapper for the ShowHierarchy method:

void 
TForm1::ShowHierarchy(TObject *Object)

{

  Hierarchy->ShowHierarchy(Object);

}

Once again, you could probably just as easily let consumers of TForm1 access this method via the Hierarchy property. However, I provide this method in part because it is easier to use than a property, and in part because I want to illustrate explicitly how aggregation works.

Finally, I have included a method of TForm1 that calls the TForm1::ShowHierarchy method:

void __fastcall TForm1::ShowHierarchyBtnClick(TObject *Sender)

{

  ShowHierarchy(this);

}

This method does nothing to forward my example of aggregation, and I include it solely for expediency's sake. A real-world example of aggregation would leave TForm1 without features of this type and would instead expect you to inherit from it in order to access the functionality exposed via Hierarchy and ShowHierarchy. For an example of this type of program, see the HIERARCHY2 program in the Chap19 directory.

A reader could object that rather than using aggregation, I could have declared a single method that looked like this:

void __fastcall TForm1::ShowHierarchyBtnClick(TObject *Sender)

{

  THierarchy *Hierarchy = new TVCLHierarchy(Memo1);

  Hierarchy->BackColor = clBlue;

  Hierarchy->TextColor = clYellow;

  
Hierarchy->ShowHierarchy();

  delete Hierarchy;

}

Once again, I have to confess that this probably is a simpler technique in this particular case. However, if you created descendants of TForm1, aggregation would allow them to call the ShowHierarchy method with one line of code. It would, in effect, add the functionality of ShowHierarchy to TForm, so that TForm1 appeared to be an "aggregate" of two objects. Admittedly, the simplicity of the example I show here makes these advantages difficult to discern, but when you are working with larger, more complex objects, this technology really starts to shine. OOP is about managing complexity, but it is easier to teach OOP if you use simple examples.

By this time, you should have a fairly good grasp of inheritance and hierarchies. The key point to understand is that, in general, a child object inherits the capability to use any of its parent's methods. In other words, it comes into an inheritance, where the inheritance is the methods, fields, and properties of its parent. Except for TObject itself, all VCL objects have parents that can trace their roots back to TObject.

Form Inheritance and Aggregation

Form inheritance is a powerful technique used by VCL programs. Here is a description of how to use it.

1. In the HIERARCHY2 program, I remove the ShowHierarchy button from the Form1 file used in the HIERARCHY program.

2.
I also rename Form1 to HierarchyForm1. I then save the old Form1 file as a new file called HierarchyForm.

3. Next I start a new project and remove its main form.

4. I add the HierarchyForm to this new project and use the Object Repository to create a new form that descends from HierarchyForm1. To do this, choose File | New, click on the HIERARCHY2 page in the Object Repository, and choose to inherit a new form from HierarchyForm1.

5. Save the new form you have created as Main.cpp.

6. Open the Options | Project menu and use the Forms page to set Form1 as the main form for your application. In other words, make the descendant of the HierarchyForm the main form for your application.

When you are done, the declaration for your main form should look like this:

///////////////////////////////////////

// HierarchyForm.h

// Copyright (c) 1997 by Charlie Calvert

//

#ifndef 
HierarchyFormH

#define HierarchyFormH

#include <vcl\Classes.hpp>

#include <vcl\Controls.hpp>

#include <vcl\StdCtrls.hpp>

#include <vcl\Forms.hpp>

#include <vcl\ExtCtrls.hpp>

#include "Myobject.h"



class 
THierarchyForm1 : public TForm

{

published:

  TMemo *Memo1;

  TPanel *Panel1;

  void __fastcall FormDestroy(TObject *Sender);

private:

  TVCLHierarchy *FHierarchy;

protected:

  void ShowHierarchy(TObject *Object);

public:

  virtual 
__fastcall THierarchyForm1(TComponent* Owner);

  __property TVCLHierarchy *Hierarchy=

    {read=FHierarchy, write=FHierarchy};

};



extern THierarchyForm1 *HierarchyForm1;

#endif



Go into the declaration for HierarchyForm1 and make sure that ShowHierarchy is declared in the public or protected section.

Now add a button to the main form called ShowHierarchy and add the following event handler to its OnClick event:

void __fastcall TForm1::ShowHierarchyBtnClick(TObject *Sender)

{

  ShowHierarchy(this);

}

Now run the program, and when you click on ShowHierarchy, you should get the following result:

TForm1

THierarchyForm1

TForm

TScrollingWinControl

TWinControl

TControl

TComponent

TPersistent

TObject

As you can see, the hierarchy now goes from TForm1 to HierarchyForm1 to TForm, and so on. TForm1 is descended from THierarchyForm1, and it therefore inherits the ability to all the things a regular form can do, plus it can show its own hierarchy.

Once again, there are a number of quibbles you could make here. For instance, it might be better to remove the visual controls from the HierarchyForm, and so on. However, the main point of this exercise is merely to illustrate how form inheritance works, and the code shown here does that.

For the sake of clarity, I have included the complete code to the program in Listings 19.10 through 19.14. Remember that you need to use the visual tools to inherit from the HierarchyForm, or Form1 will not have the correct appearance.

Listing 19.10. The header for the HierarchyForm from the HIERARCHY2 program.

///////////////////////////////////////

// HierarchyForm.h

// Copyright (c) 1997 by Charlie Calvert

//

#ifndef HierarchyFormH

#define HierarchyFormH

#include <vcl\Classes.hpp>

#include <vcl\Controls.hpp>

#include 
<vcl\StdCtrls.hpp>

#include <vcl\Forms.hpp>

#include <vcl\ExtCtrls.hpp>

#include "Myobject.h"



class THierarchyForm1 : public TForm

{

__published:

  TMemo *Memo1;

  TPanel *Panel1;

  void __fastcall 
FormDestroy(TObject *Sender);

private:

  TVCLHierarchy *FHierarchy;

protected:

  void ShowHierarchy(TObject *Object);

public:

  virtual __fastcall THierarchyForm1(TComponent* Owner);

  __property TVCLHierarchy *Hierarchy=

    {read=FHierarchy, 
write=FHierarchy};

};



extern THierarchyForm1 *HierarchyForm1;



#endif



Listing 19.11. The main form for the HierarchyForm.

///////////////////////////////////////

// HierarchyForm.cpp

// Copyright (c) 1997 by Charlie Calvert

//

#include <vcl\vcl.h>

#pragma hdrstop

#include "HierarchyForm.h"

#pragma resource "*.dfm"


THierarchyForm1 *HierarchyForm1;

__fastcall THierarchyForm1::THierarchyForm1(TComponent* Owner)

  : TForm(Owner)

{

  FHierarchy = new TVCLHierarchy(Memo1);

  Hierarchy->BackColor = clBlue;

  Hierarchy->TextColor = clYellow;

}



void 
__fastcall THierarchyForm1::FormDestroy(TObject *Sender)

{

  delete Hierarchy;

}



void THierarchyForm1::ShowHierarchy(TObject *Object)

{

  Hierarchy->ShowHierarchy(Object);

}

Listing 19.12. The header for the programs main form.

///////////////////////////////////////

// Main.h

// Hierarchy2

// Copyright (c) 1997 by Charlie Calvert

//

#ifndef MainH

#define 
MainH

#include <vcl\Classes.hpp>

#include <vcl\Controls.hpp>

#include <vcl\StdCtrls.hpp>

#include <vcl\Forms.hpp>

#include <vcl\ExtCtrls.hpp>

#include "Myobject.h"

#include "HierarchyForm.h"






class TForm1 : public THierarchyForm1

{

__published:

  TButton *ShowHierarchyBtn;

  void __fastcall ShowHierarchyBtnClick(TObject *Sender);

private:

public:

  virtual __fastcall TForm1(TComponent* Owner);

};



extern TForm1 *Form1;



#endif



Listing 19.13. The main form for the program.

///////////////////////////////////////

// Main.cpp

// Hierarchy2

// Copyright (c) 1997 by 
Charlie Calvert

//

#include <vcl\vcl.h>

#pragma hdrstop

#include "Main.h"

#include "HierarchyForm.h"

#pragma link "HierarchyForm"

#pragma link "HierarchyForm"

#pragma resource "*.dfm"




TForm1 *Form1;

//--------------------------------------------------------------------------

__fastcall TForm1::TForm1(TComponent* Owner)

  : THierarchyForm1(Owner)

{

}



void __fastcall TForm1::ShowHierarchyBtnClick(TObject *Sender)

{

  
ShowHierarchy(this);  

}

//--------------------------------------------------------------------------



Listing 19.14. The project source for the HIERARCHY2 program.

#include <vcl\vcl.h>

#pragma hdrstop



USERES("Hierarchy2.res");

USEFORM("Main.cpp", Form1);

USEFORM("HierarchyForm.cpp", HierarchyForm1);


USEUNIT("..\..\Utils\MyObject.cpp");



WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)

{

  try

  {

    Application->Initialize();

    Application->CreateForm(__classid(TForm1), &Form1);

    
Application->CreateForm(__classid(THierarchyForm1), &HierarchyForm1);

    Application->Run();

  }

  catch (Exception &exception)

  {

    MessageBox(NULL, exception.Message.c_str(), Application->Title.c_str(),

       MB_OK);

  }

  
return 0;

}

I have included the complete source for this program so you can double-check your work against it in case anything goes wrong. The key parts you need to concentrate on are the class declarations in the headers, the declarations for the global variable for the HierarchyForm, and the headers for the various methods used in the forms. The actual implementation of the methods was already cleared up in the HIERARCHY program, so you don't need to check them. The HIERARCHY2 program is shown in Figure 19.6.

FIGURE 19.6. The HIERARCHY2 program at runtime.

You should spend some time adding controls to the HierarchyForm and watching how they then automatically appear on the main form for the program. Notice that if you move a control on the HierarchyForm, the corresponding control on the main form will also move. If you move a control on the main form, however, that breaks the connection between that control and the corresponding control on the HierarchyForm. To restore the connection, right-click on the control in the main form and choose Revert To Inherited from the menu.

You can disconnect and revert properties one at a time if you wish. For instance, if you move the left side of a component on the main form, that will break the connection for that one property, but it will leave the remaining properties still connected to the HierarchyForm. To restore the Left property, choose that property in the Object Inspector with the right mouse button and select Revert To Inherited from the menu. To test this, you might want to drop a single button in the middle of the HierarchyForm, then switch back to the main form and work on changing just one of the inherited button's properties, such as its Left property or its Caption.

Summary

In this chapter, you have had a good chance to start working with object-oriented programming. However, there are still several big topics to tackle, including encapsulation and polymorphism. Rather than trying to cover such big topics inside this already lengthy chapter, I have decided to break things up and give them their own chapters where they can have plenty of room to unfold naturally.

Plenty of material was covered in this chapter, including inheritance, virtual methods, aggregation, and form inheritance. If you are new to this material, it would only be natural that some of it did not sink in the first time around. If so, you can either re-read the chapter or go on to the next chapter and see if some of it doesn't start to become clear when you work with new examples that approach the material from a slightly different angle.

Inheritance and virtual methods are everywhere in the BCB, and the more you work with them, the easier it will be to understand the principles involved. However, this is material that you must master if you want to be good at using BCB--and particularly if you want to start creating your own components.

TOCBACKFORWARD

©Copyright, Macmillan Computer Publishing. All rights reserved.