I tried to do data encapsulation in C based on this post here https://alastairs-place.net/blog/2013/06/03/encapsulation-in-c/.
In a header file I have:
#ifndef FUNCTIONS_H
#define FUNCTIONS_H
// Pre-declaration of struct. Contains data that is hidden
typedef struct person *Person;
void getName(Person obj);
void getBirthYear(Person obj);
void getAge(Person obj);
void printFields(const Person obj);
#endif
In ´functions.c´ I have defined the structure like that
#include "Functions.h"
enum { SIZE = 60 };
struct person
{
char name[SIZE];
int birthYear;
int age;
};
pluss I have defined functions as well.
In main.c I have:
#include "Functions.h"
#include <stdlib.h>
int main(void)
{
// Works because *Person makes new a pointer
Person new = malloc(sizeof new);
getName(new);
getAge(new);
getBirthYear(new);
printFields(new);
free(new);
return 0;
}
Is it true, that when I use Person new
, new
is already pointer because of typedef struct person *Person;
.
How is it possible, that linker cannot see the body and members that I have declared in my struct person
Is this only possible using pointer?
Is the correct (and only) way to implement OOP prinicples in my case to make a different struct
in functions.h
like so:
typedef struct classPerson
{ // This data should be hidden
Person data;
void (*fPtrGetName)(Person obj);
void (*fPtrBirthYear)(Person obj);
void (*fPtrGetAge)(Person obj);
void (*fPtrPrintFields)(const Person obj);
} ClassPerson;
First of all, it is usually better to not hide pointers behind a typedef, but to let the caller use pointer types. This prevents all kinds of misunderstandings when reading and maintaining the code. For example
void printFields(const Person obj);
looks like nonsense if you don't realize thatPerson
is a pointer type.Yes. You are confused because of the mentioned typedef.
The linker can see everything that is linked, or you wouldn't end up with a working executable.
The compiler however, works on "translation units" (roughly means a .c file and all its included headers). When compiling the caller's translation unit, the compiler doesn't see functions.c, it only sees functions.h. And in functions.h, the struct declaration gives an incomplete type. Meaning "this struct definition is elsewhere".
Yes, it is the only way if you want to do proper OO programming in C. This concept is sometimes called opaque pointers or opaque type.
(Though you could also achieve "poor man's private encapsulation" though the
static
keyword. Which is usually not really recommended, since it wouldn't be thread-safe.)Pretty much, yeah (apart from the nit-pick about the mentioned pointer typedef). Using function pointers to the public functions isn't necessary though, although that's how you implement polymorphism.
What your example lacks though is a "constructor" and "destructor". Without them the code wouldn't be meaningful. The malloc and free calls should be inside those, and not done by the caller.
With or without typedef, in C you hide data by declaring incomplete types. In
/usr/include/stdio.h
, you'll find fread(3) takes aFILE *
argument:and
FILE
is declared something like this:Using
stdio.h
you cannot define a variable of typeFILE
, because typeFILE
is incomplete: it's declared, but not defined. But you can happily passFILE *
around, because all data pointers are the same size. You're just going to have to call fopen(3) to make it point to an open file.To partially define a type, as in your case:
is a little trickier. First of all, you should have a really good reason, namely that two implementations of
fPtrGetName
are implemented. Otherwise you're just building complexity on the altar of OOP.A good example of a good reason is bind(2). You can bind a unix domain socket or a network socket, among others. Both types are represented by
struct sockaddr
, but that's just a stand-in type forstruct sockaddr_un
andstruct sockaddr_in
. Functions that takestruct sockaddr
depend on the fact that all such structures start with the membersun_family
, and branch accordingly. Et voila, polymorphism: one function, many types.For an example of a struct full of function pointers, I recommend looking at SQLite. Its API is loaded with structures to isolate it from the OS and let the user define plug-ins.
BTW, if I may say so,
fPtrGetName
is a terrible name. It's not interesting that it's a function pointer and (controversy!) "get" is noise on a function that takes no arguments. CompareWhich would you rather use? I reserve "get" (or similar) for I/O functions; at least then you're getting something, not just moving it from one pocket to another! For setting, in C++ I overload the function, so that get/set functions have the same name, but in C I wind up with e.g.
set_name(const char name[])
.