0

empowering creative people

C Programming for Makers

Welcome to the C Programming Workshop for Makers! Here you'll be able to follow along with our series of easily digestible videos that cover everything you'll need to know to on your way with C programming and start making awesome projects. As you progress through the workshops, you'll find helpful material next to each video - these could be code snippets, diagrams, reference tables, or links to other resources. My name is Josh, and I'm a computer scientist and maker who has been writing with C and related languages for years, so let me be your guide into the wonderful world of lower level coding.

To follow along, you'll just need a computer (I'll be explaining for Windows and Raspberry Pi), an Internet connection and a desire to learn some amazing skills.

Course outline:

If you run into any issues throughout this workshop, then please reach out to us on our forum. We're full-time makers and are here to help.

Let's get started!


CHAPTER 1: WHAT IS IT AND HOW DO I SET IT UP?

1.0 CHAPTER OVERVIEW

In Chapter 1 we cover a basic rundown of what the C language is, and how to set up your computer so you can actually start writing it. We'll then go over the first program anybody writes when they learn a new programming language, Hello World. We'll then dissect this and talk about the different elements that make up all C programs.


1.1 WHAT IS C?

In this section, we discuss what C is, how it came to be, and how it compares to the other common programming languages Arduino and Python.

C vs C++ vs C#

Even though these languages have similar names, they're vastly different.

  • C is a low-level procedural language that is compiled into small binaries, generally with the highest performance, which makes it more suitable for direct hardware control or where maximum performance is desirable. This comes at the cost of requiring the programmer to be careful as there are few safeguards. With great power comes great responsibility.
  • C++ is what's called an object-oriented language, which is a way to manipulate tightly related data and functions in blocks called objects, such as players in a game each having their own health, equipment and stats, rather than trying to maintain arrays of everybody's data. As such, C++ is commonly used in game programming and server applications, where they can benefit from a mid-level language that still has a high performance but is filled with extra features that make programming easier (and the extra file size this causes is not such an issue).
  • C# was created by Microsoft to be a Java competitor and is therefore targeted to the .NET runtime on the Windows platform. It is also object-oriented like C++ but has many safeguards added to remove the risk of breaking something in environments like web and desktop applications, where it is most often used. Having all these extra features does mean that the performance is lower than the other languages, but in the applications that it is used, this is not so noticeable.

Language levels

Shown below are examples of the same basic function written in some of the languages mentioned in the video, and as you can see the further down you go, the harder it is to write and easily understand.

Python

for a in range(1, 10)
print(a)

C#

using System;

public class TestProgram
{
	public static void Main()
	{
		int a = 1;
		int b = 10;
		while (a <= b)
		{
			Console.WriteLine(a);
			a++;
		}
	}
}

Arduino

void setup() {
  Serial.begin(9600);
  int a = 1;
  int b = 10;
  while (a <= b)
  {
    Serial.println(a);
    a++;
  }
}

void loop() {
}

C++

#include <iostream>

int main()
{
    int a = 1;
    int b = 10;
    while (a <= b)
    {
        std::cout << a << endl;
        a++;
    }
    return 0;
}

C

#include <stdio.h>

int main(void)
{
  int a = 1;
  int b = 10;
  while (a <= b)
  {
    printf("%d\n", a);
    a++;
  }
  return 0;
}

Assembly

.LC0:
.string "%d\n"
main:
push rbp
mov rbp, rsp
sub rsp, 16
mov DWORD PTR [rbp-4], 1
mov DWORD PTR [rbp-8], 10
jmp .L2
.L3:
mov eax, DWORD PTR [rbp-4]
mov esi, eax
mov edi, OFFSET FLAT:.LC0
mov eax, 0
call printf
add DWORD PTR [rbp-4], 1
.L2:
mov eax, DWORD PTR [rbp-4]
cmp eax, DWORD PTR [rbp-8]
jl .L3
mov eax, 0
leave
ret

Machine Code (AKA Binary)

ff 25 12 0c 20 68 00 00
00 00 e9 e0 ff ff ff 55
48 89 e5 48 83 ec 10 c7
45 fc 01 00 c7 45 f8 0a
00 eb 18 8b 45 fc 89 c6
bf d4 05 40 00 b8 00 00
00 00 e8 cd fe ff ff 83
45 fc 01 8b 45 fc 3b 45
f8 7c e0 b8 00 00 00 00
c9 c3 66 2e 0f 1f 84

1.2 SETTING UP THE DEVELOPMENT ENVIRONMENT

The software that I'll be using during this course is Atom (with the gpp-compiler package installed) and MinGW on Windows. I'll be showing you how to set up and use these, as well as how to compile your programs on a Raspberry Pi if you want to (HINT: everything you need is already installed).


1.3 HELLO WORLD

The first program that anybody learning a new programming language is called Hello World, which simply prints that to a console window. It's also useful to check that your set-up is working correctly.

The code used in this video:

/*
  My first program prints "Hello World!"
  to the command prompt and then exits
*/

#include <stdio.h> // printf is part of this library

int main (void)
{
  printf("Hello World!\n"); // prints to the screen
  return 0; // ends program and sends a 0 back to the command promt
}

1.4 BREAKING DOWN THE HELLO WORLD

Here we'll dissect the Hello World program from the previous section, and explain the different parts of what makes up every C program and what they mean.


CHAPTER 2: C LANGUAGE STRUCTURE

2.0 CHAPTER OVERVIEW

Chapter 2 goes more in depth with the structure of C programs, including how to store and manipulate data, as well as a few tricks you can use in your projects.


2.1 TYPES

In this section, we discuss the different types of data you can have and the best ways to store them in your programs.

Listed below are the most common data types you will come across, as well as what you can store in them - in this case, using the MinGW compiler on a Windows PC, other platforms and compilers may change the sizes slightly (the Raspberry Pi does this!).

Type Size Range
Integer types
char 8 bits (1 byte) -128 to 127 (designed to store ASCII characters)
unsigned char 8 bits (1 byte) 0 to 255
short 16 bits (2 bytes) -32,768 to 32,767
unsigned short 16 bits (2 bytes) 0 to 65,535
int 32 bits (4 bytes) -2,147,483,648 to 2,147,483,647
unsigned int 32 bits (4 bytes) 0 to 4,294,967,295
long 32 bits (4 bytes) -2,147,483,648 to 2,147,483,647
unsigned long 32 bits (4 bytes) 0 to 4,294,967,295
long long 64 bits (8 bytes) -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807
unsigned long long 64 bits (8 bytes) 0 to 18,446,744,073,709,551,615
Floating point types
float 32 bits (4 bytes) 1.17549e-038 to 3.40282e+038
double 64 bits (8 bytes) 2.22507e-308 to 1.79769e+308
long double 96 bits (12 bytes) 3.3621e-4932 to 1.18973e+4932

If you want to see if your platform is different, here is the code to recreate the table above. Don't worry too much about what's in the code (everything will be explained in due course), just copy it to a .c file on your platform and run it.

/*
  Prints a table of the data types and their sizes on this system
*/

#include <float.h>
#include <limits.h>
#include <stdio.h>

/*
  this is because Windows doesn't support printing numbers >4 bytes easily
  MinGW actually implements the Linux version as well, so we'll use it instead
*/
#ifdef __MINGW32__
  #define printf __mingw_printf
#endif

int main(void)
{
  printf("\tType\t\t\tSize\t\t\tRange\n");
  printf("\t\t\t\tInteger types\n");
  printf("char\t\t\t%d bits (%d bytes)\t%hhd to %hhd\n", sizeof(char) * CHAR_BIT, sizeof(char), CHAR_MIN, CHAR_MAX);
  printf("unsigned char\t\t%d bits (%d bytes)\t%hhu to %hhu\n", sizeof(unsigned char) * CHAR_BIT, sizeof(unsigned char), 0, UCHAR_MAX);
  printf("short\t\t\t%d bits (%d bytes)\t%hd to %hd\n", sizeof(short) * CHAR_BIT, sizeof(short), SHRT_MIN, SHRT_MAX);
  printf("unsigned short\t\t%d bits (%d bytes)\t%hu to %hu\n", sizeof(unsigned short) * CHAR_BIT, sizeof(unsigned short), 0, USHRT_MAX);
  printf("int\t\t\t%d bits (%d bytes)\t%d to %d\n", sizeof(int) * CHAR_BIT, sizeof(int), INT_MIN, INT_MAX);
  printf("unsigned int\t\t%d bits (%d bytes)\t%u to %u\n", sizeof(unsigned int) * CHAR_BIT, sizeof(unsigned int), 0, UINT_MAX);
  printf("long\t\t\t%d bits (%d bytes)\t%ld to %ld\n", sizeof(long) * CHAR_BIT, sizeof(long), LONG_MIN, LONG_MAX);
  printf("unsigned long\t\t%d bits (%d bytes)\t%lu to %lu\n", sizeof(unsigned long) * CHAR_BIT, sizeof(unsigned long), 0UL, ULONG_MAX);
  printf("long long\t\t%d bits (%d bytes)\t%lld to %lld\n", sizeof(long long) * CHAR_BIT, sizeof(long long), LLONG_MIN, LLONG_MAX);
  printf("unsigned long long\t%d bits (%d bytes)\t%llu to %llu\n", sizeof(unsigned long long) * CHAR_BIT, sizeof(unsigned long long), 0ULL, ULLONG_MAX);
  printf("\t\t\t\tFloating point types\n");
  printf("float\t\t\t%d bits (%d bytes)\t%g to %g\n", sizeof(float) * CHAR_BIT, sizeof(float), FLT_MIN, FLT_MAX);
  printf("double\t\t\t%d bits (%d bytes)\t%g to %g\n", sizeof(double) * CHAR_BIT, sizeof(double), DBL_MIN, DBL_MAX);
  printf("long double\t\t%d bits (%d bytes)\t%Lg to %Lg\n", sizeof(long double) * CHAR_BIT, sizeof(long double), LDBL_MIN, LDBL_MAX);
  return 0;
}

2.2 VARIABLES AND CONSTANTS

Here I'll show you how you can store and retrieve data in variables, as well as how to use constant values that won't change but will help with code readability and reduce human error.

Here is the code used in this video

/*
  Code used to give examples of how to use variables and constants
*/

#include <math.h>
#include <stdio.h>

#define CONSTANT 42

int main(void)
{
  int a; // not setting an initial value
  printf("a default value: %d\n", a);

  int b = 0; // setting an initial value
  printf("b value: %d\n", b);

  b = 2; // changing a pre-existing variable
  printf("b new value: %d\n", b);

  int c = CONSTANT + 1; // using a constant value
  printf("c value: %d\n", c);

  double d = 100.7;
  printf("d value: %f\n", d);

  double e = 1.007e-2; // scientific notation
  printf("e value: %f\n", e);

  short f = -1; // signed value into signed variable
  printf("f value: %hd\n", f);

  unsigned short g = -1; // signed value into unsigned variable
  printf("g value: %hu\n", g);

  int h = 0x2a; // hex notation
  printf("h value: %d\n", h);

  int i = 052; // octal notation
  printf("i value: %d\n", i);

  float j = 1.79769e+308; // putting a value too big into a float variable
  printf("j value: %f\n", j);

  double k = sqrt(2); // getting the return value of a function
  printf("k value: %f\n", k);

  return 0;
}

2.3 EXPRESSIONS

This lesson will follow on from the previous one, showing you how to manipulate the data you've stored in various ways, allowing you to do more than just maths. I'll also go over some pitfalls you may come across during your time coding, and how to fix them.

Operators

Operator Example Meaning
Arithmetic
= a = b Assignment
+ a + b Addition
- a - b Subtraction
* a * b Multiplication
/ a / b Division
% a % b Modulo
+ +a Positive
- -a Negative
++ ++a Prefix increment
++ a++ Postfix increment
-- --a Prefix decrement
-- a-- Postfix decrement
Comparison
== a == b Equal to
!= a != b Not equal to
< a < b Less than
<= a <= b Less than or equal to
> a > b Greater than
>= a >= b Greater than or equal to
Logical
! !a NOT a
&& a && b a AND b
|| a || b a OR b
Bitwise
~ ~a Complement a
& a & b a AND b
| a | b a OR b
^ a ^ b a XOR b
<< a << b a left shifted by b bits
>> a >> b a right shifted by b bits
Compound assignment
+= a += b a = a + b
-+ a -= b a = a - b
*= a *= b a = a * b
/= a /= b a = a / b
%= a %= b a = a % b
&= a &= b a = a & b
|= a |= b a = a | b
^= a ^= b a = a ^ b
<<= a <<= b a = a << b
>>= a >>= b a = a >> b
Others
[ ] a[b] element b of array a
& &a reference (get the memory address of) a
* *a dereference (get the data at memory address in) a
. a.b member b of struct a
-> a->b member b of struct at the memory address in a
( ) (a + b) used to group expressions for higher precedence
( ) a(b) call function a with parameter b
, a(b, c) used to separate parameters
? : a ? b : c ternary statement; if a is true, b, otherwise c
sizeof (type) sizeof (int) the number of char-sized units this type occupies
sizeof (variable) sizeof (a) the number of char-sized units this variable occupies
(type) value (double) a temporarily turn (cast) value into a 'type'

A quick note on prefix vs postfix

In most cases, choosing whether to use prefix or postfix won't make a difference (the most common use is just incrementing or decrementing a variable). But when combined with other statements to make more powerful or compact code, which one you choose to use does matter. For example, take the following code snippet:

int i = 0;
printf("%d\n", i++);
printf("%d\n", ++i);

When run, what prints out is:

0
2

This is because, in the case of the first printf, the value of i is taken (0) and printed, then incremented (from 0 to 1). In the 2nd printf, the value is incremented (from 1 to 2), then taken (2) and printed. Similar behaviour can be seen for decrement.

A problem with division of integer types

Because integer types can't hold fractions, if you did something like 2 / 5, the answer would be 2 and not 2.5. If the extra 0.5 is important to you (such as being part of a larger expression), you need to change at least 1 of the values in the expression to any of the floating point types, either by adding a .0 to the end or explicitly telling the compiler to treat a value as a specific type (this is called type casting) by putting the desired type in parentheses before the value. This will also promote any other types in the expression that haven't already been evaluated to the most precise type. Ways you could do this would be:

2.0 / 5
2 / 5.0
(double) 2 / 5
2 / (double) 5
(double) 2 / (double) 5

Precedence

Higher rows get evaluated first, and all operators in the same row have the same precedence and are evaluated in the order shown in the right-hand column.

Operators Descriptions Evaluation order

++

--

( )

( )

[ ]

.

->

Postfix increment

Postfix decrement

Precedence increase

Function call

Array element

Struct member

