Some Notes on Programming in C

This document has some notes on the C programming language for my CS 431 course in Fall 2016. The course will use the gcc compiler in a Linux command-line environment. The intent here is not to cover all features of C, but to provide basic information about features that are likely to be encountered and used in this course, with an emphasis on some of the things that make C different from Java.

In some ways, C is similar to Java. Their basic control structures (if and switch statements, for and while loops) are mostly the same, although C does not have exceptions or try..catch statements. Basic variable types (int, double, char) are similar, at least on the surface. So are function definitions. C does not have classes, but it has "structs." A struct is similar to a class that has only public data members, and no methods. On a deeper level, however, C and Java are very different.

Compiling, Linking, Loading

It's important to understand that in C, every source code file is compiled separately. All of the separately compiled components are then linked to produce an executable program. To run an executable, it must be loaded into memory. An executable can include references to dynamically loaded libraries, which have been compiled separately and stored as separate files. Dynamically loaded libraries are linked to the executable at run time.

Source files are compiled separately, but a source file can use variables and functions that are defined in other files. It needs some way to know about those variables and functions. That's usually done with #include directives such as

#include <stdio.h>

This statement in a C file says to physically include the contents of the file stdio.h in the code that is actually compiled. In this case, stdio.h contains declarations of standard input/output functions such as printf. Including stdio.h in a C file makes it possible to use those functions in that file. The .h extension in the file name indicates that stdio.h is a "header" file. The convention is that header files contain definitions and are meant to be included in in other files, but the content of a header file could technically be any legal C code. The angle brackets around <stdio.h> indicate that this is a standard header file that is to be found in some standard set of directories. (On our Linux computers, stdio.h is in the directory /usr/include.) If you use quotation marks instead of angle brackets, as in #include "myheader.h", the compiler will look for myheader.h in the working directory before it looks in the usual include directories. That is, use quotation marks for your own header files and angle brackets for standard header files.


Compiling with gcc. In its simplest usage, the gcc command takes one or more .c source code files. The .c files can #include header files, but the header files are not listed in the gcc command. An example might look like

gcc sortnamelist.c list.c nameio.c

A gcc command of this form compiles and links the files and creates an executable program named a.out. To get a different name for the executable, use the -o option:

gcc -o sortnames sortnamelist.c list.c nameio.c

The executable can then be run in the usual way:

./sortnames

The C source code files in the above command must contain exactly one function named main, which is the main routine for the program. The code can refer to standard functions, which will be automatically linked to the executable. (The functions are in the dynamically loaded library libc.so, which is automatically linked to the executable.) If the program uses functions from other libraries, the libraries must be listed in the command. For example, there is a library named libncurses.so. If a program uses functions from that library, the library must be specified in the gcc command using the option -lncurses. Similarly, certain math functions require the library libm.so, which is specified using the option -lm. The option consists of the lower case letter "L" plus the name of the library without the lib or the .so For example,

gcc -o sortnames sortnamelist.c list.c nameio.c -lm -lncurses

(On our computers, you can find libraries in /usr/lib/x86_64-linux-gnu.)


Compile without linking. To make things more complicated, gcc can compile files without linking them. It does this if the -c option is used. When a .c file is compiled in this way, you get an "object file" with the same name as the .c file, except with the extension .o. for example,

gcc -c list.c

creates a file named list.o. An object file is not executable and is usually not a complete program, but it can be linked with other files to create a complete executable. For example:

gcc -c list.c
gcc -c sortnamelist.c
gcc -c nameio.c
gcc -o sortnames sortnamelist.o nameio.o list.o -lm -lncurses

The advantage of doing things this way is that when you change just one file, only that file needs to be recompiled. This can be very significant in a large project.

The process of compiling an linking an executable is usually automated using the make command and a Makefile, which contains instructions about how to create the executable.

The Preprocessor

There is actually a step before compilation, known as the preprocessor. It's the preprocessor that expands a #include directive, replacing it with the full text of the specified file. Any line that begins with a # is a preprocessor directive. Besides #include, there are a few other preprocessor directives that you should be familiar with.

