*ฅ^•ﻌ•^ฅ* ✨✨  HWisnu's blog  ✨✨ о ฅ^•ﻌ•^ฅ

Object Oriented Programming in C and Zig

Introduction

One of the primary differences between C and modern programming languages is often cited as the lack of object-oriented programming (OOP) in C, which was later introduced in C++. However, it is possible to implement OOP concepts in C, as well as in other more modern languages like Zig.

Implementing OOP in C using Function Pointers

To illustrate this, let's examine a simple example of implementing OOP in C using function pointers within a struct:

Section 1: Defining a Struct to Represent a Game Object

This code defines a struct called GameObject that contains three function pointers: init, update, and draw. Each function pointer takes a void* parameter called self, which represents the game object itself.

#include <stdio.h>

#define UNUSED(x) (void)(x)

typedef struct {
    // below are 3 function pointers
    void (*init)(void* self);
    void (*update)(void* self);
    void (*draw)(void* self);
} GameObject;

Section 2: Implementing the Game Object's Functions

These three functions implement the game object's behavior. Each function takes a void* parameter self, which is not used in this example. The UNUSED macro is used to suppress compiler warnings about the unused self parameter. The functions simply print a message to the console.

void init_obj(void* self)
{
    UNUSED(self);
    printf("Initializing object\n");
}

void update_obj(void* self)
{
    UNUSED(self);
    printf("Updating object\n");
}

void draw_obj(void* self)
{
    UNUSED(self);
    printf("Drawing object\n");
}

Section 3: Using the Game Object

This function takes a GameObject* parameter obj and calls its init, update, and draw functions if they are not null. This allows the game object to perform its behavior.

void use_GameObject(GameObject* obj)
{
    if (obj->init) obj->init(obj);
    if (obj->update) obj->update(obj);
    if (obj->draw) obj->draw(obj);
}

Section 4: Creating and Using a Game Object

In the main function, a GameObject is created and its function pointers are set to the init_obj, update_obj, and draw_obj functions. The use_GameObject function is then called with the game object as an argument, which causes the game object to perform its behavior.

int main(void)
{
    GameObject obj = {
        .init = init_obj,
        .update = update_obj,
        .draw = draw_obj
    };
    
    use_GameObject(&obj);

    return 0;
}

Output and Explanation

if you compile and run the program, the output will be:

Initializing object
Updating object
Drawing object

This approach enables the implementation of fundamental OOP paradigms, including:

Equivalent Code in Zig

We can further explore how to expand this implementation in future posts, particularly in the context of game development, where OOP concepts are frequently employed.

Now let's see the equivalent code written in Zig:

Section 1: Defining a Struct to Represent a Game Object

This code defines a struct called GameObject that contains three methods: initObj, updateObj, and drawObj. Each method takes a *GameObject parameter self, which represents the game object itself. The _ = self; line is used to suppress compiler warnings about the unused self parameter. The methods simply print a message to the console.

Note that in Zig, methods are defined directly on the struct, unlike in C where function pointers are used.

const std = @import("std");
const print = std.debug.print;


const GameObject = struct {
    // methods on struct instead of function pointers
    fn initObj(self: *GameObject) void {
        _ = self;
        print("Initializing object\n", .{});
    }

    fn updateObj(self: *GameObject) void {
        _ = self;
        print("Updating object\n", .{});
    }

    fn drawObj(self: *GameObject) void {
        _ = self;
        print("Drawing object\n", .{});
    }
};

Section 2: Creating and Using a Game Object

In the main function, a GameObject is created and its methods are called directly on the object. This is a more object-oriented approach than the C example, where function pointers were used.

pub fn main() void {

    var obj: GameObject = .{};
    obj.initObj();
    obj.updateObj();
    obj.drawObj();
}

Compile and run the program, you'll get the same output:

Initializing object
Updating object
Drawing object

Conclusion

Comparing the C and Zig implementations, we can see that the Zig version is more concise and easier to read, with fewer lines of code. Both implementations are valid, but they differ in style: the C version follows a traditional C-style approach, whereas the Zig version utilizes a more modern implementation, known as "methods on struct." This approach allows us to define functions directly within a struct, making the code more readable and maintainable.

In C, it is not possible to define methods or functions directly within a struct. However, Zig's more modern design enables this feature, contributing to the cleaner and more readable code.


Note: I understand some programmers despise the use of function pointers and at this point I'm not going to debate. For those who prefer a more traditional OOP approach, C++ is a viable option.

#OOP #abstraction #c #encapsulation #function pointer #inheritance #low level #programming #static polymorphism #struct #syntax #zig