How to implement object-oriented programming (OOP) in C

Unlike the OOP languages C++ and Objective-C, C does not include object-oriented features. However, since the language has become widely used and object-oriented programming has gained widespread popularity, strategies for implementing OOP in C have been created.

Is OOP in C possible?

The C programming language is not intended for object-oriented programming and is a prime example of the structured programming style in imperative programming. However, it is possible to replicate object-oriented approaches in C. In fact, C has all the components needed for it and contributed to forming the basis for object-oriented programming in Python.

Individual abstract data types (ADT) can be defined with OOP. An ADT can be thought of as a set of possible values with functions operating on them. It is important that the externally visible interface and the internal implementation are decoupled from each other. This ensures that the type’s objects behave according to description.

Object-oriented languages such as Python, Java and C++ use the class concept to model abstract data types. Classes serve as a template for creating similar objects, which is also referred to as instantiation. C does not have classes, and these cannot be modeled within the language. However, there are several approaches for implementing OOP features in C.

How does OOP work in C?

Understanding how OOP works in C requires first asking, “What exactly is object-oriented programming (OOP)?” OOP is a programming style that is commonly seen in the imperative programming paradigm. This sets OOP apart from declarative programming and its specialization, functional programming.

The basic idea of object-oriented programming is to model objects and let them interact with each other. The program flow is a result of the objects interacting and is only fixed at runtime. OOP covers three characteristics:

  1. Objects encapsulate their internal state.
  2. Objects receive messages through their methods.
  3. The methods are assigned dynamically at runtime.

An object in a pure OOP language such as Java is a self-contained unit. This includes a random complex data structure and methods (functions) that operate on it. The object’s internal state, which is represented in the data it contains, can only be read and changed through the methods. The language feature “garbage collector” is usually used for the objects’ memory management.

Connecting a data structure and functions to objects is not available in C. The user must put together a manageable system of data structures, type definitions, pointers and functions. The programmer is responsible for allocating and releasing memory in C.

The resulting object-based C code doesn’t look quite like what you’re probably used to in other OOP languages. Nevertheless, it does work. Below is an overview of the main OOP concepts with their equivalents in C:

OOP concept Equivalent in C
Class Struct type
Class instance Struct instance
Instance method Function that accepts pointers to Struct variable
this/self variable Pointer to Struct variable
Instantiation Allocation and reference through pointer
new keyword Call malloc

How to model objects as data structures

Let’s look at how an object’s data structure can be modeled in C in a way that is similar to OOP languages. C is a compact language that doesn’t work with many language constructs. Structs are used to create random complex data structures, with the name “struct” being derived from the term “data structure”.

A struct in C defines a data structure that has fields which are referred to as “members”. This type of construct is called a “record” in other languages. A struct can be thought of as a row in a database table, like a composite with several fields and different types.

The syntax for a struct declaration in C is very simple:

struct struct_name;
C

We can also define the struct by specifying the members’ name and type. Let’s consider a point in a two-dimensional space with x and y coordinates as an example. We’ll outline the struct definition:

struct point {
    /*X-coordinate*/
    int x;
    /*Y-coordinate*/
    int y;
};
C

This is followed by the struct variable’s instantiation in conventional C code. We’ll create the variable and initialize both fields with 0:

struct point origin = {0, 0};
C

Subsequently, the values in the fields can be read and reset. The member access is done using the syntax origin.x and origin.y, which you may know from other languages:

/*Read struct member*/
origin.x == 0
/*Assign struct member*/
origin.y = 42
C

However, this violates the encapsulation requirement. The object’s internal state may only be accessed using methods defined for this purpose. This means that our approach is still missing something.

How to define types for creating objects

As mentioned before, C does not have class concepts. Instead, types can be defined with a typedef statement. We’ll give the data type a new name with typedef:

typedef <old-type-name> <new-type-name>
C

This allows us to define a point type for our point struct:

typedef struct point Point;
C

The combination of typedef with a struct definition is like a class definition in Java:

typedef struct point {
    /*X-coordinate*/
    int x;
    /*Y-coordinate*/
    int y;
} Point;
C
Note

In the example, “point” is the name of the struct and “Point” is the name of the defined type.

This would be the corresponding class definition in Java:

class Point {
    private int x;
    private int y;
};
Java

Using typedef allows us to create a point variable without using the struct keyword:

Point origin = {0, 0}
/*Instead of*/
struct point origin = {0, 0}
C

The internal state’s encapsulation is still missing.

How to encapsulate the internal state

Objects display their internal state in their data structure. In other OOP languages, such as Java, the keywords “private”, “protected”, etc. are used to restrict access to object data. This prevents unauthorized access and ensures that the interface and implementation are separated.

