Design patterns in C
A design pattern is a shape of code that solves a recurring problem, and in C the shapes are built from structs and function pointers. None of them needs a class: the data lives in a struct, the behaviour in functions, and the choice is a pointer stored in the struct or in a table.
Strategy
A strategy replaces an if chain with a function chosen at run time. The struct holds the function pointer next to the name, so the caller iterates over strategies without knowing which one it calls. Adding a strategy is one more entry in the table, not another branch in the code.
#include <stdio.h>
struct Pricing {
const char *name;
double (*price)(double base);
};
static double regular(double base)
{
return base;
}
static double sale(double base)
{
return base * 0.8;
}
int main(void)
{
struct Pricing methods[2] = {
{"regular", regular},
{"sale", sale},
};
for (int i = 0; i < 2; i++) {
printf("%-8s %.2f\n", methods[i].name, methods[i].price(100.0));
}
return 0;
}
gcc -Wall -Wextra strategy.c -o strategy
./strategy
regular 100.00
sale 80.00
The price function receives the base value and returns the final one, so the struct is the whole interface. The same shape works for comparators, hash functions and output formats, which is why this is the most reused pattern in C.
Observer
An observer keeps a list of callbacks and calls each one when something happens. In C the list is an array of function pointers, and the event is the argument they all receive. The subject does not know what the listeners do, only that they accept the event.
#include <stdio.h>
typedef void (*Listener)(int value);
static void log_listener(int value)
{
printf("log: %d\n", value);
}
static void alert_listener(int value)
{
printf("alert: %d\n", value);
}
int main(void)
{
Listener listeners[2] = {log_listener, alert_listener};
for (int i = 0; i < 2; i++) {
listeners[i](42);
}
return 0;
}
gcc -Wall -Wextra observer.c -o observer
./observer
log: 42
alert: 42
A real subject stores the array and its count, adds a listener with a function, and skips empty slots. The pattern is the reason a program can log and alert on the same event without the emitter including either module.
Factory
A factory function centralises the construction of a value: the caller passes a kind and receives a fully built struct, with the fields and the defaults chosen in one place. Returning the struct by value is fine for a small type and avoids allocation, as lesson 36 showed.
#include <stdio.h>
struct Message {
int kind;
const char *text;
};
static struct Message make_message(int kind)
{
if (kind == 1) {
return (struct Message) {1, "info"};
}
return (struct Message) {2, "error"};
}
int main(void)
{
struct Message first = make_message(1);
struct Message second = make_message(2);
printf("%d %s\n", first.kind, first.text);
printf("%d %s\n", second.kind, second.text);
return 0;
}
gcc -Wall -Wextra factory.c -o factory
./factory
1 info
2 error
The compound literal (struct Message) {1, "info"} builds the value at the return. A factory that allocates instead returns a pointer and transfers ownership, the rule from lesson 32.
State table
A state machine maps a state and an event to the next state. When the enumeration is small, a two-dimensional array of enum values is the whole machine: no switch, no branches, and the table is easy to review. The indexes are the enum values, which start at zero.
#include <stdio.h>
enum State { IDLE, RUNNING, DONE };
enum Event { START, FINISH };
static enum State next_state(enum State state, enum Event event)
{
static const enum State table[3][2] = {
{RUNNING, IDLE},
{RUNNING, DONE},
{DONE, DONE},
};
return table[state][event];
}
int main(void)
{
enum State state = IDLE;
state = next_state(state, START);
printf("after start: %d\n", state);
state = next_state(state, FINISH);
printf("after finish: %d\n", state);
return 0;
}
gcc -Wall -Wextra state.c -o state
./state
after start: 1
after finish: 2
The numbers are the enum values, so the output is the state after the transition: 1 is running and 2 is done. The row for the done state maps both events back to done, which makes the machine ignore events it no longer cares about.
Missing case, null factory
A switch over an enumeration is checked by the compiler: leave one value out and -Wswitch tells you, even without a default. The message names the value that has no case:
#include <stdio.h>
enum State { IDLE, RUNNING, DONE };
int main(void)
{
enum State state = RUNNING;
switch (state) {
case IDLE:
printf("idle\n");
break;
case RUNNING:
printf("running\n");
break;
}
return 0;
}
gcc -Wall -Wextra missingcase.c -o missingcase
missingcase.c: In function 'main':
missingcase.c:9:5: warning: enumeration value 'DONE' not handled in switch [-Wswitch]
9 | switch (state) {
| ^~~~~~
A factory that returns a pointer can return NULL, and the caller must check before using it. The analyzer follows the path and reports the dereference, then the program crashes when it runs:
#include <stdio.h>
struct Config {
int value;
};
static struct Config *make_config(int enabled)
{
static struct Config config = {7};
if (!enabled) {
return NULL;
}
return &config;
}
int main(void)
{
struct Config *config = make_config(0);
printf("%d\n", config->value);
return 0;
}
gcc -fanalyzer -Wall -Wextra nullfactory.c -o nullfactory 2>&1 | head -n 3
./nullfactory; echo "exit: $?"
nullfactory.c: In function 'main':
nullfactory.c:21:5: warning: dereference of NULL 'config' [CWE-476] [-Wanalyzer-null-dereference]
21 | printf("%d\n", config->value);
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
exit: 139
The analyzer prints a path after those lines, and the shell reports 139 when the program runs. The fix is the check from lesson 27: test the result of every factory that may fail before it is dereferenced.
Exercises
- Add a third pricing strategy that gives a fixed discount of ten units and print all three results for a base of 50.
- Turn the observer into a subject that stores up to four listeners and has an emit function.
- Draw the transition table for a traffic light with three states and two events, then encode it as an array like the one in this lesson.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does the strategy pattern store in its struct?
Answer
A function pointer chosen at run time
The struct holds the function pointer next to the data, so the caller picks the behaviour at run time.
-
How does an observer notify its listeners?
Answer
It calls every callback with the event value
The subject keeps an array of callbacks and calls each one with the event, without knowing what they do.
-
What does a factory function centralise?
Answer
The construction of a value and its defaults
The caller passes a kind and receives a fully built value, with fields and defaults chosen in one place.
-
How is a small state machine written as a table?
Answer
A two-dimensional array indexed by state and event
The rows are states and the columns are events, so the next state is a single array lookup.
-
What does this program print?
#include <stdio.h> static double regular(double b) { return b; } static double sale(double b) { return b * 0.8; } int main(void){ printf("%.2f %.2f\n", regular(100.0), sale(100.0)); return 0; }Answer
100.00 80.00
The regular price is the base, and the sale price takes twenty percent off.
-
What does this program print?
#include <stdio.h> static void log_it(int v) { printf("log: %d\n", v); } static void alert_it(int v) { printf("alert: %d\n", v); } int main(void){ void (*listeners[2])(int)={log_it,alert_it}; for(int i=0;i<2;i++) listeners[i](42); return 0; }Answer
log: 42 / alert: 42
The array lists the log listener first, so its line appears before the alert line.
-
What does this program print?
#include <stdio.h> struct Message { int kind; const char *text; }; static struct Message make(int k){ return k==1 ? (struct Message){1,"info"} : (struct Message){2,"error"}; } int main(void){ struct Message a=make(1), b=make(2); printf("%d %s\n%d %s\n", a.kind,a.text,b.kind,b.text); return 0; }Answer
1 info / 2 error
Kind 1 builds the info message and kind 2 builds the error message.
-
What does this program print?
#include <stdio.h> enum State { IDLE, RUNNING, DONE }; enum Event { START, FINISH }; static enum State next(enum State s, enum Event e){ static const enum State t[3][2]={{RUNNING,IDLE},{RUNNING,DONE},{DONE,DONE}}; return t[s][e]; } int main(void){ enum State s=IDLE; s=next(s,START); printf("%d\n",s); s=next(s,FINISH); printf("%d\n",s); return 0; }Answer
1 / 2
The table moves idle to running (1) and running to done (2).
-
What does GCC report when a switch over an enum omits one value?
Answer
A warning that the value is not handled
-Wswitchnames the enumerator that has no case, even when there is no default label. -
What happens when a factory returns NULL and the caller dereferences it?
Answer
The analyzer warns and the run exits 139
-fanalyzerreports the null dereference, and the program then crashes with the shell status 139.
The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev