Learning C by comparison #3: Using includes and multiple files

This is the third part of my Learning code by comparison series of learning C with explanations in Ruby.

It’s always normal when writing a library, application, or other things in Ruby to use multiple files. It’s common to do this with C too, but since we compile it’s not as simple as our typical require.

To start off our Ruby example we’ll create a Person module with common methods we’ll want to include in our James class. This would allow us to give James extra methods that are unique to that class.


module Person
  def full_name
    "#{@first_name} #{@last_name}"
  end
end
      

We’ll save this to person.rb. Now we’ll create james.rb and make use of Person.


require './person'

class James
  include Person

  def initialize(first_name, last_name, age)
    @first_name = first_name
    @last_name  = last_name
    @age        = age
  end

  def old_enough_to_drink?
    @age > 18
  end
end

james = James.new('James', 'Newton', 21)
james.full_name            #=> "James Newton"
james.old_enough_to_drink? #=> true
      

One of the important lines to notice here is the require where we’re including Person from person.rb.

So, all in all; super easy. Now we move into C.

In C there is no way to just “include” another C file. Since C is compiled, we supply the extra C files to our compiler. But that itself isn’t enough. We have to make use of “header” files to fill in some gaps.

In C we’ll start with a very basic james.c:


int main(int argc, const char *argv[])
{
    return 0;
}
      

Now, we’ll create a person.h. This header file will define our struct’s and functions.


#include <stdio.h>

#ifndef __PERSON_H__
#define __PERSON_H__

typedef struct {
    char *first_name;
    char *last_name;
    int  age;
} person_t;

#endif
      

In this we’re wrapping everything in a ifndef which is a “if not defined”. This ensures that things are not redeclared when it’s included in multiple places. The second line is defining __PERSON_H__ if the ifndef is false. Alternatively there is a ifdef.

You’ll also notice we’re including stdio.h. We’re including this so we can make use of printf.

Now we’ll create a person.c which will define a function to print out our persons information:


#include "person.h"

void person_print(person_t person)
{
    printf("Name: %s %s\n", person.first_name, person.last_name);
    printf("Age:  %i\n", person.age);
}
      

Here we’re including person.h so we can make use of the definition of person_t, and printf.

Now we’re going to want to make use of person_print inside of james.c, but we cannot include a C file. This is where how we compile starts to become important, which we’ll touch on soon.

But first we need to add to james.c to actually make the program do something:


#include "person.h"

int main(int argc, const char *argv[])
{
    person_t james = {"James", "Newton", 21};

    person_print(james);

    return 0;
}
      

Now we need to do one more thing, and that is declare the person_print method inside of person.h:


// ...

extern void person_print(person_t person);

// ...
      

The reason we do this is because james.c does not know our function exists. By adding a prototype declaration we’re letting james.c know that the function exists, just not what it does yet. This’ll also allow us to use person_print in more files if we wanted to, rather then having to redefine it multiple times.

Now, to compile our program.

Usually in my compile examples I just do gcc file.c, but now that we’re using multiple files we’ll get a little more detailed.

We’ll use the following command to compile:


$ gcc -o james james.c person.c -I .
      

In this command -o james tells our compiler that the program will be compiled to the executable james. The next two params, james.c person.c tell the compiler the files to compile, and -I . tells the compiler the “include search path” (or, where to find our person.h). Since everything is in the same directory for my example -I isn’t needed, but there for the sake of example.

After compiling there will be an executable file named james. Compiling and running will yield these results:


$ gcc -o james james.c person.c -I .
$ ./james
Name: James Newton
Age:  21
      

Full code samples.