Extreme C
上QQ阅读APP看书,第一时间看更新

Encapsulation

In the previous sections, we saw that each object has a set of attributes and a set of functionalities attached to it. Here, we are going to talk about putting those attributes and functionalities into an entity called an object. We do this through a process called encapsulation.

Encapsulation simply means putting related things together into a capsule that represents an object. It happens first in your mind, and then it should be transferred to the code. The moment that you feel an object needs to have some attributes and functionalities, you are doing encapsulation in your mind; that encapsulation then needs to be transferred to the code level.

It is crucial to be able to encapsulate things in a programming language, otherwise keeping related variables together becomes an untenable struggle (we mentioned using naming conventions to accomplish this).

An object is made from a set of attributes and a set of functionalities. Both of these should be encapsulated into the object capsule. Let's first talk about attribute encapsulation.

Attribute encapsulation

As we saw before, we can always use variable names to do encapsulation and tie different variables together and group them under the same object. Following is an example:

int pixel_p1_x = 56;

int pixel_p1_y = 34;

int pixel_p1_red = 123;

int pixel_p1_green = 37;

int pixel_p1_blue = 127;

int pixel_p2_x = 212;

int pixel_p2_y = 994;

int pixel_p2_red = 127;

int pixel_p2_green = 127;

int pixel_p2_blue = 0;

Code Box 6-3: Some variables representing two pixels grouped by their names

This example clearly shows how variable names are used to group variables under p1 and p2, which somehow are implicit objects. By implicit, we mean that the programmer is the only one who is aware of the existence of such objects; the programming language doesn't know anything about them.

The programming language only sees 10 variables that seem to be independent of each other. This would be a very low level of encapsulation, to such an extent that it would not be officially considered as encapsulation. Encapsulation by variable names exists in all programming languages (because you can name variables), even in an assembly language.

What we need are approaches offering explicit encapsulation. By explicit, we mean that both the programmer and the programming language are aware of the encapsulation and the capsules (or objects) that exist. Programming languages that do not offer explicit attribute encapsulation are very hard to use.

Fortunately, C does offer explicit encapsulation, and that's one of the reasons behind why we are able to write so many intrinsically object-oriented programs with it more or less easily. On the other hand, as we see shortly in the next section, C doesn't offer explicit behavior encapsulation, and we have to come up with an implicit discipline to support this.

Note that having an explicit feature such as encapsulation in a programming language is always desired. Here, we only spoke about encapsulation, but this can be extended to many other object-oriented features, such as inheritance and polymorphism. Such explicit features allow a programming language to catch relevant errors at compile time instead of runtime.

Resolving errors at runtime is a nightmare, and so we should always try to catch errors at compile time. This is the main advantage of having an object-oriented language, which is completely aware of the object-oriented way of our thinking. An object-oriented language can find and report errors and violations in our design at compile time and keep us from having to resolve many severe bugs at runtime. Indeed, this is the reason why we are seeing more complex programming languages every day – to make everything explicit to the language.

Unfortunately, not all object-oriented features are explicit in C. That's basically why it is hard to write an object-oriented program with C. But there are more explicit features in C++ and, indeed, that's why it is called an object-oriented programming language.

In C, structures offer encapsulation. Let's change the code inside Code Box 6-3, and rewrite it using structures:

typedef struct {

int x, y;

int red, green, blue;

} pixel_t;

pixel_t p1, p2;

p1.x = 56;

p1.y = 34;

p1.red = 123;

p1.green = 37;

p1.blue = 127;

p2.x = 212;

p2.y = 994;

p2.red = 127;

p2.green = 127;

p2.blue = 0;

Code Box 6-4: The pixel_t structure and declaring two pixel_t variables

