Information Hiding in C Using Incomplete Structs
Although the C programming language is procedural and lacks native class-based constructs, it can strictly enforce object-oriented information hiding and encapsulation using incomplete struct declarations. Often referred to as the "opaque pointer" or "Cheshire Cat" technique, this approach hides the internal representation of data from consuming code. This article explains how declaring structures without defining their members in public header files creates robust application programming interfaces (APIs), prevents unwanted state manipulation, and decouples interface from implementation in C.
The Mechanism of Incomplete Structs
An incomplete type in C is a type that describes an identifier but lacks the information needed to determine its size or internal member layout. While the compiler cannot instantiate an incomplete struct directly or access its fields, it can allocate memory for a pointer to that struct because all object pointers share a uniform platform-specific size.
By placing only the forward declaration of a struct in a
public header file, the library author restricts external code to
interacting exclusively with pointers to that type.
Implementing the Opaque Pointer Pattern
Implementing this design requires separating the public interface
from the internal layout across header (.h) and source
(.c) files.
1. The Public Interface
(database.h)
The header file declares an incomplete struct type and the function prototypes that operate on it. It acts as the public contract.
#ifndef DATABASE_H
#define DATABASE_H
/* Incomplete struct declaration */
typedef struct Database Database;
/* Public API functions */
Database* db_create(const char *connection_string);
void db_connect(Database *db);
void db_disconnect(Database *db);
void db_destroy(Database *db);
#endifConsumers include database.h, allowing them to declare
variables of type Database*. However, attempting to perform
operations like sizeof(Database) or accessing fields via
db->field triggers a compilation error because the
structure's contents remain undefined in this scope.
2. The Private
Implementation (database.c)
The corresponding source file contains the concrete definition of the
struct along with the memory management and implementation
logic.
#include <stdlib.h>
#include <string.h>
#include "database.h"
/* Concrete struct definition hidden from the public interface */
struct Database {
char *connection_string;
int is_connected;
int query_timeout;
};
Database* db_create(const char *connection_string) {
Database *db = malloc(sizeof(struct Database));
if (!db) return NULL;
db->connection_string = strdup(connection_string);
db->is_connected = 0;
db->query_timeout = 30;
return db;
}
void db_connect(Database *db) {
if (db) {
db->is_connected = 1;
}
}
void db_disconnect(Database *db) {
if (db) {
db->is_connected = 0;
}
}
void db_destroy(Database *db) {
if (db) {
free(db->connection_string);
free(db);
}
}The fields connection_string, is_connected,
and query_timeout are only visible within translation units
that include this concrete definition.
Object-Oriented Principles Achieved
Using incomplete structs reproduces several foundational object-oriented benefits:
- Strict Encapsulation: Client code cannot read or modify member variables directly. All mutations must pass through vetted interface functions, preserving internal invariants.
- Decoupling and ABI Stability: The concrete struct can be modified—such as by adding, reordering, or removing fields—without altering the header file. Consequently, consuming code does not need to be recompiled when internal layout changes occur, preserving the Application Binary Interface (ABI).
- Compile-Time Access Control: Unlike languages with
keywords like
privatewhere metadata is still readable in headers, incomplete structs leverage the C compilation model to physically prevent the compiler from knowing the implementation details in client modules.