Here it is in a nutshell.
Let’s say we are working with rational numbers. We define a type for them named RationalNumber and put it in the header file RationalNumber.h
RationalNumber.h
#ifndef RationalNumber_h
#define RationalNumber_h
struct RationalNumber {
long numerator;
long denominator;
};
typedef struct RationalNumber RationalNumber;
#endif /* RationalNumber_h */
Now that we have the type, we use it to compute the sum of two rational numbers.
// Two rational numbers
RationalNumber r1 = {1, 5};
RationalNumber r2 = {3, 5};
// Add them
RationalNumber sum = {r1.numerator + r2.numerator, r1.denominator};
That was easy to do because the denominators of both r1 and r2 are the same. If they were different, we would have to do more work to handle the delicate details.
Rather than directly handling the numerators and denominators of rational numbers when adding them, we can define a function to do all the hard work required. Whenever we add two numbers, we just use that function.
So we put the declaration of the function in the header file, along with the function that properly initialises a rational number.
RationalNumber.h
#ifndef RationalNumber_h
#define RationalNumber_h
#ifndef RationalNumber_h
#define RationalNumber_h
struct RationalNumber {
long numerator;
long denominator;
};
typedef struct RationalNumber RationalNumber;
RationalNumber RationalNumber_init (const long numerator, const long denominator);
RationalNumber RationalNumber_add (const RationalNumber r1, const RationalNumber r2);
#endif /* RationalNumber_h */
Now we can add any two rational numbers without having to worry about the delicate details.
// Two rational numbers
RationalNumber r1 = RationalNumber_init (1, 7);
RationalNumber r2 = RationalNumber_init (3, 35);
// Add them
RationalNumber sum = RationalNumber_add (r1, r2);
We can also define functions for other possible operations such as multiplication and division.
RationalNumber.h
#ifndef RationalNumber_h
#define RationalNumber_h
#ifndef RationalNumber_h
#define RationalNumber_h
struct RationalNumber {
long numerator;
long denominator;
};
typedef struct RationalNumber RationalNumber;
RationalNumber RationalNumber_init (const long numerator, const long denominator);
RationalNumber RationalNumber_add (const RationalNumber r1, const RationalNumber r2);
RationalNumber RationalNumber_multiply (const RationalNumber r1, const RationalNumber r2);
RationalNumber RationalNumber_divide (const RationalNumber r1, const RationalNumber r2);
#endif /* RationalNumber_h */
Now to make the transition to an opaque type, we remove the declaration of the struct RationalNumber from the header file, but we leave the typedef intact.
RationalNumber.h
#ifndef RationalNumber_h
#define RationalNumber_h
#ifndef RationalNumber_h
#define RationalNumber_h
typedef struct RationalNumber RationalNumber;
RationalNumber * RationalNumber_init (const long numerator, const long denominator);
void RationalNumber_release (const RationalNumber *);
RationalNumber * RationalNumber_add (const RationalNumber *, const RationalNumber *);
RationalNumber * RationalNumber_multiply (const RationalNumber *, const RationalNumber *);
RationalNumber * RationalNumber_divide (const RationalNumber *, const RationalNumber *);
#endif /* RationalNumber_h */
RationalNumber has now become an opaque type.
Also notice that we now use pointers to the opaque type in the header file because the definition of the actual type itself is no longer available there.
We hide the details of the opaque type in implementation files along with the function definitions. This means the details can safely be changed without having to change other code that uses rational numbers.
RationalNumber.c
#include "RationalNumber.h"
struct RationalNumber {
long numerator;
long denominator;
};
RationalNumber * RationalNumber_init (const long numerator, const long denominator) {
...
}
void RationalNumber_release (const RationalNumber * r) {
...
}
RationalNumber * RationalNumber_add (const RationalNumber * r1, const RationalNumber * r2) {
...
}
...
...
...