Skip to content

Cheat sheet · C

C Cheat Sheet

A scannable C reference covering the program skeleton, printf and scanf format specifiers, types and sizeof, operators, control flow, functions, arrays, strings and string.h, pointers, malloc and free, structs, enums and the preprocessor.

A cheat sheet is for the thing you have understood once and cannot quite remember the shape of. It is written to be scanned, so the common cases come first. Starting from nothing? The C exercises are the right first step; come back here once the syntax is something you are recalling rather than meeting. Looking for another language? See every cheat sheet.

A fast-scanning reference for the C you reach for constantly - not a tutorial. Each section stands on its own, so jump straight to the part you need. Snippets with a text block underneath are complete programs and that block is exactly what they print; the rest are fragments annotated with /* comments */ showing what a line produces.

Sizes and limits shown here are those of this site's target, 32-bit WebAssembly, unless a line says otherwise. Only sizeof(char) == 1 is guaranteed by the language - everything else is the platform's choice, so write sizeof rather than a number.

The Program Skeleton

C
#include <stdio.h>

int main(void) {
    printf("Hello, C.\n");
    return 0;
}
Output
Hello, C.
  • #include is a preprocessor directive - no semicolon, ever.
  • int main(void) is where execution starts. (void) means "no arguments"; plain () means "unchecked".
  • return 0; means success. Any other value signals failure to whatever launched the program.
  • Every statement ends in ;. Newlines are irrelevant to the compiler.
  • /* ... */ comments may span lines; // ... runs to the end of the line.

Headers you will include most:

C
#include <stdio.h>      /* printf, scanf, fgets, snprintf, FILE  */
#include <stdlib.h>     /* malloc, calloc, realloc, free, atoi, exit, abs */
#include <string.h>     /* strlen, strcpy, strcmp, strcat, memcpy, memset */
#include <math.h>       /* sqrt, pow, fabs, floor, ceil, round */
#include <ctype.h>      /* isdigit, isalpha, isspace, toupper, tolower */
#include <limits.h>     /* INT_MAX, INT_MIN, UINT_MAX, CHAR_BIT */
#include <stdbool.h>    /* bool, true, false */
#include <stddef.h>     /* size_t, ptrdiff_t, NULL */

printf Format Specifiers

The specifier tells printf how to interpret the bytes it was handed; it never converts. A mismatch is undefined behaviour, so get the type right.

C
%d      /* int                                  */
%u      /* unsigned int                         */
%ld     /* long          (%lu for unsigned)     */
%lld    /* long long     (%llu for unsigned)    */
%zu     /* size_t - what sizeof and strlen give */
%f      /* double, six decimals by default      */
%e      /* double, exponent form                */
%g      /* double, the shorter of %f and %e     */
%c      /* one character                        */
%s      /* string: a char * up to its '\0'      */
%x  %o  /* unsigned in lowercase hex, in octal  */
%p      /* a pointer value - never reproducible */
%%      /* a literal per cent sign              */

Width, alignment and precision go between the % and the letter:

C
#include <stdio.h>

int main(void) {
    printf("%d|%5d|%-5d|\n", 42, 42, 42);
    printf("%.3f|%8.2f|\n", 2.5, 2.5);
    printf("%s|%10s|%-10s|\n", "hi", "hi", "hi");
    printf("%c|%%|\n", 'Z');
    return 0;
}
Output
42|   42|42   |
2.500|    2.50|
hi|        hi|hi        |
Z|%|
  • %8.2f - at least 8 characters wide, exactly 2 after the point.
  • %-8s - left-aligned in a field 8 wide (the - flips the default right alignment).
  • %05d - zero-padded to width 5. %+d - always show a sign.
  • A * takes the width from an argument: printf("%*d", 6, n).

Escape sequences: \n newline, \t tab, \\ backslash, \" double quote, \' single quote, \0 the NUL byte.

printf returns the number of characters written. putchar('x') writes one character; puts("text") writes a string and a newline.

Reading Input

C
int n;
if (scanf("%d", &n) == 1) { /* one value read successfully */ }

double d;
scanf("%lf", &d);              /* NOTE: %lf for a double with scanf, not %f */

char line[64];
if (fgets(line, sizeof line, stdin) != NULL) { /* bounded - safe */ }

scanf("%s", word);             /* UNBOUNDED - can overrun `word`; avoid */

scanf needs & on the address of every non-array target, and returns how many items it assigned - check it. fgets keeps the trailing newline if it fits, so strip it: line[strcspn(line, "\n")] = '\0';

Types, sizeof and Limits

C
int      count   = 42;         /* whole number, the default choice   */
unsigned tally   = 42u;        /* whole number, never negative       */
long     big     = 42L;
double   ratio   = 0.75;       /* decimal, the default choice        */
float    small   = 0.75f;      /* half the precision; rarely worth it */
char     grade   = 'B';        /* one byte, an integer type          */
bool     ready   = true;       /* needs <stdbool.h>                  */
const int LIMIT  = 100;        /* cannot be assigned to again        */
C
#include <stdio.h>

int main(void) {
    printf("char %zu, short %zu, int %zu, long %zu\n",
           sizeof(char), sizeof(short), sizeof(int), sizeof(long));
    printf("long long %zu, float %zu, double %zu, void * %zu\n",
           sizeof(long long), sizeof(float), sizeof(double), sizeof(void *));
    return 0;
}
Output
char 1, short 2, int 4, long 4
long long 8, float 4, double 8, void * 4
C
#include <limits.h>
INT_MAX     /*  2147483647 where int is 4 bytes */
INT_MIN     /* -2147483648 */
UINT_MAX    /*  4294967295 */
CHAR_BIT    /*  bits in a byte, 8 in practice   */

Conversions:

C
(double) a / b            /* cast one operand to escape integer division */
(int) 19.99               /* 19 - truncates toward zero, never rounds    */
atoi("42");               /* 42  - no error reporting; 0 on failure      */
strtol("42", NULL, 10);   /* 42L - the checkable version                 */
round(19.5); floor(19.9); ceil(19.1);   /* <math.h>, all return double   */

Operators

C
a + b   a - b   a * b   a / b   a % b      /* % is integers only */
a++     ++a     a--     --a                /* post/pre increment and decrement */
a += b  a -= b  a *= b  a /= b  a %= b

a == b  a != b  a < b   a <= b  a > b  a >= b    /* result is int 1 or 0 */
a && b  a || b  !a                               /* left side first, short-circuits */
c ? x : y                                        /* conditional expression */

a & b   a | b   a ^ b   ~a   a << 1   a >> 1     /* bitwise */

Integer division truncates toward zero, and % takes the sign of the left operand:

C
 7 / 2    /*  3 */      7 % 2    /*  1 */
-7 / 2    /* -3 */     -7 % 2    /* -1 */
 7 / 2.0  /*  3.5 - one double operand makes it a floating-point division */

Precedence, highest first, for the parts that bite: postfix ++ -- and . ->; then unary ! ~ - * & (cast) sizeof; then * / %; then + -; then << >>; then relational; then == !=; then &, ^, |; then &&, then ||; then ?:; then assignment; then comma. When in doubt, parenthesise - (a & mask) == 0 is not the same as a & (mask == 0).

Control Flow

Zero is false; every other value is true. There is no separate boolean at the core of the language.

C
if (x > 10) {
    /* ... */
} else if (x > 5) {
    /* ... */
} else {
    /* ... */
}

switch (ch) {                  /* ch must be an integer or character type */
    case 'a':
    case 'b':                  /* shared body: 'a' falls through to 'b'   */
        handle_letter();
        break;                 /* without break, control runs into the next case */
    default:
        handle_other();
        break;
}

while (n > 0)      { n--; }              /* test first  */
do                 { n--; } while (n > 0);   /* body first, at least once; note the ; */
for (int i = 0; i < n; i++) { /* ... */ }    /* the array-walking shape  */

break;      /* leave the innermost loop or switch */
continue;   /* skip to the next pass of the loop  */

Traps: if (x = 3) assigns and is always true - use ==. if (0 <= x <= 10) is always true - use x >= 0 && x <= 10. A stray semicolon, as in if (x > 0);, makes the block below unconditional.

Functions

C
int  area(int w, int h);              /* prototype: header plus a semicolon */
void log_line(const char *text);      /* void = returns nothing             */
int  main(void);                      /* void = takes nothing               */

int area(int w, int h) {
    return w * h;
}
  • The compiler must have seen a prototype or the definition before the first call.
  • Every parameter needs its own type: int f(int a, int b), never int f(int a, b).
  • Arguments are copied. A function cannot change a caller's variable unless it is given a pointer.
  • Every path in a non-void function must return a value.
  • static on a function limits it to the current file; static on a local keeps it alive between calls.

Arrays

C
int a[5] = {1, 2, 3, 4, 5};      /* indexes 0..4                          */
int b[5] = {1, 2};               /* remaining elements are zero           */
int c[5] = {0};                  /* all zero                              */
int d[]  = {1, 2, 3};            /* size taken from the initialiser: 3    */
int e[5];                        /* INDETERMINATE contents - do not read  */

a[0] = 9;                        /* first  */
a[4] = 9;                        /* last; a[5] is out of bounds           */

size_t n = sizeof a / sizeof a[0];   /* 5 - only where the declaration is in scope */

