OOP programming with data encapsulation in C

2019-09-16 02:26发布

问题:

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;

回答1:

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 that Person is a pointer type.

Have I understood correctly, that when I use Person new, new is already pointer because of typedef struct person *Person;.

Yes. You are confused because of the mentioned typedef.

How is it possible, that linker cannot see the body and members that I have declared in my ´struct person´?

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".

Is this only possible using pointer?

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.)

Is the correct (and only) way to implement OOP prinicples in my case to make a different struct in functions.h like so:

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.



回答2:

With or without typedef, in C you hide data by declaring incomplete types. In /usr/include/stdio.h, you'll find fread(3) takes a FILE * argument:

extern size_t fread (void *__restrict __ptr, size_t __size,
                     size_t __n, FILE *__restrict __stream) __wur;

and FILE is declared something like this:

struct _IO_FILE;
typedef struct _IO_FILE FILE;

Using stdio.h you cannot define a variable of type FILE, because type FILE is incomplete: it's declared, but not defined. But you can happily pass FILE * 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:

struct classPerson
{   // This data should be hidden
    Person data;

    void (*fPtrGetName)(Person obj);
...
};

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 for struct sockaddr_un and struct sockaddr_in. Functions that take struct sockaddr depend on the fact that all such structures start with the member sun_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. Compare

struct classPerson sargent;
sargent.fPtrGetName();
sargent.name();

Which 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[]).