There are some important things to note regarding Code Box 6-4:

  • The attribute encapsulation happens when we put the x, y, red, green, and blue attributes into a new type, pixel_t.
  • Encapsulation always creates a new type; attribute encapsulation does this particularly in C. This is very important to note. In fact, this is the way that we make encapsulation explicit. Please note the _t suffix at the end of the pixel_t. It is very common in C to add the _t suffix to the end of the name of new types, but it is not mandatory. We use this convention throughout this book.
  • p1 and p2 will be our explicit objects when this code is executed. Both of them are of the pixel_t type, and they have only the attributes dictated by the structure. In C, and especially C++, types dictate the attributes to their objects.
  • The new type, pixel_t, is only the attributes of a class (or the object template). The word "class," remember, refers to a template of objects containing both attributes and functionalities. Since a C structure only keeps attributes, it cannot be a counterpart for a class. Unfortunately, we have no counterpart concept for a class in C; attributes and functionalities exist separately, and we implicitly relate them to each other in the code. Every class is implicit to C and it refers to a single structure together with a list of C functions. You'll see more of this in the upcoming examples, as part of this chapter and the future chapters.
  • As you see, we are constructing objects based on a template (here, the structure of pixel_t), and the template has the predetermined attributes that an object should have at birth. Like we said before, the structure only stores attributes and not the functionalities.
  • Object construction is very similar to the declaration of a new variable. The type comes first, then the variable name (here the object name) after that. While declaring an object, two things happen almost at the same time: first the memory is allocated for the object (creation), and then, the attributes are initialized (construction) using the default values. In the preceding example, since all attributes are integers, the default integer value in C is going to be used which is 0.
  • In C and many other programming languages, we use a dot (.) to access an attribute inside an object, or an arrow (->) while accessing the attributes of a structure indirectly through its address stored in a pointer. The statement p1.x (or p1->x if p1 is a pointer) should be read as the x attribute in the p1 object.

As you know by now, attributes are certainly not the only things that can be encapsulated into objects. Now it is time to see how functionalities are encapsulated.

Behavior encapsulation

An object is simply a capsule of attributes and methods. The method is another standard term that we usually use to denote a piece of logic or functionality being kept in an object. It can be considered as a C function that has a name, a list of arguments, and a return type. Attributes convey values and methods convey behaviors. Therefore, an object has a list of values and can perform certain behaviors in a system.

In class-based object-oriented languages such as C++, it is very easy to group a number of attributes and methods together in a class. In prototype-based languages such as JavaScript, we usually start with an empty object (ex nihilo, or "from nothing") or clone from an existing object. To have behaviors in the object, we need to add methods. Look at the following example, which helps you gain an insight into how prototype-based programming languages work. It is written in JavaScript:

// Construct an empty object

var clientObj = {};

// Set the attributes

clientObj.name = "John";

clientObj.surname = "Doe";

// Add a method for ordering a bank account

clientObj.orderBankAccount = function () {

...

}

...

// Call the method

clientObj.orderBankAccount();

Code Box 6-5: Constructing a client object in JavaScript

As you see in this example, on the 2nd line, we create an empty object. In the following two lines, we add two new attributes, name and surname, to our object. And on the following line, we add a new method, orderBankAccount, which points to a function definition. This line is an assignment actually. On the right-hand side is an anonymous function, which does not have a name and is assigned to the orderBankAccount attribute of the object, on the left-hand side. In other words, we store a function into the orderBankAccount attribute. On the last line, the object's method orderBankAccount is called. This example is a great demonstration of prototype-based programming languages, which only rely on having an empty object at first and nothing more.

The preceding example would be different in a class-based programming language. In these languages, we start by writing a class because without having a class, we can't have any object. The following code box contains the previous example but written in C++:

class Client {

public:

void orderBankAccount() {

...

}

std::string name;

std::string surname:

};

...

Client clientObj;

clientObj.name = "John";

clientObj.surname = "Doe";

...

clientObj.orderBankAccount ();

Code Box 6-6: Constructing the client object in C++

As you see, we started by declaring a new class, Client. On the 1st line, we declared a class, which immediately became a new C++ type. It resembles a capsule and is surrounded by braces. After declaring the class, we constructed the object clientObj from the Client type.

On the following lines, we set the attributes, and finally, we called the orderBankAccount method on the clientObj object.

Note:

In C++, methods are usually called member functions and attributes are called data members.

