This article is part of the series of educational articles for ‘Einführung in die Informatik’. Dieser Artikel ist Teil des Zusatzmaterials zu den Tutorien ‘Einführung in die Informatik’ C/C++.
We use C++ to learn all the essentials and beauties (did I really just say that?) of object oriented programming. The language supplies us with tools to create classes, methods and everything that comes with it, like inheritance and polymorphism. Based on our previous brief knowledge of structs in I call it ‘Plain-C’, we’ve developed an understanding how classes work. But can we write object oriented code in plain C? C as of 1989 had exactly 32 keywords and I challenge you to name at least 20 of them! C itself was never designed for OOP code, so I’ll meanwhile accept the challenge and try to write OOP code in plain C, let’s go!
Before we take off
… we need to know about one more fundamental what is called a function pointer. Yes, I’m aware that I cause strong allergic reactions by mentioning this four letter word. So no worries, let’s tackle it gently.
We want to talk about communication, more specifically about how Fluorescence by The VFD Collective communicates. Fast forward, you can use both USB and Bluetooth to message Fluorescence - the language is the very same (and very simple, if you’re interested).
So exactly one function is made to decipher incoming messages. But just like texting a person, some kind of feedback is appreciated. Here Fluorescence needs to know exactly whether it should reply over USB or Bluetooth. One solution is passing over the reply function. Oh wait, we can’t pass functions, or can we? Yes we can!
Functions have addresses, just like variables. We can store the address of a function just like we stored the address of a variable. Tada, we have a function pointer. Here’s a self explaining example how to work with them:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | #include <stdio.h> // Declaring a simple function float senselessFunction(int a, double b, char c) { return (a * b) - (double)c; } float anotherSenselessFunction(int a, double b, char c) { return (a * b) + (double)c; } int main() { int a = 3; // Create a simple int variable, a int *p = &a; // Create a pointer to int. Address of a is stored in p // Declaring a function pointer that can point to a function that // accepts (int, double, char) as parameters, returning float float (*functionPointer)(int, double, char); // Assign to first senseless function and print result functionPointer = &senselessFunction; printf("%f\n", (*functionPointer)(*p, 3, a)); // Should result 6.0 // Assign to the other senseless function // Don't need the ampersand operator. Don't need to deference it either functionPointer = anotherSenselessFunction; printf("%f\n", functionPointer(*p, 3, a)); // Should result 12.0 } |
The syntax to declare a function pointer is just like any simple function declaration, except that you now have brackets around the name and prefix it with a star (*). Assigning and calling (dereferencing) is even easier if you have a look at the alternative syntax in line 26 and 27.
In Fluorescence, we have two reply functions, both taking a 10 byte unsigned char reply message buffer:
1 2 3 4 5 6 7 8 9 10 | // Serial command encode function for USB Serial void serialCommandEncodeUSB(uint8_t *transferBuffer){ // Write to USB Serial instance (constant 10 byte response) ... } void serialCommandEncodeBluetooth(uint8_t *transferBuffer){ // Write to Bluetooth Serial instance (constant 10 byte response) ... } |
This is how the decoder works:
1 2 3 4 5 6 7 8 9 10 11 12 13 | void serialCommandDecode(uint8_t *inputBuffer, void (*encoderInstance)(uint8_t *)) { // If aligned protocol is detected if(received data matches protocol format){ // Do some bla if(set clock command is detected) { // If time set command is detected, do some bla bla bla and // ... // reply with a message. If it's all good, the PC will complete the message :p uint8_t transferBuffer[10] = {0x23,0x10,'T','i','m','e',' ','S','y',0x24}; encoderInstance(transferBuffer); } } } |
It just calls encoderInstance, which is passed over to the decoder as the second argument and assumes that it’s the right function. serialRoutine() takes care of passing the right function pointer:
1 2 3 4 5 6 7 8 9 10 11 12 13 | void serialRoutine(){ if(there is any USB data incoming) { uint8_t *inputBuffer = readSerialData(); // Decode and reply if needed serialCommandDecode(inputBuffer, serialCommandEncodeUSB); } if(there is any Bluetooth data incoming) { uint8_t *inputBuffer = readBluetoothData(); // Decode and reply if needed serialCommandDecode(inputBuffer, serialCommandEncodeBluetooth); } } |
Easy, right? Well, the syntax is awful, but function pointers are easy themselves and pretty fun! So now that we have the basics, let’s create the Student class we are familiar with in homework 7. I’ve introduced some slight changes:
A simple Object Definition
1 2 3 4 5 6 7 8 | typedef struct Student { // Attributes unsigned int registration_number; char *last_name, *first_name, *major, *string; } Student; |
class Student { private: unsigned int registration_number; string last_name, first_name, major; };
The C version is just a struct, isn’t it? Really that simple - and here is why: The data type to group variables, called attributes, is struct. And hey, that’s what a simple class without any methods is.
You know typedef? What typedef does is to replace the nasty struct Student by Student. To give you an idea, let’s look into the standard integer library stdint.h. We see that uint8_t is just an unsigned char:
typedef unsigned char uint8_t;
More challenging: Methods!
How do we declare methods? First, this is how our complete C++ class looks like (what we know already):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class Student { private: unsigned int registration_number; string last_name, first_name, major; public: Student(unsigned int, string, string, string); unsigned int getRegistrationNumber(); string getFirstName(); string getLastName(); string getMajor(); string toString(); }; |
How does it look like in C? Well, function pointers, finally!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | typedef struct Student{ // Attributes unsigned int registration_number; char *last_name, *first_name, *major, *string; // Methods unsigned int (*getRegistrationNumber)(struct Student *this); char *(*getFirstName)(struct Student *this); char *(*getLastName)(struct Student *this); char *(*getMajor)(struct Student *this); char *(*toString)(struct Student *this); } Student; |
See the additional this-pointer in every method? This is characteristic and absolutely necessary (I’ll talk about it in a sec). Moreover, instead of declaring methods we declare function pointers with the same name, in our example 4 getters and one toString(). Since the function pointers point to nowhere at creation, we need to link right functions to them. So this forces at least one function (constructor) to not be a function pointer inside the struct!
But before we talk about the constructor, let’s first have a look at all the other methods:
string Student::getMajor() { return major; }
char *_student_getMajor(Student *this) { return this->major; }
I really want you to think about where getMajor() in C++ gets to know which major we mean: It’s the hidden this-pointer to every method to let it know which object has called it. C++ hides it for convenience, but in Plain-C we can’t use the language to hide the pointer, so it’s necessary to pass it over as an argument.
Noticed the super unintuitive function name in C? I did that on purpose to prevent direct calling, as we’d like to call it from an object.
Do you know a better way to stringstream?
This is not so much about OOP in C but I’m just curious whether there is any better way to do it in C.
C++ Version of toString():
1 2 3 4 5 6 7 | string Student::toString() { stringstream sStr; sStr << "Student Nr. " << registration_number << ". Name: " << first_name << " " << last_name << ". Major: " << major; return sStr.str(); } |
C Version of toString():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | const char _toStringFormat[] = "Student Nr. %d. Name: %s %s. Major: %s."; char *_student_toString(Student *this) { if(this->string) free(this->string); // Determine size of output string unsigned int toStrSize = snprintf(NULL, 0, _toStringFormat, \ this->getRegistrationNumber(this), \ this->getFirstName(this), \ this->getLastName(this), \ this->getMajor(this)); char *toStrString = calloc(toStrSize + 1, sizeof(char)); // Put it into output and return sprintf(toStrString, _toStringFormat, \ this->getRegistrationNumber(this), \ this->getFirstName(this), \ this->getLastName(this), \ this->getMajor(this)); this->string = toStrString; return toStrString; } |
Basically I use snprintf to determine how many chars to malloc and then sprintf it into the buffer using a constant format. Since it’s better dynamic, an attribute (Student.string) keeps track of the address, and at every re-generation it’s free’d and remalloc’ed. Do you know something more elegant?
Tough Talk: Constructor
toString() in C already set the mood for how much more the plain-C counterpart can be. But simple things first:
Student::Student(string fn, string ln, string m, unsigned int rn) : registration_number(rn), last_name(ln), first_name(fn), major(m) {}
Now C it yourself:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | Student *Student_Init(char *fn, char *ln, char *m, unsigned int rn) { Student *this = calloc(1, sizeof(Student)); this->first_name = calloc(strlen(fn) + 1, sizeof(char)); this->last_name = calloc(strlen(ln) + 1, sizeof(char)), this->major = calloc(strlen(m) + 1, sizeof(char)); strcpy(this->first_name, fn); strcpy(this->last_name, ln); strcpy(this->major, m); this->registration_number = rn; this->string = NULL; this->getFirstName = _student_getFirstName; this->getLastName = _student_getLastName; this->getMajor = _student_getMajor; this->getRegistrationNumber = _student_getRegistrationNumber; this->toString = _student_toString; return this; } |
Here’s what it does, it’s quite intuitive actually:
Reserve enough memory for one Student object
Initialize all reference attributes (char *) by copying into cleanly allocated, new strings
Linking functions to function pointers of the object, so that they become methods
Return the new, fully initialized object as reference
Since every string and even the object itself is created dynamically in the C version, they need to be free’d before they rest in eternal peace of your DDR4 SDRAM. The destructor can be placed inside the object definition, to create a symmetry however, I prefer to keep it external. Have a look yourself:
void Student_Delete(struct Student *this) { free(this->first_name); free(this->last_name); free(this->major); if(this->string) {free(this->string); this->string = NULL;} free(this); }
What comes straight into your mind when I say ‘Create’?
This kind of create? Or the C++ kind?
#include <iostream> #include <string> #include <sstream> #include "student.hpp" using namespace std; int main() { Student *s = new Student("Katelyn", "Tarver", "Music", 333333); cout << s->toString() << endl; delete s; Student **students = new Student *[2]; students[0] = new Student("Brandon", "Woelfel", "Photography", 444444); students[1] = new Student("Megan", "Davies", "Music", 555555); for(unsigned char i = 0; i < 2; ++i) { cout << students[i]->toString() << endl; delete students[i]; } delete[] students; }
See how similar and easy it is to create the same objects in plain C code:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include "student.h" int main() { Student *s = Student_Init("Katelyn", "Tarver", "Singing", 333333); printf("%s\n", s->toString(s)); Student_Delete(s); Student **students = calloc(2, sizeof(Student *)); students[0] = Student_Init("Brandon", "Woelfel", "Photography", 444444); students[1] = Student_Init("Megan", "Davies", "Music", 555555); for(unsigned char i = 0; i < 2; ++i) { printf("%s\n", students[i]->toString(students[i])); Student_Delete(students[i]); } free(students); }
An absolutely amazing thing to note is the sizes of our executable files. I’m using a MacBook Air with the latest Mojave installed and use gcc/g++ 4.2.1 to compile. While the C++ executable is as large as 40 KB, the C version is exactly 9 KB, which is about 80% smaller. I’ve found a great StackOverflow question that describes why this happens.
Also, quite interesting is that while our manually written C version uses 198 total dynamic memory allocations, C++ uses 196 with only four of them manually as you can see in the main.cpp file. Feel free to check it out on your computer using valgrind. That’s the same program I use to keep track of memory leaks. Compile on your own, download the files here:
Excited to see more?
I’m happy to tell you that there’s more on the way. We will expand the Student class written in C++ and plain C by adding inheritance. Together we will learn how to use file I/O and build a student database. Before we dive into deep pointer stuff, I’m excited to tell you something about my MP3 player project written in plain C with a lot of OOP style code and inheritance!