int grid[2][3] = {{1, 2, 3}, {4, 5, 6}};
grid[1][2];                      /* 6 - both indexes count from zero      */

There is no bounds checking, at compile time or run time. Out-of-range access is undefined behaviour and in a WebAssembly sandbox it will often appear to work while quietly damaging something else.

Passing an array passes a pointer, so the length must travel with it:

C
int sum_of(const int *data, int length);   /* same as int data[] */
sum_of(a, 5);
sum_of(a + 2, 3);          /* any position can be the start of a shorter array */

Strings

A string is a char array ending in a '\0' byte. Everything follows from that.

C
char word[] = "cargo";     /* 6 bytes: 'c','a','r','g','o','\0' */
strlen(word);              /* 5 - characters before the terminator (a scan) */
sizeof word;               /* 6 - bytes of storage (compile time)           */
word[0];                   /* 'c' */
word[4] = '\0';            /* string is now "carg"; the array is unchanged   */

const char *literal = "cargo";   /* const: literals must not be modified    */
char buffer[32];                 /* room for 31 characters plus terminator  */
C
#include <string.h>

strlen(s);                       /* length, excluding the terminator        */
strcpy(dst, src);                /* copy including terminator - UNBOUNDED   */
strcat(dst, src);                /* append - UNBOUNDED                      */
strcmp(a, b);                    /* 0 equal, <0 a sorts first, >0 b first   */
strncmp(a, b, n);                /* compare at most n characters            */
strchr(s, 'x');                  /* pointer to first 'x', or NULL           */
strstr(s, "needle");             /* pointer to first match, or NULL         */
strcspn(s, "\n");                /* characters before any of those bytes    */
memcpy(dst, src, n);             /* copy n bytes; regions must not overlap  */
memmove(dst, src, n);            /* copy n bytes; overlap is fine           */
memset(dst, 0, n);               /* fill n bytes with a value               */

Bounded alternatives, which is what to reach for:

C
snprintf(buffer, sizeof buffer, "%s-%d", name, id);   /* never overruns, always terminates */
fgets(buffer, sizeof buffer, stdin);                  /* bounded read                      */

Rules that prevent the classic bugs:

  • A buffer of n bytes holds at most n - 1 characters. Always leave room for the terminator.
  • buffer = "text"; is not a copy and not legal for an array - use strcpy or snprintf.
  • a == b on two char * compares addresses. Compare text with strcmp(a, b) == 0.
  • After building a string by hand, write the '\0' yourself.
  • strcpy, strcat and scanf("%s", ...) take no destination size. That is what a buffer overrun is: a write whose length came from the data rather than from the space available.

Pointers

C
int count = 42;
int *p = &count;      /* & takes an address; int * holds one        */
*p;                   /* 42 - dereference: the value at that address */
*p = 7;               /* count is now 7                              */
p = NULL;             /* points at nothing; testable                 */

sizeof(int *);        /* 4 here; every pointer is the same size      */
C
int a[5] = {10, 20, 30, 40, 50};
int *q = a;           /* no & needed: an array decays to a pointer   */
*(q + 1);             /* 20 - +1 moves one ELEMENT, not one byte     */
q[2];                 /* 30 - p[i] is defined as *(p + i)            */
q++;                  /* advance one element                         */
&a[4] - a;            /* 4 - subtracting pointers gives an element count */
a + 5;                /* one past the end: legal to hold, never to dereference */

Letting a function change a caller's variable:

C
void add_one(int *target) { *target = *target + 1; }
add_one(&tally);

void swap(int *a, int *b) { int t = *a; *a = *b; *b = t; }
swap(&x, &y);

const on a pointer parameter says the function will not write through it, and the compiler enforces it:

C
int  sum_of(const int *data, int n);     /* reads only            */
void fill  (int *data, int n);           /* writes                */

Rules:

  • Initialise every pointer - to a real address or to NULL.
  • Check any pointer you did not create before the first dereference.
  • Set a pointer to NULL the moment it stops being valid.
  • Dereferencing NULL is undefined behaviour. In a WebAssembly sandbox it does not reliably crash, so "it ran" is never evidence that a pointer was valid.
  • Never return a pointer to a local variable; the local is gone when the function returns.

Dynamic Memory

C
#include <stdlib.h>

int *p = malloc(n * sizeof *p);      /* n elements, INDETERMINATE contents */
int *z = calloc(n, sizeof *z);       /* n elements, all zero               */

if (p == NULL) { /* handle failure before using p */ }

int *bigger = realloc(p, 2 * n * sizeof *p);   /* into a TEMPORARY */
if (bigger != NULL) { p = bigger; }            /* else p is still valid */