If you look at the techniques employed by open source and well-known C projects in order to encapsulate some items, you notice that there is a common theme among them. In the rest of this section, we are going to propose a behavior encapsulation technique which is based on the similar techniques observed in such projects.

Since we'll be referring back to this technique often, I'm going to give it a name. We call this technique implicit encapsulation. It's implicit because it doesn't offer an explicit behavior encapsulation that C knows about. Based on what we've got so far in the ANSI C standard, it is not possible to let C know about classes. So, all techniques that try to address object orientation in C have to be implicit.

The implicit encapsulation technique suggests the following:

  • Using C structures to keep the attributes of an object (explicit attribute encapsulation). These structures are called attribute structures.
  • For behavior encapsulation, C functions are used. These functions are called behavior functions. As you might know, we cannot have functions in structures in C. So, these functions have to exist outside the attribute structure (implicit behavior encapsulation).
  • Behavior functions must accept a structure pointer as one of their arguments (usually the first argument or the last one). This pointer points to the attribute structure of the object. That's because the behavior functions might need to read or modify the object's attributes, which is very common.
  • Behavior functions should have proper names to indicate that they are related to the same class of objects. That's why sticking to a consistent naming convention is very important when using this technique. This is one of the two naming conventions that we try to stick to in these chapters in order to have a clear encapsulation. The other one is using _t suffix in the names of the attribute structures. However, of course, we don't force them and you can use your own custom naming conventions.
  • The declaration statements corresponding to the behavior functions are usually put in the same header file that is used for keeping the declaration of the attribute structure. This header is called the declaration header.
  • The definitions of the behavior functions are usually put in one or various separate source files which include the declaration header.

Note that with implicit encapsulation, classes do exist, but they are implicit and known only to the programmer. The following example, example 6.1, shows how to use this technique in a real C program. It is about a car object that accelerates until it runs out of fuel and stops.

The following header file, as part of example 6.1, contains the declaration of the new type, car_t, which is the attribute structure of the Car class. The header also contains the declarations required for the behavior functions of the Car class. We use the phrase "the Car class" to refer to the implicit class that is missing from the C code and it encompasses collectively the attribute structure and the behavior functions:

#ifndef EXTREME_C_EXAMPLES_CHAPTER_6_1_H

#define EXTREME_C_EXAMPLES_CHAPTER_6_1_H

// This structure keeps all the attributes

// related to a car object

typedef struct {

char name[32];

double speed;

double fuel;

} car_t;

// These function declarations are

// the behaviors of a car object

void car_construct(car_t*, const char*);

void car_destruct(car_t*);

void car_accelerate(car_t*);

void car_brake(car_t*);

void car_refuel(car_t*, double);

#endif

Code Box 6-7 [ExtremeC_examples_chapter6_1.h]: The declarations of the attribute structure and the behavior functions of the Car class

As you see, the attribute structure car_t has three fields – name, speed, and fuel – which are the attributes of the car object. Note that car_t is now a new type in C, and we can now declare variables of this type. The behavior functions are also usually declared in the same header file, as you can see in the preceding code box. They start with the car_ prefix to put emphasis on the fact that all of them belong to the same class.

Something very important regarding the implicit encapsulation technique: each object has its own unique attribute structure variable, but all objects share the same behavior functions. In other words, we have to create a dedicated variable from the attribute structure type for each object, but we only write behavior functions once and we call them for different objects.

Note that the car_t attribute structure is not a class itself. It only contains the attributes of the Car class. The declarations all together make the implicit Car class. You'll see more examples of this as we go on.

There are many famous open source projects that use the preceding technique to write semi-object-oriented code. One example is libcurl. If you have a look at its source code, you will see a lot of structures and functions starting with curl_. You can find the list of such functions here: https://curl.haxx.se/libcurl/c/allfuncs.html.

The following source file contains the definitions of the behavior functions as part of example 6.1:

#include <string.h>

#include "ExtremeC_examples_chapter6_1.h"

// Definitions of the above functions