The #define preprocessor directive is used to define "macros." Macros are similar to constants or to function definitions, but they are interpreted by the preprocessor, and the action taken by the preprocessor is simply to substitute some text for the macro wherever it appears in the source code (but not inside strings). As an example of using a macro like a constant, the directive

#define PI 3.141592654

defines PI to represent the text 3.141592654, so that the preprocessor will change "area = PI*r*r;" to "area = 3.141592654*r*r; before the code is compiled. Note that it will also change "double PI = 3.14;" to "double 3.141592654 = 3.14;", which will give a compiler error. In general, using macros can be prone to errors if you aren't careful. A macro can have parameters. For example, with

#define MAX(x,y) ((x) > (y) ? (x) : (y))

the preprocessor will change "z = 5*MAX(x+1,y-7);" to "z = 5*((x+1) > (y-7) ? (x+1) : (y-7));". Note that all the parentheses in the macro definition are there to make sure that operator precedence is correct in the transformed text. Remember that macro expansion does simple textual substitution. Also note that the definition of the macro extends to the end of the line and can include spaces. It is possible to define a macro with an empty definition:

#define FOO

This is useful mostly in combination with #ifdef and #ifndef.


Conditional compilation. The preprocessor directives #if, #ifdef, and #ifndef are used to implement "conditional compilation." That is, it is possible to include or exclude certain code from the preprocessed source code that is passed from the preprocessor to the compiler. For example, if the original source code says,

#ifdef CHECK_FOR_ERRORS
   if (x < 0) {
      printf("Error: x has a negative value.\n");
      exit(1);
   }
#endif

then the code between #ifdef and #endif is compiled only if CHECK_FOR_ERRORS has been defined as a macro. If not, then the preprocessor deletes that code, so that it won't be included in the compiled executable. Similarly, #ifndef can be used to check whether an identifier has not been defined as a macro. This is made more useful by the fact that macros can be defined in a gcc command using the -D option. That is, the command

gcc -DCHECK_FOR_ERRORS prog.c
will define CHECK_FOR_ERRORS as a macro (with an empty definition) before compiling prog.c. You will often see the #ifndef directive used in header files to avoid including the same header file more than once in the same compiled source code. The idiom looks like this, using stdio.h as an example:

#ifndef __STDIO_H
#define __STDIO_H
   .
   .   // code declaring stdio functions
   .
#endif

Declarations and Definitions

C makes a distinction between declaring a variable or function and defining it. A program can include several declarations of an identifier (as long as they all declare it in the same way), but it can include only one definition. An identifier should be declared before it is used, but does not have to be defined before it is used. In fact, an identifier can be used in a file even if it has no definition in that file — it can be defined in another file or library that will be linked into the complete program. Header files ordinarily contain declarations, not definitions.

A function can be declared with a prototype, which includes the return type, name, and parameter list of the function, followed by a semicolon. A function definition replaces the semicolon by the body of the function. Some function prototypes:

void rand();
int abs(int x);
void error(char *error_message);
double array_sum( double array[], int size );

An ordinary variable declaration is also a definition. To get a variable declaration that is not a definition, add the modifier extern to the declaration:

int array_size = 100;  // Define the variable array_size, and initialize it to 100.

extern int array_size; // Declare array_size, but it must be defined elsewhere;
                       // Initialization here would not make sense and is not allowed.

(This variable should really be a constant. To declare a constant, add the "const" modifier, as in "const int array_size = 100;" and "extern const int array_size;".);

By default, an identifier that is defined in one file can be used in other files. Sometimes, however, you want to define a variable or function that is "private" to a file, meaning that it is invisible to other files. You do that by using the modifier static in the prototype or definition:

static int size;  // Valid only in the file that contains it.

static void helper_function() {
   .
   .
   .
}

(Note that the word "static" does not mean the same thing in C that it does in Java!)

Basic Types

