Object-Oriented C: Creating Foundation Classes Part 1
Transforming a structured language into an object-oriented language may cause some pain at first. However, the benefits of rapid behavioral changes far exceed any early discomfort.
Have you ever felt trapped using a limited compiler language after working with a highly developed language with many features? I encountered this problem three years ago. After using several C++ compilers and some fourth-generation languages, I was presented with the challenge of developing a complex embedded application with nothing more than a C compiler. Faced with this obstacle, I devised techniques to develop a sophisticated facet of C, which I call object-oriented C (OOC). With OOC, we will explore the techniques necessary to transform a structured language into an object-oriented language.
In Part 1 this month, we will explore object-oriented principles to see if they are supported by OOC. I will also discuss how to create the C++ Class construct, which lays the groundwork for developing our foundation classes. In Part 2 next month, I will discuss how to create Application Window classes, which will be used to develop a sample application.
The C language is a structured (non object-oriented) language by nature. The C language is well suited for many tasks. However, when used in an object-oriented fashion, it leaves a lot to be desired. To make the C language behave in an object-oriented manner requires some additional encouragement by means of pointers.
In creating OOC, I had three objectives: the benefits should outweigh the overhead involved in producing them; it should easy to use; and I wanted to keep it simple.
The first objective of OOC was minimizing the amount of overhead required to transform C into OOC. I carefully tried to balance the benefit of each object-oriented feature with the overhead to support such a feature. This balance is necessary to have other software engineers embrace the OOC concept. Remember, something has to be relatively easy to use if you want to gain mass appeal. In making the transition from a structured language to an object-oriented language, a software engineer must understand the concept of data abstraction and encapsulation. This, I believe, is the greatest hurdle to overcome when entering the world of object-oriented programming.
Data encapsulation and abstraction. To encapsulate data, one must first package the desired states and behaviors into a single entity. Encapsulation contains the necessary states (data members) and behaviors (member functions) required to perform operations on the states. A data abstraction is created when an instance of the class (object) is created.
Access restriction. To protect against outside changes in the class states, most object-oriented languages provide protection schemes. For example C++ provides for three levels of protection: private, protected, and public. OOC only provides for two levels of protection: private and public. Ideally, the software engineer should only have access to the states of a class through the member functions. The public level of protection is prevalent due to the open nature of OOC. The private level of protection was accomplished by declaring a static variable within the class source file.
Information hiding. The ability to hide the behavior of an algorithm inside a member function is what I term information hiding. OOC supports this ability by providing an insulated layer where the underlying behavior can be changed without causing a change in the interface, which can result in a change in the application code.
Inheritance. In C++ inheritance is supported by derivation, which allows you to create a child class from the parent class with a "kind of" derived relationship. You can think of a child class that is "kind of" a parent class. To support this relationship in OOC would require much more overhead than the resulting benefit. So I turned to aggregation, an alternative relationship. An aggregation relationship is used when you want to show a whole-part relationship between two classes. You can say that this class is "part of" the other class. The class at the parent end of the aggregate relationship is sometimes called the aggregate class.
Dynamic binding. This is when the functions are bonded to their associated functions at run time using virtual functions. In OOC, assignments of functional pointers to their associated function definitions are assigned at run time. However this needs to be set up by the software engineer. This run-time assignment occurs only once when the application tasks are created, so OOC's binding is static.
Polymorphism. With polymorphism, a parent function can be overridden by the child class, which could include the function interface. OOC does not currently support this mechanism, since this would increase the overhead that the software engineer would have to support.
Summary of object-oriented methodology. When developing large and complex software projects, I have found that object-oriented methods provide for better abstraction and encapsulation capabilities. If you can establish a set of objectives that can be broken into states and behaviors, object-oriented methodology can provide a substantial benefit. Transforming a structured language into an object-oriented language may cause some initial pain. However, the benefits of rapid behavioral changes far exceed the initial discomfort. In the upcoming examples, I will focus more on GUI applications, but these object-oriented methodologies can be applied at any level, whether you are developing a menuing system, a set of message queue APIs, or even low-level drivers.
Talking about "this" syntax
Before jumping into discussions about the implementation of OOC, I should point out some special object-oriented syntax that is necessary to provide clear and precise understanding of the code. As in many object-oriented languages an implicit pointer or self-referencing pointer is called to refer to the class object, which calls a particular member function. The keyword this is a standard C++ reserved word. Since we don't have this implicit capability in C, we must simulate this in OOC. In C++ when a member function is called, the this pointer is automatically passed to the function. In OOC we need to pass the class object explicity. The this pointer should be the first argument passed into any member function.
OOC's this pointer is established by first declaring the address to the struct within the pointer-to-function interface. This is accomplished in the struct function declaration, as shown in Listing 1. Carrying this through to the function definition, you will find reception on the address of the struct. Once the interface is developed, the calling of the function requires only the passing of the struct address.
|Table 1: Object-oriented principle supported by OOC|
|Data encapsulation and abstraction||Yes||This is found in the class construct.|
|Access restrictions||Yes||Public and private only.|
|Information hiding||Yes||Accomplished by hiding behavior in member functions.|
|Inheritance (derivations)||No||OOC supports aggregation.|
|Dynamic binding||No||Static binding. At run-time, OOC assigns the member functions to the functional pointers within each instance of a class.|
|Polymorphism||No||No support exists.|
Coding style and organization
Coding style and code organization are paramount in developing a successful project. As we will see in the following code listings, all code is organized with the clearest representation in mind. I have also tried to use commonly accepted labeling and syntax to allow the software engineer to feel at ease.
File organization is also very important. Each class should have its own source file with a corresponding interface file. When developing the application, specialized files should be set aside for particular types of data and functions. It is easy to get confused with the increasing degree of abstraction inherent in object-oriented designs. File organization will be explained in more detail in the second part of the article.
Developing your toolbox
Just like any software project, you need to develop a toolbox of classes that a software engineer could have at his disposal, as necessary for a particular task. I have noticed that this makes software engineers more efficient with their code, because program size decreases due to code reuse, which in turn improves software performance.
In our example GUI application, we require GUI objects that will allow the user of our application to perform some basic operations. In our example, three types of classes built upon our CObject class. Hence, our toolbox will contain four classes: one core base class (CObject) in which all GUI-based classes will be used to develop their behavior, and three derived classes (CText, CButton, and CPanel) which will be used to develop our upcoming application, as shown in Figure 1.
CObject: top of the class
When developing a set of classes, it is important to define a core class that will be the cornerstone of your foundation. Since we are starting from the ground up, our cornerstone class will be called CObject, borrowing a name from the folks at Microsoft. In our example, we are focusing on developing a GUI application. Therefore it makes sense that we start with a CObject class that contains member functions that access graphical APIs. The CObject class allows the aggregate classes to be insulated from the inner workings of the graphical APIs. It enables the code to become more portable and independent of any particular graphical driver API.
In our GUI application, we only need limited support to implement our user interface. The CObject only has three member functions. These display text, draw a filled rectangle, and display a simple 3D bevel, as shown in Listing 2.
Creating a class in OOC
To create a class in C, you first need to develop the kinds of behaviors and characteristics you would like a particular object to exhibit. The answers to these kinds of questions will be the building blocks on which you will build your foundation to your application. So therefore, be very careful in how you structure your class hierarchy, because it could be painful later on in the development process.
To manipulate an object, the software engineer should use only the member functions declared within the class. In C, a struct can be an aggregate of different types of data. Though functions cannot be declared inside a struct, pointers to functions can overcome this shortcoming, as shown in Listing 3. The corresponding functions are declared outside the struct.
After further examination, we can see that the CButton class contains a collection of states or attributes used to position an object on the screen and to decide what color appearance it should display. Also, four pointers to functions are declared:
When naming functions, it's best to use verbs in a concise manner, since these will be used in the application when calling the member functions. Notice that the CButton class is broken into two access regions: private and public. The private region contains the data members and the public region contains the member function declarations. The member functions provide public access to the private data members. Hence, direct application access to the private data members is strictly forbidden.
When developing objects in C, it is imperative that you become a zealot for organization. I can't emphasize this point enough. I have found that it is useful to create specialized files for storing and organizing various functions and data. An example of a specialized file is the resource file. The resource file will contain the object's initialized properties, such as position, appearance, and so on.
An additional resource class needs to be created and placed in the cbutton.h file. This class will contain only the class states, which allow us to assign default properties to the class, as shown in Listing 4.
The declaration of the instance of the object should to be placed in a file named resource.rc, as shown in Listing 5. The initialization of this class should be placed in a corresponding file named resource.c, as shown in Listing 6.
The positioning of any object on the screen will be placed on a finite spaced grid and labels representing the row and column will be used. The spacing between each row and column is referred to as a ROW_STEP and COL_STEP, respectively. As we proceed in developing more objects from different classes, the code can become rather complex and organization of your code is essential to make rapid changes that will affect the application's appearance and behavior.
At a minimum, the construct member function and the new function are essential to each OOC class. The construct member function is used in a manner that you would expect. It assigns attributes to an object and performs any other initial setup procedures for that object, as shown in Listing 7. The constructor can also call other constructs when developing a class that is an aggregate of other classes.
At the top of a class source file, private data members can be declared static. This will allow them to be accessed only from the class member functions.
The new function is actually not part of the class; it is used to perform functional binding at run time. This is where the external functions are assigned to the class pointer to functions, as shown in Listing 8. If a class consists of other sub-classes, then these new sub-class functions need to be called in the class. In Listing 8, the CButton class contains the CObject; therefore, the CObjectNew function needs to be called in the CButtonNew function. It is necessary to include the call to CObjectNew function, since we will be using its member function when creating our buttons. This CObjectNew function will bind the CObject functions to their corresponding functional pointers.
Resource vs. runtime
The member functions Paint, GotFocus, and LostFocus are trivial classes, which contain what we would expect-routines for displaying basic shapes, as shown in Listing 9. However, a few areas are worth pointing out.
When creating member functions, most will derive their characteristics from their resources; however, if you want to override the resource with run-time application characteristics, then we will need to provide that mechanism. Therefore, I created an UpdateType argument. This aggregate type contains two types: Resource and RunTime. When the Resource argument is supplied, the override arguments are ignored; however, the converse is true when the RunTime argument is present. The RunTime argument allows the class states to be overridden. When the UpdateType parameter is used in the function argument list, it should always follow the self-reference pointer. This will be explained in more detail in Part 2 of the article.
Classes that consist of an aggregate of multiple classes, which are an aggregate of even more classes, are what I would classify as an advanced class. In our example, we developed classes consisting of two layers. However, there is really no limit to the depth of aggregate classes. You just need to keep building upon pointers to pointers to pointers to.... In fact, you will later see how the application classes can be composed of many class layers.
As in C++, OOC can be represented in the same fashion for class and object representation as shown in Figure 2. In the class representation, the CObject is the parent class of the CButton, CPanel, and CText classes, which in turn are the parent class of the CAdvanced class. Using the object representation, you can see that an instance of the CObject class is used in each instance of the CButton, CPanel, and CText. These instances are then used in the aggregate collection of CAdvanced child class.
In the constructor for this class, the CAdvanced class would need to call the constructor for each instance of the CButton, CPanel, and CText classes. Finally the CAdvanced class new function would also need to call the new functions for each of the instances of the CButton, CPanel, and CText classes.
No short cuts
In OOC, the lack of protection in our classes requires that software engineers agree to obey the following rules:
Rule 1: Class property access
It should be understood that the user of a class can only use the pointers to functions when assigning values to the class properties. Only in extreme circumstances should one violate this rule.
Rule 2: Member function access
When calling member functions of a particular class, only the object of the class should be allowed to access the member functions. All member functions are global by declaration in the class header file. The calling of these member functions in this manner would break the spirit of what we are trying to accomplish. So far, I have found no reason to break this rule.
What lies ahead
Together we have explored how to create an object-oriented construct, the class, in C. In developing single classes, we have developed a method in which to join classes together in an aggregation relationship. This allows us to develop advanced classes, which have collective behaviors and states. We also examined the minimum necessary class functions (constructor and new function) and how to initialize objects of a class in a resource file.
In part 2 next month, I will discuss how to take these foundation classes and apply them to the application classes. I will also explain how to establish an Application Object and what is involved in creating Application Window classes.
Matthew Curreri is a senior software engineer at Panasonic AVC American Laboratories. Matthew has developed UI/Application software for both HDTV and TV satellite set-top boxes. He holds an MS degree in electrical engineering from Drexel and a BS degree in electrical engineering from Rutgers. He may be reached at firstname.lastname@example.org.
- Listing 1: The self-referencing pointer
- Listing 2: The CObject base class (cobject.h)
- Listing 3: The button class structure (cbutton.h)
- Listing 4: The button class resource structure (cbutton.h)
- Listing 5: The declaration of the resources (resource.rc)
- Listing 6: The resource structure (resource.c)
- Listing 7: The constructor (cbutton.c)
- Listing 8: The new function (cbutton.c)
- Listing 9: The CButton paint member function (cbutton.c)
Return to Table of Contents