void car_construct(car_t* car, const char* name) {

strcpy(car->name, name);

car->speed = 0.0;

car->fuel = 0.0;

}

void car_destruct(car_t* car) {

// Nothing to do here!

}

void car_accelerate(car_t* car) {

car->speed += 0.05;

car->fuel -= 1.0;

if (car->fuel < 0.0) {

car->fuel = 0.0;

}

}

void car_brake(car_t* car) {

car->speed -= 0.07;

if (car->speed < 0.0) {

car->speed = 0.0;

}

car->fuel -= 2.0;

if (car->fuel < 0.0) {

car->fuel = 0.0;

}

}

void car_refuel(car_t* car, double amount) {

car->fuel = amount;

}

Code Box 6-8 [ExtremeC_examples_chapter6_1.c]: The definitions of the behavior functions as part of the Car class

The Car's behavior functions are defined in Code Box 6-8. As you can see, all the functions accept a car_t pointer as their first argument. This allows the function to read and modify the attributes of an object. If a function is not receiving a pointer to an attribute structure, then it can be considered as an ordinary C function that does not represent an object's behavior.

Note that the declarations of behavior functions are usually found next to the declarations of their corresponding attribute structure. That's because the programmer is the sole person in charge of maintaining the correspondence of the attribute structure and the behavior functions, and the maintenance should be easy enough. That's why keeping these two sets close together, usually in the same header file, helps in maintaining the overall structure of the class, and eases the pain for future efforts.

In the following code box, you'll find the source file that contains the main function and performs the main logic. All the behavior functions will be used here:

#include <stdio.h>

#include "ExtremeC_examples_chapter6_1.h"

// Main function

int main(int argc, char** argv) {

// Create the object variable

car_t car;

// Construct the object

car_construct(&car, "Renault");

// Main algorithm

car_refuel(&car, 100.0);

printf("Car is refueled, the correct fuel level is %f\n",

car.fuel);

while (car.fuel > 0) {

printf("Car fuel level: %f\n", car.fuel);

if (car.speed < 80) {

car_accelerate(&car);

printf("Car has been accelerated to the speed: %f\n",

car.speed);

} else {

car_brake(&car);

printf("Car has been slowed down to the speed: %f\n",

car.speed);

}

}

printf("Car ran out of the fuel! Slowing down ...\n");

while (car.speed > 0) {

car_brake(&car);

printf("Car has been slowed down to the speed: %f\n",

car.speed);

}

// Destruct the object

car_destruct(&car);

return 0;

}

Code Box 6-9 [ExtremeC_examples_chapter6_1_main.c]: The main function of example 6.1

As the first instruction in the main function, we've declared the car variable from the car_t type. The variable car is our first car object. On this line, we have allocated the memory for the object's attributes. On the following line, we constructed the object. Now on this line, we have initialized the attributes. You can initialize an object only when there is memory allocated for its attributes. In the code, the constructor accepts a second argument as the car's name. You may have noticed that we are passing the address of the car object to all car_* behavior functions.

Following that in the while loop, the main function reads the fuel attribute and checks whether its value is greater than zero. The fact that the main function, which is not a behavior function, is able to access (read and write) the car's attributes is an important thing. The fuel and speed attributes, for instance, are examples of public attributes, which functions (external code) other than the behavior functions can access. We will come back to this point in the next section.

Before leaving the main function and ending the program, we've destructed the car object. This simply means that resources allocated by the object have been released at this phase. Regarding the car object in this example, there is nothing to be done for its destruction, but it is not always the case and destruction might have steps to be followed. We will see more of this in the upcoming examples. The destruction phase is mandatory and prevents memory leaks in the case of Heap allocations.

It would be good to see how we could write the preceding example in C++. This would help you to get an insight into how an OOP language understands classes and objects and how it reduces the overhead of writing proper object-oriented code.

The following code box, as part of example 6.2, shows the header file containing the Car class in C++:

#ifndef EXTREME_C_EXAMPLES_CHAPTER_6_2_H

#define EXTREME_C_EXAMPLES_CHAPTER_6_2_H

