Defining Opaque Structs
Part of the Object Oriented Programming section of Coddy's C journey — lesson 18 of 61.
Now that you understand the concept of opaque pointers, let's see how to complete the pattern. The header declares that a type exists, but the source file is where you define what's actually inside.
Here's the complete setup. First, the header with only the forward declaration:
// counter.h
#ifndef COUNTER_H
#define COUNTER_H
typedef struct Counter Counter; // Incomplete type
Counter *create_counter(int start);
void free_counter(Counter *c);
#endif
Then, the source file contains the actual struct definition:
// counter.c
#include "counter.h"
#include <stdlib.h>
struct Counter {
int value; // Hidden from outside!
int step;
};
Counter *create_counter(int start) {
Counter *c = malloc(sizeof(Counter));
c->value = start;
c->step = 1;
return c;
}
void free_counter(Counter *c) {
free(c);
}
The key point: struct Counter { ... } appears only in the .c file. Any file that includes counter.h knows Counter exists but cannot see value or step. Attempting to write c->value in main.c would cause a compiler error—the members are truly invisible.
This separation is the foundation of encapsulation in C. The next step is providing controlled access through getter and setter functions.
Challenge
EasyLet's build a Timer module that demonstrates the opaque pointer pattern. You'll practice hiding the struct definition inside the source file, making its internal members completely invisible to any code that uses your module.
You'll create three files:
timer.h: This is your public interface. Declare an opaqueTimertype using only a forward declaration—no struct body here! Also declare two functions:create_timer(takes an integer for seconds and returns a Timer pointer) andfree_timer(takes a Timer pointer and cleans up). Use include guards with the symbolTIMER_H.timer.c: This is where the secret lives. Define the actualstruct Timerwith two hidden members:int secondsandint running(use 1 for running, 0 for stopped). Implement your constructor to allocate the struct, initializesecondswith the provided value, and setrunningto 1. Implement the destructor to free the memory.main.c: Use your Timer module to create a timer and confirm it was created successfully. Since the struct members are hidden, you cannot access them directly—this is the whole point! Simply print a message confirming creation, then free the timer and print a cleanup message.
You will receive one input: the number of seconds (an integer) to initialize the timer with.
In your main file, create a timer with the provided seconds value. Print a creation message, then free the timer and print a confirmation.
Print the output in this format:
Timer created
Timer freedThe key insight here is that main.c can work with Timer pointers but has no way to access seconds or running directly. Attempting to write t->seconds in main.c would cause a compiler error because the struct body is only defined in timer.c. This is true encapsulation—the implementation details are completely hidden from users of your module.
Cheat sheet
The opaque pointer pattern separates the declaration of a type from its definition, hiding implementation details.
Header file contains only a forward declaration:
// counter.h
#ifndef COUNTER_H
#define COUNTER_H
typedef struct Counter Counter; // Incomplete type
Counter *create_counter(int start);
void free_counter(Counter *c);
#endif
Source file contains the actual struct definition:
// counter.c
#include "counter.h"
#include <stdlib.h>
struct Counter {
int value; // Hidden from outside
int step;
};
Counter *create_counter(int start) {
Counter *c = malloc(sizeof(Counter));
c->value = start;
c->step = 1;
return c;
}
void free_counter(Counter *c) {
free(c);
}
The struct definition appears only in the .c file. Files that include the header know the type exists but cannot access its members directly. This provides true encapsulation in C.
Try it yourself
#include <stdio.h>
#include "timer.h"
int main() {
int seconds;
scanf("%d", &seconds);
// TODO: Create a timer with the provided seconds value
// TODO: Print "Timer created"
// TODO: Free the timer
// TODO: Print "Timer freed"
return 0;
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Modular Programming Basics
Header FilesInclude GuardsSource FilesStatic FunctionsRecap: Modular Calculator4Encapsulation
Opaque Pointers ConceptDefining Opaque StructsGetters and SettersValidation in SettersRecap: Secret Box2Objects and Methods
Structs as ObjectsThe 'Self' PointerConst CorrectnessPointer vs ValueHelper MethodsRecap: Point Manager5Project: Simple Bank Account
Project SetupImplementation of Account3Object Lifecycle
Constructor PatternDestructor PatternStack InitializationDeep CopyRecap: String Wrapper6Inheritance via Composition
Struct EmbeddingThe First Member RuleAccessing Parent MembersUpcastingRecap: Shape Hierarchy