Jump to content


Check out our Community Blogs

Register and join over 40,000 other developers!


Recent Status Updates

View All Updates

Photo
- - - - -

C with Classes...

c

  • Please log in to reply
No replies to this topic

#1 0xDEADBEEF

0xDEADBEEF

    CC Devotee

  • Senior Member
  • PipPipPipPipPipPip
  • 790 posts
  • Programming Language:C, Java, C++, C#, (Visual) Basic, Perl, Transact-SQL, Bash, Prolog, Others
  • Learning:Others

Posted 05 January 2013 - 01:34 PM

This tutorial is how, using C, you can add classes to the language. This isn't really so you'd do it; but so you can understand classes and how they might be implemented.

This tutorial will iterate over the design, refining it and making it better. This way you can hopefuly see why C++ works the way it does and understand it at a deeper level.

So lets start with our specification:

"We wish to add the notion of a class using C. A class is a compound data type, which groups data and functions together. Further more we wish to implement inhiertance, in which another class can extend the definition of the 'base' class."

So lets implement this: lets start with a simple class, we'll call (in the time honoured tradition), foo.

Class foo, contains a single data item bar, and two functions, getBar and doAction.

So we could implement it this way:
typedef struct foo {
int bar;
} foo;

int getBar(foo* this) {
return this->bar;
}

void doAction(foo* this) {
... we do somthing here with this->bar;
}

Notice that all the functions for the class need a pointer to the instance of the class. We can use this class as follows:

...
foo myfoo;

cout << getBar(&myfoo) << endl;

In the last example we notice that users of our 'class' would still need to initialise myfoo correctly before calling any functions. So we should provide class specific initialiser function:

void initialiseFoo(foo* myfoo, int initialBar) {
myfoo.bar = initialBar;
}

This is the basics of a class; so lets turn our hand to inheiritance. We wish to define another class, subfoo, this takes foo and adds another data item, subbar, and another function getSubBar

So our first attempt might be:
typedef struct subfoo {
int subbar;
int bar;
} subfoo;

int getSubBar(subfoo* this) {
return this->subbar;
}

Now on the surface subfoo contains all the data items of foo, plus the extra (we've skipped the initalising function for now). So maybe were done.
But what happens when we try an call one of the foo functions with a subfoo; we can call like this:
....
subfoo f;
getBar( (foo *)&f);

getBar thinks the memory pointed to by f is of type foo, and not subfoo. It will attempt to return the parameter bar. But because of how we've defined the class, we'd actually return subbar. Not the intended consequence.

So we must layout the class, with the base classes data items first. So subfoo must be defined as:

typedef struct subfoo {
int bar;
int subbar;
};

we can now correctly write an initialisation function, and all functions for 'foo' will work with a subfoo.

void initialiseSubFoo(subfoo* this, int subbar, int bar) {
initialiseFoo((foo*)this, bar);
this->subbar = subbar;
}


Now, lets say we wish for doAction to be overriden by a subclass; we could say define two versions of the function:

void doAction(foo* this);
void doActionSub(subfoo* this);

We then have to call the second function for subfoo. But this is silly, we want to define the functions via the classes structure and not by our code.

Todo this, we require another layer of indirection. That is a function pointer. So in the foo class, we define a function pointer to the doAction function:

typedef struct foo {
	 void (*pdoAction)(foo*);
	 int bar;	
} foo;

Then in the initialisation function; we setup the pointer:

void initialiseFoo(foo* this, int bar) {
this->bar = bar;
this->pdoAction = doAction;
}

We can then call doAction in a function using a foo var:

void callDoAction(foo* f) {
f->pdoAction(f);
}

So lets override this in subfoo:

typedef struct subfoo {
void (*pdoAction)(foo*);
int bar;
int subbar;
} subfoo;

Then in our initialiser, we'll set pdoAction to a different function. Then when callDoAction is called, we'll call the correct function.

Its easy to see how, we can extend the subclass with virtual functions of its own, and how further subclasses have to be defined.

But you might notice that it seems a little inefficient. We will store pointers in each instance of the classes, but there values are static accros the class definition. Imagine instead of a single function, we've got a 100. Each time you create a foo, it has 100 function pointers. But each foo has the same 100 function pointers - can't we store them outside the instances - and point to them?

So we define a function table for the virtual functions of a class:

void * fooTable[] = { (void*)doAction };
void * subfooTable[] = { (void*)doActionSub };

Then inside foo & subfoo we point to these tables:

typedef struct foo {
void ** functiontable;
int bar;
} foo;

typedef struct subfoo {
void ** functiontable;
int bar;
} subfoo;

void initialiseFoo(foo* this, int bar) {
this->functiontable = fooTable;
this->bar = bar;
}
void initialiseSubFoo(subfoo* this, int bar, int subbar) {
initialiseFoo((foo*)this, bar);
this->functiontable = subfooTable;
this->subbar = subbar;
}

So to call doAction we have to reach into the function table:

foo * fp;
...
((void (*)(foo*))fp->functiontable[0])(fp);
...

And this is how to implement inhiertance and polymorphism in C. Note the rules here, the memory structure of the subclasses needs to be similar to the base class. i.e the offsets of the variables must be the same. The same is true for the structure of the function table, they can be different sizes, but the subclass must be bigger and the entries in the same order for the base class.

Hopefully you can see how useful having a compiler do all the work for you is.
  • 1

Creating SEGFAULTs since 1995.






Also tagged with one or more of these keywords: c

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download