class Car {

public:

// Constructor

Car(const char*);

// Destructor

~Car();

void Accelerate();

void Brake();

void Refuel(double);

// Data Members (Attributes in C)

char name[32];

double speed;

double fuel;

};

#endif

Code Box 6-10 [ExtremeC_examples_chapter6_2.h]: The declaration of the Car class in C++

The main feature of the preceding code is the fact that C++ knows about classes. Therefore, the preceding code demonstrates an explicit encapsulation; both attribute and behavior encapsulations. More than that, C++ supports more object-oriented concepts such as constructors and destructors.

In the C++ code, all the declarations, both attributes and behaviors, are encapsulated in the class definition. This is the explicit encapsulation. Look at the two first functions that we have declared as the constructor and the destructor of the class. C doesn't know about the constructors and destructors; but C++ has a specific notation for them. For instance, the destructor starts with ~ and it has the same name as the class does.

In addition, as you can see, the behavior functions are missing the first pointer argument. That's because they all have access to the attributes inside the class. The next code box shows the content of the source file that contains the definition of the declared behavior functions:

#include <string.h>

#include "ExtremeC_examples_chapter6_2.h"

Car::Car(const char* name) {

strcpy(this->name, name);

this->speed = 0.0;

this->fuel = 0.0;

}

Car::~Car() {

// Nothing to do

}

void Car::Accelerate() {

this->speed += 0.05;

this->fuel -= 1.0;

if (this->fuel < 0.0) {

this->fuel = 0.0;

}

}

void Car::Brake() {

this->speed -= 0.07;

if (this->speed < 0.0) {

this->speed = 0.0;

}

this->fuel -= 2.0;

if (this->fuel < 0.0) {

this->fuel = 0.0;

}

}

void Car::Refuel(double amount) {

this->fuel = amount;

}

Code Box 6-11 [ExtremeC_examples_chapter6_2.cpp]: The definition of the Car class in C++

If you look carefully, you'll see that the car pointer in the C code has been replaced by a this pointer, which is a keyword in C++. The keyword this simply means the current object. I'm not going to explain it any further here, but it is a smart workaround to eliminate the pointer argument in C and make behavior functions simpler.

And finally, the following code box contains the main function that uses the preceding class:

// File name: ExtremeC_examples_chapter6_2_main.cpp

// Description: Main function

#include <iostream>

#include "ExtremeC_examples_chapter6_2.h"

// Main function

int main(int argc, char** argv) {

// Create the object variable and call the constructor

Car car("Renault");

// Main algorithm

car.Refuel(100.0);

std::cout << "Car is refueled, the correct fuel level is "

<< car.fuel << std::endl;

while (car.fuel > 0) {

std::cout << "Car fuel level: " << car.fuel << std::endl;

if (car.speed < 80) {

car.Accelerate();

std::cout << "Car has been accelerated to the speed: "

<< car.speed << std::endl;

} else {

car.Brake();

std::cout << "Car has been slowed down to the speed: "

<< car.speed << std::endl;

}

}

std::cout << "Car ran out of the fuel! Slowing down ..."

<< std::endl;

while (car.speed > 0) {

car.Brake();

std::cout << "Car has been slowed down to the speed: "

<< car.speed << std::endl;

}

std::cout << "Car is stopped!" << std::endl;

// When leaving the function, the object 'car' gets

// destructed automatically.

return 0;

}

Code Box 6-12 [ExtremeC_examples_chapter6_2_main.cpp]: The main function of example 6.2

The main function written for C++ is very similar to the one we wrote for C, except that it allocates the memory for a class variable instead of a structure variable.

In C, we can't put attributes and behavior functions in a bundle that is known to C. Instead, we have to use files to group them. But in C++, we have a syntax for this bundle, which is the class definition. It allows us to put data members (or attributes) and member functions (or behavior functions) in the same place.

Since C++ knows about the encapsulation, it is redundant to pass the pointer argument to the behavior functions, and as you can see, in C++, we don't have any first pointer arguments in member function declarations like those we see in the C version of the Car class.

