My Quest to Mix OOP and C
Using the dot notation and information hiding
Part II of II
Contents
- Step 4: Function pointers
- Step 5: Recovering complete information hiding
- Step 6: Syntactic sugar for the constructor
- The whole picture
- Final words
In the first part of this article, we used an IntStack type to demonstrate how we could introduce the dot notation in C, while achieving true information hiding. We left off at a point where we had totally hidden IntStack’s implementation from the type’s clients.
Now let’s introduce the dot notation.
Step 4: Introducing function pointers
Function pointers have a perfectly descriptive name: they are pointers to a function. Their syntax is not the most straightforward, but apart from that they are relatively simple.
Function pointers are going to help us bind our pseudo-methods to the IntStack instances. Here’s the interface and the client rewritten with dot notation — finally.
Encouraging, but we are still a long way from home.
The last changes broke the implementation in several ways. First of all, notice that we now have to define struct int_stack in the header file. However, an identifier cannot be defined more than once, so we’ll have to give up the definition we had hidden in the implementation file. For now, we can store the IntNode* top member in the header file type definition. But this is a setback as far as information hiding is concerned. We will adress that shortly.
Also, the IntStack type now includes function pointers. But those pointers are like any other members: they have to be initialized at object creation. In other words, we have to bind those pointers to the pointed functions. We do that in the constructor function:
A lot is going on here, compared to what we had before. Let’s go through the changes one by one.
- The functions are now declared with the static modifier, since they are not needed outside the implementation file.
- And because they will only be used in that file, we can drop the “_stack” suffixes.
- Most of the function pointers have the same name as the function they point to, but notice that this is not the case with the isEmpty pointer. This is perfectly fine. In a List class, you could very well see something like:
- These assignments represent the bridge between an interface and an implementation.
- Finally, because this is the IntStack class, we rename the IntStack parameter of all the methods to “this”, just to follow the convention used by most object-oriented languages.
Step 5: Recovering complete information hiding
Remember earlier how we had compromised our perfect information hiding by re-including the IntNode* top member into the IntStack type? We will now address that and recover complete information hiding.
The idea is to introduce a struct internals* member in the IntStack type definition. But since we’re trying to hide information, we have to avoid defining struct internals in the header file.
We instead define struct internals in the implementation file.
This restructuration of IntStack’s internal representation has a few impacts on the implementation.
Step 6: One last thing
At this point, we are enjoying our recovered information hiding, and we have plenty of syntactic sugar to celebrate.
But wouldn’t we get the real icing on the cake if instead of:
… we could write something like:
Well, we won’t be able to achieve exactly that. IntStack is a type name, and C does not let you attach properties to a type name. But we can get something pretty close, using IntStackClass.new().
To do that, we need to bind a function pointer named “new” to a variable named “IntStackClass”. Here’s how we can achieve that:
- We declare the IntStackClass variable in the header file. The file will provide a unique declaration for every client of the IntStack class.
- A lot is going on here. First, a struct int_stack_class type is defined: it contains a pointer to a function that returns an IntStack* (i.e., the constructor). Then, a variable of that type, named IntStackClass, is declared. Using the “extern” modifier prevents the variable from being defined. Defining an object identifier means implementing it, i.e. reserving storage for it. This should not be done in a header file. The variable also has the “const” modifier, meaning that it will not be modifiable past initialization.
- IntStackClass is defined and initialized in the implementation file:
The whole picture
Here’s what our IntStack class and its client look like at the end of the run.
Final words
The article intended to demonstrate how to use the dot notation and enforce strong information hiding while working with the C programming language. We have presented this technique for illustrative purposes only, and make no claims whatsoever as to its efficiency or its merits.
Importantly, each object needs to store pointers to all its available methods. This is quite pleasant syntactically, but incurs high memory costs. For higher efficiency, we could instead have objects each holding only one pointer to a unique, static data structure that would hold all the (shared) function pointers for all instances. Here’s what the client would look like if we implemented this technique with “_” as the identifier for the pointer to the unique methods container:
Next logical steps would be the introduction of polymorphism and inheritance.
Thank you for following me all the way through this journey towards the dot notation and strong information hiding in C. If you want to dive further — much further — into the topic of OOP in C, you can read Object-Oriented Programming with ANSI-C.