C is an Object-Oriented language (almost)
What do you mean Object-Oriented? The key concepts of Object-Oriented programming languages (OOPs) are encapsulation, inheritance, polymorphism, and abstraction. Most OOPs have straightforward, clearly defined keywords and processes for all key concepts. C does not. If you want to use OOP concepts, you will have to work for it. Objects Objects is C are relatively straightforward. In C, we would use structures and unions to achieve OOP. struct Object { int field; const char *another_field; union { float f; int i; } float_or_int; }; Encapsulation The idea of encapsulation is to hide away data and only exposing controlled interfaces. This helps manage the state of your application and prevent the data from external modification. C does not have a good way of doing this. If you want to encapsulate data, you will have a hard time. Everything in a structure is public and if you want that changed, you might as well be using C++. Inheritance Building many similar objects is tedious, which is why wr have inheritance. Inheritance is the idea of creating an object that extends the functionality of another object. These extended objects are called children, which inherit properties and functionality from their parent. In a regular OOP language, inheritance is inbuilt with keywords to make life easy. In C++ you would write classes which extend other classes. class Animal { std::string name; }; class Dog : public Animal { std::string breed; }; Animal defines a field name which is a string. Dog extends Animal with the field breed, so Dog has both name and breed. Writing the equivalent in C is fairly simple. struct Animal { const char* name; }; struct Dog { union { const char* name; struct Animal super; }; const char *breed; }; However, with more complex inheritance and Objects it can become messy to deal with. Which is where macros help us out. #define struct$Object(NAME) \ struct NAME { \ int field; \ double another_field; \ int an_array[8]; \ bool last_field; \ } // allows us to easily inherit the object and reference // as `child.FIELDNAME` while also pushing the field // declarations to top-level in the structure. // i.e. `child.field` instead of `child.super.field` #define using(TYPE, FIELDNAME) \ union { \ struct$##TYPE(); \ struct TYPE FIELDNAME; \ } struct$Object(Object); // define the structure type struct OtherObject { using(Object, super); bool extra_field; }; This looks like a lot more work and needlessly complex, but with a good auto formatter, it becomes easier. And it saves a lot of time and line space in future. Again, you could use C++ and make life simple. But that's less fun. If you want to use C++, you're in the wrong place. Polymorphism The ability to treat objects as an parent object. The most utilised form of this concept is when children override parent functions. A simple C++ example from the W3Schools C++ Polymorphism Demo // ohttps://www.w3schools.com/cpp/trycpp.asp?filename=demo_polymorphism // Base class class Animal { public: void animalSound() { cout FIELD) #endif #define parentFromPtr(T, FIELD, PTR) \ ((T *)((size_t)PTR - offsetof(T, FIELD))) While this macro may look daunting at first, it becomes relatively simple when you break it down. We get the address of the FIELD in the struct type T. We return it as a size_t type because we want an integer value. We cast PTR to size_t to tell our compiler we want to subtract integers instead of pointers (which would produce varying results). Then we cast the result to our struct pointer. Now we can gain the correct child pointer if the super construct isn't the first element in a struct, or if the struct extends multiple structs. #define struct$Hero(NAME) \ struct NAME { \ const char *name; \ void (*sayCatchphrase)(struct Hero *); \ } #define using(TYPE, FIELDNAME) \ union { \ struct$##TYPE(); \ struct TYPE FIELDNAME; \ } struct$Hero(Hero); void Hero_sayCatchphrase(struct Hero *self) { printf("Never fear! %s is here!\n", self->name); } struct Hero Hero_create(const char *name) { return (struct Hero){ .name = name, .sayCatchphrase = Hero_sayCatchphrase, }; } struct GunSlinger { const char *gun_model; using(Hero, super); }; void GunSlinger_sayCatchphrase(struct Hero *super) { struct GunSling

