Object-Oriented C

A Dr. Taylor Tutorial

Programming languages like Java, C++, and Smalltalk have built in support for object-oriented constructs. In C we have the basic building blocks, but we need to do much more of the heavy lifting ourselves. The intent of this discussion is to give you a feel for how this could be done.

If you haven't read the article on structs, do that first. For those of you waiting for the others to get done reading the other page, wake up. This is asynchonous learning going on here. You don't need to wait for anyone. But, if you haven't read the other article recently, please go read it. Yeah, I really mean it. I even wrote please.

Welcome back. At this point our Rectangle has attributes, but it doesn't have behaviors. Let's see if we can add some behaviors. Consider this:

#include "Point.h"
#include "Color.h"
  
#ifndef RECTANGLE_H
#define RECTANGLE_H
  
typedef struct _Rectangle Rectangle;
  
struct _Rectangle {
  Point upperRight;
  Point lowerLeft;
  Color color;
  
  void (*translate)(Rectangle *this, Point offset);
};
  
#endif

I've added a function pointer. Don't know what a function pointer is? See you in a little while. If everything works right here, the function pointer will point to a function which will move the rectangle by a specified amount (specified by offset, if you want to get specific).

Hopefully you spent enough time reviewing the article on structs to notice that I'm declaring the struct differently here. The typedef prior to the declaration of the _Rectangle struct is called a forward reference. Essentially we need to use the Rectangle identifier before its declaration is complete. We can typedef the forward reference here because it is just a memory address (in this case, a pointer to a struct).

In any case, we now have a pointer to a function that is part of our Rectangle, and this makes it possible for us to have the rectangle perform a behavior. Another thing to note here is that the first parameter is a pointer to a rectangle object. All of the functions that we want to behave like an object method in Java must accept a pointer to rectangle. When we call the method, we pass in the address of the calling "object." This pointer acts as our this pointer (hence, the clever name).

We could now make use of the rectangle like this:

Rectangle rect;
  
rect.upperLeft.x = -5;
rect.upperLeft.y = 5;
rect.lowerRight.x = 5;
rect.lowerRight.y = -5;
  
Point delta;
delta.x = 5;
delta.y = 5;
  
rect.translate(&rect, delta);

Unfortunately, there is one major problem with this. Currently the function pointer for rect.translate is not pointing to our implementation of translate. Hopefully that isn't too surprising since we haven't implemented it yet... so let's do that now:

/* Rectangle.c */
#include "Rectangle.h"
  
void translate(Rectangle *this, Point offset)
{
  this->upperLeft.x += offset.x;
  this->upperLeft.y += offset.y;
  
  this->lowerRight.x += offset.x;
  this->lowerRight.y += offset.y;
}

Next we need to get rect.translate pointing to our translate implementation. First we need to add the function prototype for translate to our Rectangle.h header file. Then we can do this:

Rectangle rect;
  
rect.translate = &translate;
  
rect.upperLeft.x = -5;
rect.upperLeft.y = 5;
rect.lowerRight.x = 5;
rect.lowerRight.y = -5;
  
Point delta;
delta.x = 5;
delta.y = 5;
  
rect.translate(&rect, delta);

