Classes in C are implemented as conventions rather than a language feature. A structure stores state. Functions receive a pointer to that structure. A public header defines the interface, while the source file can hide the representation. Function pointers add dynamic dispatch when several implementations share one contract.
Classes without class syntax
An object has identity, state and behavior. C separates these concerns explicitly. The address of a struct provides identity, its members hold state, and functions implement behavior. This direct model avoids hidden allocation and dispatch but places ownership and invariants in the API design.
typedef struct {
double balance;
} Account;
void Account_init(Account *self, double opening_balance) {
self->balance = opening_balance;
}
bool Account_deposit(Account *self, double amount) {
if (self == NULL || amount <= 0.0) return false;
self->balance += amount;
return true;
} The first parameter plays the role commonly called this or self. Naming functions with a consistent type prefix prevents collisions and makes the interface discoverable.
Keep data and operations together by contract
A public struct is appropriate for a small value type whose representation is part of the contract. Callers can allocate it on the stack and inspect fields directly. The trade-off is that field layout and invariants become public.
Use functions even with a public struct when an update must enforce a rule. Directly changing balance can bypass validation, audit behavior or thread-safety policy.
Hide private fields with an opaque type
For stronger encapsulation, place only a forward declaration in the header:
/* account.h */
typedef struct Account Account;
Account *Account_create(double opening_balance);
bool Account_deposit(Account *self, double amount);
double Account_balance(const Account *self);
void Account_destroy(Account *self);The source file defines the fields. Callers know the pointer type but cannot use sizeof(Account) or access members. This lets the implementation change layout without recompiling callers against a new public struct definition.
/* account.c */
struct Account {
double balance;
unsigned long revision;
}; Add runtime methods only when dispatch is needed
Plain prefixed functions are sufficient for one implementation. A table of function pointers is useful when different implementations must satisfy one interface, such as file, memory and network streams.
typedef struct Writer Writer;
typedef struct {
size_t (*write)(Writer *self, const void *data, size_t size);
void (*destroy)(Writer *self);
} WriterVTable;
struct Writer {
const WriterVTable *vtable;
};
size_t Writer_write(Writer *self, const void *data, size_t size) {
return self->vtable->write(self, data, size);
}Keep the table immutable and validate that object construction always installs a complete implementation. Avoid copying a polymorphic object by value unless the interface explicitly defines copy behavior.
Make ownership and lifecycle visible
| Pattern | Allocation | Cleanup |
|---|---|---|
| Public value struct | Caller stack or containing struct | Type_deinit if resources exist |
| Opaque object | Type_create | Type_destroy |
| Caller-provided storage | Caller buffer | Explicit deinit |
Document whether functions borrow or take ownership of pointers, whether null is permitted and whether returned references remain valid after a mutation. These rules matter more than whether the interface resembles another language’s class syntax.
Treat inheritance as a layout convention
A derived struct can embed a base struct as its first member. Its address can then be converted to the base address under the documented layout convention. This supports interface reuse but does not provide automatic constructors, access control or safe downcasts.
Composition is usually simpler: store a component and delegate selected operations. Use simulated inheritance when substitutability is a real requirement, not merely to reproduce a hierarchy from C++ or Java.
In C, the interface is the class. The struct layout is an implementation decision unless the header deliberately makes it public.
Frequently asked questions
01Does C have classes?
C has no built-in class syntax, but structures, functions, opaque types and function pointers can provide encapsulation, methods and polymorphic interfaces.
02How do you create an object in C?
Define a struct for object state, initialize an instance through a constructor-style function and pass a pointer to functions that operate on that state.
03What is an opaque struct in C?
A header forward-declares the struct without revealing its fields. The source file contains the complete definition, so callers can use pointers but cannot access private representation directly.
04Can C support methods?
Yes. A method can be represented as a normal function whose first parameter is a pointer to the object. Function pointers inside a struct can provide runtime dispatch.
05How is inheritance implemented in C?
A derived struct can place a base struct as its first member and share an interface table. This convention requires careful layout, ownership and casting rules and is not language-enforced inheritance.
06What replaces constructors and destructors in C?
Initialization and cleanup functions establish and release invariants. Heap-allocated opaque objects often use create and destroy functions, while stack objects use init and deinit.
07Are function pointers required for C classes?
No. Plain functions taking a struct pointer are simpler and often preferable. Function pointers are useful only when implementations must be selected at runtime through one interface.
08Is object-oriented C safe?
It can be robust when ownership, nullability, struct layout and lifecycle rules are explicit. The compiler cannot enforce every convention, so small interfaces and tests are important.