Overview
This document describes a method by which features of Object-Oriented Programming (OOP) can be used in straight C, including Standard/ANSI C, and some variants of pre-ANSI C. The following compilers have been tested:
- Visual Studio 6.x +
- GCC
- LCC
- Turbo C 2.0
OOP constructs are implemented via C macros so that source code is easily readable. Knowledge of OOP is assumed as well as rudimentary knowledge of C++.
Background
This can serve as an academic exercise for the reader who wants to see how object-oriented features can be built by-hand into a non-object-oriented language. It can also have a practical purpose in scenarios where the C compiler needs to be used as as an intermediate step of a domain-specific language, or when the target device has limited compiler support.
Defining a class
To use this framework, each file should include the SOOC.h file.
Every class in the SOOC system is derived from another class. If a class does not require a base class, the special class called Object
is used as the base class. The definition of each class file is in a separate header file.
Since the best way to learn is by example, one is shown here.
In this example, the class Animal
is derived from Object
. The class contains one data member, a 31 character string called _species
, two virtual methods called Talk
and IsDomesticated
, and two non-virtual methods called Report
and SaySpecies
.
Here's a side-by-side comparison of the class definition in C++ and in SOOC:
C++ | SOOC |
class Animal
{
public:
char m_species[32];
virtual void Talk();
virtual int
IsDomesticated();
void Report();
void SaySpecies();
};
|
#undef CLASS
#define CLASS Animal
#undef BASECLASS
#define BASECLASS Object
BEGIN_CLASS
char m_species[32];
BEGIN_VIRTUAL_METHODS
VIRTUAL_METHOD( Talk )
VIRTUAL_METHOD( IsDomesticated )
END_VIRTUAL_METHODS
BEGIN_NONVIRTUAL_METHODS
NONVIRTUAL_METHOD( void, Report )( CLASS* this );
NONVIRTUAL_METHOD( void, SaySpecies )( CLASS* this );
END_NONVIRTUAL_METHODS
END_CLASS
|
Difference from C++
- Unlike in C++, the virtual methods
Talk
and IsDomesticated
do not specify an argument list. In SOOC, the argument list is specified when the method is implemented, and must be compatible with the parameters specified when the method is called. - Unlike in C++, the
this
argument is explicitly specified as the first argument of the non-virtual method. Note that the special macro CLASS
, as defined at the top of the class, is used to reference the class name in a more generic way.
The macro pairs BEGIN_VIRTUAL_METHODS
, END_VIRTUAL_METHODS
, BEGIN_NONVIRTUAL_METHODS
, and END_NONVIRTUAL_METHODS
are required even if no virtual or non-virtual methods are specified.
The implementation of each class file is defined in a separate source file. To continue with the above example, by implementing the class:
C++ | SOOC |
Animal::Animal()
{
strcpy( this->m_species,
"Animal" );
}
Animal::~Animal()
{}
void Animal::SaySpecies()
{
...some implementation...
}
void Animal::Report()
{
...some implementation...
}
void Animal::Talk()
{
...some implementation...
}
int Animal::IsDomesticated()
{
...some implementation...
}
|
BEGIN_CLASS_IMPLEMENTATION
BEGIN_CONSTRUCTOR
{
strcpy( this->m_species, "Animal" );
}
END_CONSTRUCTOR
BEGIN_DESTRUCTOR
{}
END_DESTRUCTOR
BEGIN_NONVIRTUAL_METHOD( void, SaySpecies )( CLASS* this )
{
...some implementation...
}
END_NONVIRTUAL_METHOD
BEGIN_NONVIRTUAL_METHOD( void, Report )( CLASS* this )
{
...some implementation...
}
END_NONVIRTUAL_METHOD
BEGIN_VIRTUAL_METHOD( void, Talk )( CLASS* this )
{
...some implementation...
}
END_VIRTUAL_METHOD
BEGIN_VIRTUAL_METHOD( int, IsDomesticated )( CLASS* this )
{
...some implementation...
}
END_VIRTUAL_METHOD
BEGIN_OVERRIDES
OVERRIDE( Animal, Talk )
OVERRIDE( Animal, IsDomesticated )
END_OVERRIDES
END_CLASS_IMPLEMENTATION
|
Difference from C++
- Every class must implement an explicit constructor and a destructor, even if they are empty.
- The implementation of each method must indicate if it is implementing a virtual or a non-virtual method.
Note the BEGIN_OVERRIDES
/ END_OVERRIDES
macros toward the end of the class. They list the virtual methods that this class overrides. The macro OVERRIDE
takes two parameters, the name of the class in which the method is defined, and the name of the method being overridden.
Here, a class named Who
which will be derived from Animal
. The top of the definition for the Who
class looks like this:
#undef CLASS
#define CLASS Who
#undef BASECLASS
#define BASECLASS Animal
The Overrides
section of the implementation file looks like this:
BEGIN_OVERRIDES
OVERRIDE( Animal, IsDomesticated )
END_OVERRIDES
Note that this indicates that the method from the Animal
class is being overridden.
As in C++, a class may be instantiated on the stack or the free-store (heap). Unlike in C++, the class constructor and destructor must be invoked manually. This is done using a special CONSTRUCT
and DESTRUCT
macro. The following is an example of instantiating the Animal
class on the stack:
Animal animal;
CONSTRUCT( Animal, &animal );
...use the instance...
DESTRUCT( &animal );
The following is an example of instantiating a class on the free-store. This is done using the special NEW
macro:
Cat* pCat = NEW( Cat );
CONSTRUCT( Cat, pCat );
...use the instance...
DELETE( pCat );
Note that, in the case where the class is instantiated dynamically, the DELETE
macro, rather than the DESTRUCT
macro, is used. The DELETE
macro internally destructs the instance and then deletes the memory associated with it.
The class Cat
derives from Who
. Its constructor sets the m_species
data member of the Animal
class. However, unlike in C++, the namespace of data-members aren't automatically inherited. That is, if this were C++, from the body of a Cat
method, one would be able to access the m_species
data-member as if it were defined in the Cat
class. However, in SOOC, the name of the class in which the data member is defined must be specifically referenced. Example:
BEGIN_CONSTRUCTOR
{
strcpy( ((Animal*)this)->m_species, "Cat" );
}
END_CONSTRUCTOR
Because m_species
is defined in the Animal
class, the Cat class instance specified by the this
variable is cast to the Animal
class first.
Method access
Methods are invoked using the CALL
and VCALL
macros, depending on whether the method is non-virtual or virtual. The following is an example of calling the GetName
method on the pCat
instance where GetName
is a method defined in the base class Who
:
CALL( Who, GetName )( (Who*)pCat, name );
The VCALL
macros requires a few more parameters. In this example, the VCALL
macro is used to call the Talk
method of the pCat
instance. Recall that the Talk
method was defined in the Animal
class and it returns void
. This information must be supplied when making the method call because the prototype for the virtual methods are not specified in the definition of the method. The call looks like this:
VCALL( RVOID, Animal, Talk, pCat )( pCat );
The parameters to VCALL
are as follows:
- The return type of the method.
RVOID
, RLONG
, etc. - The class in which the virtual method is defined.
- The method to be called.
- The instance being operated on.
The second set of parenthesis, in this case (pCat
), indicates the parameters to the function. In this case, only the this
pointer is passed.
Before the classes can be used, one source file, typically the one that defines the main function, will include the list of classes within the special declaration macros BEGIN_DECLARE_APPLICATION_CLASSES
and END_DECLARE_APPLICATION_CLASSES
. Example:
BEGIN_DECLARE_APPLICATION_CLASSES
DECLARE_APPLICATION_CLASS( Animal )
DECLARE_APPLICATION_CLASS( Who )
DECLARE_APPLICATION_CLASS( Human )
DECLARE_APPLICATION_CLASS( Dog )
DECLARE_APPLICATION_CLASS( ShihTzu )
DECLARE_APPLICATION_CLASS( Beagle )
DECLARE_APPLICATION_CLASS( Cat )
DECLARE_APPLICATION_CLASS( Giraffe )
DECLARE_APPLICATION_CLASS( Cow )
END_DECLARE_APPLICATION_CLASSES
The class must be listed in the derivation order.
This concludes the description of the SOOC library. See the included sample code for more information on how classes are instantiated, used, defined, and implemented.