So, what happened? We wrote an object-oriented program in both C, which is a procedural programming language, and in C++, which is an object-oriented one. The biggest change was using car.Accelerate() instead of car_accelerate(&car), or using car.Refuel(1000.0) instead of car_refuel(&car, 1000.0).

In other words, if we are doing a call such as func(obj, a, b, c, ...) in a procedural programming language, we can do it as obj.func(a, b, c, ...) in an object-oriented language. They are equivalent but coming from different programming paradigms. Like we said before, there are numerous examples of C projects that use this technique.

Note:

In Chapter 9, Abstraction and OOP in C++, you will see that C++ uses exactly the same preceding technique in order to translate high-level C++ function calls to low-level C function calls.

As a final note, there is an important difference between C and C++ regarding object destruction. In C++, the destructor function is invoked automatically whenever an object is allocated on top of the Stack and it is going out of scope, like any other Stack variable. This is a great achievement in C++ memory management, because in C, you may easily forget to call the destructor function and eventually experience a memory leak.

Now it is time to talk about other aspects of encapsulation. In the next section, we will talk about a consequence of encapsulation: information-hiding.

Information hiding

So far, we've explained how encapsulation bundles attributes (which represent values) and functionalities (which represent behaviors) together to form objects. But it doesn't end there.

Encapsulation has another important purpose or consequence, which is information-hiding. Information-hiding is the act of protecting (or hiding) some attributes and behaviors that should not be visible to the outer world. By the outer world, we mean all parts of the code that do not belong to the behaviors of an object. By this definition, no other code, or simply no other C function, can access a private attribute or a private behavior of an object if that attribute or behavior is not part of the public interface of the class.

Note that the behaviors of two objects from the same type, such as car1 and car2 from the Car class, can access the attributes of any object from the same type. That's because of the fact that we write behavior functions once for all objects in a class.

In example 6.1, we saw that the main function was easily accessing the speed and fuel attributes in the car_t attribute structure. This means that all attributes in the car_t type were public. Having a public attribute or behavior can be a bad thing because it might have some long-lasting and dangerous.

As a consequence, the implementation details could leak out. Suppose that you are going to use a car object. Usually, it is only important to you that it has a behavior that accelerates the car; and you are not curious about how it is done. There may be even more internal attributes in the object that contribute to the acceleration process, but there is no valid reason that they should be visible to the consumer logic.

For instance, the amount of the electrical current being delivered to the engine starter could be an attribute, but it should be just private to the object itself. This also holds for certain behaviors that are internal to the object. For example, injecting the fuel into the combustion chamber is an internal behavior that should not be visible and accessible to you, otherwise, you could interfere with that and interrupt the normal process of the engine.

From another point of view, the implementation details (how the car works) vary from one car manufacturer to another but being able to accelerate a car is a behavior that is provided by all car manufacturers. We usually say that being able to accelerate a car is part of the public API or the public interface of the Car class.

Generally, the code using an object becomes dependent on the public attributes and behaviors of that object. This is a serious concern. Leaking out an internal attribute by declaring it public at first and then making it private can effectively break the build of the dependent code. It is expected that other parts of the code that are using that attribute as a public thing won't get compiled after the change.

This would mean you've broken the backward compatibility. That's why we choose a conservative approach and make every single attribute private by default until we find sound reasoning for making it public.

To put it simply, exposing private code from a class effectively means that rather than being dependent on a light public interface, we have been dependent on a thick implementation. These consequences are serious and have the potential to cause a lot of rework in a project. So, it is important to keep attributes and behaviors as private as they can be.

The following code box, as part of example 6.3, will demonstrate how we can have private attributes and behaviors in C. The example is about a List class that is supposed to store some integer values:

#ifndef EXTREME_C_EXAMPLES_CHAPTER_6_3_H

#define EXTREME_C_EXAMPLES_CHAPTER_6_3_H

#include <unistd.h>

// The attribute structure with no disclosed attribute

struct list_t;

// Allocation function

struct list_t* list_malloc();

// Constructor and destructor functions

void list_init(struct list_t*);

