Skip to main content

A simple C project

Part of an ongoing series of essays tentatively entitled Don’t embarrass me, Don’t embarrass yourself: Notes on thinking in C and Unix.

This essay isn’t so much about how C programmers think or how Unix programmers think. Rather, it’s about how I think about some working on code. I’ve written this essay for a variety of reasons: It will help you understand a bit more what I mean by Thinking like SamR; it should expand your horizons a bit; and it will serve as a kind of prelude to our upcoming discussion of GNU Make.

Let’s think about what you would do if you were creating a simple C project. In order to create a project, you start with a problem. So, we’re going to build a library of math utilities. Since I only have limited space, or at least limited time, our library will have only one procedure, gcd, which computes the greatest common divisor of two integers.

Are you ready to think about that project? Good. Let’s get started.

Here’s the first question: What files do you expect to create? I mean the files that you will write with a text editor, such as your .c and .h files. Don’t include the ones that you’ll use a compiler or other program to generate from the initial ones, such as .o files.

Have you made the list? No? I was serious. I want you to think about what goes in a simple library project.

Okay, I’ll assume that you’ve followed the instructions this time. Compare your answer to mine [1]. Did you come up with the same list I did? Past experience suggests that your list is smaller [4]. That’s okay; that’s why we’re doing these exercises.

Next question: What files do you expect to generate from these source files? It’s okay if you don’t come up with all of the ones I’d expect; I just want you to think through the problem.

Are you done? Good. Compare your answers to mine [5].

Next, write the instructions to generate that second set of files. For example, how are you going to generate the executables?

You know what’s next. Compare your answers to mine [6].

We’re almost ready to start writing code. Just one more question: What is the signature of gcd?

A Header

Now that you’ve figured that out, it’s time to write gcd.h. Here’s my version.

/**
 * mathlib.h
 *   A simple library of math utilities.
 *
 * <Insert appropriate open-source license.>
 */
 
#ifndef __MATHLIB_H__
#define __MATHLIB_H__

// +------------+----------------------------------------------------
// | Procedures |
// +------------+

/**
 * Compute the greatest common divisor of two positive integers.
 */
extern int gcd(int x, int y);

#endif // __MATHLIB_H__

You can ignore the lines that begin with pound signs. While most C programmers include them in their headers, you’ll need to know more about the preprocessor to understand why we have them.

Tests

What next? Well, we’ll think better about the procedure if we write the tests. And yes, you should write your own list of tests before reading my list of tests.

Here’s a question that came up while I was writing my tests and thinking about edge cases. What should we do if the user gives a zero or a negative integer for one or both of the parameters? There are two basic approaches: We can make it the responsibility of gcd to check the parameters and to return a special value when it gets an invalid parameter, or we can make it the responsibility of the caller to ensure that preconditions are met. We’ll use the latter approach.

What are my other edge cases? Well, we know that the greatest common divisor of 1 and anything (or anything and 1) is 1. So we should do some tests using 1 in each parameter position. We should probably worry about large numbers, so maybe something close to the largest integer. I know that many people use Euclid’s algorithm to implement GCD, so I might choose some pairs that require a lot of repetitions in that algorithm, such as neighboring pairs of Fibonacci numbers.

I’ll try a variety of moderately small values for the other experiments.

Here’s my test suite. Yours may differ. I’ve made it fairly thorough because I expect to be using my utilities in a variety of cases, so it’s particularly important to make sure that it works right.

/**
 * test-gcd.c
 *   Tests for the mathlib gcd procedure.
 *
 * <Insert appropriate open-source license.>
 */

// +---------+-------------------------------------------------------
// | Headers |
// +---------+

#include <assert.h>
#include <limits.h>
#include "mathlib.h"

// +------+----------------------------------------------------------
// | Main |
// +------+

