Introduction
The C language was born in 1972 and has been around for 47 years, making it quite an old language. However, it remains very popular and continues to rank among the top programming languages, demonstrating remarkable vitality.
C is often labeled as <span>procedural</span>, and many students have not considered or practiced developing C code using <span>object-oriented</span> principles. When your C system is relatively simple, either procedural or object-oriented approaches can be used for development. However, when your C system becomes large and complex, adopting an object-oriented approach can significantly enhance the system’s understandability, maintainability, and scalability.
This article uses a well-known design pattern (the State Pattern) as an example to guide readers through an object-oriented journey in C, enabling them to quickly grasp the object-oriented development techniques in C.
Review of the State Pattern

The State Pattern encapsulates an object’s behavior within different state objects, each of which shares a common abstract state class. When an object’s internal state changes, it allows its behavior to change, making it appear as if the object has changed its class.
Usage scenarios:
- • An object’s behavior depends on its state, and it must change its behavior at runtime based on its state.
- • The code contains numerous conditional statements related to the object’s state.
Context and State Handling Objects:
- • In the State Pattern, the context is the object that holds the state, but the context does not handle state-related behavior; instead, it delegates the functionality of handling states to the corresponding state handling classes.
- • In specific state handling classes, it is often necessary to access the context’s data, and when necessary, to call methods of the context. Therefore, the context is usually passed as a parameter to the specific state handling classes.
C Language Practical Application
Water can exist in three states: solid, liquid, and gas, and these states can transform into one another, as shown in the following diagram:

Water has a temperature, and we can consider:
- • When the temperature is below 0 degrees, water is in a solid state.
- • When the temperature is between 0 and 100 degrees, water is in a liquid state.
- • When the temperature is above 100 degrees, water is in a gaseous state.
Class Design
- • Water Class: Environment class that holds a collection of state objects.
- • State Class: Abstract class that declares the common behaviors of all state objects.
- • SolidState: Concrete subclass representing the solid state of water.
- • LiquidState: Concrete subclass representing the liquid state of water.
- • GaseousState: Concrete subclass representing the gaseous state of water.
The class diagram is identical to that in the design pattern and will not be elaborated further.
Class Definition
In C, classes can only be simulated using structures (struct), which have properties and methods:
- • The struct does not have methods; we simulate them using function pointers.
- • The struct has data members, which simulate the properties of a class.
- • The visibility of struct members can only be public, meaning methods and properties can only be declared as public, but we should adhere to the principle of least visibility in programming.
Definition of the Water class, code example:
// Water.h
struct State;
typedef struct Water
{
int (*getTemperature)(struct Water* self);
void (*riseTemperature)(struct Water* self, int step);
void (*reduceTemperature)(struct Water* self, int step);
void (*behavior)(struct Water* self);
void (*changeState)(struct Water* self);
struct State* states[MAX_STATE_NUM];
struct State* currentState;
int temperature;
} Water;
Code explanation:
- • Water holds multiple State pointers, so a forward declaration of State is needed.
- • All methods are simulated using function pointers, and the first parameter self simulates the this pointer. The parameter name is self instead of this to prevent compilation issues when mixing C and C++, and the platform’s basic subsystems or modules will not involve mixing C and Python.
- • Methods are defined first, followed by properties.
- • When the temperature changes (rise or reduce), it triggers the changeState operation, thereby updating the currentState property value in a timely manner.
Class Inheritance
Concrete subclasses inherit the methods and properties of the abstract class, meaning the methods and properties declared in the concrete subclass are based on the declarations of the abstract class, and then expanded as needed. It is advisable to consider making the abstract class’s variables the first members of the concrete subclass so that both the abstract class pointer and the concrete subclass pointer can point to the address of the concrete subclass object.
Abstract class code example:
// State.h
struct Water;
typedef struct State
{
ABSTRACT(void (*handle)(struct State* self, struct Water* water));
ABSTRACT(Boolean (*match)(struct State* self, int temperature));
const char* (*getName)(struct State* self);
char* name;
} State;
Code explanation:
- • The abstract method handle in the abstract class State needs to call methods of the Water pointer, so a forward declaration of Water is required.
- • The abstract methods of the abstract class State are explicitly marked using the macro keyword
<span>ABSTRACT</span>. - • Non-abstract methods of the abstract class State are implemented within the abstract class, and concrete subclasses inherit and reuse these methods directly.
The concrete subclasses SolidState, LiquidState, and GaseousState all inherit from the abstract class State, code example:
// State.h
typedef struct SolidState
{
State base;
} SolidState;
typedef struct LiquidState
{
State base;
} LiquidState;
typedef struct GaseousState
{
State base;
} GaseousState;
Code explanation:
- • Concrete subclasses only need to inherit from the abstract class State; no additional methods or properties need to be defined unless the subclass requires expansion.
- • Concrete subclasses must implement the abstract methods while reusing the inherited non-abstract methods and properties.
Object Lifecycle Management
A core idea of object-oriented development is the object itself, treating anything that can be typed as an object, and implementing interactions and calls between programs in the form of message passing between objects. How objects are created and destroyed is the main concern of object lifecycle management.
Object Creation
We simulate the new operator in C++ using a create function: first allocate memory for the object, then call the constructor to complete object initialization.
For the Water object, we independently encapsulate the creation function waterCreate and the constructor waterInit:
// Water.c
Water* waterCreate(int temperature)
{
Water* water = (Water*)malloc(sizeof(Water));
if (water != NULL)
{
ErrCode err = waterInit(water, temperature);
if (err != ERR_SUCC)
{
return NULL;
}
}
return water;
}
ErrCode waterInit(Water* self, int temperature)
{
self->states[0] = stateCreate(SOLID);
self->states[1] = stateCreate(LIQUID);
self->states[2] = stateCreate(GASEOUS);
for (int i = 0; i < MAX_STATE_NUM; i++)
{
if (self->states[i] == NULL)
{
return ERR_MEM_MALLOC_FAILED;
}
}
self->temperature = temperature;
waterChangeState(self);
self->getTemperature = waterGetTemperature;
self->riseTemperature = waterRiseTemperature;
self->reduceTemperature = waterReduceTemperature;
self->changeState = waterChangeState;
self->behavior = waterBehavior;
return ERR_SUCC;
}
Code explanation:
- • The parameter of waterCreate is equivalent to the constructor’s parameter, which is the user’s initialization input.
- • When memory allocation fails, it returns NULL.
- • The constructor may allocate system resources, and when system resources are insufficient, the constructor may fail. In this case, the return value should be defined as ErrCode, which differs from C++ constructors.
- • The first parameter of the constructor must be the this pointer.
- • In the constructor of Water, the object creation function stateCreate is called for the State objects, requiring the specific subclass’s enumeration type to be injected while uniformly returning a pointer to the abstract class.
- • The initialization of the currentState property of Water is dynamically completed through the waterChangeState function, which is strongly related to the user input parameter temperature.
For the State object, we uniformly encapsulate the creation function stateCreate:
State* stateCreate(StateIdentifier identifier)
{
switch(identifier)
{
case SOLID: return solidStateCreate();
case LIQUID: return liquidStateCreate();
case GASEOUS: return gaseousStateCreate();
default: return NULL;
}
}
void stateInit(State* self,
void (*handle)(struct State* self, struct Water* water),
Boolean (*match)(struct State* self, int temperature),
char* name)
{
self->handle = handle;
self->match = match;
self->getName = stateGetName;
self->name = name;
}
State* solidStateCreate()
{
State* state = (State*)malloc(sizeof(SolidState));
if (state != NULL)
{
solidStateInit((SolidState*)state);
}
return state;
}
void solidStateInit(SolidState* self)
{
stateInit((State*)self, solidStateHandle, solidStateMatch, "SolidState");
}
Code explanation:
- • The creation function stateCreate is essentially a simple factory: it creates specific subclass objects based on the subclass identifier while uniformly returning a pointer to the abstract class.
- • The creation function xStateCreate is used to create specific subclass objects and return pointers to the abstract class.
- • The abstract class provides a constructor stateInit to initialize the methods and properties declared in the abstract class, which can only be called by the constructors of concrete subclasses.
- • Each concrete subclass must provide a constructor xStateInit to complete the initialization of the subclass object, which includes two parts: (1) calling the base class constructor to initialize the base class methods and properties; (2) independently completing the initialization of the methods and properties unique to that subclass.
Object Destruction
We simulate the delete operator in C++ using a destroy function: first call the destructor to release the resources held by the object, then free the object’s memory.
For the Water object, we independently encapsulate the destruction function waterDestroy and the destructor waterCleanUp:
void waterDestroy(Water* water)
{
waterCleanUp(water);
free(water);
}
void waterCleanUp(Water* self)
{
for (int i = 0; i < MAX_STATE_NUM; i++)
{
stateDestroy(self->states[i]);
self->states[i] = NULL;
}
self->currentState = NULL;
}
Code explanation:
- • The parameters of the destroy function and destructor only include the this pointer.
- • A typical scenario for the destructor to release resources held by the object is to destroy other objects held by the object and then set the held object pointers to NULL.
For the State object, we uniformly encapsulate the destruction function stateDestroy:
void stateDestroy(State* state)
{
free(state);
}
Code explanation:
- • The destruction function is necessary, while the destructor is optional and should be determined based on specific circumstances.
- • The destruction of concrete subclass objects uniformly reuses the abstract class’s destruction function stateDestroy, meaning that the creation function xStateCreate of concrete subclass objects does not need a corresponding destruction function.
Object Polymorphism
The environment class Water holds pointers to the abstract class State:
// Water.h
struct State* states[MAX_STATE_NUM];
struct State* currentState;
In the constructor of Water, these State pointer variables are initialized through the State creation function:
self->states[0] = stateCreate(SOLID);
self->states[1] = stateCreate(LIQUID);
self->states[2] = stateCreate(GASEOUS);
for (int i = 0; i < MAX_STATE_NUM; i++)
{
if (self->states[i]->match(self->states[i], self->temperature))
{
self->currentState = self->states[i];
return;
}
}
It can be seen that the abstract class pointer points to the address of the concrete subclass object. By making the base class variable the first member of the subclass, polymorphism can be easily achieved through type casting. Is it that simple? Yes, it is that simple, and this type of polymorphism is essentially a compile-time polymorphism.
Client Invocation
The process of client invocation:
- • Create a Water object with an initial temperature of 25 degrees Celsius, making the water liquid and exhibiting liquid behavior.
- • Increase the water temperature by 50 degrees Celsius, changing the temperature to 75 degrees Celsius, and the water remains liquid, exhibiting liquid behavior.
- • Decrease the water temperature by 100 degrees Celsius, changing the temperature to -25 degrees Celsius, and the water becomes solid, exhibiting solid behavior.
- • Increase the water temperature by 200 degrees Celsius, changing the temperature to 175 degrees Celsius, and the water becomes gaseous, exhibiting gaseous behavior.
- • Destroy the Water object.
Example of client code:
// Client.c
void statePatternRun()
{
Water* water = waterCreate(25);
if (water == NULL) return;
water->behavior(water);
water->riseTemperature(water, 50);
water->behavior(water);
water->reduceTemperature(water, 100);
water->behavior(water);
water->riseTemperature(water, 200);
water->behavior(water);
waterDestroy(water);
}
Running the program, the log is as follows:
$ ./design-pattern-in-c state-pattern
LiquidState: I have a gentle personality, my current temperature is 25 degrees Celsius, I can nourish everything, drinking me can boost your vitality...
LiquidState: I have a gentle personality, my current temperature is 75 degrees Celsius, I can nourish everything, drinking me can boost your vitality...
SolidState: I have a cold personality, my current temperature is -25 degrees Celsius, I am as strong as steel, like a cold-blooded animal, please use me to smash people...
GaseousState: I have a passionate personality, my current temperature is 175 degrees Celsius, flying to the sky is my lifelong dream, here you will not see my existence, I will reach a state of no self...
Conclusion
Thus, an object-oriented journey in C has reached its conclusion.
In this wonderful journey, we have mastered some methods and techniques for practicing object-oriented principles in C:
- • Class definition: Implemented using structures, simulating methods with function pointers.
- • Class inheritance: Making the base class’s variables the first members of the subclass, allowing subclasses to only extend based on the parent class, which can be understood as the
<span>Liskov Substitution Principle</span>. - • Object lifecycle management: Mainly discussed object creation and destruction.
- • Object polymorphism: Type casting, which is a form of compile-time polymorphism.
Link to the example code: https://github.com/agiledragon/design-pattern-c