To implement OOP in C, a different mechanism is used. A forward declaration in the header file serves as an interface, resulting in an “Incomplete type”:

/*In C header file*/
struct point;
/*Incomplete type*/
typedef struct point Point;
C

The point struct’s implementation is in a separate C source code file. This embeds the header using include macro. This approach prevents the creation of static variables of the point type. It is still possible to use pointers from this type. Objects are dynamically created data structures, so they are referenced with pointers anyway. Pointers to struct instances correspond roughly to the object references used in Java.

How to replace methods with functions

In OOP languages such as Java and Python, objects include the functions that operate on them in addition to their data. These are called methods and instance methods. We use functions that take a pointer to a struct instance when OOP code is written in C:

/*Pointer to `Point` struct*/
Point * point;
C

C does not have classes. This makes it impossible to group functions belonging to a type under a common name. Instead, we provide the function names with a prefix containing the type’s name. The corresponding function signatures are declared in the C header file:

/*In C header file*/
/*Function to move update a point's coordinates*/
void Point_move(Point * point, int new_x, int new_y);
C

It is necessary to implement the function in the C source code file:

/*In C source file*/
void Point_move(Point * point, int new_x, int new_y) {
    point->x = new_x;
    point->y = new_y;
};
C

The approach has similarities to Python methods, which are normal functions that take self as the first parameter. Furthermore, the pointer to a struct instance is roughly equivalent to the variable in Java or JavaScript. However, the pointer is passed explicitly when the C function is called in this case:

/*Call function with pointer argument*/
Point_move(point, 42, 51);
C

The point object is available in the method as a variable in the Java function call:

// Call instance method from outside of class
point.move(42, 51)
// Call instance method from within class
this.move(42, 51)
Java

Methods can be called as functions with an explicit self-argument in Python:

# Call instance method from outside or from within class
self.move(42, 51)
# Function call from within class
move(self, 42, 51)
Python

How to instantiate objects

One of C’s defining characteristics is manual memory management. Programmers must allocate memory for data structures. This is not required in object-oriented and dynamic languages such as Java and Python. In Java, the new keyword is used to instantiate an object. Memory is allocated automatically in the background:

// Create new Point instance
Point point = new Point();
Java

We define a special constructor function for instantiation when we write OOP code in C. This allocates memory for our struct instance, initializes it, and returns a pointer to it:

Point * Point_new(int x, int y) {
    /*Allocate memory and cast to pointer type*/
    Point *point = (Point*) malloc(sizeof(Point));
    /*Initialize members*/
    Point_init(point, x, y);
    // return pointer
    return point;
};
C

Our example decouples the struct members’ initialization from the instantiation. A function with the point prefix is used again:

void Point_init(Point * point, int x, int y) {
    point->x = x;
    point->y = y;
};
C

How can a C project be rewritten in an object-oriented manner?

Rewriting an existing project in C using OOP techniques is recommended only in exceptional cases. The following approaches would be more worthwhile:

  1. Rewrite project in a language like C with OOP features and use the existing C code base as a specification.
  2. Rewrite parts of the project in an OOP language and keep specific C components.

The second approach should be the most efficient provided the C code base is clean. It is common practice to implement performance-critical program parts in C and access them from other languages. There probably isn’t another language better suited to this than C. But which languages are suitable for rebuilding an existing C project using OOP principles?

Object-oriented languages like C

There is a wide selection of languages like C with built-in object orientation. C++ is probably the most well-known. However, the language is notoriously complex, which has led many to move away from it in recent years. C code is relatively easy to incorporate into C++ due to major similarities in the basic language constructs.

Objective-C is more lightweight than C++. The C dialect, which is based on the original OOP language Smalltalk, was primarily used for programming applications on Mac and early iOS operating systems. It was later followed by Apple’s development of its own Swift language. Functions written in C can be called from both languages.

Object-oriented languages based on C

There are other OOP programming languages that are suitable for rewriting a C project but are not related to C’s syntax. Standard approaches for including C code exist for Python, Rust, and Java.

Python bindings allow for the inclusion of C code. Python data types may have to be translated into the corresponding ctypes. The C Foreign Function Interface (CFFI) also automates type translation.

Rust also supports calling C functions. The external keyword can be used to define a Foreign Function Interface (FFI). Rust functions that access external functions must be declared unsafe:

external "C" {
    fn abs(input: i32) -> i32;
}
fn main() {
    unsafe {
        println!("Absolute value of -3 according to C: {}", abs(-3));
    }
}
Rust
We use cookies on our website to provide you with the best possible user experience. By continuing to use our website or services, you agree to their use. More Information.