void list_destroy(struct list_t*);

// Public behavior functions

int list_add(struct list_t*, int);

int list_get(struct list_t*, int, int*);

void list_clear(struct list_t*);

size_t list_size(struct list_t*);

void list_print(struct list_t*);

#endif

Code Box 6-13 [ExtremeC_examples_chapter6_3.h]: The public interface of the List class

What you see in the preceding code box is the way that we make the attributes private. If another source file, such as the one that contains the main function, includes the preceding header, it'll have no access to the attributes inside the list_t type. The reason is simple. The list_t is just a declaration without a definition, and with just a structure declaration, you cannot access the fields of the structure. You cannot even declare a variable out of it. This way, we guarantee the information-hiding. This is actually a great achievement.

Once again, before creating and publishing a header file, it is mandatory to double-check whether we need to expose something as public or not. By exposing a public behavior or a public attribute, you'll create dependencies whose breaking would cost you time, development effort, and eventually money.

The following code box demonstrates the actual definition of the list_t attribute structure. Note that it is defined inside a source file and not a header file:

#include <stdio.h>

#include <stdlib.h>

#define MAX_SIZE 10

// Define the alias type bool_t

typedef int bool_t;

// Define the type list_t

typedef struct {

size_t size;

int* items;

} list_t;

// A private behavior which checks if the list is full

bool_t __list_is_full(list_t* list) {

return (list->size == MAX_SIZE);

}

// Another private behavior which checks the index

bool_t __check_index(list_t* list, const int index) {

return (index >= 0 && index <= list->size);

}

// Allocates memory for a list object

list_t* list_malloc() {

return (list_t*)malloc(sizeof(list_t));

}

// Constructor of a list object

void list_init(list_t* list) {

list->size = 0;

// Allocates from the heap memory

list->items = (int*)malloc(MAX_SIZE * sizeof(int));

}

// Destructor of a list object

void list_destroy(list_t* list) {

// Deallocates the allocated memory

free(list->items);

}

int list_add(list_t* list, const int item) {

// The usage of the private behavior

if (__list_is_full(list)) {

return -1;

}

list->items[list->size++] = item;

return 0;

}

int list_get(list_t* list, const int index, int* result) {

if (__check_index(list, index)) {

*result = list->items[index];

return 0;

}

return -1;

}

void list_clear(list_t* list) {

list->size = 0;

}

size_t list_size(list_t* list) {

return list->size;

}

void list_print(list_t* list) {

printf("[");

for (size_t i = 0; i < list->size; i++) {

printf("%d ", list->items[i]);

}

printf("]\n");

}

Code Box 6-14 [ExtremeC_examples_chapter6_3.c]: The definition of the List class

All the definitions that you see in the preceding code box are private. The external logic that is going to use a list_t object does not know anything about the preceding implementations, and the header file is the only piece of code that the external code will be dependent on.

Note that the preceding file has not even included the header file! As long as the definitions and function signatures match the declarations in the header file, that's all that's needed. However, it is recommended to do so because it guarantees the compatibility between the declarations and their corresponding definitions. As you've seen in Chapter 2, Compilation and Linking, the source files are compiled separately and finally linked together.

In fact, the linker brings private definitions to the public declarations and makes a working program out of them.

Note:

We can use a different notation for private behavior functions. We use the prefix __ in their names. As an example, the __check_index function is a private function. Note that a private function does not have any corresponding declaration in the header file.

The following code box contains example 6.3's main function that creates two list objects, populates the first one, and uses the second list to store the reverse of the first list. Finally, it prints them out:

#include <stdlib.h>

#include "ExtremeC_examples_chapter6_3.h"

int reverse(struct list_t* source, struct list_t* dest) {

list_clear(dest);

for (size_t i = list_size(source) - 1; i >= 0; i--) {

int item;

if(list_get(source, i, &item)) {

return -1;

}

list_add(dest, item);

}

return 0;

}