What do you mean Object-Oriented?
The key concepts of Object-Oriented programming languages (OOPs) are encapsulation, inheritance, polymorphism, and abstraction. Most OOPs have straightforward, clearly defined keywords and processes for all key concepts. C does not. If you want to use OOP concepts, you will have to work for it.
Objects
Objects is C are relatively straightforward. In C, we would use structures and unions to achieve OOP.
struct Object {
int field;
const char *another_field;
union {
float f;
int i;
} float_or_int;
};
Encapsulation
The idea of encapsulation is to hide away data and only exposing controlled interfaces. This helps manage the state of your application and prevent the data from external modification. C does not have a good way of doing this. If you want to encapsulate data, you will have a hard time. Everything in a structure is public and if you want that changed, you might as well be using C++.
Inheritance
Building many similar objects is tedious, which is why wr have inheritance. Inheritance is the idea of creating an object that extends the functionality of another object. These extended objects are called children, which inherit properties and functionality from their parent.
In a regular OOP language, inheritance is inbuilt with keywords to make life easy. In C++ you would write classes which extend other classes.
class Animal {
std::string name;
};
class Dog : public Animal {
std::string breed;
};
Animal defines a field name
which is a string. Dog extends Animal with the field breed
, so Dog has both name
and breed
.
Writing the equivalent in C is fairly simple.
struct Animal {
const char* name;
};
struct Dog {
union {
const char* name;
struct Animal super;
};
const char *breed;
};
However, with more complex inheritance and Objects it can become messy to deal with. Which is where macros help us out.
#define struct$Object(NAME) \
struct NAME { \
int field; \
double another_field; \
int an_array[8]; \
bool last_field; \
}
// allows us to easily inherit the object and reference
// as `child.FIELDNAME` while also pushing the field
// declarations to top-level in the structure.
// i.e. `child.field` instead of `child.super.field`
#define using(TYPE, FIELDNAME) \
union { \
struct$##TYPE(); \
struct TYPE FIELDNAME; \
}
struct$Object(Object); // define the structure type
struct OtherObject {
using(Object, super);
bool extra_field;
};
This looks like a lot more work and needlessly complex, but with a good auto formatter, it becomes easier. And it saves a lot of time and line space in future. Again, you could use C++ and make life simple. But that's less fun. If you want to use C++, you're in the wrong place.
Polymorphism
The ability to treat objects as an parent object. The most utilised form of this concept is when children override parent functions. A simple C++ example from the W3Schools C++ Polymorphism Demo
// ohttps://www.w3schools.com/cpp/trycpp.asp?filename=demo_polymorphism
// Base class
class Animal {
public:
void animalSound() {
cout << "The animal makes a sound \n";
}
};
// Derived class
class Pig : public Animal {
public:
void animalSound() {
cout << "The pig says: wee wee \n";
}
};
// Derived class
class Dog : public Animal {
public:
void animalSound() {
cout << "The dog says: bow wow \n";
}
};
int main() {
Animal myAnimal;
Pig myPig;
Dog myDog;
myAnimal.animalSound();
myPig.animalSound();
myDog.animalSound();
return 0;
}
C Structures do not natively support function declarations. For C, you will have to write your own constructor functions, destructor functions, and member functions separately from the structure.
#include
#define struct$Animal(NAME) \
struct NAME { \
void (*animalSound)(void); \
}
#define using(TYPE, FIELDNAME) \
union { \
struct$##TYPE(); \
struct TYPE FIELDNAME; \
}
// Define the Animal structure
struct$Animal(Animal);
// Declare the default animalSound for Animal
void Animal_animalSound(void) { printf("The animal makes a sound\n"); }
// Declare a constructor for Animal
struct Animal Animal_default(void) {
return (struct Animal){
.animalSound = Animal_animalSound,
};
}
struct Pig {
using(Animal, super);
};
// Declare the default animalSound for Pig
void Pig_animalSound(void) { printf("The pig says: wee wee\n"); }
// Declare a constructor for Pig
struct Pig Pig_default(void) {
return (struct Pig){
.animalSound = Pig_animalSound,
};
}
struct Dog {
using(Animal, super);
};
// Declare the default animalSound for Dog
void Dog_animalSound(void) { printf("The dog says: bow wow\n"); }
// Declare a constructor for Dog
struct Dog Dog_default(void) {
return (struct Dog){
.animalSound = Dog_animalSound,
};
}
int main(void) {
// Setup each animal as their default
struct Animal myAnimal = Animal_default();
struct Pig myPig = Pig_default();
struct Dog myDog = Dog_default();
myAnimal.animalSound();
myPig.animalSound();
myDog.animalSound();
return 0;
}
Writing polymorphism in C can vary in complexity. It all depends on where the super construct lies in the structure. If the super structure is the first field, you can automatically cast from a parent pointer to a child pointer. Otherwise you will have to calculate the offset. Some C compilers have a macro offsetof
which calculates the offset of a field in a structure and therefore find the pointer of the wanted structure. To find the pointer of the structure, we can define a macro parentFromPtr
.
#ifndef offsetof
#define offsetof(T, FIELD) ((size_t)&((T*)0)->FIELD)
#endif
#define parentFromPtr(T, FIELD, PTR) \
((T *)((size_t)PTR - offsetof(T, FIELD)))
While this macro may look daunting at first, it becomes relatively simple when you break it down. We get the address of the FIELD
in the struct type T
. We return it as a size_t
type because we want an integer value. We cast PTR
to size_t
to tell our compiler we want to subtract integers instead of pointers (which would produce varying results). Then we cast the result to our struct pointer. Now we can gain the correct child pointer if the super construct isn't the first element in a struct, or if the struct extends multiple structs.
#define struct$Hero(NAME) \
struct NAME { \
const char *name; \
void (*sayCatchphrase)(struct Hero *); \
}
#define using(TYPE, FIELDNAME) \
union { \
struct$##TYPE(); \
struct TYPE FIELDNAME; \
}
struct$Hero(Hero);
void Hero_sayCatchphrase(struct Hero *self) {
printf("Never fear! %s is here!\n", self->name);
}
struct Hero Hero_create(const char *name) {
return (struct Hero){
.name = name,
.sayCatchphrase = Hero_sayCatchphrase,
};
}
struct GunSlinger {
const char *gun_model;
using(Hero, super);
};
void GunSlinger_sayCatchphrase(struct Hero *super) {
struct GunSlinger *self = parentFromPtr(struct GunSlinger, super, super);
printf("Take cover from 'The %s' or taste the lead of my %s!\n", self->name,
self->gun_model);
}
struct GunSlinger GunSlinger_create(const char *gun_model) {
return (struct GunSlinger){
.gun_model = gun_model,
.name = "Gun Slinger",
.sayCatchphrase = GunSlinger_sayCatchphrase,
};
}
int main(void) {
struct Hero hero = Hero_create("Foobar");
struct GunSlinger gun_slinger = GunSlinger_create("Revolver");
hero.sayCatchphrase(&hero);
gun_slinger.sayCatchphrase(&gun_slinger.super);
return 0;
}
Abstraction
The key point of abstraction is to make code simpler and more understandable. It hides the internal details and creates high level functionality that can be easily understood and used. Abstraction is similar to encapsulation but is often thought of as an extension to encapsulation. Commonly, you see abstraction in the form of Interfaces or Abstract classes which define general characteristics. In C, every structure you create follows this pattern. You can define Interfaces
as a struct with function pointers, often referred to as a virtual table. An abstract class in C is indistinguishable from a regular structure. You can create new classes to fill in structure data but you have to specify every data field.
Conclusion
C is almost an object-oriented language. You can utilize language features and macros to have a smoother OOP experience but it wasn't natively designed to have good systems in place. You miss out on easy encapsulation. Abstraction is almost always present. Inheritance requires extra work to make it a seamless transition. And polymorphism requires a good understanding of how to write function pointers. If you want an easy OOP experience, C++ or Java are far better. If you want to have complete control over your OOP system, C will force you to learn