The words int, long, short, double, and float can be used as type names in C with the same meanings as in Java. (That's true on our computers, but the exact meaning of some of these types can vary between different platforms and different versions of C.) There is no boolean type (but a similar bool type is defined in stdbool.h). There is no byte type, but char is available as an 8-bit integer type.

Things are actually more complicated, however. First of all, C has "unsigned" integer types, which can only have non-negative values. The type unsigned short has values in the range 0 through 65535 while signed short has values in the range -32768 through 32767. By itself, "short" really means "signed short" — in fact, it actually means "signed short int", and you can use the word "unsigned" by itself as a type meaning "unsigned int". Similarly, "long" is actually the same as "signed long int". Furthermore, there is a long double type, which on our computers is a 16-byte floating point type. (However, long double probably only uses 80 bits for computation, since that is what is supported on x86 hardware.) There are no unsigned floating point types. The char type is signed on our computers, but it can be unsigned on other computers, so you could use the types signed char and unsigned char to be safe when the difference is important.

Note that you should get used to type names that consist of more than one word! You could see a function declared as "long double foo(unsigned short x)"!

You can use type-casts to change between numerical types, but C is very free about doing conversions without explicit type casts. For example, you can say "char c = 2000;", and the 2000 will be converted to a (signed) char value, so that c actually gets the value -48.

A "typedef" can be used to give a new name to a type. For example, if you don't like using unsigned int as a type, you can say

typedef uint32_t unsigned int;

and after that you can say uint32_t instead of unsigned int. The header file stdint.h defines several standard integer types in this way, including int8_t, int16_t, int32_t, and int64_t for signed 8-bit, 16-bit, 32-bit, and 64-bit integer types. And it defines uint8_t, uint16_t, uint32_t, and uint64_t for unsigned integer types.


Boolean values. I should note that C does not traditionally have a boolean type because any numeric expression can be used as a boolean. In a boolean context, zero means "false" and any non-zero value is "true". For example, you can write an infinite loop using "while (1)...", and you can test whether an integer x is zero by saying "if (!x)...". This is equivalent to "if (x == 0)...".

Similarly, a pointer can be used in a boolean context. A NULL pointer is "false" while any non-NULL pointer is "true". So, you can test whether a pointer ptr is non-NULL using "if (ptr)...".

Struct Types

C does not have classes, but it does have "structs". A struct is a collection of variables, which can have different types. It is similar to a class that has only (public) member variables, and no methods. An example of defining a struct type:

struct coords {
   int x;
   int y;
};

The name of the type is "struct coords", not "coords", so that variables p1 and p2 of this type would be defined as "struct coords p1,p2;". Note that p1 and p2 are not pointers, as they would be in Java!

Members of a struct variable can be referenced using the same "dot" notation as in Java; the members of p1 are p1.x and p1.y. A struct can be initialized when it is defined, using a notation similar to array initialization. When stucts are assigned, all member values are copied from one struct to the other:

struct coords p1 = { 17, 42 }; // p1.x is 17 and p1.y is 42
struct coords p2 = p1; // p2.x and p2.y get their values from p1.x and p1.y

This is all pretty simple — but it will get more complicated when we start using pointers to structs.

Pointer Types

In Java, all values, except for the primitive types, are pointers. In C, pointers are made explicit, and you can have pointers to primitive type values as well as to structured values. A variable of type int holds an integer. A variable of type struct coords holds a data structure consisting of two intgers. To get a variable, iptr, whose value is of type "pointer to integer", you would declare it as

int *iptr;

To get a variable, coords_ptr, whose value is of type "pointer to struct coords", declare it as

struct coords *coords_ptr;

To declare two pointer variables, you need two stars:

int *p1, *p2;

The name of the type for the variables p1 and p2 is int*, pronounced "int star" or "pointer to int". (So sometimes, the * is placed next to the variable and sometimes next to the type.)

The value of a pointer variable can be NULL; the identifier "NULL" is not built-in in C, but is defined in stddef.h, which is also included automatically by many other standard header files. NULL is almost the same as 0, and you can assign 0 to a pointer variable to make it NULL (although there is actually some automatic type-casting going on in that case). To get a non-NULL value for a pointer variable, you can use the standard malloc function, which is defined in stdlib.h, which allocates a block of memory from the heap and returns a pointer to that block of memory (something like the new operator in Java). The parameter to malloc is the size of the memory block, specified in bytes. The size is often specified using the sizeof operator, which gives the number of bytes required to hold a given type or variable. (But warning: the sizeof a struct is not necessarily equal to the sum of the sizes of its members. The compiler can add extra padding between members for more efficient access.) For example:

iptr = malloc( sizeof(int) );
coords_ptr = malloc( sizeof(struct coords) );

The parameter to malloc is of type size_t, which on our system is the same as unsigned long. The return value of malloc is a pointer of type void*, which is a type that can be assigned to any pointer type. To make it clearer, you could explicitly type-cast the return value:

iptr = (int*) malloc( sizeof(int) );

In fact, you can actually type-cast any pointer type to any other pointer type, even when it doesn't make sense. On our computers, a pointer is actually a 64-bit value that can also be type-cast to type unsigned long with no loss of information.

C does not do garbage collection. Memory that is allocated by malloc should be de-allocated by free when it is no longer in use. Failure to do so would be a "memory leak." free takes a pointer as parameter:

free(iptr);  // frees the memory allocated for iptr by the call to malloc
free(coords_ptr);

Operators for pointers. Another way to get a value of type int* is by applying the unary "address of" operator, denoted by &, to a variable of type int. In general, if x is a variable of type T, then &x is a value of type T* representing the address of x in memory. For example,

int n;
int *p = &n;  // The value of p is the address of n
struct coords point;
struct coords *q = &point; // The value of q is the address of point.
int *r = &point.y;  // You can also get the address of a member of a struct.

You can do a conversion in the other direction by using * as a unary operator. That is, if ptr is a pointer of type T*, then *ptr is of type T and represents the value stored at the address specified by ptr. In fact, *ptr is essentially a variable of type T — you can assign a value to it:

int n;
int *p = &n;  // p contains the address of n
*p = 17;  // Stores a 17 in n; here, *p is really an alias for n

Note that *p is an error if p is not a well-defined non-NULL pointer value. (There is a confusing difference between "int *p = &n; and "*p = n;". The first does initialization; the second does assignment. In the first, the value of &n is assigned to p (not to *p), while in the second, the value of n is assigned to *p. The initialization "int *p = &n;" is equivalent to "int *p; p = &n;".)

For parameter passing in functions, C only supports pass-by-value. That is, the value of the actual parameter is copied into the formal parameter when the function is called. A function cannot change the value of the actual parameter; it can only change its copy. However, by passing a pointer value, you make it possible to change the value that it points to. The obvious example,

function swap_ints( int *p, int *q ) {  // parameters are of pointer type
   int temp = *q;
   *q = *p;
   *p = temp;
}

This function would usually be called by passing in the addresses of two integer variables:

int a = 17;
int b = 42;
swap_ints( &a, &b );  // pass addresses of a and b as pointer values
// now, the value of a is 42, and the value of b is 17

The -> operator. Suppose that you have a variable, pt, of type struct coords *, where struct coords is defined as above. Assuming that the value of pt is a valid, non-NULL pointer, then *pt represents a value of type struct coords, which contains members named x and y. You can access those members with the notations (*pt).x and (*pt).y. The parentheses in these expressions are necessary because of the precedence of the star and dot operators: *pt.x would be interpreted as *(pt.x).  C has the -> operator to make it easier to access struct members through pointers. In our example, pt->x is the same as (*pt).x. More generally, pointer->member is defined to mean (*pointer).member. It means to follow pointer to a struct and then access the variable named member within that struct. Keep in mind that -> is only used with pointers; when working with actual structs, use the dot operator.


Pointer Arithmetic. C lets you add integers to pointers and subtract integers from pointers. The result is a new pointer, offset from the original. The amount of offset depends on two things: the integer that you add or subtract and the type that the pointer points to. That is, for a pointer of type pointer-to-T, adding an integer n to the pointer will offset the pointer by n items of type T.

For example, if ptr is a pointer of type int*, then ptr + 1 points to the next int in memory, following the one the ptr points to. Another way of saying this is that the actual numeric value of ptr + n is the numeric value of ptr plus n times sizeof(int).

As a more concrete example, if A is an array of items of type T, and if p is of type pointer to T that holds the address of A[3], then p + 1 would be a pointer to A[4], and p - 1 would be a pointer to A[2], and in general, p + n would be a pointer to A[3+n] (as long as 3+n is a valid index for the array — if not, p + n would point to a memory location outside the array). By the way, another way to get the same thing as p + n would be

(int*) (( (char*)p ) + n*sizeof(T))

and if you understand that, you understand type-casting of pointers as well as pointer arithmetic.

It is also possible to take the difference between two pointers. The two pointers should be of the same type, and the result of subtracting one pointer from the other is an integer giving the number of items of that type between the two pointers. (So p+(q-p) has the same value as q.)

One more note: It is also possible to use the unary operators ++ and -- to offset a pointer by one item.

Arrays and Strings

C has arrays, but in effect, an array variable is little more than a pointer to the beginning of the array in memory. In fact, pointers and array variables can be used interchangeably in many contexts. The simplest way to make an array is to declare a variable of array type and provide either an array length or an array initializer with values to be stored in the array:

int A[10];  // An array of length 10, with space for 10 integers
double B[] = { 2, 3, 5, 7 };  // An array of length 4, with initial values

Note, again, that although an array of int is said to be of type int[], when an array variable is declared, the [] goes with the variable name, not with the type. For example,

int A[10], B;

creates a simple integer variable, B, and an array, A, of length 10.

No record of an array's length is kept at run time. There is no such thing as A.length. There is nothing to stop you from referring to A[100], even though A only contains 10 elements. In fact, you can even refer to A[-1]! However, referring to memory outside the array in this way has an undefined result. It might crash your program, or it might just return some arbitrary value.

To see how array variables are like pointers, suppose we create a pointer and allocate some memory for it on the heap:

int *p = malloc( 10*sizeof(int) );

The value of p is the address of a block of memory large enough to hold 10 integers. This is very similar to an array of length 10. p points to the first item in that array, p + 1 points to the second element, and so on. Now, in fact, even though p is of pointer type, not array type, you can still use array notation with p. That is, p[0] is the same variable as *p, p[1] is the same variable as *(p+1), and in general, p[n] has the same meaning as *(p+n). Surprisingly, this even works in the reverse direction: If A is an array variable, you can use the notations A[n] and *(A+n) interchangeably! In fact, the bracket notation can be considered as no more than syntactic sugar for the pointer notation.

There are some differences between arrays and pointers. For one thing, in our example, sizeof(A) would be 40 (on our systems, where an int is 4 bytes), while sizeof(p) would be 8 (on our systems, where a pointer occupies 8 bytes). But sizeof is an operator that works at compile time, and the distinction is pretty much lost at run time.

When a function has a parameter of array type, the actual parameter will be passed by reference, not by value. That is, it is only a pointer to the array that is passed to the function; the contents of the array are not copied. The function gets to work with the actual array. In the function prototype, you can provide a length for an array, but the length is completely ignored. Thus, both parameters in the following function prototype are legal:

void foo( int A[], int B[10] );

but the 10 is completely ignored. (In fact, you could just as well use pointer types for the parameters, except that using array types can make the intention more clear.) Note that since arrays don't remember their length, it is common to pass the size of the array or the number of array elements to be passed as an additional parameter. For example,

int int_array_sum( int A[], int items) {
   int total = 0;
   for (int i = 0; i < items; i++)
      total += A[i];
   return total;
}

A string in C is simply an array of char, with the convention that the array contains a zero to mark the end of the string. Most often the type associated with strings is char* rather than the mostly equivalent char[]. You can't assign strings (since you can't assign arrays), you can't concatenate strings with +, and there is no record of the length of a string (except for the zero marker at the end, which you can only hope is really there!). All this makes dealing with C strings very error-prone — even more so when you start working with dynamically allocated strings.

The standard header file string.h contains a large number of functions for working with strings. For example, if str is of type char*, then the function call strlen(str) returns the length of the string. It does this by looking for the zero at the end of the string; if it's not there, you don't get a valid result (and you might get a program crash). There is also a function strnlen(str,maxlen), where maxlen is an integer. This function returns the length of str, except that if it doesn't find a zero within the first maxlen chars, then it stops looking and returns maxlen as its value. Many functions in string.h come in similar pairs. For example, strcpy(s1,s2) will copy chars from s2 to s1, stopping after copying the final zero in s2; however, there is no guarantee that the zero is there or that s1 has enough memory to hold all of the copied characters. The function strncpy(s1,s2,n) copies s2 to s1, but will stop after copying n chars, even if it has not reached the end of s2; note that is this case, it is not guaranteed that the result in s1 will have a terminating zero.

A string variable can be declared as type char* or char[]. The pointer type is more common for dynamically allocated strings and for string parameters. A string variable can be initialized with a constant string when it is declared:

char str[11]; // a string of length up to 10, with space for the zero at the end
char *dynstr = malloc(11);  // Dynamically allocated; remember it must be freed!
char *greet = "Hello World";  // length is 12, including the zero at the end

The main routine of a program is usually declared as

int main(int argc, char **argv)

The parameters are used to pass the command-line arguments into the program when the program is run. (You can leave the parameters out if you don't need the command-line arguments.) The parameter argc gives the number of command-line arguments, while argv is an array of strings that contains the arguments themselves. In C, the name that is used to call the program is considered to be the first command-line argument, and it can be accessed as argv[0]. Here is a program that will print out the command line arguments:

#include <stdio.h>

int main(int argc, char **argv) {
   for (int i = 0; i < argc; i++)
      printf("%s\n", argv[i]);
}

If you compile this program with gcc and run it with

./a.out "Hello World" one two three

then the output will be:

./a.out
Hello World
one
two
three

(The return value from main is sent back to the system as an error code. A zero value indicates there was no error. A non-zero value indicates that the program ended with an error. If you don't return a value explicitly, the compiler will effectively add a "return 0" at the end.)

It's worth looking at how char  **argv represents an array of strings. The first * can be taken as meaning that argv points to an array. The second * means that each element in the array is of type char*, which can represent a string. Note that the type for argv can also be written as char  *argv[].

Function Types

A compiled function is really just a block of machine language code in the computer's memory, and it is possible to have a pointer to that memory block. You might think of the name of a function as a pointer variable whose value is the address of the function's code in memory. In fact, C really only implements this idea for function parameters. You can pass the name of a function as an actual parameter to another function. The type for the formal parameter looks like a function prototype, with the parameter names omitted. For example, the formal parameter type "void apply(int)" will match any function that has one int parameter and a void return type. I won't belabor this idea, but here is a sample program that uses a parameter of function type:

#include <stdio.h>

/* Calls apply(A[i]) for i from 0 to size-1 */
void apply_all( int A[], int size, void apply(int) ) {
   for (int i = 0; i < size; i++)
      apply(A[i]);
}

void printint(int x) {
   printf("     %d\n", x);
}

int tot;
void add_to_tot(int x) {
   tot += x;
}

int main(int argc, char *argv[]) {
   int A[] = { 1, 2, 3, 4, 5 };
   printf("\nOutput from apply_all(A,5,printint):\n\n");
   apply_all(A,5,printint);
   
   tot = 0;
   apply_all(A,5,add_to_tot);
   printf("\nResult of apply_all(A,5,add_to_tot): %d\n\n", tot);
}

Assembly Language

One final note: It is possible to embed assembly language code inside a C program, using the asm command. It is also possible to assemble assembly language files and to link the resulting compiled object file into a C program. You will see both techniques used in code that you will be working with in CPSC 431. However, you will not have to write your own assembly code, and you will not have to understand the assembly code that is given to you.