Skip to content
Home » Software Design » Encapsulation and Information Hiding in C — With Examples

Encapsulation and Information Hiding in C — With Examples

Encapsulation and Information Hiding featured image with purple background, Principles badge, En icon, Information Hiding in C subtitle in the Software Design in C series
Software Design Principles in C
Part 17 of 31View Full Path →

KEY TAKEAWAYS

  • Use opaque pointers to hide struct internals from callers
  • Use static for functions and variables that should not leave the source file
  • Headers define the public API; source files hold the private implementation
  • Encapsulation in C is a deliberate design choice — the language will not enforce it automatically

What Is Encapsulation?

Encapsulation means bundling data with the functions that operate on it, and hiding the internal details so that outside code cannot directly access or depend on them. In object-oriented languages, this is done with classes and private/public keywords. In C, we achieve it through opaque pointers, header/source file separation, and static functions.The goal: other modules interact with your code through a defined API, not by poking at internal variables or struct fields.

Example 1: Opaque Pointer (The Gold Standard in C)

Bad — Exposed Struct in Header

// stack.h — struct visible to everyone
typedef struct {
    int items[100];
    int top;
} Stack;

void stack_push(Stack *s, int val);
int  stack_pop(Stack *s);
int  stack_is_empty(Stack *s);

// Problem: any code can do this
// Stack s;
// s.top = 50;         // Bypass the API!
// s.items[99] = 42;   // Direct internal access
Because the struct is in the header, nothing prevents code from reaching into the internals. A “helpful” developer might set s.top directly, bypassing bounds checking.

Good — Opaque Pointer

// stack.h — only a forward declaration
typedef struct Stack Stack;

Stack *stack_create(int capacity);
void   stack_destroy(Stack *s);
void   stack_push(Stack *s, int val);
int    stack_pop(Stack *s);
int    stack_is_empty(const Stack *s);

// stack.c — struct definition is PRIVATE
#include "stack.h"
#include <stdlib.h>

struct Stack {
    int *items;
    int  top;
    int  capacity;
};

Stack *stack_create(int capacity) {
    Stack *s = malloc(sizeof(Stack));
    s->items = malloc(sizeof(int) * capacity);
    s->top = -1;
    s->capacity = capacity;
    return s;
}

void stack_push(Stack *s, int val) {
    if (s->top capacity - 1) {
        s->items[++s->top] = val;
    }
}

int stack_pop(Stack *s) {
    if (s->top >= 0) {
        return s->items[s->top--];
    }
    return -1;  // error
}

int stack_is_empty(const Stack *s) {
    return s->top < 0;
}
Outside code can only use Stack * as a pointer — it cannot see the fields. The struct definition lives in the .c file, making it truly private. You can change the internal representation (array to linked list, add a mutex for thread safety) without changing any caller.

Example 2: Static Functions for Internal-Only Logic

Bad — Helper Functions Exposed Globally

// sensor.h
void sensor_init(void);
float sensor_read(void);
float sensor_apply_calibration(float raw);  // Internal detail — should not be public
int   sensor_convert_adc(int raw_adc);      // Internal detail — should not be public
Anyone can call sensor_apply_calibration() directly. If you change the calibration logic, code outside your module might break.

Good — Static Functions Hide Implementation

// sensor.h — clean public API only
void  sensor_init(void);
float sensor_read(void);

// sensor.c
#include "sensor.h"

static float calibration_offset = 0.0f;

// static = visible only in this file
static float apply_calibration(float raw) {
    return raw * 1.02f + calibration_offset;
}

static int convert_adc(int raw_adc) {
    return (raw_adc * 3300) / 4096;
}

void sensor_init(void) {
    calibration_offset = read_calibration_from_flash();
}

float sensor_read(void) {
    int adc = read_adc_channel(0);
    float mv = (float)convert_adc(adc);
    return apply_calibration(mv);
}
The static keyword makes apply_calibration and convert_adc invisible outside sensor.c. The header exposes only what callers need. You can freely change, rename, or remove the internal functions without affecting any other file.

The Three Layers of Encapsulation in C

TechniqueWhat It HidesWhen to Use
static functionsInternal helper functionsAlways — default to static unless a function must be public
static variablesModule-level stateWhen state should not be accessible from outside the file
Opaque pointersStruct layout and fieldsWhen multiple instances exist and internals may change

Benefits of Encapsulation

  • Safe refactoring — change internals without breaking callers
  • Enforced contracts — callers must use your API, not bypass it
  • Reduced compilation dependencies — changing a .c file does not force recompilation of files that include the .h
  • Cleaner headers — smaller public APIs are easier to understand and maintain

Key Takeaways

  • Use opaque pointers to hide struct internals from callers
  • Use static for functions and variables that should not leave the source file
  • Headers define the public API; source files hold the private implementation
  • Encapsulation in C is a deliberate design choice — the language will not enforce it automatically

Leave a Reply

Your email address will not be published. Required fields are marked *