This page does not represent the most current semester of this course; it is present merely as an archive.

This is intended to be a practical guide (rather than an authoritative guide) to C, as implemented by clang and gcc for the x86-64 processor family.

1 Data Types

The sizeof(...) operator returns the size of a type in bytes. Thus, sizeof(int) is 4, not 32.

1.1 Primitive

1.1.1 Integer

The integer data types are

name bits representation notes
_Bool 1 or more undefined rarely used; for all types, 0 is false, anything else is true
char 8 signedness undefined usually used for characters, sometimes for bytes
signed char 8 2’s complement
unsigned char 8 unsigned integer
short 16 2’s complement
int 32 2’s complement
long 32 or 64 2’s complement 32 bits if compiled in 32-bit mode; for 64-bit, add the -m64 flag when compiling
long long 64 2’s complement

Each has an unsigned version (e.g., unsigned short, etc). If unsigned is used as a type by itself, it means unsigned int.

Integer literals will be implicitly cast to the correct type upon assignment; thus char x = -3 will turn -3 into an 8-bit value automatically, as int x = 'x' will turn 'x' into a 32-bit value. This only works up to int-sized literals.

To force a literal to be long add a l or L to the end; to force it to be unsigned add a u or U. This is generally only needed for very large constants, like unsigned long very_big = 9223372036854775808ul.

Character literals are integer literals written with a different syntax. There is no significant difference between '0' and 48 other than legibility.

1.1.2 Floating-point

The floating-point datatypes are

name exponent bits fraction bits total size literal syntax
float 8 23 32 bits (4 bytes) 3.1415ff or F for float
double 11 52 64 bits (8 bytes) 3.1415 – no suffix
long double 15 64 80 bits (10 bytes) 3.1415ll or L for long

Note that long double has traditionally only differed from double on x86 architectures.

1.1.3 Enumerations

The enum keyword is a special way of defining named integer constants, typically in ascending order unless otherwise specified.

1.1.4 void and casting

There is also a special void type that means either “a byte with no known meaning” (if used as part of a pointer type) or “nothing at all” (if used as a return type or parameter list).

Casting between integer types truncates (if going smaller) or zero- or sign-extends (if going larger, depending on the signedness of the value) to fit the available space. Casting to or from floating-point types converts to a nearby1 representable value (which may be infinity), with the exception that casting from float to int truncates the reminder instead of rounding.

1.2 Pointers

For every type, there is a type for a pointer to a value of that type. These are written with a * after the type:

A pointer to any value stored in memory can be taken by using the address-of operator & Thus &x is the address of the value stored in x, but &3 is an error because 3 is a literal and does not have an address. You also can’t take the address of the result of an expression: &(x + y) or &&x are both errors as well.

You de-reference pointers with the same syntax used to create them: a * before the variable.

You can also de-reference pointers with subscript notation; *x and x[0] are entirely equivalent, as are *(x + n) and x[n].

There is syntactic ambiguity when combining * and [1]. Is *a[1] the same as *(a[1]) or (*a)[1]? This is solved by operator precedence ([] before *), but is not intuitive to most programmers so you should always use parentheses in these cases.

All pointers are the same size (the size of an address in the underlying ISA) regardless of the size of what they are pointing to; thus sizeof(char *) == sizeof(long double *). Two special int types2 are used to be “an integer the size of a pointer”: size_t is an unsigned integer of this size, and ssize_t is a signed integer of this size. With the compilers and ISAs we are using this semester size_t is the same as unsigned long and ssize_t is the same as long.

When you add an integer to a pointer, the address stored in the pointer increases by a multiple of the sizeof the pointed-to type.

1.3 Composite

There are two basic compound types in C: the struct and the array.

1.3.1 Array

An array is zero or more values of the same type stored contiguously in memory.

Except when used with sizeof and &, arrays act exactly like pointers to their first element; notably, this means that array[23] does what you expect it to do: access the 24th element of the array.

The sizeof an array is the total bytes used by all elements of the array:

The & an array is the & of its first element (i.e., &array == &(array[0])).

Parentheses are allowed when declaring types, although their meaning is counter-intuitive to many students:

The rule here is that we declare variables exactly as we would use them: a point to an array would first be dereferenced ((*pc)) and then indexed ((*pc)[i]) to get a char so we declare it as char (*pc)[10].

Arrays literals use curly braces and commas.

Unless initialized with a literal like this, the contents of an array are undefined (i.e., may be any random values the compiler thinks is most efficient) when created.

Arrays cannot be resized after being created.

1.3.2 Struct

A struct also stores values contiguously in memory, but the values may be of different types and are accessed by name, not index.

The name of the resulting type includes the word struct

struct foo x;
unsigned long a = sizeof(struct foo);
x.b = 1234;
x.a = x.b - 5;

Compilers are free to lay out the data elements of a structure with padding between elements if they wish; this is often done in practice to improve data alignment, so in the above example we expect a to have a value larger than the minimal 15 bytes needed to store those fields.