free(p);
p = NULL;           /* so a later mistake is testable  */
free(NULL);         /* defined to do nothing           */
  • sizeof *p means "one of whatever p points at" and stays correct if the type changes.
  • Check for NULL before the first use, every time.
  • Exactly one free per successful allocation, with exactly the address you were given.
  • A string needs strlen(s) + 1 bytes. The + 1 is the terminator.

The four failure modes, with their names:

  • Memory leak - allocated and never freed. The block stays reserved and is now unreachable.
  • Use after free - reading or writing through a pointer whose block has already been freed.
  • Double free - calling free twice on the same block. It corrupts the allocator's own bookkeeping, so the damage usually surfaces somewhere unrelated.
  • Invalid free - freeing something malloc never returned, including a pointer into the middle of a block after p++.

All four except the leak are undefined behaviour, and none of them reliably announces itself. Write the free at the same moment you write the malloc, and make sure every early return after a successful allocation releases it.

Structs, typedef and enum

C
struct Point { int x; int y; };            /* note the closing semicolon */
struct Point a = {3, 4};
struct Point b = {.x = 3, .y = 4};         /* designated initialisers, any order */
struct Point zero = {0};                   /* every member zeroed                */

a.x;                                       /* member of a struct          */
struct Point *p = &a;
p->y;                                      /* member through a pointer; same as (*p).y */
C
typedef struct {
    char code[8];
    int quantity;
    double unit_price;
} Item;                                    /* now the type is just `Item` */

Item i = {"BLT-10", 250, 0.75};
Item copy = i;                             /* assignment copies EVERY member */
C
#include <stdio.h>
#include <string.h>

typedef struct {
    char name[12];
    int score;
} Entry;

int total_score(const Entry *entries, int count) {
    int total = 0;

    for (int i = 0; i < count; i++) {
        total += entries[i].score;
    }
    return total;
}

int main(void) {
    Entry board[3] = {{"ana", 12}, {"bruno", 30}, {"chidi", 7}};

    for (int i = 0; i < 3; i++) {
        printf("%-6s %3d\n", board[i].name, board[i].score);
    }

    printf("total %d\n", total_score(board, 3));
    printf("name length %zu\n", strlen(board[1].name));
    return 0;
}
Output
ana     12
bruno   30
chidi    7
total 49
name length 5
  • Structs are copied on assignment, on being passed, and on being returned. Pass a const Type * to avoid the copy when the function only reads.
  • sizeof a struct is at least the sum of its members and often more, because of padding. Never work it out by hand, and never compare two structs by their bytes.
  • There is no == for structs. Compare the members you care about.
  • A char array member cannot be assigned from a string: use strcpy, or initialise at declaration.
C
enum Status { PENDING, ACTIVE, CLOSED };       /* 0, 1, 2 by default */
enum Status s = ACTIVE;                        /* usable in a switch as a case label */

typedef enum { LOW = 1, MEDIUM = 5, HIGH = 10 } Priority;

The Preprocessor

C
#include <stdio.h>          /* a system header  */
#include "helpers.h"        /* one of your own  */

#define MAX_ITEMS 100                       /* a plain text substitution */
#define SQUARE(x) ((x) * (x))               /* parenthesise EVERY parameter */

#ifndef HELPERS_H                           /* include guard, top of a header */
#define HELPERS_H
/* ... declarations ... */
#endif

#ifdef DEBUG
    printf("trace\n");
#endif

A macro is text, not a function: #define DOUBLE(x) x * 2 makes DOUBLE(1 + 1) expand to 1 + 1 * 2, which is 3. Wrap the whole body and every parameter in parentheses, and prefer const variables or real functions wherever they will do the job.

Things That Bite

  • Integer division. 3 / 4 is 0. Cast one operand: (double) 3 / 4.
  • = instead of == in a condition. Legal, always true, silent.
  • A wrong format specifier. Undefined behaviour, not a conversion. %d for int, %f for double, %zu for sizeof, %lf for a double with scanf.
  • Off-by-one. Valid indexes are 0 to n - 1. i <= n in a loop over n items runs once too many.
  • strlen versus sizeof. One counts characters and scans; the other counts bytes and is free. Allocate with sizeof, iterate with strlen.
  • Unbounded copies. strcpy, strcat, sprintf, scanf("%s", ...). Use snprintf and fgets.
  • Uninitialised anything. Locals and malloc blocks have indeterminate contents. Globals and calloc blocks are zeroed.
  • Signed overflow. Undefined behaviour, not wrapping. Unsigned overflow is defined to wrap.
  • Comparing doubles with ==. Compare fabs(a - b) < tolerance instead.
  • Undefined behaviour that looks fine. Out-of-bounds reads, null dereferences and use-after-free all frequently appear to work, especially in a sandbox. A run that produced no complaint has proved nothing.

Try any snippet with your own values in the C playground.