Struct member

Left to right

++

--

+

-

!

~

(type)

&

*

sizeof

Prefix increment

Prefix decrement

Positive

Negative

Logical NOT

Bitwise complement

Type cast

Reference

Dereference

Size of value

Right to left

*

/

%

Multiplication

Division

Modulo

Left to right

+

-

Addition

Subtraction

Left to right

<<

>>

Bitwise left shift

Bitwise right shift

Left to right

<

<=

>

>=

Less than

Less than or equal

Greater than

Greater than or equal

Left to right

==

!=

Equal to

Not equal to

Left to right
& Bitwise AND Left to right
^ Bitwise XOR Left to right
| Bitwise OR Left to right
&& Logical AND Left to right
|| Logical OR Left to right
? : Ternary statement Left to right

=

+=

-=

*=

/+

%=

<<=

>>=

&=

^=

|-

Assignment

Addition assignment

Subtraction assignment

Multiplication assignment

Division assignment

Modulo assignment

Bitwise left shift assignment

Bitwise right shift assignment

Bitwise AND assignment

Bitwise XOR assignment

Bitwise OR assignment

Right to left
, Comma Left to right

Here is an example of the evaluation order of a maths statement:

expression-example


2.4 SCOPES

This lesson explains what scope is, and how you can use it to your advantage in your projects.

Here is the code that was used in the video:

/*
  Program used to illustrate scope
*/

#include <stdio.h>

/*
  global variables are created outside a function
  and are available anywhere in the program
*/
int globalVariable = 0;
int anotherGlobalVariable = 1;

// function prototype (don't worry, this will be explained in a later chapter)
int otherFunction(int globalVariable, int otherParameter);

int main(void)
{
  // local variables are only available inside the function they are created in
  int localVariable = 2;
  int localVariable2 = 3;
  printf("localVariable value: %d\n", localVariable);

  // global variables can be accessed from anywhere in the same program
  printf("globalVariable value: %d\n", globalVariable);
  // global variables can also be changed from anywhere, so be careful
  globalVariable = 4;
  printf("globalVariable new value: %d\n", globalVariable);

  /*
    local variables that have the same name as a global variable are used
    instead of the global one
  */
  int anotherGlobalVariable = 5;
  printf("anotherGlobalVariable value: %d\n", anotherGlobalVariable);

  printf("\n");

  // run other function
  otherFunction(localVariable, localVariable2);

  printf("\n");

  // run function again to show static variable changes
  otherFunction(localVariable, localVariable2);

  printf("\n");

  // checking which variables got changed
  printf("final globalVariable value: %d\n", globalVariable);
  printf("final anotherGlobalVariable value: %d\n", anotherGlobalVariable);
  printf("final localVariable value: %d\n", localVariable);
  printf("final localVariable2 value: %d\n", localVariable2);

  return 0;
}

/*
  function parameters are seen as local variables, and follow the same naming
  rules.
  the value of localVariable from main is copied and placed in a parameter
  called globalVariable, and the value of localVariable2 from main is copied
  and placed in a variable called otherParameter.
  any changes to them don't go back to main (yet, that's in another chapter)
*/
int otherFunction(int globalVariable, int otherParameter)
{
  // local variables created in a function are deleted once the function ends
  int temporaryVariable = 6;
  printf("temporaryVariable value: %d\n", temporaryVariable);

  // unless they are made as static variables
  static int persistentVariable = 7;
  printf("persistentVariable value: %d\n", persistentVariable);

  /*
    make a change to some variables to show that the static variable will be
    maintained the next time this function is run and the other ones won't
  */
  persistentVariable++;
  temporaryVariable++;

  // you can see this is not the same value as what was created globally
  printf("otherFunction globalVariable value: %d\n", globalVariable);

  /*
    variables with same name in a different function are different variables
  */
  int localVariable = 8;
  printf("otherFunction localVariable value: %d\n", localVariable);

  /*
  trying to change a local variable outside the function it was created in
  doesn't work, even if you use the same names in both functions.
  this changes the variable in this function, not main
  */
  localVariable = 9;
  printf("otherFunction localVariable new value: %d\n", localVariable);

  /*
    this will cause a compile error, because there is no variable with that
    name in this function
  */
  //localVariable2 = 10;

  /*
    changes to parameters also don't flow back to the function that sent them
  */
  otherParameter = 11;

  // you can change global variables though
  anotherGlobalVariable = 12;

  return 0;
}

CHAPTER 3: CONTROL STRUCTURES

3.0 CHAPTER OVERVIEW

In Chapter 3 we cover control structures, which will allow you to create a program flow that is more complex, but also more useful, than we have used so far. We go through how to make decisions and branch off into other bits of code, how to repeat something without having to copy and paste it as many times as you need, and also how to represent these structures graphically in order to visually see what's happening.


3.1 FLOWCHART NOTATION

In this section, I'll show you how to understand the flowcharts I'll be using to explain the structures in the rest of this chapter.

Name Description
flowchart-process A normal statement
flowchart-decision Tests an expression and gets a true/false answer
flowchart-arrow The direction the program execution flows

This is an example of the sort of programs we have covered so far, but in flowchart format. As you can see, the flow goes from top to bottom like you would reading the C code, following the arrows. Up until now though, the programs have only had 1 direction to go in, limiting their usefulness somewhat.

flowchart-simple

The code snippet for that flowchart would be:

a = 1;
b = 2;
c = a + b
printf("%d\n", c);

This workshop is in progress, check back next week for updates!


3.2 DECISION MAKING

This is where we start to use what you've learned in the previous chapters to make decisions based on your variables and expressions.

Name Flowchart Code Example Description
If flowchart-if
a = 1;
b = 5;
if (a < b)
{
  printf("a is less than b\n");
}
printf("done\n");
Tests a true/false expression and does something only if the expression is true.
If-else flowchart-if-else
a = 1;
b = 5;
if (a < b)
{
  printf("a is less than b\n");
}
else
{
  printf("a is not less than b\n");
}
Tests a true/false expression and does one thing if the expression is true, and something else if the expressions is false.
If-else if-else flowchart-if-elseif-else
a = 1;
if (a == 1)
{
printf("a is equal to 1\n");
}
else if (a == 2)
{
printf("a is equal to 2\n");
}
else if (a == 3)
{
printf("a is equal to 3\n");
}
else
{
printf("a is something else\n");
}
Tests multiple true/false expressions and works through them in order, doing only the first thing where the matching expression is true, or the last one if they're all false.
Switch
a = 1;
switch (a) {
case 1:
printf("a is equal to 1\n");
break;
case 2:
printf("a is equal to 2\n");
break;
case 3:
printf("a is equal to 3\n");
break;
default:
printf("a is something else\n");
break;
}
Same as above, in a more compact and easier to read format, with the caveat that the test expressions can only be a set of equality checks against the same integer. If you don't put break at the end of each case, your program will ALSO EXECUTE the next section, and so on until it reaches a break.
Ternary flowchart-ternary
a = 1;
b = (a < 2) ? (2) : (3);
printf("b = %d\n", b);
A simplified, single line if-else, with the caveat that you can only have 1 statement each for the expression's true and false results.

3.3 REPEATING OURSELVES

Here we go through the different ways to create loops in our code, and when you would use one way over another.

Name Flowchart Code Example Description
While flowchart-while
a = 1;
b = 5;
while (a < b)
{
  printf("a = %d\n", a);
  a++;
}
printf("done\n");
Continues to execute while the expression being tested remains true. This may be 0 or more times.
For
for (a = 1, b = 5; a < b; a++)
{
  printf("a = %d\n", a);
}
printf("done\n");
Same as above, with the initialise, test and step statements built into the same line (in that order), separated by semicolons. These are all optional. In this example, there are 2 variables being set in the initialise section, which only works when all but up to 1 of the variables has previously been created (and if there is 1 new variable it HAS to be the first one initialised).
Infinite loop flowchart-infinite
for (;;)
{
printf("infinitely printed\n");
}
Same as above, but when an empty test section is provided, the test section will always be true. This example is shown with all the optional sections removed (note that the semicolons remain), the common way to create an infinite loop, but the effect is the same if just the test section is removed.
Do-while flowchart-do-while
a = 1;
b = 5;
do
{
  printf("a = %d\n", a);
  a++;
} while(a < b);
printf("done\n");
Same as a while, except the check happens at the end, rather than at the beginning. This means that the statements are run 1 or more times.
Break flowchart-break
a = 1;
b = 5;
while (a < b)
{
  printf("a = %d\n", a);
  a++;
  if (a == 3)
  {
    printf("leaving loop early\n");
    break;
  }
}
printf("done\n");
Break is used to exit a loop or switch early. If the break is inside a loop or switch, itself inside a loop or switch, it will only exit the innermost loop or switch early.
Continue flowchart-continue
a = 1;
b = 5;
while (a < b)
{
  printf("a = %d\n", a);
  a++;
  if (a == 3)
  {
    printf("skipping\n");
    continue;
  }
  printf("b = %d\n", b);
}
printf("done\n");
Continue immediately finishes the current iteration of a loop, skipping all the statements from that point on, and starts the next one. In the case of a for loop though, the step statement is still executed. The same rule for multi-level loops as in a break applies here, but it doesn't apply for switches (continue in a switch doesn't affect the switch itself, but will affect a loop the switch is in).

3.4 SOME PRACTICAL EXAMPLES OF EVERYTHING SO FAR

This is just a demonstration of what can be done with what has been covered in the course so far.

/*
  Creates a list of temperature conversions from Celsius to Fahrenheit,
  making note of some commonly known temperatures
*/

#include <stdio.h>

int main(void)
{
  // goes through every celsius temperature from -100 to 100
  for (int celsius = -100; celsius <= 100; celsius++)
  {
    // switch used to get specific values out or to print a multiple of 10
    switch (celsius)
    {
      case -78:
        printf("Celsius: %4d = Fahrenheit: %4g\t(Sublimation of dry ice into CO2 gas)\n", celsius, (celsius * 9.0 / 5) + 32);
        break;
      case -40:
        printf("Celsius: %4d = Fahrenheit: %4g\t(Celsius and Fahrenheit match)\n", celsius, (celsius * 9.0 / 5) + 32);
        break;
      case 0:
        printf("Celsius: %4d = Fahrenheit: %4g\t(Water freezing temperature)\n", celsius, (celsius * 9.0 / 5) + 32);
        break;
      case 37:
        printf("Celsius: %4d = Fahrenheit: %4g\t(Human body temperature)\n", celsius, (celsius * 9.0 / 5) + 32);
        break;
      case 100:
        printf("Celsius: %4d = Fahrenheit: %4g\t(Water boiling temperature)\n", celsius, (celsius * 9.0 / 5) + 32);
        break;
      // any number not covered by the above rules
      default:
        // get every temperature divisible by 10, otherwise do nothing
        if (celsius % 10 == 0)
        {
          printf("Celsius: %4d = Fahrenheit: %4g\n", celsius, (celsius * 9.0 / 5) + 32);
        }
        break;
    }
  }
}
/*
  Generates the times tables for 1 to 15
*/

#include <stdio.h>

int main(void)
{
  printf("  x |");
  // loop used to print out the column numbers
  for (int col = 1; col <= 15; col++)
  {
    printf("%4d", col);
  }
  // print horizontal dividing line
  printf("\n----+------------------------------------------------------------\n");
  // loop to go through the rows
  for (int row = 1; row <= 15; row++)
  {
    // print the row number
    printf("%3d |", row);
    // loop to go through the columns
    for (int col = 1; col <= 15; col++)
    {
      printf("%4d", row * col);
    }
    printf("\n");
  }
  return 0;
}
/*
  An infinitely running bingo callling program,
  including names for the named numbers

  Names from https://en.wikipedia.org/wiki/List_of_British_bingo_nicknames
*/

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
  int number = 0;

  // infinite for loop
  for (;;)
  {
    // call the rand function from stdlib and get a random number from 1-90
    number = rand() % 90 + 1;
    printf("Next number...number %d! ", number);
    /*
      using a switch instead of lots of if, else if, else if, ..., else
      to make it shorter/easier to read. breaks stop case fall-through
    */
    switch (number)
    {
      case 1:
        printf("Kelly's Eye!\n");
        break;
      case 2:
        printf("One little duck!\n");
        break;
      case 3:
        printf("Cup of tea!\n");
        break;
      case 4:
        printf("Knock at the door!\n");
        break;
      case 5:
        printf("Man alive!\n");
        break;
      case 6:
        printf("Tom Mix!\n");
        break;
      case 7:
        printf("Lucky!\n");
        break;
      case 8:
        printf("Garden gate!\n");
        break;
      case 9:
        printf("Doctor's Orders!\n");
        break;
      case 10:
        printf("Theresa's Den!\n");
        break;
      case 11:
        printf("Legs eleven!\n");
        break;
      case 12:
        printf("One dozen!\n");
        break;
      case 13:
        printf("Unlucky for some!\n");
        break;
      case 14:
        printf("The Lawnmower!\n");
        break;
      case 15:
        printf("Young and Keen!\n");
        break;
      case 16:
        printf("Never been kissed!\n");
        break;
      case 17:
        printf("Dancing Queen!\n");
        break;
      case 18:
        printf("Coming of Age!\n");
        break;
      case 19:
        printf("Goodbye Teens!\n");
        break;
      case 20:
        printf("One Score!\n");
        break;
      case 21:
        printf("Key of the Door!\n");
        break;
      case 22:
        printf("Two little ducks!\n");
        break;
      case 23:
        printf("The Lord is My Shepherd!\n");
        break;
      case 24:
        printf("Knock at the door!\n");
        break;
      case 25:
        printf("Duck and dive!\n");
        break;
      case 26:
        printf("Half a crown!\n");
        break;
      case 27:
        printf("Duck and a crutch!\n");
        break;
      case 28:
        printf("In a state!\n");
        break;
      case 29:
        printf("Rise and Shine!\n");
        break;
      case 30:
        printf("Burlington Bertie!\n");
        break;
      case 31:
        printf("Get Up and Run!\n");
        break;
      case 32:
        printf("Buckle My Shoe!\n");
        break;
      case 33:
        printf("All the threes!\n");
        break;
      case 34:
        printf("Ask for More!\n");
        break;
      case 35:
        printf("Jump and Jive!\n");
        break;
      case 36:
        printf("Three dozen!\n");
        break;
      case 39:
        printf("Steps!\n");
        break;
      case 44:
        printf("Droopy drawers!\n");
        break;
      case 45:
        printf("Halfway there!\n");
        break;
      case 48:
        printf("Four Dozen!\n");
        break;
      case 50:
        printf("It's a bullseye!\n");
        break;
      case 52:
        printf("Danny La Rue!\n");
        break;
      case 53:
        printf("Here comes Herbie!\n");
        break;
      case 54:
        printf("Man at the door!\n");
        break;
      case 55:
        printf("Musty Hive!\n");
        break;
      case 56:
        printf("Shotts Bus!\n");
        break;
      case 57:
        printf("Heinz Varieties!\n");
        break;
      case 59:
        printf("The Brighton Line!\n");
        break;
      case 60:
        printf("Grandma's getting frisky!\n");
        break;
      case 62:
        printf("Tickety-boo!\n");
        break;
      case 64:
        printf("Almost retired!\n");
        break;
      case 65:
        printf("Stop work!\n");
        break;
      case 66:
        printf("Clickety click!\n");
        break;
      case 67:
        printf("Stairway to Heaven!\n");
        break;
      case 68:
        printf("Pick a Mate!\n");
        break;
      case 69:
        printf("Anyway up!\n");
        break;
      case 71:
        printf("Bang on the Drum!\n");
        break;
      case 72:
        printf("Danny La Rue!\n");
        break;
      case 73:
        printf("Queen Bee!\n");
        break;
      case 74:
        printf("Hit the Floor!\n");
        break;
      case 76:
        printf("Trombones!\n");
        break;
      case 77:
        printf("Two little crutches!\n");
        break;
      case 78:
        printf("39 more steps!\n");
        break;
      case 80:
        printf("Gandhi's Breakfast!\n");
        break;
      case 81:
        printf("Fat Lady with a walking stick!\n");
        break;
      case 83:
        printf("Stop Farting!\n");
        break;
      case 84:
        printf("Seven dozen!\n");
        break;
      case 85:
        printf("Staying alive!\n");
        break;
      case 86:
        printf("Between the sticks!\n");
        break;
      case 87:
        printf("Torquay in Devon!\n");
        break;
      case 88:
        printf("Two Fat Ladies!\n");
        break;
      case 89:
        printf("Nearly there!\n");
        break;
      case 90:
        printf("Top of the shop!\n");
        break;
      default:
        // using divide and mod to separate the 2 digits
        printf("%d and %d!\n", number / 10, number % 10);
    }
    printf("Press enter for next number...");
    // getchar is used here to wait until you press enter
    getchar();
    // fseek is used here to skip anything else the user types
    fseek(stdin, 0, SEEK_END);
  }
  return 0;
}