int
main (int argc, char *argv[])
{
  // Identical numbers
  assert (gcd (5, 5) == 5);
  assert (gcd (11, 11) == 11);
  assert (gcd (24, 24) == 24);

  // Some "obvious" answers
  assert (gcd (6, 3) == 3);
  assert (gcd (3, 6) == 3);
  assert (gcd (21, 6) == 3);
  assert (gcd (6, 21) == 3);
  assert (gcd (10, 25) == 5);
  assert (gcd (5, 25) == 5);
  assert (gcd (50, 70) == 10);
  assert (gcd (70, 50) == 10);

  // Relatively prime
  assert (gcd (10, 9) == 1);
  assert (gcd (9, 10) == 1);
  assert (gcd (502, 9) == 1);
  assert (gcd (9, 205) == 1);

  // One of the parameters is 1.
  assert (gcd (1, 1) == 1);
  assert (gcd (1, 5) == 1);
  assert (gcd (5, 1) == 1);
  assert (gcd (24, 1) == 1);
  assert (gcd (1, 25) == 1);

  // Large integers
  assert (gcd (INT_MAX, INT_MAX) == INT_MAX);
  assert (gcd (3 * (INT_MAX / 3), INT_MAX / 3) == INT_MAX / 3);
  assert (gcd (INT_MAX / 3, 3 * (INT_MAX / 3)) == INT_MAX / 3);
  assert (gcd (5 * (INT_MAX / 5), INT_MAX / 5) == INT_MAX / 5);
  assert (gcd (INT_MAX / 5, 5 * (INT_MAX / 5)) == INT_MAX / 5);
  assert (gcd (14 * (INT_MAX / 14), INT_MAX / 14) == INT_MAX / 14);
  assert (gcd (INT_MAX / 14, 14 * (INT_MAX / 14)) == INT_MAX / 14);
  assert (gcd (311 * (INT_MAX / 311), INT_MAX / 311) == INT_MAX / 311);
  assert (gcd (INT_MAX / 311, 311 * (INT_MAX / 311)) == INT_MAX / 311);

  // Fibonacci
  assert (gcd (21, 13) == 1);
  assert (gcd (13, 21) == 1);
  assert (gcd (63, 39) == 3);
  assert (gcd (39, 63) == 3);
  assert (gcd (55, 34) == 1);
  assert (gcd (34, 55) == 1);
  assert (gcd (550, 340) == 10);

  // And we're done.
  return 0;
} // main

Implementation

Now, we’re ready to implement the function. We’ll use Euclid’s algorithm. You should sketch the code yourself, particularly if you’ve never written it before.

Here’s my program.

/**
 * mathlib-gcd.c
 *   The greatest-common-divisor procedure for mathlib.
 *
 * <Insert appropriate open-source license.>
 */

// +---------+-------------------------------------------------------
// | Headers |
// +---------+

#include "mathlib.h"

// +---------------------+-------------------------------------------
// | Exported Procedures |
// +---------------------+

/**
 * Compute the greatest common divisor of two positive integers.
 */
int 
gcd(int x, int y)
{
  if (y == 0)
    return x;
  else 
    return gcd (y, x % y);
} // gcd

Running our tests

Hmmm … is it really that simple? Should I make sure that x is greater than y and swap them if it isn’t? Well, let’s see if this version works.

$ cc -g -Wall   -c -o test-gcd.o test-gcd.c
$ cc -g -Wall   -c -o mathlib-gcd.o mathlib-gcd.c
$ cc -o test-gcd test-gcd.o mathlib-gcd.o
$ ./test-gcd
$

Yay! Our tests passed. I’m pretty confident that the original is correct. (I know that Euclid’s algorithm is correct. I wanted to make sure that I didn’t get something wrong in trying to implement it, particularly since I didn’t pay attention to which input was larger.)

A Command-Line Application

We’re now ready for the last step, writing a short command-line utility. After all, who knows when you’ll need the gcd of two numbers or more numbers?

Guess what? Writing this utility is your next assignment. Write a C program that uses our library to compute the greatest common divisor of a sequence of one or more integers entered on the command line.

% gcd 5 25
5
% gcd 17 82
1
% gcd 3570 4200
210
% gcd 3570 4200 48
6
% gcd 3
3

Make sure to do appropriate error checking. You should make sure that there’s at least one command-line parameter, that all of the parameters represent valid integers, and so on and so forth.

What about the library?

We’ll consider building and libraries in future modules.


[1] I would probably create five files: A .c file containing the code for gcd; a .h file for others to include, a program that tests gcd, a command-line executable [2], and a Makefile to manage the whole project. We’ll call those mathlib-gcd.c, mathlib.h, test-gcd.c, gcd.c, and Makefile. In some cases, I’d think about creating an interactive test program, but I generally don’t like interactive test programs. I should also write a separate test for the command-line program, but I’ll leave that aside for now [3].

[2] As a long-time Unix programmer, I believe in the worth of creating simple command-line tools from my libraries.

[3] That decision is probably a bad one. I seem to recall that the last time I tried to write the command-line program, I inserted a subtle bug.

[4] I guess there’s a small chance that your list is larger. In three or so offerings of this class, I don’t think I’ve encountered other suggestions, but I suppose that some student will surprise me at some point.

[5] Every .c file will have a corresponding .o file. That’s three. I’ll have two executables, one for the test and one for the command-line program. We should also build a library (or shared library) from mathlib-gcd.o. But you probably don’t know about libraries yet, so we’ll leave that for a separate essay.

[6] Surprise! I’m not putting the answers here. You’ll find my answers later in the essay.


Version 1.0 of 2017-01-06.