Structures are passed by value; that is, using them as arguments, return types, or with = means that all of their fields are copied. This is inefficient for all by the smallest structs, so often pointers to structures are passed, not the structures themselves.

Because all pointers are the same size, you can have code use a pointer to a struct without knowing what is inside the struct; the only need to be known for sizeof and the . operator to work.

Structure literals are written using curly braces and commas, optionally with .fieldname = prefixes

Unless initialized with a literal like this, the values of fields of a struct are undefined (i.e., may be any random values the compiler thinks is most efficient) when created.

1.4 Constant

If a type is preceded by const, the compiler is free to perform optimizations that assume that no code will ever change the values of this type after they are first initialized.

As a special syntax, a string literal like "hello" does two things:

  1. It ensures there exists somewhere an array of characters {'h', 'e', 'l', 'l', 'o', 0}, typically in read-only memory.
    • Note the 0 at the end (that’s byte-0 not character-0). This is how C knows the string is over.
  2. It returns a const char * pointing to the h.

1.5 typedef

You can give new names to any type by using the typedef statement:

typedef type names are aliases to the old names; the compiler will treat both the original and new name as equivalent in all type checking.

Sometimes typedef is used with anonymous structs:

1.6 Union

A union is like a struct, except that all of the fields are stored in the same memory address. In practice, this means only one of them has a meaningful value at a time.

1.7 You can do bad things

C does not try to prevent you from doing bad things.

C’s general attitude is “every rule has an exception” and “the programmer knows best”. It might make you do some complicated casting to do things, but it won’t stop you if you are determined.

2 Control constructs

2.1 Braces and scope

Any statement may be replaced with a sequence of statements inside braces. Variables declared inside a set of braces vanish at the end of those braces.

2.2 Flow of control

2.2.1 Nice and common ones

2.2.1.1 if

Any statement may be preceded by if ( ... ); the statement will only be executed if the expression inside the parentheses yields a non-zero value.

Any statement following a statement preceded by if ( ... ) may be preceded by else; the statement will only be executed if the expression inside the if’s parentheses yields a zero value.

2.2.1.2 while

Any statement may be preceded by while ( ... ); the statement will only be executed if the expression inside the parentheses yields a non-zero value, and will continue to be executed until that condition stops being true.

2.2.1.3 for

The special construct for (e1; e2; e3) s; is equivalent to the following:

with a slight twist: if s contains a continue, it jumps to e3 instead of to while (e2).

If e2 is omitted, it is assumed to be 1, so for(;;) s; repeats s forever.

2.2.2 Ugly and uncommon ones

2.2.2.1 do-while

The syntax do s; while (e); means the same as s; while (e) s;: that is, it always does s once before first checking e. In my experience, this is used for less than 1% of loops.

2.2.2.2 label and goto

Any line of code may be preceded by a label: an identifier followed by a colon.

The goto some_label; statement unconditionally jumps to the code identified by that label.

In 1968 Edgar Dijkstra write an article “Go To Statement Considered Harmful”. Since then, the use of goto in code has dropped significantly; it’s now usually a sign either of over-emphasis on optimization or a shim to avoid having to redesign poorly-organized code. However, there are a few situations where it can be handy, so it does sometimes show up in high-quality code.

2.2.2.3 switch

The switch statement in C may be implemented in several ways by the compiler, but it is designed to be a good match for the “jump table” approach.

The syntax of the switch is as follows:

Conceptually, this is

  • a block of code
  • with multiple labels
  • where the labels are numbered, not named

and it operates like the (invalid) code

The break (as with a break in a loop) stops running the code block and goes to the first statement after it.

Many people think of a switch as being a nice way to write a long if/else if sequence, and are then annoyed by its limitations and quirks: it has to have an integer selector (as this is really an index), and it “falls through” to the next case if there is no break. Hence the following example, taken from wikipedia:

Because many programmers make mistakes with switch, it is common to see them banned by style, or augmented with a special style, or later languages to use a similar syntax in ways a jump table cannot handle, or mostly C-compatible languages augmenting them with rules like “each case bust either end with break or with an explicit fallthrough/goto case”.

Most compilers have several different implementations of switch they can pick between; they might use a jump table, a sequence of if/else ifs, a binary search, etc.

3 Functions

3.1 Most common use

The most common use of functions in C looks much like you are used to from other languages: a return type, a name, a list of typed parameters in parentheses, and a body in braces.

It is also common to declare functions before defining them, in part because C requires functions to be declared before use.

Often the declarations or function headers are put in a separate file, called a header file and traditionally named with the suffix .h. The #include directive can thus grab all of these at once, simplifying coding without increasing the size of the resulting .c file or the compiled binary.

3.2 Syntax variations

However, C allows several variations on this theme.

3.3 It’s all convention

C passes arguments using a calling convention. This is obeyed blindly by both the caller and the callee; so if the caller thought the callee had different argument types than it did, neither will notice they have a problem; they’ll just silently do the wrong thing.

Consider the following pair of files:

baz.c
bar.c