Unfortunately we still have a couple problems. I tell you about one now and one later. For all you folks who just can't wait, you'll have to scroll down until you find the other problem... or just read really fast. One problem with this approach is that the translate method is available to everyone (it's a global function). We'd like to hide it, but we can't make it static since we need access in main in order to connect rect.translate to it.

We can get around this problem by creating a separate function. I'll call this function RectangleCtr because it's going to act a lot like a constructor. In fact, let's make a few of these functions to give us multiple constructors. While I'm at it, I'll introduce zoom as well.

/* Rectangle.h */
#include "Point.h"
#include "Color.h"
  
#ifndef RECTANGLE_H
#define RECTANGLE_H
  
// Foward declaration that allows us to use "Rectangle" in
// the function parameters within the _Rectangle struct.
typedef struct _Rectangle Rectangle;
  
/**
 * Fake Rectangle class
 */
struct _Rectangle {
  Point upperLeft;
  Point lowerRight;
  Color color;
  
  void (*translate)(Rectangle *this, Point offset);
  void (*zoom)(Rectangle *this, float factor);
};
  
/**
 * Fake construtor
 */
void RectangleCtr(Rectangle *this);
  
/**
 * Fake construtor
 */
void RectangleCtr2(Rectangle *this, Point upperLeft, Point lowerRight, Color color);
  
#endif

I've now introduced a RectanglePrivate.h to declare the static ("private") references:

/* RectanglePrivate.h */
#include "Rectangle.h"
  
#ifndef RECTANGLEPRIVATE_H
#define RECTANGLEPRIVATE_H
  
static void translate(Rectangle *this, Point offset);
static void zoom(Rectangle *this, float factor);
  
#endif
/* Rectangle.c */
#include "Rectangle.h"
#include "RectanglePrivate.h"
  
static void translate(Rectangle *this, Point offset)
{
  this->upperLeft.x += offset.x;
  this->upperLeft.y += offset.y;
  
  this->lowerRight.x += offset.x;
  this->lowerRight.y += offset.y;
}
  
static void zoom(Rectangle *this, float factor)
{
  this->upperLeft.x *= factor;
  this->upperLeft.y *= factor;
  
  this->lowerRight.x *= factor;
  this->lowerRight.y *= factor;
}
  
void RectangleCtr(Rectangle *this) {
  this->translate = &translate;
  this->zoom = &zoom;
}
  
void RectangleCtr2(Rectangle *this, Point upperLeft, Point lowerRight, Color color) {
  RectangleCtr(this);
  this->upperLeft = upperLeft;
  this->lowerRight = lowerRight;
  this->color = color;
}

We can then make use of the Rectangle "class" like this:

#include <avr/io.h>
#include <inttypes.h>
#include "Point.h"
#include "Rectangle.h"
  
int main()
{
  Point offset;
  
  offset.x = offset.y = 1;
  
  Point top;
  top.x = 5;
  top.y = -5;
  
  Point bottom;
  bottom.x = -5;
  bottom.y = 5;
  
  Color color;
  color.r = color.g = color.b = 255;
  
  Rectangle rect;
  
  RectangleCtr2(&rect, top, bottom, color);
  
  rect.upperLeft.x = 0;
  rect.upperLeft.y = 0;
  
  rect.lowerRight.x = 10;
  rect.lowerRight.y = 10;
  
  rect.translate(&rect, offset);
  
  return 0;
}

Suppose we wanted a default color value. We could do the following:

// Add following line to RectanglePrivate.h
const static Color DEFAULT_COLOR = {255, 255, 255};
  
// Add the following line to RectangleCtr in Rectangle.c
  this->color = DEFAULT_COLOR;

Here is a revised RectanglePrivate.h file. In addition to including default values for the corners of the rectangle "objects" and a couple new functions, we have made use of the const keyword to help the compiler catch our mistakes.

#ifndef RECTANGLEPRIVATE_H
#define RECTANGLEPRIVATE_H
  
#include <inttypes.h>
#include "Rectangle.h"
  
static void translate(Rectangle* const this, const Point offset);
static void zoom(Rectangle* const this, float factor);
static uint16_t calcPerimeter(const Rectangle* const this);
static void fixPoints(Point* const upperLeft, Point* const lowerRight);
  
const static Color DEFAULT_COLOR = {255, 255, 255};
const static Point DEFAULT_UPPER_LEFT = {-5, 5};
const static Point DEFAULT_LOWER_RIGHT = {-5, 5};
  
#endif

The Rectangle* const this indicates to the compiler that the pointer must remain constant. That is, this cannot but reassigned. However, it is permissible for us to change the value of the rectangle that this points to. More succinctly, this is illegal: this = whatever; while this is legal: this->upperLeft = somePoint;.

The const Rectangle* const this in the calcPerimeter prototype indicates to the compiler that neither the this pointer nor the rectangle that it points to may be changed. This allows the compiler to catch our mistake if incorrectly attempt to reassign the pointer of change the value of the rectangle whose perimeter we are calculating. After all, you shouldn't need to modify the rectangle in order to calculate its perimeter.

Here is the updated Rectangle.h file:

#ifndef RECTANGLE_H
#define RECTANGLE_H
  
#include <inttype.h>
#include "Point.h"
#include "Color.h"
  
// Foward declaration that allows us to use "Rectangle" in
// the function parameters within the _Rectangle struct.
typedef struct _Rectangle Rectangle;
  
/**
 * Fake Rectangle class
 */
struct _Rectangle {
  Point upperLeft;
  Point lowerRight;
  Color color;
  
  void (*translate)(Rectangle* const this, const Point offset);
  void (*zoom)(Rectangle* const this, const float factor);
  uint16_t (*calcPerimeter)(const Rectangle const *this);
};
  
/**
 * Factory method for constructing a rectangle
 */
Rectangle* createRectangle(const Point upperLeft, 
  const Point lowerRight, const Color color);
  
/**
 * Factory method for constructing a rectangle
 */
Rectangle* createDefaultRectangle();
  
/**
 * Destructor
 */
void* destroyRectangle(Rectangle* const this);
  
#endif

The constructors in the header file have been replaced with functions that will "dynamically" create a new rectangle (allocating space on the heap instead of the stack) and return a pointer to the newly created rectangle. In addition, a destroyRectangle() function has been declared. This function is used to free up the memory used by the rectangle. It should be called whenever we are done using the dynamically created rectangle.

One other potentially interesting thing in the above header file is the return type on the destroyRectangle() function. It indicates that a pointer of some unknown type is being returned... more on why we might want to return a pointer later.

#include "Rectangle.h"
#include "RectanglePrivate.h"
#include <stdlib.h>
  
Rectangle* createDefaultRectangle()
{
  return createRectangle(DEFAULT_UPPER_LEFT, DEFAULT_LOWER_RIGHT, DEFAULT_COLOR);
}
  
Rectangle* createRectangle(Point upperLeft, 
  Point lowerRight, const Color color)
{
  Rectangle *this = malloc(sizeof(Rectangle));
  this->translate = &translate;
  this->zoom = &zoom;
  this->calcPerimeter = &calcPerimeter;
  fixPoints(&upperLeft, &lowerRight);
  this->upperLeft = upperLeft;
  this->lowerRight = lowerRight;
  this->color = color;
  
  return this;
}
  
void* destroyRectangle(Rectangle* const this)
{
  free(this);
  return 0;
}
  
static void translate(Rectangle* const this, const Point offset)
{
  this->upperLeft.x += offset.x;
  this->upperLeft.y += offset.y;
  
  this->lowerRight.x += offset.x;
  this->lowerRight.y += offset.y;
}
  
static void zoom(Rectangle* const this, const float factor)
{
  this->upperLeft.x *= factor;
  this->upperLeft.y *= factor;
 
  this->lowerRight.x *= factor;
  this->lowerRight.y *= factor;
}
  
static uint16_t calcPerimeter(const Rectangle* const this)
{
  uint16_t perimeter = this->lowerRight.x - this->upperLeft.x;
  perimeter += this->upperLeft.y - this->lowerRight.y;
  return 2*perimeter;
}
  
static void fixPoints(Point* const upperLeft, Point* const lowerRight)
{
  if(upperLeft->x > lowerRight->x)
  {
    int16_t temp = upperLeft->x;
    upperLeft->x = lowerRight->x;
    lowerRight->x = temp;
  }
  
  if(upperLeft->y > lowerRight->y)
  {
    int16_t temp = upperLeft->y;
    upperLeft->y = lowerRight->y;
    lowerRight->y = temp;
  }
}

Of particular note here are the createRectangle() and destroyRectangle() functions. The createRectangle() function makes use the malloc() function (declared in stdlib.h) to dynamically allocate space on the heap for us to store our rectangle object. The malloc() function accepts one argument representing the number of bytes that we wish to allocate. It returns a pointer to the beginning of the memory block that it allocates for our use. sizeof(Rectangle) determines the number of bytes required to store a rectangle in memory.

The destroyRectangle() function uses free() to deallocate the memory used by the rectangle that we no longer need. This memory is now available to be used elsewhere in our program. Since C does not have a garbage collector, we are obligated to free up the memory associated with dynamically allocated data. Failing to do so leaves unused memory allocated so that it can never be used for anything else. This is known as a memory leak and can be a particular difficult defect to track down. In addition to calling destroyRectangle() when we are done with it, we should also assign the pointer to null so that it doesn't point to memory that doesn't belong to us (remember, we just gave it back when we called free()).

A pointer that is not assigned to null after the memory to which it is pointing is freed up is called a dangling pointer. Dangling pointers are dangerous because they could be used to inadvertently modify the memory location that is no longer reserved for our use. Dangling pointers can also be a particularly difficult defect to detect. Your program may continue to work just fine as long as the memory that the dangling pointer is pointing to is not reallocated for another purpose. However, once it is reallocated, if the dangling pointer is used to modify data, it will corrupt the other data that is legitimately residing in that memory location. The destroyRectangle() function returns a null pointer as a convenience to those who use this class. It allows client code to deallocate memory and assign the pointer to null all in one step.

The following is simple example of how to make use of the Rectangle "class."

#include <inttypes.h>
#include "Point.h"
#include "Rectangle.h"
  
int main()
{
  Point offset;
  
  offset.x = offset.y = 1;
  
  Point top;
  top.x = -50;
  top.y = 50;
  
  Point bottom;
  bottom.x = 50;
  bottom.y = -50;
  
  Color color;
  color.r = color.g = color.b = 255;
  
  Rectangle *rPtr = createRectangle(top, bottom, color);
  Rectangle *rPtr2 = createDefaultRectangle();
  
  rPtr->translate(rPtr, offset);
  rPtr2->zoom(rPtr2, 2.3);
  
  rPtr = destroyRectangle(rPtr);
  
  rPtr = createRectangle(top, bottom, color);
  uint16_t perim = rPtr->calcPerimeter(rPtr);
  
  rPtr = destroyRectangle(rPtr);
  rPtr2 = destroyRectangle(rPtr2);
  
  return 0;
}

Tutorials for Other Courses

Additional Links

Site Mirror

This is a mirror to be used when http://myweb.msoe.edu/taylor/, (EDU instead of US) is not available.