C Structures: Building Class-Like Objects
This article is intended for intermediate and advanced C programmers. It explores how C structures can encapsulate both data and the functions that operate on that data, enabling modular, object-oriented design principles without using C++ classes. It also highlights the similarities and differences between C structures and C++ classes, demonstrating how structures form the foundation of many large-scale C software systems.
1. Why Think Beyond Data?
When most programmers first learn the C language, they are introduced to structures as a convenient way to group related variables.
For example, instead of storing a rectangle's dimensions in separate variables,
they are grouped into a single structure.
This is certainly one of the primary purposes of a structure, and for many C programmers, that is where the story ends.
However, in professional software development, structures play a much larger role.
Rather than viewing a structure as simply a collection of related data, experienced C programmers often treat it as the center of a software component. The structure represents an entity, while a carefully designed set of functions defines everything that can be done with that entity.
Rectangle
├── Data
│ ├── width
│ └── height
│
└── Operations
├── Area()
├── Perimeter()
├── Union()
├── Intersection()
└── Scale()
Instead of scattering unrelated functions throughout the program, all operations concerning a Rectangle are naturally grouped together.
This design offers several advantages:
- Improves code organization.
- Clearly separates responsibilities.
- Makes APIs easier to understand.
- Encourages modular software design.
- Simplifies maintenance as projects grow.
This concept is known as encapsulation, bringing together the data that represents an object and the operations that manipulate it.
Although C does not provide classes as a language feature, nothing prevents us from organizing our software in a similar way. By combining structures with well-designed functions, we can create reusable, object-like components that are easy to understand and maintain.
In fact, this design philosophy is widely used in many large C codebases, including operating systems, embedded firmware, networking libraries, graphics frameworks, and device drivers.
Throughout this article, we will see that a C structure is much more than a passive container for variables. It can serve as the foundation of a clean, modular, and class-like software design.
2. Structures as a Collection of Related Data
Before exploring how structures can be used to build class-like objects, it is important to understand their original purpose.
A structure is a user-defined data type that groups multiple related variables into a single logical unit.
Without a structure, the properties of a rectangle might be stored as separate variables.
This approach is manageable for a single rectangle, but quickly becomes difficult when multiple rectangles are involved.
There is no relationship between these variables other than their names. The programmer must remember which width belongs to which height.
A structure solves this problem by grouping related data together.
Now each rectangle is represented by a single object.
Visually, the structure can be thought of as a blueprint.
Each variable declared from this blueprint contains its own copy of the data.
rect1
├── width = 10
└── height = 5
rect2
├── width = 25
└── height = 12
rect3
├── width = 40
└── height = 18
Grouping related data offers several advantages:
- Improves readability.
- Reduces programming errors.
- Simplifies passing related data to functions.
- Makes programs easier to maintain.
- Allows the data to represent a real-world entity.
For example, instead of writing:
we can simply write:
The function receives a single object rather than multiple independent variables.
At this point, the structure is still nothing more than a collection of data. It defines what a rectangle is, but not what can be done with it.
The next section takes the next logical step by associating functions such as calculating the area, perimeter, union, and intersection with the Rectangle structure, transforming it from a passive data container into a class-like software component.
3. Adding Behavior to Structures
So far, our Rectangle structure only describes what a rectangle is.
While this is useful, it tells us nothing about what we can do with a rectangle.
For example, given a rectangle, we may want to:
- Calculate its area.
- Calculate its perimeter.
- Determine whether a point lies inside it.
- Find the union of two rectangles.
- Find the intersection of two rectangles.
- Scale its dimensions.
- Move it to a new position.
These are all operations that naturally belong to the Rectangle entity.
Instead of creating unrelated functions throughout the program, a better approach is to associate these operations with the structure they manipulate.
Rectangle
├── Data
│ ├── width
│ └── height
│
└── Operations
├── Area()
├── Perimeter()
├── Contains()
├── Union()
├── Intersection()
├── Move()
└── Scale()
Notice that although the functions are not physically stored inside the structure, they are conceptually part of its interface.
This is exactly how programmers begin thinking in terms of objects rather than merely data.
3.1 Associating Functions with a Structure
Consider the following declarations.
typedef struct
{
int width;
int height;
} Rectangle;
int Rectangle_Area(const Rectangle *rect);
int Rectangle_Perimeter(const Rectangle *rect);
Rectangle Rectangle_Union(const Rectangle *r1,
const Rectangle *r2);
Rectangle Rectangle_Intersection(const Rectangle *r1,
const Rectangle *r2);
Every function accepts one or more Rectangle objects and performs an operation that is meaningful for that type.
For example,
Rectangle rect = {20, 10};
int area = Rectangle_Area(&rect);
int perimeter = Rectangle_Perimeter(&rect);
Even though these are ordinary C functions, they clearly belong to the Rectangle abstraction.
3.2 Treating the Structure as an Object
As the number of related operations grows, programmers naturally stop thinking of Rectangle as merely a collection of variables.
Instead, they begin thinking of it as an object that exposes a well-defined set of operations.
Rectangle
├── Data
│ ├── width
│ └── height
│
└── Interface
├── Rectangle_Area()
├── Rectangle_Perimeter()
├── Rectangle_Union()
├── Rectangle_Intersection()
├── Rectangle_Move()
└── Rectangle_Scale()
This organization provides several benefits.
- Related code stays together.
- The API becomes easier to understand.
- New operations can be added without affecting unrelated modules.
- Other programmers immediately know where to find functionality related to a rectangle.
As projects grow from hundreds to thousands of source files, this organization becomes increasingly valuable.
3.3 Similar to Methods in a Class
If you have experience with C++, the design should look familiar.
In C++, you might write:
In C, the equivalent operation is typically written as:
Although the syntax is different, the intent is the same.
Both statements ask the Rectangle abstraction to compute its area.
The difference is that C expresses this relationship through ordinary functions that accept a pointer to the structure, whereas C++ expresses it through member functions that are built into the language.
This simple design pattern forms the foundation of many object-like software architectures written entirely in C.
4. Encapsulation in C
One of the fundamental principles of object-oriented programming is encapsulation.
Encapsulation means bringing together:
- The data that represents an object.
- The operations that manipulate that data.
In C++, the language enforces this relationship through classes.
In C, we achieve the same objective through program organization rather than language features.
4.1 Organizing Related Code
A common practice is to place the structure definition and all related function declarations into a single header file.
For example:
rectangle.h
├── Rectangle structure
├── Rectangle_Create()
├── Rectangle_Area()
├── Rectangle_Perimeter()
├── Rectangle_Union()
├── Rectangle_Intersection()
└── Rectangle_Scale()
The corresponding implementation resides in a single source file.
rectangle.c
├── Rectangle_Create()
├── Rectangle_Area()
├── Rectangle_Perimeter()
├── Rectangle_Union()
├── Rectangle_Intersection()
└── Rectangle_Scale()
Everything related to a rectangle is located in one module.
This makes the code easier to navigate and understand.
4.2 A Well-Defined Interface
Applications do not need to know how the rectangle operations are implemented.
They simply use the interface provided by the module.
Rectangle rect = {20, 10};
int area = Rectangle_Area(&rect);
Rectangle_Scale(&rect, 2);
Rectangle_Move(&rect, 15, 8);
Notice that the application interacts only with the published functions.
It does not need to know how the area is calculated or how scaling is performed internally.
This separation between interface and implementation is a key aspect of encapsulation.
4.3 Thinking in Terms of Modules
As software grows, each structure naturally evolves into its own software module.
Project
├── rectangle.c
├── rectangle.h
├── circle.c
├── circle.h
├── line.c
├── line.h
├── polygon.c
└── polygon.h
Each module is responsible for a single entity.
For example:
Rectangle
├── Data
│ ├── width
│ └── height
│
└── Operations
├── Create()
├── Area()
├── Perimeter()
├── Union()
├── Intersection()
├── Move()
└── Scale()
This organization keeps related code together while minimizing dependencies between modules.
4.4 Encapsulation Without Classes
Unlike C++, C does not force data and functions to be bundled together.
Nothing prevents a programmer from writing unrelated functions that directly manipulate a structure.
However, disciplined C programmers follow a simple convention:
- Define a structure to represent an entity.
- Place all related functions in the same module.
- Expose only the operations that make sense for that entity.
Over time, other programmers naturally begin to think of the module as a single reusable component rather than as a collection of independent functions.
Although C structures are not classes, this disciplined approach achieves one of the primary goals of object-oriented programming: organizing related data and behavior into a coherent, reusable abstraction.
5. Public by Default
One important difference between a C structure and a C++ class is member accessibility.
In C, every structure member is public by default.
Any part of the program that has access to a Rectangle object can directly read or modify its members.
Nothing prevents another function from writing:
even if a negative width is considered invalid.
Unlike C++, the language provides no mechanism to prevent such direct access.
5.1 C++ Provides Access Control
A C++ class can restrict access to its internal data.
class Rectangle
{
private:
int width;
int height;
public:
void SetWidth(int w);
int Area() const;
};
Only the public interface is visible to users of the class.
Rectangle
├── Private
│ ├── width
│ └── height
│
└── Public
├── SetWidth()
├── SetHeight()
├── Area()
└── Perimeter()
This prevents external code from modifying the object's internal state arbitrarily.
5.2 Structures Do Not Enforce Encapsulation
Although we organize our code around structures and related functions, the compiler does not stop other code from accessing the structure members directly.
Whether these values are valid depends entirely on the programmer.
The compiler performs no access checks.
5.3 Achieving Data Hiding in C
Although C has no private keyword, data hiding is still possible.
A common technique is to expose only an opaque pointer in the public header while keeping the actual structure definition private.
rectangle.h
├── typedef struct Rectangle Rectangle;
├── Rectangle_Create()
├── Rectangle_Destroy()
├── Rectangle_Area()
└── Rectangle_Scale()
The actual structure definition remains inside rectangle.c.
Since the application cannot see the structure members, it must interact with the object through the published API.
This effectively provides data hiding, even though the C language has no built-in access specifiers.
Note: Opaque pointers are a widely used technique for implementing encapsulation in C. If you're unfamiliar with this approach, refer to the earlier article on Opaque Pointers, where the technique is discussed in detail.
5.4 Encapsulation Is a Design Discipline
One of the strengths of C is that it gives programmers complete freedom.
That freedom also comes with responsibility.
Unlike C++, the compiler does not enforce encapsulation. Instead, encapsulation is achieved through careful API design, disciplined programming practices, and well-defined module boundaries.
This flexibility is one of the reasons why experienced C programmers can build software that exhibits many object-oriented characteristics without requiring language-supported classes.
6. Two Ways to Associate Functions with Structures
So far, we have associated functions with a structure by keeping the functions outside the structure. This is the traditional style used in C programming.
However, there is another approach.
Instead of storing only data inside a structure, we can also store function pointers, allowing the structure to reference the operations associated with it.
Both approaches are valid, and each has its own advantages.
6.1 Method 1: Functions Outside the Structure
This is the approach used throughout the previous sections.
The structure contains only data.
typedef struct
{
int width;
int height;
} Rectangle;
int Rectangle_Area(const Rectangle *rect);
int Rectangle_Perimeter(const Rectangle *rect);
Rectangle Rectangle_Union(const Rectangle *r1,
const Rectangle *r2);
Rectangle Rectangle_Intersection(const Rectangle *r1,
const Rectangle *r2);
The implementation of the functions resides in the source file.
int Rectangle_Area(const Rectangle *rect)
{
return rect->width * rect->height;
}
int Rectangle_Perimeter(const Rectangle *rect)
{
return 2 * (rect->width + rect->height);
}
Using the API is straightforward.
Rectangle rect =
{
.width = 20,
.height = 10
};
int area = Rectangle_Area(&rect);
int perimeter = Rectangle_Perimeter(&rect);
The organization can be visualized as follows.
Rectangle
├── Data
│ ├── width
│ └── height
│
└── Shared Operations
├── Rectangle_Area()
├── Rectangle_Perimeter()
├── Rectangle_Union()
└── Rectangle_Intersection()
All Rectangle objects share the same functions.
Only one copy of each function exists in the program.
This is why most C libraries adopt this design.
6.2 Method 2: Function Pointers Inside the Structure
Another approach is to store pointers to the functions inside the structure itself.
typedef struct Rectangle
{
int width;
int height;
int (*Area)(const struct Rectangle *);
int (*Perimeter)(const struct Rectangle *);
struct Rectangle (*Union)(const struct Rectangle *,
const struct Rectangle *);
struct Rectangle (*Intersection)(const struct Rectangle *,
const struct Rectangle *);
} Rectangle;
The implementations remain ordinary C functions.
int Rectangle_Area(const Rectangle *rect)
{
return rect->width * rect->height;
}
int Rectangle_Perimeter(const Rectangle *rect)
{
return 2 * (rect->width + rect->height);
}
The difference is that the function pointers must now be initialized.
Rectangle rect =
{
.width = 20,
.height = 10,
.Area = Rectangle_Area,
.Perimeter = Rectangle_Perimeter,
.Union = Rectangle_Union,
.Intersection = Rectangle_Intersection
};
The functions are called through the object.
The structure now contains both its data and references to its operations.
Rectangle
├── Data
│ ├── width
│ └── height
│
└── Function Pointers
├── Area()
├── Perimeter()
├── Union()
└── Intersection()
If you are familiar with C++, this syntax should look familiar.
Although C still requires the object pointer to be passed explicitly, the programming style becomes much more object-oriented.
6.3 Which Method Should You Use?
At first glance, Method 2 appears to be superior because the functions seem to belong to the object itself.
However, consider the following.
With Method 2, every one of those one thousand objects contains four function pointers.
Rectangle #1
├── Area() ─────────────┐
├── Perimeter() ────────┤
├── Union() ────────────┤
└── Intersection() ─────┤
│
Rectangle #2 │
├── Area() ─────────────┤
├── Perimeter() ────────┤
├── Union() ────────────┤
└── Intersection() ─────┤
│
Rectangle #3 │
├── Area() ─────────────┤
├── Perimeter() ────────┤
├── Union() ────────────┤
└── Intersection() ─────┘
▼
Rectangle_Area()
Rectangle_Perimeter()
Rectangle_Union()
Rectangle_Intersection()
Every object stores exactly the same four addresses.
The function pointers are duplicated thousands of times even though all rectangles behave identically.
Method 1 avoids this duplication because every object shares the same implementation.
For this reason, Method 1 is the preferred design for most C software.
Function pointers become valuable only when different objects need different implementations. For example, when implementing callbacks, device drivers, plug-in architectures, or runtime polymorphism.
6.4 Why Can't C Store Ordinary Functions Inside a Structure?
After seeing Method 2, a natural question arises.
Why do we need function pointers at all? Why can't we simply place the functions inside the structure as we do in C++?
For example, it might seem reasonable to write the following.
However, this is not valid C.
Unlike C++, the C language does not permit structures to contain member functions. A structure may contain only data members.
Even declaring a function inside a structure is illegal.
The C compiler treats structure members as objects occupying storage within the structure. Since a function is not an object, it cannot be stored as a member.
The only way to associate behavior with a structure is therefore one of the following:
- Keep the functions outside the structure.
- Store pointers to those functions inside the structure.
This explains why Method 2 uses function pointers rather than ordinary functions.
It is not a design preference; it is a language requirement.
This is also one of the fundamental differences between C structures and C++ classes.
C Structure C++ Class
------------- ----------------
Data Members Data Members
Function Pointers (optional) Member Functions
In C++, member functions are part of the language itself.
In C, the closest equivalent is to associate ordinary functions with the structure or, when necessary, store pointers to those functions inside the structure.
7. How C Structures Differ from C++ Classes
By now, it should be clear that C structures can be used to build software components that closely resemble classes.
Both organize related data and operations into a single abstraction, making programs easier to understand and maintain.
However, despite these similarities, a C structure is not a C++ class.
Several important language features found in C++ are not available in C.
7.1 Similarities
Both C structures and C++ classes allow programmers to model real-world entities.
For example, both can represent a rectangle.
Rectangle
├── Data
│ ├── width
│ └── height
│
└── Operations
├── Area()
├── Perimeter()
├── Move()
└── Scale()
In both languages, software can be organized around objects instead of unrelated variables and functions.
This encourages:
- Better code organization
- Modular design
- Reusable components
- Easier maintenance
From a design perspective, the overall architecture can look remarkably similar.
7.2 Constructors and Destructors
One major difference is object lifetime management.
A C++ class can automatically initialize and clean up an object.
The constructor is called automatically when the object is created.
Likewise, the destructor is automatically invoked when the object goes out of scope.
C provides no equivalent language feature.
Initialization and cleanup must be performed explicitly.
Whether these functions are called correctly depends entirely on the programmer.
7.3 Access Control
As discussed in the previous section, C++ supports:
privateprotectedpublic
These keywords allow the compiler to enforce encapsulation.
C structures have no such capability.
Every member is publicly accessible unless the programmer deliberately hides the structure using techniques such as opaque pointers.
7.4 Inheritance
One of the defining features of C++ is inheritance.
A new class can extend an existing class by inheriting its data and behavior.
C has no built-in mechanism for inheritance.
If code reuse is required, it must be achieved through composition or other design techniques.
7.5 Polymorphism
C++ also supports runtime polymorphism through virtual functions.
A program can invoke the same function on different objects, with each object providing its own implementation.
The calling code remains identical.
In C, similar behavior can be implemented manually using function pointers, but the language provides no built-in support for polymorphism.
7.6 Comparison
| Feature | C Structure | C++ Class |
|---|---|---|
| Group related data | ✓ | ✓ |
| Associate functions with data | ✓ | ✓ |
| Encapsulation | ✓ (by design) | ✓ (language support) |
| Access control | ✗ | ✓ |
| Constructors / Destructors | ✗ | ✓ |
| Inheritance | ✗ | ✓ |
| Runtime Polymorphism | ✗ (manual implementation) | ✓ |
Although C lacks several object-oriented language features, it is still capable of producing highly modular and maintainable software.
By combining structures, carefully designed APIs, opaque pointers, and, where appropriate, function pointers, C programmers can build software architectures that exhibit many of the same design principles found in object-oriented languages.
8. Where This Design Is Used
If you've followed this design pattern throughout the article, you might wonder whether it is merely an academic exercise.
The answer is no.
Organizing software around structures and their associated functions is a well-established practice in C programming. Many of the world's largest and most successful C codebases rely on this approach because it produces software that is modular, maintainable, and easy to extend.
Let's look at a few examples.
8.1 Linux Kernel
The Linux kernel makes extensive use of structures to represent kernel objects.
Examples include:
- Processes
- Devices
- File systems
- Network sockets
- Memory regions
Each structure has a well-defined set of functions that operate on it.
struct file
├── Data Members
│ ├── f_pos
│ ├── f_flags
│ └── ...
│
└── Operations
├── open()
├── read()
├── write()
├── close()
└── ioctl()
Rather than exposing unrelated global functions, the kernel groups operations around the object they manipulate.
8.2 Embedded Software
Embedded software often follows exactly the same design philosophy.
A peripheral driver typically consists of a structure representing the device together with a set of functions that operate on it.
UART
├── Data
│ ├── Baud Rate
│ ├── Buffer
│ └── Status
│
└── Operations
├── Init()
├── Send()
├── Receive()
├── Flush()
└── DeInit()
To the application developer, the driver behaves much like an object.
8.3 Graphics and GUI Libraries
Graphics libraries frequently model graphical objects using structures.
Examples include:
- Windows
- Buttons
- Images
- Fonts
- Rectangles
- Circles
Each object exposes operations that naturally belong to it.
This organization makes the API intuitive because related functionality is grouped together.
8.4 Networking Libraries
Networking stacks also organize their software around structures.
A socket, for example, is more than just a collection of variables.
Once again, the structure represents the object, while the associated functions define its behavior.
8.5 Why This Design Scales
As software projects grow, organizing code around structures provides several advantages.
Instead of thinking about hundreds of independent functions, developers think in terms of reusable software components.
Project
├── Rectangle
│ ├── Data
│ └── Operations
│
├── Circle
│ ├── Data
│ └── Operations
│
├── Image
│ ├── Data
│ └── Operations
│
└── Socket
├── Data
└── Operations
Each component has:
- A clearly defined purpose.
- A well-defined interface.
- A dedicated implementation.
- Minimal dependency on unrelated modules.
This modular organization is one of the primary reasons why large C codebases remain maintainable even after decades of development.
Although C structures are not classes, they allow programmers to organize software in a remarkably similar way. Combined with disciplined API design, they provide a practical and effective foundation for building reusable, object-like software components.
9. Summary
When C structures are first introduced, they are often presented simply as a way to group related variables.
While this is certainly their primary purpose, it is only the beginning.
Throughout this article, we have seen that a structure can become the foundation of a much larger software component by associating it with the functions that operate on its data.
Rectangle
├── Data
│ ├── width
│ └── height
│
└── Operations
├── Area()
├── Perimeter()
├── Union()
├── Intersection()
├── Move()
└── Scale()
This organization allows programmers to think in terms of objects rather than isolated variables and functions.
We also explored how this approach naturally leads to:
- Better code organization
- Modular software design
- Well-defined interfaces
- Easier maintenance
- Reusable software components
Although C does not provide language features such as classes, constructors, inheritance, or built-in polymorphism, it still allows developers to apply many object-oriented design principles through disciplined programming and careful API design.
For applications requiring even greater flexibility, function pointers can be used to simulate methods and enable runtime behavior selection, further strengthening the object-oriented style.
Perhaps the most important takeaway is this:
A C structure is not merely a collection of related data. It is the foundation upon which an entire software abstraction can be built.
This philosophy has been successfully applied for decades in operating systems, embedded firmware, graphics libraries, networking stacks, and countless other large C projects.
Mastering this way of thinking transforms structures from simple data containers into powerful building blocks for writing clean, modular, and maintainable software in C.
Relevant Links
C/C++ Material on this website
(such as memory management, pointers, embedded C programming etc.)