When executed,

  1. baz will put the address of the first character of "hello" into the %rdi register and then callq bar.
  2. bar will look in %rdi for an integer, modulo it by 10, and use it to put an address of a character in the string "ten letter" into %rax
  3. baz will look in %rax for an integer, add x to it, and return
This is almost certainly not what was wanted, but no part of it violates the rules.

3.4 Variadic functions

The number of arguments in a function is known as the functions arity. Many functions have fixed arity, requiring the same number of arguments each time they are invoked, but sometimes it is nice to have a function that has variable arity, or a variadic function.

In C, when invoking a function of variable arity the invoking code simply follows the calling convention, putting some arguments in registers and others on the stack. The invoked function then needs to know how many arguments it received. Since it can’t tell anything without consulting at least one argument, all variadic functions in C require at least one argument, and almost all use that argument to decide how many (and what type) the other arguments are.

By far the most famous variadic function in C is printf, which is defined as

Note the trailing ... means “this is a variadic function.” Thus, printf may be invoked with any arguments you want, as long as the first is a const char * (that is, a string):

The printf function uses fairly involved rules about %s in its first argument to determine how many and what type the other arguments should be.

Writing a variadic function is somewhat complicated by the fact that the extra arguments do not have names. C provides (declared in stdarg.h) a special data type va_list and a set of special macros to use in accessing variadic arguments.

To use these, you might do something like

If you want to write variadic functions, you should

  1. Read all of man stdarg.h twice
  2. Look up variadic security vulnerabilities like the format string attack
  3. Write good tests, including too-few- and too-many- and wrong-type-argument invocations.

4 Preprocessor

Before compilers compile code, they run the C Preprocessor. This does several uninteresting tasks like removing comments, but also processes various macros and directives.

#include <somefile.h>

Looks for somefile.h in the include path, a set of directories typically including /usr/include and sometimes a few others.

Upon finding the file, it dumps its entire contents into this part of the file, as if you had copy-pasted it here.

#include "somefile.h"

Looks for somefile.h in the current source directory and, if not found there, in the include path.

Upon finding the file, it dumps its entire contents into this part of the file, as if you had copy-pasted it here.

#if expression, #else, #elif expression, and #endif

Upon encountering an #if, the preprocessor evaluates the truth of the expression, which must contain only literals (and operators) because the preprocessor is not running code. If it is false, all code from that #if to the matching #else, #elif, or #endif is removed from the source code as if you had deleted it.

#else and #elif expression behave like else and else if (expression) would in C.

#define NAME anything at all
Defines an object-like macro. Anywhere NAME appears in the source code, this tells the preprocessor to replace it with anything at all—literally those exact tokens, as if you had done a global find-and-replace in your source file.
#define NAME(a,b,c) anything including a and b and c

Defines a function-like macro. Anywhere NAME(x,y,z) appears in the source code, this tells the preprocessor to replace it with anything including x and y and z—that is, it does a find-and-replace with some parameterization.

This is a lexical replacement, not a syntactic one, so you should almost always add parentheses around each argument and around the full expression:

If you decide to become a C expert, there is more to know about macros; see https://en.wikipedia.org/wiki/C_preprocessor#Special_macros_and_directives for a reasonable overview.

#ifdef NAME, #ifndef NAME

These act like #if, except instead of checking if something is true they check if a name has been #defined (#ifdef) or not (#ifndef).

A very common use of these macros is to ensure only one copy of an .h file is included. For example, my_file.h might look like

This way if a file #include "my_file.h" twice (as, for example, because it includes two other .h files that each #include my_file.h) then the first one will define __MY_FILE_HAS_BEEN_INCLUDED__ and the second one, seeing __MY_FILE_HAS_BEEN_INCLUDED__ is already defined, will have all its contents removed by the #ifndef.

If we have something like

foo.c foo.h

the #include processing will create

The first #ifndef is true, since __FOO_H had not been defineed before that line

That means that the second #ifndef is false, since the first defined __FOO_H

__FILE__ and __LINE__

The preprocessor is guaranteed to define __FILE__ as an object-like macro expanding to the name of the current file, in quotes, like "my_file.c". The preprocessor is also guaranteed to define __LINE__ as an object-like macro expanding to the line number on which LINE appears, like 23.

These are often used in debugging messages, as e.g. printf("Error in %s on line %d\n", __FILE__, __LINE__);.

Because the preprocessor redefines these on its own on each new line of code, they have a special #line directive to change them if you need to do that (not the usual #define). The #include processing and comment removing adds such #line directives so that it does not change source line numbers.

#error "error message"
Shows error message as an error message during compilation.

Most C compilers add several other compiler-specific preprocessor directives, like #warning, #pragma message, #pragma once, #include_next, #import, etc. Each is added to simplify some common task, but also makes code harder to port to other platforms.


  1. Oddly, not always the nearest value; floating-point numbers use a “round to even” rule that sometimes rounds in a different direction than you expect in order to get the last bit of the fraction to be a 0.

  2. Defined using typedef in <types.h>