CHAPTER 4: MAKING OUR OWN FUNCTIONS AND TYPES

4.0 CHAPTER OVERVIEW

In Chapter 4 we get to build our own functions from scratch, and create custom data types that allow much more flexibility than what we've seen so far.


4.1 FUNCTIONS

In this section we create our own functions, that we can use to make our code more modular. In later chapters we can create our own libraries with these functions so that we don't have to rewrite the same code in each of our projects.

Normal functions

Just as we create the main function in every program we write, creating any extra functions we might need is exactly the same. We start with a type, then a name for our function, and finally any parameters we will need. Unlike the main function though, our own functions have a couple of differences:

  • While main can only return an int type (some C implementations also allow void, but this course will assume not just to make things easier and more universal), your functions can return any type you want. Or if you don't want to return anything, your function type is void and you don't need to include a return statement in your function
  • main can only have either void or 2 special parameters (we'll cover that in a later chapter), but your functions can have 0-127 parameters
  • Our functions need to be both declared and defined, whereas main only needs to be defined.

To declare a new function, you need to put (generally at the top of your .c file underneath the includes and new type definitions):

functionType functionName(parameters);

For example, let's create a function that takes a floating point number and returns the cube of it. So our function declaration would be:

double cube(double number);

We then need to define what our function actually does. We need to put this outside the main function (I like putting it under main, but it just has to be some point after your declarations):

double cube(double number)
{
return number * number * number;
}

You'll notice that the first line matches what was in our declaration, and because our function type wasn't void, we have to return a value.

Variadic functions

Variadic functions are just like normal functions, but they have an unknown (but greater than 0) number of parameters. To use them, you must put #include <stdarg.h> at the top of your file. If we were to recreate the SUM function you can find in Microsoft Excel that takes at least 1 parameter but can take as many as you want, we would create a function like this:

Declaration:

double sum(double number, ...);

Definition:

double sum(double number, ...)
{
  double total = 0;

  va_list parameters;
  va_start(parameters, number);

  for (int counter = 0; counter < number; counter++)
  {
    total += va_arg(parameters, double);
  }

  va_end(parameters);

  return total;
}

This function is slightly different to the Excel version, in that the first parameter (number) tells our function how many other parameters are in the ... part. An example of using this function would be:

mySum = sum(5, 1.2, 3.4, 5.6, 7.8, 9.0);

4.2 TYPES

In this section we create our own types, which can be aliases for existing types, creating lists of options like in a drop down box, or grouping existing types together to create structures that hold different types of data together.

Typedef

Typedef (short for type definition) is used to create an alias of a type. This could be to create an easier name for the complex types we cover here and in later chapters, or to allow you to change the type of many variables and functions by changing one definition at the top of your code, rather than throughout your code and possibly missing some. An example would be storing a coordinate:

typedef int COORD_TYPE;

would create an alias to an int type, called COORD_TYPE. If we had lots of COORD_TYPE's stored in our program, and we later decide that they should all be double's rather than int's, we could just change this one line and apply that change throughout the rest of the code.

Struct

Structs (short for structures) are used to group multiple pieces of data into a well defined format, and to store them under a single name. For instance, if we wanted to store a point that has an X and Y coordinate, we could do:

struct XY_POINT
{
COORD_TYPE x;
COORD_TYPE y;
};

Giving the struct a name (in this case XY_POINT) is optional, but if you leave it out, you can't refer to that structure type later without also using a typedef. As structs are just another type, you can store structs inside structs (as long as there is no circular links like a struct including itself). If you wanted to extend the coordinate system, and create a bounding box that stores the coordinates of 2 opposite corners, you could do:

struct BOUNDING_BOX
{
struct XY_POINT corner;
struct XY_POINT oppositeCorner;
};

If you wanted to create some boxes at the same time as defining the struct (you can just do it elsewhere in your code like a normal type if you'd prefer) you would do this:

struct BOUNDING_BOX
{
struct XY_POINT corner;
struct XY_POINT oppositeCorner;
} box1, box2;

This way of creating variables when defining also works for the types in the remainder of this chapter. You could then access the variables inside the structs, called member variables, by using:

box1.corner.x = 1;
box1.corner.y = 2;
box1.oppositeCorner.x = 4;
box1.oppositeCorner.y = 0;

If you want to change all the variables of a struct at once, you can put them in order inside a pair of braces:

struct XY_POINT myPoint = {1, 2}; // {x = 1, y = 2}

Enum

Enums (short for enumerations) are used when you want to create a list of all possible choices that a variable can hold, and you want to use the choices by name instead of value. For instance, if you wanted to store a day of the week, you only have 7 choices. Instead of using the numbers 0-6 or 1-7, which could get confusing, you could use an enum that stores them by name instead:

enum DAY
{
NONE = 0, // 0000000
MONDAY = 1, // 0000001
TUESDAY = 2, // 0000010
WEDNESDAY = 4, // 0000100
THURSDAY = 8, // 0001000
FRIDAY = 16, // 0010000
SATURDAY = 32, // 0100000
SUNDAY = 64 // 1000000
};

I could have just used the values 1-7, but setting an enum like this (using powers of 2 for the values) allows you to store multiple choices in the same variable by simply adding the values together (imagine a set of tick boxes in a form, you could use 1 variable to know which ones were ticked), or create more compact logic expressions by using bitwise operators. If you wanted to use this enum to run an action every Monday, Wednesday and Friday, if you had the values 1-7 you would need to do something like:

enum DAY today;
...
if (today == MONDAY || today == WEDNESDAY || today == FRIDAY)
{
// do something
}

Using powers of 2 instead, you could do:

enum DAY today;
...
if (today & (MONDAY | WEDNESDAY | FRIDAY))
{
  // do something
}

which would check if the today variable shares any of the same bits with MONDAY, WEDNESDAY and FRIDAY, and then run whatever code you put inside the if.

Bitfield

Bitfields (a special type of struct) are used when you want to only use a small part of a type's range for multiple variables and you want to more tightly pack them, reducing overall memory usage. If you have a set of 8 LEDs that are either on or off, you could store their values in a normal struct as chars, but you would be wasting 7 out of 8 bits for each one for a total of 56 bits wasted (7 bytes). For memory limited applications like embedded programming, you want to save as much memory as possible. If you put them in a bitfield instead, you could specify that each one is 1 bit:

struct LED_BITFIELD
{
unsigned char LED1 : 1;
unsigned char LED2 : 1;
unsigned char LED3 : 1;
unsigned char LED4 : 1;
unsigned char LED5 : 1;
unsigned char LED6 : 1;
unsigned char LED7 : 1;
unsigned char LED8 : 1;
};

As all of these were stored as a char, the minimum size of the bitfield is 1 byte (8 bits). If just one of them was an int, even if we still specify 1 bit, the minimum size would be that of an int (4 bytes, 32 bits). If you were to go over that amount, an additional block of memory would be used to hold it (9 bits stored inside 16 bits and 33 bits stored in 64 bits respectively). You can use as many bits per variable that you'd like, or not specify the number of bits for certain variables to use the default amount.

Union

Unions are used to access the same memory location and interpret it as different data types. They take up as much memory as it would take to store the largest of the types you specify. A float is internally stored as 3 parts: sign, exponent and mantissa (also called significand or fraction). If you wanted to look at or change these parts individually to change the value of the overall float, you could combine a bitfield and a float inside a union:

union FLOAT_PARTS
{
struct
{
unsigned int mantissa : 23;
unsigned int exponent : 8;
unsigned int sign : 1;
} raw;
float f;
}

In this example, if you then wanted to change the exponent for instance, you would use:

union FLOAT_PARTS myFloat;
...
myFloat.raw.exponent = 7;

4.3 COMBINING THESE

So let's look at some example code of how you might use your new functions and variables. This example takes the coordinates of 2 opposite corners of a box, and gives you some measurements of the box.

#include <math.h> // pow, sqrt
#include <stdio.h> // printf

// our coordinates will be floating point types
typedef double COORD_TYPE;

// a place to store (x, y) of a point
struct XY_POINT
{
COORD_TYPE x;
COORD_TYPE y;
};
// create a box made up of 2 opposite corner coordinates struct BOUNDING_BOX
{
struct XY_POINT corner;
struct XY_POINT oppositeCorner;
};
// our function declarations COORD_TYPE distance2points(struct XY_POINT point1, struct XY_POINT point2); COORD_TYPE width(struct BOUNDING_BOX box); COORD_TYPE height(struct BOUNDING_BOX box); COORD_TYPE aspectRatio(struct BOUNDING_BOX box); COORD_TYPE perimeter(struct BOUNDING_BOX box); COORD_TYPE area(struct BOUNDING_BOX box); int main(void) { struct BOUNDING_BOX box = {{1000, 1000}, {2920, 2080}}; printf("width:\t\t%g\n", width(box)); printf("height:\t\t%g\n", height(box)); printf("aspect:\t\t%g\n", aspectRatio(box)); printf("perimeter:\t%g\n", perimeter(box)); printf("area:\t\t%g\n", area(box)); } // calculates the distance between 2 points in space COORD_TYPE distance2points(struct XY_POINT point1, struct XY_POINT point2) { return (COORD_TYPE) sqrt(pow(point2.x - point1.x, 2) + pow(point2.y - point1.y, 2)); } // calculates the width of a box COORD_TYPE width(struct BOUNDING_BOX box) { // create missing point temporarily and return width return distance2points(box.corner, (struct XY_POINT) {box.oppositeCorner.x, box.corner.y}); } // calculates the height of a box COORD_TYPE height(struct BOUNDING_BOX box) { // create missing point temporarily and return height return distance2points(box.corner, (struct XY_POINT) {box.corner.x, box.oppositeCorner.y}); } // calculates the aspect ratio (width:height) of a box COORD_TYPE aspectRatio(struct BOUNDING_BOX box) { return width(box) / height(box); } // calculates the perimeter of a box COORD_TYPE perimeter(struct BOUNDING_BOX box) { return 2 * (width(box) + height(box)); } // calculates the area of a box COORD_TYPE area(struct BOUNDING_BOX box) { return width(box) * height(box); }

CHAPTER 5: ARRAYS AND POINTERS

5.0 CHAPTER OVERVIEW

In this chapter, we look at arrays that we can use to store many values together without having to make a separate variable for each of them, and pointers that let us go right down to the memory address level to manipulate and share our variables.


5.1 WHAT IS AN ARRAY?

An array is a collection of values of the same type, stored under the same variable name. Think of them like pages in a very small notebook: each page only has enough room to store a single value, and all the pages travel together as part of the book. If you want a specific value, you have to know the page number to look at, or search through them all. If you run out of room or just want to carry around more values at once, you need to get a new book. Similarly, an array is a series of values in a container that has a specified size. Each element of the array can store a single value, and they are accessed with an index number. To search through the array, you just look at every index.

An example of creating an int array would be:

int myArray[10];

This would create an array called myArray that could store 10 ints. If you wanted to store more than that, you would have to create a whole new one as they can't grow. When creating an array, you can specify its initial contents by putting the values inside braces. You don't have to specify all the values, the remainder will default to 0. If you do specify all the values though, then you don't have to include the size inside the [ ]. To do that for the array above, you could do:

int myArray[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

Array indexes start at 0 (a reason that you will commonly see for loops starting their counter variable at 0), so this array would have indexes from 0-9. To access the 3rd and 4th elements in this array, you would therefore use:

printf("%d\n", myArray[2]);
myArray[3] = 0;

This would print the number 3 and change the 4th element to 0. As you can store any type inside an array, this also includes structs and unions.

struct XY_POINT
{
COORD_TYPE x;
COORD_TYPE y;
};
struct XY_POINT pointArray[7]; struct XY_POINT points[] = {{2, 3}, {4, 5}, {6, 7}};

5.2 DIMENSIONS

The arrays we covered in the previous section can also be called one-dimensional arrays. They are accessed using a single index number, and you could write them out in a single line. There are also multi-dimensional arrays, or "arrays of arrays", which use more than one index number and start to become more difficult if you want to write them out. For instance, a 2-dimensional array would be like a table, having row and column index numbers; a 3-dimensional array is like a cube with 3 index numbers, etc. To create these, you just need to specify the number of sizes you want when creating the array, or give the correct number of values when specifying all the values. Some examples might be:

int array1D[10]; // 1-by-10 array
int array2D[][] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}; // 3-by-4 array with specified values
int array3D[x][y][z]; // x-by-y-by-z array

To access the values in these arrays, it's basically the same as when using a one-dimensional array: you just specify the index values in the same order. For instance, with array3D above, you would specify the x index, then y, then z.


5.3 STRINGS

Strings are a special type of array, storing char or unsigned char values and generally being used to store words, sentences, and user input (we use them for this purpose in the next chapter). A string is created big enough to store the maximum number of characters that might need to be stored, plus 1 extra to store a special null character that signifies the end of a string. Strings are generally set by initially putting them inside double quotes ("") when you know what the string will be, or using the functions inside the string.h library. Like any other array, you can also change individual values in the string, but setting them like this initally should be avoided if possible. The most common functions in the library that you will use are:

strcat - concatenate one string onto another
strncat - concatenate one string onto another, up to n characters
strcmp - compare two strings
strncmp - compare up to n characters of two strings
strcpy - copy one string to another
strncpy - copy up to n characters of one string to another
strlen - get the length of the string, not including the end null character
strstr - find one string inside another
strtok - split up a string into chunks (tokens), using a specific string as a delimiter

A quick example

#include <stdio.h>
#include <string.h>

int main(void)
{
  char string1[10] = "Hello";
  char string2[15];

  printf("%s\n", string1);

  strcat(string1, " ");
  strncat(string1, "World!", 3);
  printf("%s\n", string1);
  printf("%d\n", strlen(string1));

  strcpy(string2, "Hello World!");
  printf("%s\n", string1);
  printf("%s\n", string2);

  if (strcmp(string1, string2) < 0)
  {
    printf("string1 comes before string2 alphabetically\n");
  }
  else if (strcmp(string1, string2) > 0)
  {
    printf("string1 comes after string2 alphabetically\n");
  }
  else if (strcmp(string1, string2) == 0)
  {
    printf("string1 is the same as string2\n");
  }
  return 0;
}

5.4 POINTERS

What is a pointer?

A pointer is a variable that contains the memory address of another variable. A pointer variable is created like a normal one, with a * before the pointer name. To get the memory address of an existing variable, you put a & (reference operator) before the variable name. To access the variable inside a pointer, you put a * (dereference operator) before the pointer name. As a pointer is just a variable, you can have pointers to pointers. You don't want to go too deep with this though with pointers to pointers to pointers to..., or you might start losing track (2 levels is commonly the limit). If you want to get to the base value inside this pointer-to-a-pointer, you would just dereference it twice (with **). There is a special pointer that is used by name, and that is NULL. This usually signifies that a pointer hasn't been set yet, or can be used as an error value for a function that uses pointers. Either way, don't try to dereference NULL or your program will crash, so make sure your programs are checking for NULL and acting accordingly.

Why use a pointer?

Imagine you have a group project that you and your friend are working on, and it's stored on your cloud drive (Dropbox, Google Drive, Nextcloud, pick your favourite). In order to both work on it at once, you download the files and put them on a USB drive, and send them to your friend. They can then work on that copy of the files, change them as they wish without changing your version, and can return them to you. You would then have to manage the changes by adding, deleting and merging the 2 parts. This is what we've been using so far, sending a copy of our data to functions and then processing what is returned. If you instead sent them a link to the live versions on the cloud instead of downloading a copy (some of the files could be huge to copy!), you could both edit the latest version of the files, and all you have to do is wait for the changes to happen. This is what a pointer would do, sending the address of a variable so that a function could change it, and when that function returns you can just keep going knowing that you don't have to merge anything. This is also the easiest way to have a function "return" multiple values, you can send it a bunch of pointers to the variables that will store each of those "returned" variables. There is a danger though, that the function may break your program if you make a mistake and try to access a memory address you don't have access to, but if you're careful you should be fine. Following the group project analogy, we'll be covering read-only permissions on those files (pointers to read-only variables) in a later chapter.

Example

#include <stdio.h>

void printResults(void);

int main(void)
{
  int myVar1;
  int myVar2;
  int *myPtr1;
  int *myPtr2;
  int **myPtrPtr;

  printf("myVar1 = 42;\n");
  printf("myVar2 = 64;\n");
  printf("myPtr1 = &myVar1;\n");
  printf("myPtr2 = &myVar2;\n");
  printf("myPtrPtr = &myPtr1;\n");
  myVar1 = 42;
  myVar2 = 64;
  myPtr1 = &myVar1;
  myPtr2 = &myVar2;
  myPtrPtr = &myPtr1;

  printResults();

  printf("myPtr2 = myPtr1;\n");
  myPtr2 = myPtr1; // setting one pointer to another copies the address, not the value at that address

  printResults();

  printf("myPtrPtr = &myPtr2;\n");
  myPtrPtr = &myPtr2;

  printResults();
}

void printResults(void)
{
  printf("\n");
  printf("\tvalue of myVar1:\t\t\t%d\n", myVar1);
  printf("\tvalue of myVar2:\t\t\t%d\n", myVar2);
  printf("\taddress of myVar1:\t\t\t0x%p\n", &myVar1);
  printf("\taddress of myVar2:\t\t\t0x%p\n", &myVar2);
  printf("\tvalue of myPtr1:\t\t\t0x%p\n", myPtr1);
  printf("\tvalue of myPtr2:\t\t\t0x%p\n", myPtr2);
  printf("\tvalue of myPtrPtr:\t\t\t0x%p\n", myPtrPtr);
  printf("\taddress of myPtr1:\t\t\t0x%p\n", &myPtr1);
  printf("\taddress of myPtr2:\t\t\t0x%p\n", &myPtr2);
  printf("\taddress of myPtrPtr:\t\t\t0x%p\n", &myPtrPtr);
  printf("\tvalue of variable myPtr1 points to:\t%d\n", *myPtr1);
  printf("\tvalue of variable myPtr2 points to:\t%d\n", *myPtr2);
  printf("\tvalue of variable myPtrPtr points to:\t0x%p\n", *myPtrPtr);
  printf("\n");
}

When I ran this program, this is the output I got (with memory addresses shortened for readability), and a graphical representation of what happens after each step.

pointers-example


5.5 GAME OF LIFE

A famous example of using arrays is Conway's Game of Life. It was created in the 1970's to study cellular automata (the "life") by having a grid of cells that were either alive or dead, and interacted with each other by following simple rules. It was originally done on grid paper, but with the advent of computers it became much easier and faster to run by storing the grid as an array. The aim of the game is simple: you set up an initial state in a 2D grid, let it run, and watch the results. This could be a completely random state like I have done, or with careful planning, you could have complex simulations of things like infinitely repeating patterns, manufacturing factories that can produce moving "spaceships" (a simple one is also in my code), or even complete computers with text displays.

/*
  Conway's Game of life
*/
// include these libraries
#include <stdio.h> // printf
#include <stdlib.h> // rand, srand, system
#include <time.h> // nanosleep, time, timespec, usleep

// only if we're running this on Windows, include this library. Used in the delay function
#ifdef WIN32
  #include <windows.h> // Sleep
#endif

// set up our parameters
#define WIDTH 50
#define HEIGHT 25
#define FILL_PERCENT 50
#define ALIVE_VAL 254
#define DEAD_VAL 32

// creates a variable called grid, macro used to simplify retyping and reduce mistakes
#define GRID unsigned char grid[HEIGHT][WIDTH]

// function declarations
void birth(GRID, int row, int col);
void death(GRID, int row, int col);
void setupGrid(GRID);
void setupGridGun(GRID);
void clearScreen();
void printGrid(GRID);
void runGeneration(GRID);
void delay(int milliseconds);

// main program
int main(void)
{
  GRID;
  srand(time(NULL)); // seed the random number generator with the current time

  // uncomment ONLY ONE of these lines to change the initial state
  setupGrid(grid);
  //setupGridGun(grid);

  printGrid(grid);
  // forever, get the next generation and display it
  for (;;)
  {
    delay(100);
    runGeneration(grid);
    printGrid(grid);
  }

  // return, even if we never get to it because of the infinite loop above
  return 0;
}

// set the cell to be alive
void birth(GRID, int row, int col)
{
  grid[row][col] = ALIVE_VAL; // filled cell
}

// set the cell to be dead
void death(GRID, int row, int col)
{
  grid[row][col] = DEAD_VAL; // empty cell
}

// set up the grid to be randomly filled up to FILL_PERCENT percent
void setupGrid(GRID)
{
  for (int row = 0; row < HEIGHT; row++)
  {
    for (int col = 0; col < WIDTH; col++)
    {
      if (rand() % 100 < FILL_PERCENT) // random value between 0-99
      {
        birth(grid, row, col);
      }
      else
      {
        death(grid, row, col);
      }
    }
  }
}

// set up the grid as a Gosper Glider Gun
void setupGridGun(GRID)
{
  // set every cell to be dead, mandatory if you're setting specific cells to be alive
  for (int row = 0; row < HEIGHT; row++)
  {
    for (int col = 0; col < WIDTH; col++)
    {
      death(grid, row, col);
    }
  }

  // set these selected cells to be alive to make the desired pattern
  birth(grid, 4, 0);
  birth(grid, 5, 0);
  birth(grid, 4, 1);
  birth(grid, 5, 1);
  birth(grid, 4, 10);
  birth(grid, 5, 10);
  birth(grid, 6, 10);
  birth(grid, 3, 11);
  birth(grid, 7, 11);
  birth(grid, 2, 12);
  birth(grid, 8, 12);
  birth(grid, 2, 13);
  birth(grid, 8, 13);
  birth(grid, 5, 14);
  birth(grid, 3, 15);
  birth(grid, 7, 15);
  birth(grid, 4, 16);
  birth(grid, 5, 16);
  birth(grid, 6, 16);
  birth(grid, 5, 17);
  birth(grid, 2, 20);
  birth(grid, 3, 20);
  birth(grid, 4, 20);
  birth(grid, 2, 21);
  birth(grid, 3, 21);
  birth(grid, 4, 21);
  birth(grid, 1, 22);
  birth(grid, 5, 22);
  birth(grid, 0, 24);
  birth(grid, 1, 24);
  birth(grid, 5, 24);
  birth(grid, 6, 24);
  birth(grid, 2, 34);
  birth(grid, 3, 34);
  birth(grid, 2, 35);
  birth(grid, 3, 35);
}

// run a different screen clearing command based on the OS, don't worry about trying to understand it yet
void clearScreen()
{
    #if defined(__linux__) || defined(__unix__) || defined(__APPLE__)
        system("clear");
    #endif
    #if defined(_WIN32) || defined(_WIN64)
        system("cls");
    #endif
}

// print the grid in a table format
void printGrid(GRID)
{
  clearScreen();
  for (int row = 0; row < HEIGHT; row++)
  {
    for (int col = 0; col < WIDTH; col++)
    {
      printf("%c ", grid[row][col]);
    }
    printf("\n");
  }
  fflush(stdout); // flush the entire output to the screen just in case it still has some buffered
}

// calculates what the next generation would be, and then applies it
void runGeneration(GRID)
{
  // create a temporary new grid for the next generation
  unsigned char newgrid[HEIGHT][WIDTH];

  // go through each cell, checking for neighbours and following the rules
  for (int row = 0; row < HEIGHT; row++)
  {
    for (int col = 0; col < WIDTH; col++)
    {
      // see how many neighbouring cells are alive
      int neighbours = 0;
      for (int y = row - 1; y <= row + 1; y++) // above, same row, below
      {
        for (int x = col - 1; x <= col + 1; x++) // left, same col, right
        {
          if (0 <= y && y < HEIGHT && 0 <= x && x < WIDTH) // cell is inside grid boundary
          {
            if (y == row && x == col) // don't include this cell as its own neighbour
            {
              continue;
            }
            if (grid[y][x] == ALIVE_VAL)
            {
              neighbours++; // add up all the neighbours
            }
          }
        }
      }
      // these are the rules for cells being alive/dead
      // survive if there are 2 or 3 alive neighbours, birth if there are 3 alive neighbours
      if ((grid[row][col] == ALIVE_VAL && neighbours == 2) || neighbours == 3)
      {
        newgrid[row][col] = ALIVE_VAL; // birth or surviving
      }
      // die from loneliness/overcrowding
      else
      {
        newgrid[row][col] = DEAD_VAL; // kill any cell that might be there
      }
    }
  }

  // copy new generation to old grid
  for (int row = 0; row < HEIGHT; row++)
  {
    for (int col = 0; col < WIDTH; col++)
    {
      grid[row][col] = newgrid[row][col];
    }
  }
  // temporary grid disappears here
}

// how to delay program depending on OS, don't worry about trying to understand it yet
void delay(int milliseconds)
{
  #ifdef WIN32 // Windows
    Sleep(milliseconds);
  #elif _POSIX_C_SOURCE >= 199309L // Posix (Mac/Linux) >= 1993
    struct timespec ts;
    ts.tv_sec = milliseconds / 1000;
    ts.tv_nsec = (milliseconds % 1000) * 1000000;
    nanosleep(&ts, NULL);
  #else // older versions of Posix
    usleep(milliseconds * 1000);
  #endif
}

CHAPTER 6: IO

6.0 CHAPTER OVERVIEW

Here we get our programs to input and output data using the 2 most common ways: command line and file. We'll be expanding on the printing we've done up until this point, as well as accepting user input on the command line. We'll then move onto file IO, which as you'll see is helpfully similar to the command line.


6.1 COMMAND LINE

Just as a computer would print out onto paper with a printer and get information back in from paper using a scanner, C uses the printf (short for print formatted) and scanf (short for scan formatted) functions. We'll be taking a look at these in this section, as well as some variants that come in handy sometimes.

Output

As we've been doing up until this point, command line output is done by printing to the screen with the printf function. You've probably noticed the different letters and numbers next to the % when we've been printing, but what do they all mean? This is called the format placeholder, and tells C to put a value in this part of the output, formatted in a certain way. The entire syntax for the placeholder is:

%[parameter][flags][width][.precision][length]type

The fields in [ ] are optional. Here's a quick explanation of each field:

Option Description
[parameter]
**Only works on POSIX, like the Raspberry Pi.**
(number)$ Allows you to reuse the same value multiple times by specifying which number parameter to use
[flags]
- Left align, rather than the default right align
+ Put a + in front of positive numbers and a - in front of negative ones, default is just - for negative. Can't be used with (space) flag
(space) Put a space in front of positive numbers and a - in front of negative ones, default is just - for negative. Can't be used with + flag
0 When [width] is specified, use leading 0's instead of spaces for numbers
#
  • For g and G type
    • Keep trailing 0's
  • For e, E, f, F, g and G type
    • Always have a decimal point
  • For o type
    • Prepend 0
  • For x type
    • Prepend 0x
  • For p and X type
    • Prepend 0X
[width]
(number) Sets the minimum number of characters used to output this value
* Use this parameter as the number for (number) above, and format the next one instead
[.precision]
(number)
  • For a, A, e, E, f, F, g or G type
    • Sets the number of digits after the decimal point
  • For s type
    • Sets the maximum number of characters to output, truncating after that
* Use this parameter as the number for (number) above, and format the next one instead. If * width is also used, the next parameter applies here, and the one after that is formatted instead.
[length]
hh
  • When combined with d or i type
    • Parameter is a char
  • When combined with u, o, x or X type
    • Parameter is an unsigned char
  • When combined with n type
    • Parameter is a char*
h
  • When combined with d or i type
    • Parameter is a short
  • When combined with u, o, x or X type
    • Parameter is an unsigned short
  • When combined with n type
    • Parameter is a short*
l
  • When combined with d or i type
    • Parameter is a long
  • When combined with u, o, x or X type
    • Parameter is an unsigned long
  • When combined with n type
    • Parameter is a long*
ll
  • When combined with d or i type
    • Parameter is a long long
  • When combined with u, o, x or X type
    • Parameter is an unsigned long long
  • When combined with n type
    • Parameter is a long long*
L
  • When combined with a, A, e, E, f, F, g or G type
    • Parameter is a long double
type
a double as hexadecimal with lowercase letters, prepending 0x
A double as hexadecimal with uppercase letters, prepending 0X
c char as text rather than its decimal value
d int as decimal
e double in scientific notation with decimal point and lowercase letters
E double in scientific notation with decimal point and uppercase letters
f double as decimal with decimal point and lowercase letters
F double as decimal with decimal point and uppercase letters
g double as with e or f, whichever is shorter. Also removes insignificant trailing 0's and decimal point if appropriate
G double as with E or F, whichever is shorter. Also removes insignificant trailing 0's and decimal point if appropriate
i int as decimal
n Outputs nothing. Instead, stores the number of characters written so far inside the variable the parameter points to (must be int*)
o unsigned int as octal
p void* (i.e. any pointer) in an implementation specific way (usually outputs address, includes leading 0x on Raspberry Pi)
s String that ends with a null character
u unsigned int as decimal
x unsigned int as hexadecimal with lowercase letters
X unsigned int as hexadecimal with uppercase letters
% Prints a literal % character. No other fields needed.

With printf, it will return the number of characters printed, or a negative number if there was an error - the error can be checked by calling ferror(stdout). There are 2 main streams that usually get printed to in C: stdout (what is used by default) and stderr (what is used by default to print errors). When running your program in a console or from a script, you can actually filter these out to, for example, only show errors on screen and hide normal output. There are also other functions you could use that work in a similar way to printf, with some differences:

  • fprintf - prints parameters to any stream, with formatting parameters
  • perror - can only print a string followed by the last error message to stderr, doesn't take formatting parameters
  • putc - can only print a single character to any stream, doesn't take formatting parameters
  • putchar - can only print a single character to stdout, doesn't take formatting parameters
  • puts - can only print a string to stdout, doesn't take formatting parameters
  • vfprintf - prints a va_list of parameters to any stream, with formatting parameters
  • vprintf - prints a va_list of parameters to stdout, with formatting parameters

Input

Input on the command line is usually done from the stdin stream with the scanf function. Just like printf, it takes a format string containing format placeholders so it knows what to expect and how to read it correctly. The entire syntax for the placeholder is:

%[*][width][length]type

The fields in [ ] are optional. As you can see, the format string is similar to printf above, but with these key differences:

Option Description
[*]
* Read the input but don't store it anywhere, useful for discarding certain parts of input
[width]
(number) Sets the maximum number of characters used to input this value
[length]
hh
  • When combined with d, i or n type
    • Parameter is a char*
  • When combined with u, o or x type
    • Parameter is an unsigned char*
h
  • When combined with d, i or n type
    • Parameter is a short*
  • When combined with u, o or x type
    • Parameter is an unsigned short*
l
  • When combined with d, i or n type
    • Parameter is a long*
  • When combined with u, o or x type
    • Parameter is an unsigned long*
  • When combined with a, e, f or g type
    • Parameter is a double*
ll
  • When combined with d, i or n type
    • Parameter is a long long*
  • When combined with u, o, x or X type
    • Parameter is an unsigned long long*
L
  • When combined with a, e, f or g type
    • Parameter is a long double*
type
a Any number of decimal digits, optionally with a decimal point, optionally with a preceeding + or -, optionally ending in e or E and a decimal integer, turned into a float. If preceded with 0x or 0X, is read as a hexadecimal format float instead.
c A single character. If [width] is specified, reads exactly that many characters into a string but DOES NOT add a null character at the end.
d Any number of decimal digits, optionally with a preceding + or -, turned into an int
e Any number of decimal digits, optionally with a decimal point, optionally with a preceeding + or -, optionally ending in e or E and a decimal integer, turned into a float. If preceded with 0x or 0X, is read as a hexadecimal format float instead.
f Any number of decimal digits, optionally with a decimal point, optionally with a preceeding + or -, optionally ending in e or E and a decimal integer, turned into a float. If preceded with 0x or 0X, is read as a hexadecimal format float instead.
g Any number of decimal digits, optionally with a decimal point, optionally with a preceeding + or -, optionally ending in e or E and a decimal integer, turned into a float. If preceded with 0x or 0X, is read as a hexadecimal format float instead.
i Any number of digits, optionally with a preceding + or -, turned into an int. Defaults to decimal digits, but if the input starts with 0x it is read as a hexadecimal number, or with 0 it is read as an octal number
n Inputs nothing. Instead, stores the number of characters read so far inside the variable the parameter points to.
o Any number of octal digits, optionally with a preceding + or -, turned into an unsigned int
p Sequence of characters representing a pointer in an implementation specific and identical way as printf (usually inputs address, includes leading 0x on Raspberry Pi)
s Any number of non-whitespace characters, stopping when it finds a whitespace character. Adds a null character at the end.
u Any number of decimal digits, optionally with a preceding + or -, turned into an unsigned int
x Any number of hexadecimal digits, optionally with a preceding + or - and optional 0x or 0X, turned into an unsigned int
% Reads a literal % character. No other fields needed.
[characters] Any number of characters specified between the [ ]
[^characters] Any number of characters NOT specified between the [^ ]

There are also some functions other than scanf you could use to get input from the command line:

  • getchar - reads a single character from stdin
  • gets - reads a line from stdin and stores it in a string variable

Command line arguments

When you run a program on the command line, you'll often notice that you can pass some options to it by putting them after the executable name, separated by spaces. These are known as command line arguments, and are a great way to get data into your program from a script without having to put it in a file and then read it. To do this, you just need to change how your main function is defined to make it:

int main(int argc, char *argv[])

The names of the 2 variables you use are up to you, but these are the ones you'll most likely see in other people's code. They stand for:

  • argc - the count of arguments
  • argv - the vector (another name for array) of arguments

What this does is create a string array of arguments of length argc + 1 containing the program name followed by each argument, and finally a NULL. For example, running:

myprogram.exe Hi "Hello world" 42

would create the following values:

  • argc
    • 4
  • argv[0]
    • "myprogram.exe"
  • argv[1]
    • "Hi"
  • argv[2]
    • "Hello world"
  • argv[3]
    • "42"
  • argv[4]
    • NULL

You can then use these variables in your program to change options, get input data, or change how it runs entirely (some programs will change how they use input/output if they think they're being used in a script).

Returning values

If your main function returns an integer, you can use that value as part of a script or input into another program. This may only be simple, but you can use that value to determine if your program has run correctly or to signal that something else needs to happen.


6.2 FILES

Working with files

Before you can read/write files, you need to do a little work. Files are accessed with a FILE* variable, which opens a stream (exactly like the command line streams) and allows us to use the file. Using the stdio.h library, we can see if a file exists, open it, do what we need to do, then close it. So, here's a quick example showing you the basic structure of how to do each of those things:

#include <stdio.h>

int main(void)
{
  char filename[] = "myfile.txt";

  // how to open file for reading and check that it exists
  FILE *myFile = fopen(filename, "r");
  if (myFile == NULL)
  {
    // myfile.txt didn't exist, error and do something
    return 1;
  }

  // how to read
  while (!feof(myFile))
  {
    // read contents of file and do something
  }

  // how to close file
  fclose(myFile);

  // how to open file for writing, overwriting if the file already exists
  myFile = fopen(filename, "w");

  // how to write
  fprintf(myFile, "Hello world\n");
  fflush(myFile); // make sure the buffer is fully emptied into the file

  // how to close file
  fclose(myFile);

  return 0;
}

The 2nd parameter to fopen is the mode, and it tells the program how to open the file. The options available are:

  • "r" - read only, file must already exist
  • "w" - write only, creates file if it doesn't exist or overwrites it if it does
  • "a" - append only, creates file if it doesn't exist
  • "r+" - read and write, file must already exist
  • "w+" - read and write, creates file if it doesn't exist or overwrites it if it does
  • "a+" - read and append, creates file if it doesn't exist

There are some more complicated things you can do with files, but these are the basic and most common you'll find. I encourage you to explore more, and ask in the forums if you're unsure where I can answer any questions you might have.

Output

To output to a file (assuming it's opened for writing first), it's basically the same as with command line output. As a stream is created when you open the file, you can use the same functions as the last section that let you choose which stream to output to:

  • fprintf - prints parameters to any stream, with formatting parameters
  • putc - can only print a single character to any stream, doesn't take formatting parameters
  • vfprintf - prints a va_list of parameters to any stream, with formatting parameters

But, because your file streams aren't unidirectional like the command line, you can actually move the file position (think of it like a virtual "cursor") around the file to output in different places. To do this, you have a few functions you can use:

  • fgetpos - get the current position and store it in a variable
  • fseek - allows you to move to an offset from the beginning, end or current position
  • fsetpos - move to the position stored in a variable that was set by fgetpos
  • ftell - get the current position
  • rewind - move to the beginning of the file

If you use these functions and then output to the file, you need to be aware that they WILL OVERWRITE anything already in the file, they don't insert in between.

Input

To get input from a file, just like with output it's really similar to using the command line (again, assuming it's opened for reading first), you just need some specific functions that let you pick which stream to read from:

  • feof - checks for end of file
  • fread - reads a set amount of data from a stream and stores it in an array
  • fscanf - reads formatted input from a stream using the same format parameters as with command line
  • fgetc - reads a single character from a stream
  • fgets - reads a line from a stream and stores it in a string variable
  • ungetc - puts a character back onto a stream, so that it's the next one read

6.3 SIMPLE CALCULATOR

As an example of how to use these different methods, we'll be creating a simple calculator program. It will take 2 numbers and an operation, and give you a formatted answer. It will also store the history in a file for later use.

#include <math.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{ char filename[] = "history.txt"; double a; double b; char op; char str[50]; // optionally change history filename if (argc > 1) { strcpy(filename, argv[1]); } // open file for reading and appending FILE *history = fopen(filename, "a+"); // get file size and print history if there is any fseek(history, 0, SEEK_END); if (ftell(history) > 0) { rewind(history); // print previous history while (!feof(history)) { if (fgets(str, 50, history) != NULL) { printf("%s\n", str); } } } // infinitely ask for input until 'q' is encountered for(;;) { printf("Enter calculation (or q to quit): "); // skip the 'enter' character from the last input if it exists, and get the first valid character do { op = (char) getchar(); } while(op == '\n'); if (op == 'q') { break; // exit loop } else { ungetc(op, stdin); // put non-q character back on the stream to be read next } scanf("%lf %c %lf", &a, &op, &b); switch (op) { case '+': printf("= %g\n", a + b); fprintf(history, "%g %c %g\n", a, op, b); fprintf(history, "= %g\n", a + b); break; case '-': printf("= %g\n", a - b); fprintf(history, "%g %c %g\n", a, op, b); fprintf(history, "= %g\n", a - b); break; case '*': printf("= %g\n", a * b); fprintf(history, "%g %c %g\n", a, op, b); fprintf(history, "= %g\n", a * b); break; case '/': printf("= %g\n", a / b); fprintf(history, "%g %c %g\n", a, op, b); fprintf(history, "= %g\n", a / b); break; case '^': printf("= %g\n", pow(a, b)); fprintf(history, "%g %c %g\n", a, op, b); fprintf(history, "= %g\n", pow(a, b)); break; default: printf("Unsupported operation\n"); fseek(stdin, 0, SEEK_END); // clear all input break; } } // close history file fclose(history); return 0; }

CHAPTER 7: MORE ADVANCED CONCEPTS

7.0 CHAPTER OVERVIEW

In this final chapter, we'll cover some things you can do with C that are much more advanced than what we have covered so far. None of these are strictly necessary to create simple, working C programs, but they can definitely make it easier once you understand them (and may soon become necessary if your programs get complex). I've tried to explain these as simply as possible, but if you're having trouble with anything in this section or anything else in this workshop, head over to the forum and shoot me a question where I'll be able to work through it with you.


7.1 MULTIPLE CODE FILES AND THE COMPILER

Multiple code files

So far, we've been playing with relatively short programs - even the Game of Life in Chapter 5 had just over 230 lines of code, including comments and blank lines. But if you were working on something like the Large Hadron Collider (which has 50 million lines of code!) or even just the Space Shuttle (a tiny 400,000 lines), having all that code in one file would create a huge mess. A file that large would make most text editors crumble (I tried opening 1 million lines of just "Hello world!" in Atom, and it nearly died), and it would be ungainly and hard to navigate. To solve this, we can use multiple files to store our code.

While there are a few ways you could achieve this, the easiest way is to break up your code into a series of header (.h) and code (.c) files. A header file is included in your code files, and contains what you would generally put at the top of your file: other #include's, #define's, new types, global variables, function declarations, etc. It may also include something called a macro-guard (optional but HIGHLY recommended), which is a simple construct that stops the compiler including your file more than once and causing multiple declaration errors, by using #ifndef, #define and #endif (fully explained further on in this chapter). As a simple example, we have this code:

#include <stdio.h>

int myVariable;

int myFunc(void);

int main(void)
{
  printf("%d\n", myFunc());
  return 0;
}

int myFunc(void)
{
  myVariable = 42;
  return myVariable;
}

We could split this up into 3 files: a main.c that contains the main function; an extra_stuff.h which contains the include, variable and function declaration; and an extra_stuff.c which contains the definition of myFunc. Using this process, we can expand this to as many functions and files as we want (this is exactly how libraries like stdio get created, they give you a .h file to include and a pre-compiled copy of the code to save compilation time). We would therefore end up with:

main.c:
  #include "extra_stuff.h"

  int main(void)
  {
    printf("Hello world\n");
    printf("%d\n", myFunc());
    return 0;
  }

extra_stuff.h:
  #ifndef EXTRA_STUFF_H
  #define EXTRA_STUFF_H

  #include <stdio.h>

  int myVariable; // make sure to only declare and not define variables in header files

  int myFunc(void);

  #endif

extra_stuff.c:
  #include "extra_stuff.h"

  int myFunc(void)
  {
    myVariable = 42;
    return myVariable;
  }

You'll notice that when we include our own files, we use " " instead of < > for the .h file. This is because < > is used for header files in a special list of folders called the include path (usually just standard headers), and " " is for header files located relative to the .c file (such as in the same folder). Using this process, you can make your programs more modular, and even create your own libraries if you want.

Running the compiler yourself

As Atom is just a simple text editor, we had to install a package to be able to build and run our C programs. Unfortunately, this package doesn't support multiple code files, so we'll have to run the compiler ourselves (Geany on the Raspberry Pi is not affected by this, but if you just want to run the compiler yourself, the instructions are exactly the same). To do this, open a Command Prompt/Terminal/PowerShell window (whichever applies to your platform), navigate to your project folder with the cd command, and type the following line (replace anything within [ ] with your respective values):

gcc -Wall -o [executable_name] [c_files]

I'll just quickly explain what each part does:

  • gcc - the name of the compiler executable (GNU C Compiler)
  • -Wall - show all warnings
  • -o [executable_name] - create an executable with this name (include .exe in the filename on Windows)
  • [c_files] - every .c file you need for this program, separated by spaces

Don't put any .h files in this command, they will automatically get pulled in when the relevant .c files are compiled. This will create the executable you specified, which you can then run from the same terminal window.

Using makefiles

The method above is fine when you've only got a few .c files like we have, but someone building the standard C libraries or a large project for instance wouldn't use the compiler like this because they might forget a file, or just because it'd be too tedious. Instead, they would use something called a makefile, which is basically a small script that can call the compiler for you, knows all the files you might need, and only compiles the ones that have changed since the last compilation. A makefile (on Windows) for our 3 file example above would look like this:

# default rule when nothing specified
all: myapp.exe

# "clean" rule
clean:
del myapp.exe main.o extra_stuff.o

# "rebuild" rule
rebuild: clean all

myapp.exe: main.o extra_stuff.o
gcc -Wall -o myapp.exe main.o extra_stuff.o

main.o: code1.c
gcc -Wall -c main.c

extra_stuff.o: code2.c
gcc -Wall -c extra_stuff.c

You could then open the terminal window as before, but instead of typing gcc..., you just type make (mingw32_make if you have MinGW installed). It will run the script and build the executable for you. The general form of each block (also called a rule) in the script is:

target: prerequisites
command

If a prerequisite is the name of a file that exists, it will check the date on that file and decide whether or not to recompile it. You can call a specific rule from the command line by putting the target name after make, for instance make clean would call the "clean" rule, deleting all the compiled files. To add another .c file to be compiled, you need to add the .o to the clean and myapp.exe rules, and create a new block at the bottom to compile the .c into the .o. If you don't want to do all this, you can use a more complex makefile that will be slightly more generic. Here's one I made (not just for Windows this time):

EXECUTABLE = myapp

OBJECTS = \
	main.o \
	extra_stuff.o

# Don't need to change anything below here

# use OS specific commands and set output filename
ifeq ($(OS),Windows_NT)
	LINK_TARGET = $(EXECUTABLE).exe
	CLEAN_CMD = @del
	ECHO_CMD = @echo
else
	LINK_TARGET = $(EXECUTABLE)
	CLEAN_CMD = rm -f
	ECHO_CMD = echo
endif

# a list of every compiled file
REBUILDABLES = $(OBJECTS) $(LINK_TARGET)

# default rule when nothing specified
all: $(LINK_TARGET)
	$(ECHO_CMD) All done

# "clean" rule
clean:
	$(ECHO_CMD) Removing $(REBUILDABLES)
	$(CLEAN_CMD) $(REBUILDABLES)
	$(ECHO_CMD) Clean done

# "rebuild" rule
rebuild: clean all

# create final executable from the .o files
$(LINK_TARGET): $(OBJECTS)
	$(ECHO_CMD) Linking $(OBJECTS) into $(LINK_TARGET)
	gcc -Wall -o $@ $^

# create every .o file from its respective .c file
%.o: %.c
	$(ECHO_CMD) Compiling $< into $@
	gcc -Wall -o $@ -c $<

This uses variables, if-else statements, and pattern matching to do a lot of the work for you (see if you can understand how it works, you might be surprised how much like C it actually is). All you need to do is change the EXECUTABLE name and add your .o names to the OBJECTS section, and the makefile will take care of the rest. The \'s in the OBJECTS section just allows us to continue a command over several lines, otherwise we would have to put all the .o files on the same line.


7.2 CONSTANTS

In section 1.4 I mentioned that we would be covering read-only pointers and variables. Well, here we are. C allows you to declare a variable as having a constant value by using the const qualifier, and where you put that qualifier and on which type (normal or pointer) changes the behaviour of the variable. It can sometimes be difficult to remember where to put the const to get the desired effect, but I've laid out the different options. Here is how const works with normal and pointer variables:

Normal

  1. int myInt
    1. CAN change value
  2. const int myConstInt1
    1. can't change value
  3. int const myConstInt2
    1. can't change value

Pointers

  1. int *myPtr
    1. CAN change value
    2. CAN change pointed-to value
  2. const int *myConstPtr1
    1. CAN change value
    2. can't change pointed-to value
  3. int const *myConstPtr2
    1. CAN change value
    2. can't change pointed-to value
  4. int * const myConstPtr3
    1. can't change value
    2. CAN change pointed-to value
  5. const int * const myConstPtr5
    1. can't change value
    2. can't change pointed-to value
  6. int const * const myConstPtr6
    1. can't change value
    2. can't change pointed-to value

Essentially what this boils down to is: const next to the type (int in this case) will create a variable with a read-only value (or a pointer that points to a read-only value), and const after the * will create a pointer that can't be changed to point at another variable. Combining these creates a pointer to a read-only value that can't be changed to point to another variable. Having 2 const qualifiers that do the same job (such as const int const) may cause a warning in some compilers as it's redundant. Using consts will enable your programs to have a level of protection from coding mistakes or malicious use of your libraries.


7.3 MEMORY MANAGEMENT

Up until now, we've been using memory to store variables without actually caring where they go. The compiler deals with this for us, and lets us get on with creating the actual program. But this has a few limitations. The compiler needs to know how much memory to allocate when you create your program, so you can't make arrays with a variable size at runtime for instance, and there are also some size limits. But there's a way around this.

When we create variables normally, they go to a part of memory called "the stack". If you think of your program like a pile of sticky notes, it starts with the "main" note where you write your variables for the main function. When you call another function, you get another note and put it on top. When that function ends, you throw its note away. But you've only got a small amount of space to write on, and you have a limited number of notes to use. Your program works the same way, using "stack frames" (the notes) which contain every variable you use and where to go back to once the function returns, and a new stack frame is created and put on top of the stack every time you call a function (this is how it keeps variables of the same name in different functions separate). It also has a limited amount of space you can use: as an example, the Raspberry Pi stack is limited to 8192 kB by default, which sounds like a lot when an int is only 4 bytes, but it can quickly fill up with even medium complexity programs. When the function returns, the stack frame is deleted.

Following on from the analogy, you're sitting at your desk writing on notes, but there's a giant whiteboard behind you. So if you've got something bigger to write, you can get up and write it on there. When you've thrown out the "main" note, you can still have stuff written on the board, so it's your responsibility to make sure it's clean beforehand. This whiteboard would be called "the heap" in C, a much larger space (really only restricted by the amount of memory you have available, less on <64 bit machines) that you can dynamically allocate at run-time and that you (the programmer) need to manage. When you want a block of memory and you don't know how big it needs to be (like an array of indeterminate size), you just don't want to fill up your stack space, or you need a variable to persist after the function ends, you can allocate memory on the heap. You need to make sure you free up that block of memory when you're done (wiping it off the board), otherwise it will be unusable by anything else until you restart the program, or in some cases the whole device. This is known as a memory leak (Firefox had a famous one years ago that required you to restart it every so often when it was getting sluggish), and can sometimes be seen when programs have other types of bugs, as a memory leak can indicate a whole section of code isn't being run when it should be.

So how do you access this vast trove of memory? Using functions in the stdlib.h library, you can create pointers that point to sections of the heap. The functions are:

  • malloc - takes a number of bytes and allocates that much memory on the heap, returning a pointer to it
  • calloc - takes a number of elements and the size of each element, allocates that much memory on the heap, and sets it all to 0
  • realloc - takes a pointer to already allocated heap memory and a new size, and changes how much is allocated to that size
  • free - takes a pointer to already allocated heap memory, and deallocates it

The 3 allocation functions all return a void* type (i.e. a pointer to any data type), and C will convert it automatically to whatever your pointer type is. For instance, creating an array of n ints could be done as either:

int *array = malloc(n * sizeof(int));
int *array = calloc(n, sizeof(int));

Remembering that calloc sets the whole block to 0, malloc does not. When you're done with the memory, don't forget to deallocate it with free:

free(array);

A quick note about static

We have previously talked static variables, that can also persist between function calls. This is not the same as allocating space on the heap though. Static variables are allocated in another section of memory (the "data" or "bss" segment, depending on whether you initialised it or not) that also contains global variables and string literals (hard coded strings between " "). The size of this section is defined at compile time, so can't be dynamically allocated at run-time like the heap. If you open an executable in a text/hex editor, you should be able to see the ".data" and ".bss" headers near the top, among others. If you want to know more (WAY beyond the scope of this workshop), you can read up on Windows .exe's or just in general.


7.4 RECURSION

When you have a big problem to solve, it's sometimes easiest to break that problem down into smaller chunks and solve them. It's even better if those small chunks can be further broken down into even smaller ones. This is the basic principle behind recursion: breaking a problem down into pieces that themselves resemble a smaller version of the entire problem, repeating this process to get as small as possible, and then solve those simpler problems. You can think of it like fractals in nature (snowflakes, trees, etc.): when you zoom in on a small section, you'll see that it's also made up of a similar structure to the whole, and you can keep doing this.

There's a famous puzzle called the Tower of Hanoi. It involves some number of discs and 3 rods, where you have to move the stack of discs from 1 rod to another whilst following some rules:

  1. You can only move 1 disc at a time
  2. You can only move the top disc of a stack
  3. You can't move bigger discs onto smaller ones

This can be solved using procedural techniques you'll already be familiar with (loops, decision statements):

void solveTowersProcedural(char towers[3][NUM_DISCS], int rods[])
{
  int movesNeeded = pow(2, NUM_DISCS) - 1;
  for (int i = 0; i < movesNeeded; i++)
  {
    if (NUM_DISCS % 2 == 0)
    {
      if (i % 3 == 0)
      {
        // make the legal move between pegs A and B (in either direction)
        if (getTopDisc(towers, rods[0]) < getTopDisc(towers, rods[1]))
        {
          printf("Moving disc %d from %c to %c\n", getTopDisc(towers, rods[0]), rods[0] + 'A', rods[1] + 'A');
          moveDisc(towers, rods[0], rods[1]);
        }
        else
        {
          printf("Moving disc %d from %c to %c\n", getTopDisc(towers, rods[1]), rods[1] + 'A', rods[0] + 'A');
          moveDisc(towers, rods[1], rods[0]);
        }
      }
      else if (i % 3 == 1)
      {
        // make the legal move between pegs A and C (in either direction)
        if (getTopDisc(towers, rods[0]) < getTopDisc(towers, rods[2]))
        {
          printf("Moving disc %d from %c to %c\n", getTopDisc(towers, rods[0]), rods[0] + 'A', rods[2] + 'A');
          moveDisc(towers, rods[0], rods[2]);
        }
        else
        {
          printf("Moving disc %d from %c to %c\n", getTopDisc(towers, rods[2]), rods[2] + 'A', rods[0] + 'A');
          moveDisc(towers, rods[2], rods[0]);
        }
      }
      else
      {
        // make the legal move between pegs B and C (in either direction)
        if (getTopDisc(towers, rods[1]) < getTopDisc(towers, rods[2]))
        {
          printf("Moving disc %d from %c to %c\n", getTopDisc(towers, rods[1]), rods[1] + 'A', rods[2] + 'A');
          moveDisc(towers, rods[1], rods[2]);
        }
        else
        {
          printf("Moving disc %d from %c to %c\n", getTopDisc(towers, rods[2]), rods[2] + 'A', rods[1] + 'A');
          moveDisc(towers, rods[2], rods[1]);
        }
      }
    }
    else
    {
      if (i % 3 == 0)
      {
        // make the legal move between pegs A and C (in either direction)
        if (getTopDisc(towers, rods[0]) < getTopDisc(towers, rods[2]))
        {
          printf("Moving disc %d from %c to %c\n", getTopDisc(towers, rods[0]), rods[0] + 'A', rods[2] + 'A');
          moveDisc(towers, rods[0], rods[2]);
        }
        else
        {
          printf("Moving disc %d from %c to %c\n", getTopDisc(towers, rods[2]), rods[2] + 'A', rods[0] + 'A');
          moveDisc(towers, rods[2], rods[0]);
        }
      }
      else if (i % 3 == 1)
      {
        // make the legal move between pegs A and B (in either direction)
        if (getTopDisc(towers, rods[0]) < getTopDisc(towers, rods[1]))
        {
          printf("Moving disc %d from %c to %c\n", getTopDisc(towers, rods[0]), rods[0] + 'A', rods[1] + 'A');
          moveDisc(towers, rods[0], rods[1]);
        }
        else
        {
          printf("Moving disc %d from %c to %c\n", getTopDisc(towers, rods[1]), rods[1] + 'A', rods[0] + 'A');
          moveDisc(towers, rods[1], rods[0]);
        }
      }
      else
      {
        // make the legal move between pegs B and C (in either direction)
        if (getTopDisc(towers, rods[1]) < getTopDisc(towers, rods[2]))
        {
          printf("Moving disc %d from %c to %c\n", getTopDisc(towers, rods[1]), rods[1] + 'A', rods[2] + 'A');
          moveDisc(towers, rods[1], rods[2]);
        }
        else
        {
          printf("Moving disc %d from %c to %c\n", getTopDisc(towers, rods[2]), rods[2] + 'A', rods[1] + 'A');
          moveDisc(towers, rods[2], rods[1]);
        }
      }
    }
    printTowers(towers);
  }
}

But if you look carefully, solving the problem for 4 discs involves solving for 3 discs, solving the 3 disc problem involves solving for 2 discs, and solving the 2 disc problem involves solving for 1 disc. As each of these are just breaking down the big problem into smaller sub-problems, we can use the same function for each step. So we can generalise our function, making it run itself to solve a smaller sub-problem before then finishing up. Each of those sub-problems are broken up further, and so on. We would then end up with something like:

void solveTowersRecursive(char towers[3][NUM_DISCS], int numDiscs, int rods[])
{
if (numDiscs == 1)
{
printf("Moving disc %d from %c to %c\n", numDiscs, rods[0] + 'A', rods[2] + 'A');
moveDisc(towers, rods[0], rods[2]);
printTowers(towers);
return;
}
solveTowersRecursive(towers, numDiscs - 1, (int[]) {rods[0], rods[2], rods[1]});
printf("Moving disc %d from %c to %c\n", numDiscs, rods[0] + 'A', rods[2] + 'A');
moveDisc(towers, rods[0], rods[2]);
printTowers(towers);
solveTowersRecursive(towers, numDiscs - 1, (int[]) {rods[1], rods[0], rods[2]});
}

This code is both shorter and slightly more efficient (41 vs 49 statements run for a 4 disc problem), and just requires you to understand how to break down the problem. If you want to run both examples yourself, I've written a whole program that you can use, split up into a number of files:

hanoi.c:
  #include "hanoi.h"

  // set up the array and put a full stack of discs on selected rod
  void buildTowers(char towers[3][NUM_DISCS], int rod)
  {
    for (int i = 0; i < 3; i++)
    {
      for (int j = 0; j < NUM_DISCS; j++)
      {
        if (i == rod)
        {
          towers[i][j] = j + 1;
        }
        else
        {
          towers[i][j] = 0;
        }
      }
    }
  }

  // move disc from one tower to another
  void moveDisc(char towers[3][NUM_DISCS], int from, int to)
  {
    int fromPos;
    int toPos;

    // find from pos
    for (int i = 0; i < NUM_DISCS; i++)
    {
      if (towers[from][i] != 0)
      {
        fromPos = i;
        break;
      }
    }

    // find to pos
    for (int i = NUM_DISCS - 1; i >= 0; i--)
    {
      if (towers[to][i] == 0)
      {
        toPos = i;
        break;
      }
    }

    // enforces rule that you can't put bigger discs on top of smaller ones, stops program if false
    assert (getTopDisc(towers, from) < getTopDisc(towers, to));

    // do the move
    towers[to][toPos] = towers[from][fromPos];
    towers[from][fromPos] = 0;
  }

  // gets the value of the top disc of a selected rod
  int getTopDisc(char towers[3][NUM_DISCS], int rod)
  {
    for (int i = 0; i < NUM_DISCS; i++)
    {
      if (towers[rod][i] != 0)
      {
        return towers[rod][i];
      }
    }
    return NUM_DISCS + 1;
  }

  // print out the towers
  void printTowers(char towers[3][NUM_DISCS])
  {
    for (int i = 0; i < NUM_DISCS; i++)
    {
      char *pos1 = getDiscSize(towers[0][i]);
      char *pos2 = getDiscSize(towers[1][i]);
      char *pos3 = getDiscSize(towers[2][i]);
      printf("%s  %s  %s\n", pos1, pos2, pos3);
      // make sure you free the memory allocated by getDiscSize
      free(pos1);
      free(pos2);
      free(pos3);
    }

    // print bottom row with labels, spaced to suit the number of discs
    for (int i = 0; i < NUM_DISCS + 1; i++)
    {
      printf("-");
    }
    printf("A");
    for (int i = 0; i < NUM_DISCS * 2 + 4; i++)
    {
      printf("-");
    }
    printf("B");
    for (int i = 0; i < NUM_DISCS * 2 + 4; i++)
    {
      printf("-");
    }
    printf("C");
    for (int i = 0; i < NUM_DISCS + 1; i++)
    {
      printf("-");
    }
    printf("\n\n");
  }

  // create a sized disc for printing
  char *getDiscSize(char disc)
  {
    char *str = calloc((NUM_DISCS * 3 + 1), sizeof(char));
    if (disc == 0)
    {
      for (int i = 0; i < NUM_DISCS + 1; i++)
      {
        strcat(str, " ");
      }
      strcat(str, "|");
      for (int i = 0; i < NUM_DISCS + 1; i++)
      {
        strcat(str, " ");
      }
    }
    else
    {
      for (int i = 0; i < NUM_DISCS - disc; i++)
      {
        strcat(str, " ");
      }
      strcat(str, "|");
      for (int i = 0; i < disc * 2 + 1; i++)
      {
        strcat(str, "=");
      }
      strcat(str, "|");
      for (int i = 0; i < NUM_DISCS - disc; i++)
      {
        strcat(str, " ");
      }
    }
    return str;
  }

hanoi.h:
  #ifndef HANOI_H
  #define HANOI_H

  #include <assert.h> // assert
  #include <math.h> // pow
  #include <stdio.h> // printf
  #include <stdlib.h> // calloc, free
  #include <string.h> // strcat

  #define NUM_DISCS 4

  void buildTowers(char towers[3][NUM_DISCS], int rod);
  void moveDisc(char towers[3][NUM_DISCS], int from, int to);
  int getTopDisc(char towers[3][NUM_DISCS], int rod);
  void printTowers(char towers[3][NUM_DISCS]);
  char *getDiscSize(char disc);

  void solveTowersRecursive(char towers[3][NUM_DISCS], int numDiscs, int rods[]);
  void solveTowersProcedural(char towers[3][NUM_DISCS], int rods[]);

  #endif

main.c:
  #include "hanoi.h"

  int main(void)
  {
    char towers[3][NUM_DISCS]; // 3 towers with maximum NUM_DISCS discs

    // procedural method
    printf("Doing procedural method:\n");
    buildTowers(towers, 0);
    printf("Starting towers:\n");
    printTowers(towers);
    solveTowersProcedural(towers, (int[]) {0, 1, 2});
    printf("Solved in %g moves.\n", pow(2, NUM_DISCS) - 1);

    // recursive method
    printf("Doing recursive method:\n");
    buildTowers(towers, 0);
    printf("Starting towers:\n");
    printTowers(towers);
    solveTowersRecursive(towers, NUM_DISCS, (int[]) {0, 1, 2});
    printf("Solved in %g moves.\n", pow(2, NUM_DISCS) - 1);
    return 0;
  }

makefile:
  EXECUTABLE = hanoi

  OBJECTS = \
  	main.o \
  	hanoi.o \
  	recursive.o \
  	procedural.o

  # Don't need to change anything below here except dependencies at the end

  ifeq ($(OS),Windows_NT)
  	LINK_TARGET = $(EXECUTABLE).exe
  	CLEAN_CMD = @del
  	ECHO_CMD = @echo
  else
  	LINK_TARGET = $(EXECUTABLE)
  	CLEAN_CMD = rm -f
  	ECHO_CMD = echo
  endif

  REBUILDABLES = $(OBJECTS) $(LINK_TARGET)

  all: $(LINK_TARGET)
  	$(ECHO_CMD) All done

  clean:
  	$(ECHO_CMD) Removing $(REBUILDABLES)
  	$(CLEAN_CMD) $(REBUILDABLES)
  	$(ECHO_CMD) Clean done

  rebuild: clean all

  $(LINK_TARGET): $(OBJECTS)
  	$(ECHO_CMD) Linking $(OBJECTS) into $(LINK_TARGET)
  	gcc -Wall -o $@ $^

  %.o: %.c
  	$(ECHO_CMD) Compiling $< into $@
  	gcc -Wall -o $@ -c $<

procedural.c:
  #include "hanoi.h"

  // procedural function to solve puzzle
  void solveTowersProcedural(char towers[3][NUM_DISCS], int rods[])
  {
    int movesNeeded = pow(2, NUM_DISCS) - 1;
    for (int i = 0; i < movesNeeded; i++)
    {
      if (NUM_DISCS % 2 == 0)
      {
        if (i % 3 == 0)
        {
          // make the legal move between pegs A and B (in either direction)
          if (getTopDisc(towers, rods[0]) < getTopDisc(towers, rods[1]))
          {
            printf("Moving disc %d from %c to %c\n", getTopDisc(towers, rods[0]), rods[0] + 'A', rods[1] + 'A');
            moveDisc(towers, rods[0], rods[1]);
          }
          else
          {
            printf("Moving disc %d from %c to %c\n", getTopDisc(towers, rods[1]), rods[1] + 'A', rods[0] + 'A');
            moveDisc(towers, rods[1], rods[0]);
          }
        }
        else if (i % 3 == 1)
        {
          // make the legal move between pegs A and C (in either direction)
          if (getTopDisc(towers, rods[0]) < getTopDisc(towers, rods[2]))
          {
            printf("Moving disc %d from %c to %c\n", getTopDisc(towers, rods[0]), rods[0] + 'A', rods[2] + 'A');
            moveDisc(towers, rods[0], rods[2]);
          }
          else
          {
            printf("Moving disc %d from %c to %c\n", getTopDisc(towers, rods[2]), rods[2] + 'A', rods[0] + 'A');
            moveDisc(towers, rods[2], rods[0]);
          }
        }
        else
        {
          // make the legal move between pegs B and C (in either direction)
          if (getTopDisc(towers, rods[1]) < getTopDisc(towers, rods[2]))
          {
            printf("Moving disc %d from %c to %c\n", getTopDisc(towers, rods[1]), rods[1] + 'A', rods[2] + 'A');
            moveDisc(towers, rods[1], rods[2]);
          }
          else
          {
            printf("Moving disc %d from %c to %c\n", getTopDisc(towers, rods[2]), rods[2] + 'A', rods[1] + 'A');
            moveDisc(towers, rods[2], rods[1]);
          }
        }
      }
      else
      {
        if (i % 3 == 0)
        {
          // make the legal move between pegs A and C (in either direction)
          if (getTopDisc(towers, rods[0]) < getTopDisc(towers, rods[2]))
          {
            printf("Moving disc %d from %c to %c\n", getTopDisc(towers, rods[0]), rods[0] + 'A', rods[2] + 'A');
            moveDisc(towers, rods[0], rods[2]);
          }
          else
          {
            printf("Moving disc %d from %c to %c\n", getTopDisc(towers, rods[2]), rods[2] + 'A', rods[0] + 'A');
            moveDisc(towers, rods[2], rods[0]);
          }
        }
        else if (i % 3 == 1)
        {
          // make the legal move between pegs A and B (in either direction)
          if (getTopDisc(towers, rods[0]) < getTopDisc(towers, rods[1]))
          {
            printf("Moving disc %d from %c to %c\n", getTopDisc(towers, rods[0]), rods[0] + 'A', rods[1] + 'A');
            moveDisc(towers, rods[0], rods[1]);
          }
          else
          {
            printf("Moving disc %d from %c to %c\n", getTopDisc(towers, rods[1]), rods[1] + 'A', rods[0] + 'A');
            moveDisc(towers, rods[1], rods[0]);
          }
        }
        else
        {
          // make the legal move between pegs B and C (in either direction)
          if (getTopDisc(towers, rods[1]) < getTopDisc(towers, rods[2]))
          {
            printf("Moving disc %d from %c to %c\n", getTopDisc(towers, rods[1]), rods[1] + 'A', rods[2] + 'A');
            moveDisc(towers, rods[1], rods[2]);
          }
          else
          {
            printf("Moving disc %d from %c to %c\n", getTopDisc(towers, rods[2]), rods[2] + 'A', rods[1] + 'A');
            moveDisc(towers, rods[2], rods[1]);
          }
        }
      }
      printTowers(towers);
    }
  }

recursive.c:
  #include "hanoi.h"

  // recursive function to solve puzzle
  void solveTowersRecursive(char towers[3][NUM_DISCS], int numDiscs, int rods[])
  {
    if (numDiscs == 1)
    {
      printf("Moving disc %d from %c to %c\n", numDiscs, rods[0] + 'A', rods[2] + 'A');
      moveDisc(towers, rods[0], rods[2]);
      printTowers(towers);
      return;
    }
    solveTowersRecursive(towers, numDiscs - 1, (int[]) {rods[0], rods[2], rods[1]});
    printf("Moving disc %d from %c to %c\n", numDiscs, rods[0] + 'A', rods[2] + 'A');
    moveDisc(towers, rods[0], rods[2]);
    printTowers(towers);
    solveTowersRecursive(towers, numDiscs - 1, (int[]) {rods[1], rods[0], rods[2]});
  }

7.5 THE PREPROCESSOR

When our programs contain a line that starts with a # (like #include, #define, the macro-guards in headers), these are actually read at compile time by the first stage of the compilation process: the C Preprocessor (CPP). These special statements (called directives) tell the CPP what needs to happen before it can pass over to the compiler. These directives are:

  • #include - Copy and paste the contents of a file to where the #include is
  • #define - Create a new macro. Any part of your code that includes the macro is simply replaced with the value, like a find/replace all. It an be used for a constant value, or complete functions.
  • #undef - Delete the macro
  • #ifdef - If macro is defined
  • #ifndef - If macro is not defined
  • #if - If block, just like in C
  • #elif - Else-if, just like in C
  • #else - Else, just like in C
  • #endif - The end of an #if block
  • #error - Print an error message to stderr
  • #pragma - Special, compiler specific instructions. A common example is #pragma once, which is the same on some platforms as a standard macro-guard for header files

There are some macros predefined by the C language and most compilers that you can use, and generally include things like the version of C, file and line numbers, and which platform the compiler is running on. I've used a few simple directives in my previous examples throughout this course, but there is so much more you can do with them. By using the #if, #ifdef, or #ifndef directives, you can only include certain libraries if the platform and compiler support them, or create constants that would be different on different platforms (folder path separators being different for Posix/Windows ("/" vs. "\\") for example).

CPP functions

Something else these directives enable is the creation of macro functions. These can execute faster than normal functions, as they don't need to do the whole function call process, but they can make your code length longer (critical if you have a very limited amount of code space like a microcontroller) and are much less safe to run as there is no checking done on them. But if you accept these risks, you can squeeze a bit more performance out of your code. A few simple examples would be:

#define square(x) ((x) * (x))
#define degToRad(x) ((x) / 57.29578)
#define max(a, b) ((a) > (b) ? (a) : (b))
#define min(a, b) ((a) < (b) ? (a) : (b))
#define log(a, b) printf(#a ": " #b "\n")
#define printVar(i) printf("var" #i " = %d", var##i)

This would work for simple numbers or expressions, but there could be some unwanted side-effects. For instance, if you had:

int i = 2;
printf("%d\n", square(i++));

The result would be 6 and i would be 4, not the expected result of 4 with i becoming 3. This is because, as with all #defines, the CPP just replaces your usage of the macro with the defined values (and then replacing variable names for functions, x with i++ in this case). So what you end up with is:

int i = 2;
printf("%d\n", ((i++) * (i++)));

So you end up incrementing i twice. Just be aware of issues such as this when using these functions, and you should be ok. You'll also notice that the last 2 functions above have #'s in the value part. Using 1 of these like in the log function, will turn literally whatever you put for that argument into a string, exactly as you type it in the code (which means there are no types), and then substitutes it. So if you wrote:

log("logging", return 0;);

you would get the output of "logging": return 0;. This example specifically could be useful if you're trying to work through your code to find a bug, you can just copy and paste a whole line of code into a log function without having to worry about it being executed, and you could print it to the screen. The final example above is a bit more complex. Using the 2 #'s as in var##i performs concatenation (adding on the end), and in this case is actually turning that into a variable name. To show what this can do, look at this example:

int main(void)
{
int var1 = 10;
int var2 = 20;
int var3 = 30;
printVar(1);
return 0;
}

This will print var1 = 10, as var combined with the 1 (turned into ##1) creates var##1, and var concatenated with 1 creates var1. This can be very powerful if used correctly (an example in the docs is filling a stuct of pointers to named functions by using just the command name). Here, we've only scratched the surface of what the CPP can do. If you want to look at what the CPP actually feeds into the compiler (especially useful if your functions have unwanted side-effects), you can get it to stop and print before passing on to the compiler by adding -E to the gcc command. If you want to know more about the CPP, Wikipedia has some general info, and the official gcc CPP documentation can let you know everything there is to know. If you have any issues with it though, hit me up on the forum and I'll work through it with you.


7.6 WHAT TO DO WHEN IT GOES WRONG

So you've written your program, and it looks correct to you, but it's doing the wrong thing. Or worse still, it doesn't run at all or just crashes at points. What do you do? Luckily, gcc (and by extension MinGW) includes a program that can run your program inside itself, and lets you control how to run it and view information about the running state (like variables). This is called a debugger, and the specific one we'll be looking at is GDB (GNU Debugger). To run it inside Atom, you can just press the F6 button and then run through your program using the commands written below. If instead you have compiled your program manually, you can run gdb myapp, where myapp is the name of your executable. When gdb detects a crash, it will give you the address of the current instruction, which isn't that helpful to most people. Instead, to get gdb work through your code line by line, (including printing the current line for you), you need to compile your code with -g added to your gcc command. You can then run through your program using the commands written below. There are almost 1000 commands you could run, but these are the commands I use most often when debugging:

  • help - shows help for a command, just put help command for each command you want to know about
  • help breakpoints - help for breakpoint commands
    • break file:line - create breakpoint at line in file
    • clear file:line - remove breakpoint at line in file
  • help data - help for data commands
    • display exp - print expression everytime gdb stops
    • print exp - print expression
    • printf string - printf with format string
    • set var = exp - evaluate exp and set variable var
  • help running - help for the running commands
    • continue - run until next breakpoint
    • finish - run until current stack frame returns
    • next - next statement
    • reverse-next - previous statement
    • run - start running program
    • start - run and break at beginning of main
    • step - next line
  • quit - quit gdb

Pressing enter with no command written will just repeat the previous command, so you could run step once, then just keep pressing enter to run through your program line by line. Debugging can seem daunting at first, but once you use it a bit, it becomes second nature.


7.7 SOME AMAZING FEATS WITH C

I thought I'd make this final section a bit of fun, by showing you what can be done with C when you REALLY put your mind to it.

Duff's device

In 1983, Tom Duff (at Lucasfilm) created a way to speed up real-time animation by combining a do-while loop with a switch statement (intentionally leaving out the break statements), using a technique borrowed from Assembly programming. It's called the Duff's device, and to think about it in terms of what we've covered so far, it copies elements from one array to another quicker than just using a simple loop and going over each element. Essentially what he was doing was reducing the number of times the counter variable in his loops was being checked by executing a bunch of them at once. Here are examples of the normal and Duff versions of a copy function:

void copy(short *from, short *to, short count)
{
  do {
    *to++ = *from++;
  } while(--count > 0);
}

void copyDuff(short *from, short *to, short count)
{
  short n = (count + 7) / 8;
  switch (count % 8)
  {
    case 0: do { *to++ = *from++;
    case 7:      *to++ = *from++;
    case 6:      *to++ = *from++;
    case 5:      *to++ = *from++;
    case 4:      *to++ = *from++;
    case 3:      *to++ = *from++;
    case 2:      *to++ = *from++;
    case 1:      *to++ = *from++;
            } while (--n > 0);
  }
}

I ran each version in the debugger and the Duff version used 25 C statements (vs 40 for the normal one) to complete the copy of a 20 element array, so you can imagine how much this would speed things up for the amount of data used in movie animations.

Programming Challenges

There are many programming challenges out there for people who want to have a bit of fun, either achieving a certain goal like making a game in 2 days, or showing off how much better than everyone else they are by creating code with a particular property like being short enough to tweet. 2 of my favourites are "code golf" (making a program that completes a certain task in the least number of characters) and The International Obfuscated C Code Contest (making the most unreadable code possible, while doing creative things like laying out the code as ASCII art). You'll notice that with these challenges, the C language structure is tortured and exploited in order to achieve whatever the author's goal is, but this will frequently result in code that will only run on certain platforms and compile with many warnings. Here are a few fun examples I found:

Rob Miles created the following piece of code as a code golf challenge, creating music in the least number of code characters (209 characters of C). When it's combined with the Linux terminal, you can play the music with a single command. The C code part of that command is:

g(i,x,t,o){return((3&x&(i*((3&i>>16?"BY}6YB6%":"Qj}6jQ6%")[t%8]+51)>>o))<<4);};main(i,n,s){for(i=0;;i++)putchar(g(i,1,n=i>>14,12)+g(i,s=i>>17,n^i>>13,10)+g(i,s/3,n+((i>>11)%3),10)+g(i,s/5,8+n-((i>>10)%3),9));}

I've taken his code, expanded and modified it as best as I can, in order to make it more readable and to remove the warnings:

#include <stdio.h>

//  sample index (8000 samples per second of audio), volume, speed (higher is faster), octave (higher is lower note)
int generate(int index, int volume, int speed, int octave)
{
  //                                                 G chord?     D chord?
  return((3 & volume & (index * ((3 & index >> 16 ? "BY}6YB6%" : "Qj}6jQ6%")[speed % 8] + 51) >> octave)) << 4);
}

int main(void)
{
  int n;
  int s;
  int instrument1;
  int instrument2;
  int instrument3;
  int instrument4;
  for (int index = 0; ; index++)
  {
    // instrument 1 - low, slow notes that start at the beginning
    instrument1 = generate(index,   1,                  n = index >> 14,                12);
    // instrument 2 - medium, 2nd instrument that comes in
    instrument2 = generate(index,   s = index >> 17,    n ^ index >> 13,                10);
    // instrument 3 - medium, fast notes
    instrument3 = generate(index,   s / 3,              n + ((index >> 11) % 3),        10);
    // instrument 4 - high, really fast notes
    instrument4 = generate(index,   s / 5,              8 + n - ((index >> 10) % 3),    9);
    // add instruments together to get the combined sounds
    putchar(instrument1 + instrument2 + instrument3 + instrument4);
  }
  return 0;
}

This entry to the 2018 IOCCC by Yusuke Endoh prints out Monty Python quotes (along with a picture of a parrot), as well as the code needed to create the next quote in the sequence. You can then compile this output, which will print the quote as well as the code needed for the 3rd quote, and so on. All this while the original code is formatted in the ASCII art of "Undead Parrot"

  #define/**/Q(   q)int*P,t,u,M                                  [99999],                                                          *S=3557\
  +M,*C=M+3800;   char*Q=#q;int                                    (main)                                                            (){q;}
    # include       <stdio.h>                                       /*IOC                                                             CCC*/
     Q(for(;*       Q>34?(*S                                        ++=*Q                                                             ++):*
     (Q+= *Q-       27?1:Q[2     ]/49+    4););                     for (          S=M+9                ;*C;u     =t%9,t              <18?*
     ++S=t<0?       *C++%92*    57-t-500:t<9?M[u]:M        [u]++:t<27?M[u       ]=*S--:u?t=*        S--,u<2?M[t]=*S--:u<     3?C+=*S--?t:0,
     0:u-6?*S       =u<4?*S-     t:u/*ABCDEFGH*/<5?*     S%t:*S<t:putchar     (t):(*S=M[*S]))t     =*C++-93;return+0**"    Ler[Tsaxiyj.V}<\
     dzSGs`Fk       {YezYTtb     x0d{iy        kNT}?d   zaxiyjE   K}TdzHe    iy`Me{      Tf|)dz   HeiyH       eiyjxiyax   Ldz2sx     Ke{'d\
     |Yd{2sy?       [saY^{tG     erjxuj        xvbqax`  {)dzc       kydj/    dzjxkya_}JdziHf}Ydz -Vw2d         rQ)qJe_y  h@*}<d       zHeh\
     yAehy)*n       x{qnxKe{     Df|Qd{         uc)e{&  dzZfh       yCehy    6fuchyex;e{ZdzIe_yn q1dqD         +u/dcyl5  L}<dzL       IETi\
     {x=b{1g|{uc2d{byaxvcDfd     }&dzDVd       {x6ecOe  z1dcydCf_{{)d|qSf2   T_{x$c{              ;g|{{u      Ffjx}4czb   q_xucx/d }Jez0dly\
       c/d{hycSf{hychycFc{      hy_)Ic}{q     _0d{qb_}.c  z:eo:T`{xp]q1d`{D`  zD+ulxcx{-dzcCez     7evOe~Hf~0dcx}/dzd~cx    x~6f~m6e0dcx}{}Ud\
          zc(*{x~c0d{Tf         |.dz'd~c5     L}Ecz1d[Ty     /*0+)IFMNFU*+0-    02;>@=,(#155        5546231rst%wvu#08OCC       C*/#include/*#"



     "exclude*/<stdio.                                                                                                  h>#def\
    ine/**/Q(q)int*P,t,u,M[                                                                                             99999],\
      *S=3557+M,*C=M+3800;cha                                                                                             r*Q=#\
       q;int(ma      in)(){q;}                                                                                           Q(@@~c\
       d`c/C@~o        `rrdc~n                                                                                        m/GSghr~o`qqns~hr~\
       mn~lnqd/       FH~g`ud~     bd`rdc~sn~a d/O@~dw ohqdc~`mc~fnmd~sn~    ldds~lx~l`jdq/GSgh       r~hr~`~k`sd     ~o`qqns/LAdqdes~ne\
       ~khed+~H~qdrs~hm~od`bd    /KH&c~ad~otrghmf~to~s  gd~c`hrhdr/X@~qtm~cn vm~sgd~btqs`hm~`mc~i   nhmdc~sgd~bgnhq~      hmuhr\
       hakd/FSghr~hr~`m~dw,     o`qqns     /6Zba4ORRZ    T`]Sa`    fdgO2bPV[   Ta5dT[    MVfbZZHY  b79bVT^    :L;S<J=     X>Q?X\
       @SAZBWCIDNEQF`GXPU      UgW^a         a[bIaZ[Z    afaf_`     Kg`ZYY[    [`gff^     Jg``8OK KT,VNa6      /TZSW`     0cRMWO       3\
       3f][T`-[K               aV,[Z         /6aPWLab    f[P[a3                f`0dPR             `3M3]8/      R[Z-Z,     VZR#MR     TfNTT\
       4d5^$S]8                RMKRVa       MM8T$9aaZ    gaRSg1                NZ1`5^              d`LK].      %&VZ,'     R#gKaZ       f1T\
     T10aa`b].TZ-               KK/28`-6VKf'ZH^P(./Ka8 a)8[O6NaTb            f*[Hd+Z(V              8&3K%)VNTR#&WbX[      H^dcOWOf]8[OR[aT\
    OM2V'$9aLR[RW*               `0+OMW[([14ZLaK%).NO6 RMRS*[0+ZO            WZS]_<<<<               <htt"/*pa*/"p:        //"/*x@*/"cult\
     ofthepartypa                    rrot.co    m/>>>>  >__<-_So             urce_of_P                   arrots                 __(")

My final example is another IOCCC entry from 1988 by Ian Phillipps, which has no discernible form or function when you look at the code (as the judges put it, "this program looked like what you would get by pounding on the keys of an old typewriter at random"), so I'll leave it to you to try and work out what it does. But if you run it (works on Windows unlike the other examples), you'll be amazed at what it can accomplish.

main(t,_,a )
char
*
a;
{
				return!

0<t?
t<3?

main(-79,-13,a+
main(-87,1-_,
main(-86, 0, a+1 )


+a)):

1,
t<_?
main( t+1, _, a )
:3,

main ( -94, -27+t, a )
&&t == 2 ?_
<13 ?

main ( 2, _+1, "%s %d %d\n" )

:9:16:
t<0?
t<-72?
main( _, t,
"@n'+,#'/*{}w+/w#cdnr/+,{}r/*de}+,/*{*+,/w{%+,/w#q#n+,/#{l,+,/n{n+,/+#n+,/#;#q#n+,/+k#;*+,/'r :'d*'3,}{w+K w'K:'+}e#';dq#'l q#'+d'K#!/+k#;q#'r}eKK#}w'r}eKK{nl]'/#;#q#n'){)#}w'){){nl]'/+#n';d}rw' i;# ){nl]!/n{n#'; r{#w'r nc{nl]'/#{l,+'K {rw' iK{;[{nl]'/w#q#n'wk nw' iwk{KK{nl]!/w{%'l##w#' i; :{nl]'/*{q#'ld;r'}{nlwb!/*de}'c ;;{nl'-{}rw]'/+,}##'*}#nc,',#nw]'/+kd'+e}+;#'rdq#w! nr'/ ') }+}{rl#'{n' ')# }'+}##(!!/")
:
t<-50?
_==*a ?
putchar(31[a]):

main(-65,_,a+1)
:
main((*a == '/') + t, _, a + 1 )
:

0<t?

main ( 2, 2 , "%s")
:*a=='/'||

main(0,

main(-61,*a, "!ek;dc i@bK'(q)-[w]*%n+r3#l,{}:\nuwloca-O;m .vpbks,fxntdCeghiry")

,a+1);}

This workshop is in progress, check back next week for updates!

Welcome to the C Programming Workshop for Makers! Here you'll be able to follow along with our series of easily digestible vi...

Have a question? Ask the Author of this guide today!