Encapsulation, abstraction, and polymorphism. Because these three things blur together, their meanings are fuzzy, and they're kinda difficult to do in C, here's how I'm defining them for this answer.
Encapsulation restricts, or in the case of C discourages, knowledge of how the underlying thing works. Ideally the data and methods are bundled together.
Abstraction hides complexity from the user, generally through a well defined interface applicable to multiple scenarios.
Polymorphism allows the same interface to be used for multiple types.
They build on each other. Very generally, encapsulation allows abstraction allows polymorphism.
First, let's start with an encapsulation violation.
for(int i=0; i < INITIAL_ARRAY_SIZE;i++){
printf("Item: %d\n", lptr->a[i]);
}
INITIAL_ARRAY_SIZE
is not part of lptr
. It's some external data. If you pass lptr
around, INITIAL_ARRAY_SIZE
won't go with it. So that loop violates encapsulation. Your list is not well encapsulated. The size should be a detail that is either part of the List struct or not necessary at all.
You could add the size to the struct and use that to iterate.
for(int i=0; i < lptr->size; i++){
printf("Item: %d\n", lptr->a[i]);
}
But this still has the user poking at struct details. To avoid this you could add an iterator and the user never knows about the size at all. This is like the C++ vector interface but more awkward because C lacks method calls.
ListIter iter;
int *value;
/* Associate the iterator with the List */
ListIterInit(&iter, lptr);
/* ListIterNext returns a pointer so it can use NULL to stop */
/* Otherwise you can't store 0 */
while( value = ListIterNext(&iter) ) {
printf("Item: %d\n", *value);
}
Now the struct has full control over how things iterate and how it stores things.
This iterator interface is inspired by Gnome Lib Hash Tables.
This iterator interface also provides abstraction. We've removed details about the struct. Now it's a thing you just iterate through. You don't need to know how the data is stored or how much there is or even if it's stored at all. It could be generated on the fly for all you know. This is the beauty of the iterator pattern.
...except we still need to know the type. This can be fixed in two ways. First is by telling the list how big each element is. Rather than modifying yours, let's look at how Gnome Lib does it with their arrays
.
GArray *garray = g_array_new (FALSE, FALSE, sizeof (gint));
for (i = 0; i < 10000; i++) {
g_array_append_val (garray, i);
}
The array is told to store things sizeof(gint)
, which it remembers. Then all other array operations are encapsulated in a function. Even getting an element out is encapsulated.
gint g_array_index(garray, gint, 5);
This is done with a clever macro that does the typecasting for you.
The second option is to store all data as pointers. I'll leave it as an exercise for you to look at Gnome Lib's pointer arrays.
And will you look at that? We have polymorphism. A single array struct can now handle data of all types.
This isn't particularly easy to do in C. It involves some macro juggling. It's good to look at things like Gnome Lib to get, conceptually, how it's done. Maybe try to do it yourself for practice and understanding.
But for production just use things like Gnome Lib. There's a tremendous number of edge cases and little details that they've thought through.