int main(int argc, char** argv) {

struct list_t* list1 = list_malloc();

struct list_t* list2 = list_malloc();

// Construction

list_init(list1);

list_init(list2);

list_add(list1, 4);

list_add(list1, 6);

list_add(list1, 1);

list_add(list1, 5);

list_add(list2, 9);

reverse(list1, list2);

list_print(list1);

list_print(list2);

// Destruction

list_destroy(list1);

list_destroy(list2);

free(list1);

free(list2);

return 0;

}

Code Box 6-15 [ExtremeC_examples_chapter6_3_main.c]: The main function of example 6.3

As you can see in the preceding code box, we wrote the main and reverse functions only based on the things declared in the header file. In other words, these functions are using only the public API (or public interface) of the List class; the declarations of the attribute structure list_t and its behavior functions. This example is a nice demonstration of how to break the dependencies and hide the implementation details from other parts of the code.

Note:

Using the public API, you can write a program that compiles, but it cannot turn into a real working program unless you provide the corresponding object files of the private part and link them together.

There are some points related to the preceding code that we explore in more detail here. We needed to have a list_malloc function in order to allocate memory for a list_t object. Then, we can use the function free to release the allocated memory when we're done with the object.

You cannot use malloc directly in the preceding example. That's because if you are going to use malloc inside the main function, you have to pass sizeof(list_t) as the required number of bytes that should be allocated. However, you cannot use sizeof for an incomplete type.

The list_t type included from the header file is an incomplete type because it is just a declaration that doesn't give any information regarding its internal fields, and we don't know its size while compiling it. The real size will be determined only at link time when we know the implementation details. As a solution, we had to have the list_malloc function defined and have malloc used in a place where sizeof(list_t) is determined.

In order to build example 6.3, we need to compile the sources first. The following commands produce the necessary object files before the linking phase:

$ gcc -c ExtremeC_examples_chapter6_3.c -o private.o

$ gcc -c ExtremeC_examples_chapter6_3_main.c -o main.o

Shell Box 6-1: Compiling example 6.3

As you see, we have compiled the private part into private.o and the main part into main.o. Remember that we don't compile header files. The public declarations in the header are included as part of the main.o object file.

Now we need to link the preceding object files together, otherwise main.o alone cannot turn into an executable program. If you try to create an executable file using only main.o, you will see the following errors:

$ gcc main.o -o ex6_3.out

main.o: In function 'reverse':

ExtremeC_examples_chapter6_3_main.c:(.text+0x27): undefined reference to 'list_clear'

...

main.o: In function 'main':

ExtremeC_examples_chapter6_3_main.c:(.text+0xa5): undefined reference to 'list_malloc'

... collect2: error: ld returned 1 exit status

$

Shell Box 6-2: Trying to link example 6.3 by just providing main.o

You see that the linker cannot find the definitions of the functions declared in the header file. The proper way to link the example is as follows:

$ gcc main.o private.o -o ex6_3.out

$ ./ex6_3.out

[4 6 1 5 ]

[5 1 6 4 ]

$

Shell Box 6-3: Linking and running example 6.3

What happens if you change the implementation behind the List class?

Say, instead of using an array, you use a linked list. It seems that we don't need to generate the main.o again, because it is nicely independent of the implementation details of the list it uses. So, we need only to compile and generate a new object file for the new implementation; for example, private2.o. Then, we just need to relink the object files and get the new executable:

$ gcc main.o private2.o -o ex6_3.out

$ ./ex6_3.out

[4 6 1 5 ]

[5 1 6 4 ]

$

Shell Box 6-4: Linking and running example 6.3 with a different implementation of the List class

As you see, from the user's point of view, nothing has changed, but the underlying implementation has been replaced. That is a great achievement and this approach is being used heavily in C projects.

What if we wanted to not repeat the linking phase in case of a new list implementation? In that case, we could use a shared library (or .so file) to contain the private object file. Then, we could load it dynamically at runtime, removing the need to relink the executable again. We have discussed shared libraries as part of Chapter 3, Object Files.

Here, we bring the current chapter to an end and we will continue our discussion in the following chapter. The next two chapters will be about the possible relationships which can exist between two classes.