Welcome to Software Development on Codidact!
Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.
How to do private encapsulation in C?
I'm using an object-oriented design for my C project and trying to implement classes with private encapsulation. How do I do this? Some things I've tried that are problematic:
-
Using a struct for my class and placing the struct definition in the header file, then designing the API around this and telling the user "please don't access the struct members" through comments/documentation. Clearly this doesn't actually give private encapsulation since the caller can access all struct members freely.
And users using private members intentionally isn't as much of a problem as them doing so unintentionally (for example by getting suggestions from "code completion" IDEs).
-
Using
static
file scope variables for private members inside the .c file of the class. This does fix the private encapsulation, but now I can't declare multiple instances of the class, it's essentially "singleton" since there is just one instance of the static file scope variables. And it isn't thread-safe either.
Are there other alternatives?
1 answer
The following users marked this post as Works for me:
User | Comment | Date |
---|---|---|
Lundin | (no comment) | Jul 8, 2023 at 10:02 |
The concept you are looking for is referred to as opaque type or opaque pointers. This is the proper method to achieve private encapsulation in C and can also be used for inheritance/polymorphism (which I won't address in this answer). It allows multiple object instances of the same class.
Opaque type is achieved by only placing a forward declaration of the struct inside the public header. This makes the struct an incomplete type which will have to be completed later in order to be used. In formal C terms, it has to be completed within the same translation unit as it is used inside. A translation unit being one .c file and every .h file it includes.
In this case, the class interface in the .h file and the class implementation in the corresponding .c file forms one translation unit. If we place the struct definition in the .c file, then it only becomes accessible there. Since the caller's .c file and the class .h file forms a different translation unit.
This allows the caller to only declare pointers to that type. They can't access the members or allocating actual instances of it. Allocation/deallocation is handled by the class.
A practical example with some snippets taken from a string class I once made (irrelevant parts of the code commented out):
cstring.h
#ifndef CSTRING_H
#define CSTRING_H
// forward declaration, struct of incomplete type:
typedef struct cstring cstring;
cstring* cstring_create (const char* str_opt); // constructor
void cstring_delete (cstring* cstr); // destructor
void cstring_assign (cstring* cstr, const char* str); // member function
...
#endif
cstring.c
#include "cstring.h"
struct cstring
{ // all of these are private member variables:
char* str;
size_t length;
size_t alloc_size;
};
cstring* cstring_create (const char* str_opt)
{
cstring* result;
/* code checking how much to allocate */
result = cstring_alloc(size); // dynamic memory allocation
if(result == NULL)
{
return NULL;
}
/* code assigning all member variables */
return result;
}
void cstring_delete (cstring* cstr)
{
free(cstr->str);
free(cstr);
}
void cstring_assign (cstring* cstr, const char* str)
{
size_t length = strlen(str);
/* code checking if more memory is needed, then allocating */
memcpy(cstr->str, str, length+1);
}
caller.c
#include "cstring.h"
cstring* cstr1 = cstring_create(0);
cstring_assign(cstr1, "hello world");
puts(cstring_cstr(cstr1));
cstring_delete (cstr1);
Regarding member functions:
Note that in this example, member functions are declared in the header rather than as function pointers in a public part of the struct. Some like to use function pointers to emulate C++-like member syntax like obj.member()
(see for example Schreiner - Object-oriented Programming in ANSI-C, a book which I don't really recommend), but I think that's a rather pointless feature.
In C, there are no automatic constructor/destructor calls, no RAII, no this
pointers. Meaning you have to pass along a reference to the object anyway.
So rather than writing foo.bar(&foo, stuff)
you might as well write bar(&foo, stuff)
. But name bar
something meaningful so it becomes clear which class it belongs to. In my example above, everything belonging to the cstring
class is prefixed with cstring
.
Also, declaring function pointers in the struct itself means that those are allocated for every instance of the class. That's potentially wasting a lot of memory.
0 comment threads