C is small, fast and close to the machine: you manage memory yourself, and the
compiler trusts you completely. The reference below is grouped by what you are
trying to do, and the filter box searches all of it at once. Type malloc and
every memory function on the page comes to you, or type C23 to see what the
latest standard added.
Every snippet is checked against C23 (ISO/IEC 9899:2024) with GCC 15,
which uses C23 by default. Anything that needs a standard newer than C99 says so
in the notes column, so you can tell at a glance whether it will build with the
compiler you have. Names like buf and count are placeholders for your own. If
you do not have GCC installed, the official Docker image is the quickest way to
try something: docker run --rm -it -v "$PWD":/src -w /src gcc:15 bash, and the
Docker cheat sheet has the rest.
Searches the task, the command and the third column. Press / from anywhere on the page.
233 commands
Compiling with gcc
| Task | Command | Notes |
|---|---|---|
| Check which version you have | gcc --version | |
| Compile one file into a program | gcc main.c -o main | Without -o the program is called a.out |
| Run it | ./main | |
| Turn on the warnings worth having | gcc -Wall -Wextra -Wpedantic main.c -o main | -Wall is not all of them, despite the name |
| Treat every warning as an error | gcc -Wall -Wextra -Werror main.c -o main | |
| Pick a C standard | gcc -std=c23 main.c -o main | Or c17, c11, c99. GCC 15 defaults to gnu23 |
| Build for a debugger | gcc -g -O0 main.c -o main | Keeps variable names and line numbers |
| Build optimised | gcc -O2 main.c -o main | -O3 is more aggressive, -Os favours size |
| Catch memory bugs as they happen | gcc -g -fsanitize=address,undefined main.c -o main | Out-of-bounds reads, use after free, overflow |
| Compile several files into one program | gcc main.c list.c -o app | |
| Compile only, without linking | gcc -c list.c | Writes list.o |
| Link object files | gcc main.o list.o -o app | |
| Link the maths library | gcc main.c -o main -lm | Libraries go after the files that use them |
| Look for headers in another folder | gcc -I include main.c -o main | |
| Link a library from another folder | gcc main.c -L lib -lfoo -o main | Finds lib/libfoo.so or lib/libfoo.a |
| Define a macro from the command line | gcc -DDEBUG -DLEVEL=2 main.c -o main | |
| See the code after the preprocessor | gcc -E main.c | |
| See the assembly | gcc -S -O2 main.c | Writes main.s |
| Bundle objects into a static library | ar rcs libutil.a util.o | Link it with -L. -lutil |
| Build a shared library | gcc -shared -fPIC util.c -o libutil.so | |
| Step through it in a debugger | gdb ./main | break main, run, next, print x |
| Find memory leaks | valgrind --leak-check=full ./main | Linux. Build with -g first |
On macOS, gcc is Apple's clang under another name unless you install GCC from Homebrew. Every flag on this page works with clang as well.
Types and sizes
| Type | Example | Size and notes |
|---|---|---|
| Character or tiny integer | char c = 'A'; | 1 byte. Signed or unsigned depends on the platform |
| Raw byte | unsigned char byte = 0xFF; | 1 byte, 0 to 255 |
| Short integer | short s = -300; | 2 bytes |
| Integer | int n = 42; | 4 bytes. The standard only promises 16 bits |
| Long integer | long big = 100000L; | 8 bytes, but 4 on 64-bit Windows |
| Long long integer | long long huge = 9000000000LL; | 8 bytes |
| Unsigned integer | unsigned int u = 42u; | Same size, never negative, wraps at the top |
| Single-precision float | float f = 3.14f; | 4 bytes, about 7 digits. Leave off f and it is a double |
| Double-precision float | double d = 3.14; | 8 bytes, about 15 digits. The default for decimals |
| Extended float | long double ld = 3.14L; | 8 to 16 bytes depending on the platform |
| Boolean | bool done = false; | C23 keyword. Before C23, #include <stdbool.h> |
| Exact-width integer | int32_t id = 7; uint64_t mask = 0; | #include <stdint.h> |
| Size of an object or array | size_t len = sizeof arr; | Unsigned. Print with %zu |
| Size of a type | sizeof(int) | Brackets needed for a type, optional for a variable |
| Pointer | int *p = &n; | 8 bytes on a 64-bit system |
| Null pointer | int *p = nullptr; | C23. NULL before that |
| Read-only variable | const double rate = 0.2; | |
| Compile-time constant | constexpr int MAX_USERS = 100; | C23. #define or enum before that |
| Named integer constants | enum color { RED, GREEN, BLUE }; | 0, 1 and 2 unless you set them |
| Give a type another name | typedef unsigned long ulong; | |
| Let the compiler infer the type | auto ratio = 1.5; | C23. Needs an initialiser |
| Same type as another variable | typeof(x) y = x; | C23 |
| Hex, octal and binary literals | 0xFF 0755 0b1010 | Binary is C23. A leading 0 means octal, so 010 is 8 |
| Digit separators | int million = 1'000'000; | C23 |
| Largest and smallest int | INT_MAX INT_MIN LLONG_MAX | #include <limits.h> |
| Convert to another type | (double)total / count | Cast one side, or the division is whole-number |
The standard fixes minimum sizes, not exact ones. When the width matters, for file formats, network protocols or hashing, use int32_t, uint8_t and friends from stdint.h rather than guessing what int is on the machine it will run on.
printf and scanf formats
| Code | Notes | |
|---|---|---|
| int | printf("%d\n", n); | |
| unsigned int | printf("%u\n", u); | |
| long and long long | printf("%ld %lld\n", l, ll); | |
| size_t | printf("%zu\n", sizeof arr); | |
| int64_t and friends | printf("%" PRId64 "\n", big); | #include <inttypes.h>. PRIu32, PRIx64 too |
| double or float | printf("%f\n", d); | Six decimal places by default |
| Two decimal places | printf("%.2f\n", price); | |
| Scientific or shortest form | printf("%e %g\n", d, d); | |
| A character | printf("%c\n", c); | |
| A string | printf("%s\n", name); | |
| First n characters of a string | printf("%.3s\n", name); | |
| A pointer's address | printf("%p\n", (void *)p); | Cast to void * first |
| Hex and octal | printf("%x %X %o\n", n, n, n); | %#x adds the 0x |
| Binary | printf("%b\n", n); | C23. glibc 2.35+ |
| A literal percent sign | printf("100%%\n"); | |
| Pad to a width | printf("%5d|%-10s|\n", n, name); | Minus means left-align |
| Pad with zeros | printf("%05d\n", 42); | Prints 00042 |
| Width from an argument | printf("%*d\n", width, n); | |
| Print to stderr | fprintf(stderr, "error: %s\n", msg); | |
| Read an int | if (scanf("%d", &n) != 1) { /* bad input */ } | Pass the address, and check the return value |
| Read a double | scanf("%lf", &d); | %lf in scanf, but %f in printf |
| Read one word, safely | scanf("%63s", word); | Width one less than the buffer |
A format that does not match its argument is undefined behaviour, not a compile error. -Wall catches most mismatches, which is one more reason to always build with it.
Operators
| Task | Code | Notes |
|---|---|---|
| Arithmetic | a + b a - b a * b a / b | |
| Whole-number division | 7 / 2 | 3. Both sides are ints, so it truncates toward zero |
| Remainder | 7 % 3 | 1. -7 % 3 is -1: the sign follows the left side |
| Add one before or after reading | ++i i++ | Only differs inside a bigger expression |
| Update in place | x += 5; x *= 2; x %= 3; | |
| Compare | a == b a != b a < b a >= b | Gives int 1 or 0 |
| And, or, not | a && b a || b !a | Stops as soon as the answer is known |
| Pick one of two values | int max = a > b ? a : b; | |
| Bitwise and, or, xor, not | a & b a | b a ^ b ~a | |
| Shift bits | 1u << 4 n >> 1 | Shift unsigned values. Shifting by the type's width or more is undefined |
| Set a bit | flags |= 1u << 3; | |
| Clear a bit | flags &= ~(1u << 3); | |
| Flip a bit | flags ^= 1u << 3; | |
| Test a bit | if (flags & (1u << 3)) { } | |
| Address of a variable | &x | |
| Value a pointer points to | *p | |
| Struct member | point.x | |
| Struct member through a pointer | ptr->x | Same as (*ptr).x |
The bitwise operators bind more loosely than comparison, so flags & MASK == 0 means flags & (MASK == 0). Bracket every bitwise test: (flags & MASK) == 0.
Control flow
| Task | Code | Notes |
|---|---|---|
| If, else if, else | if (a) { } else if (b) { } else { } | Any non-zero value counts as true |
| Switch | switch (c) { case 'y': yes(); break; default: no(); } | Integers and chars only, not strings |
| Several cases, one branch | case 'a': case 'e': case 'i': vowels++; break; | |
| Fall through on purpose | [[fallthrough]]; | C23. Silences the warning |
| Counting loop | for (int i = 0; i < n; i++) { } | Declaring i in the loop is C99+ |
| Loop over an array | for (size_t i = 0; i < sizeof arr / sizeof arr[0]; i++) { } | |
| While loop | while (n > 0) { n /= 10; } | |
| Run at least once | do { } while (retry); | |
| Loop forever | for (;;) { } | Or while (true) |
| Skip to the next iteration | continue; | |
| Leave the loop | break; | Only the innermost loop or switch |
| Jump to cleanup code | goto cleanup; cleanup: free(buf); | The accepted use of goto in C |
| Mark code that cannot be reached | unreachable(); | C23. #include <stddef.h>. Undefined if it is reached |
C has no exceptions. Functions report failure through their return value, often -1 or NULL, and set errno for the detail. Check every return value that can fail, and use goto cleanup to free what you allocated on the way out.
Functions
| Task | Code | Notes |
|---|---|---|
| Declare a function (prototype) | int add(int a, int b); | Usually in a header, before any call |
| Define it | int add(int a, int b) { return a + b; } | |
| Take no arguments | int roll(void); | In C23 () means the same. Before that it meant any arguments |
| Return nothing | void log_line(const char *msg); | |
| Let the function change a variable | void inc(int *n) { (*n)++; } inc(&count); | C passes everything by value, so pass a pointer |
| Take an array | int sum(const int *nums, size_t count); | The array arrives as a pointer, so pass its length too |
| Return several values | bool divide(int a, int b, int *quotient, int *remainder); | Out parameters, plus a success flag |
| Visible in this file only | static int helper(void) { } | |
| Keep a value between calls | static int calls = 0; | Inside the function. Starts at zero once |
| Program entry point | int main(void) | Or int main(int argc, char *argv[]) |
| Command-line arguments | argv[0] argv[1] argc | argv[0] is the program name |
| End the program with a status | return EXIT_SUCCESS; exit(EXIT_FAILURE); | #include <stdlib.h> |
| Pointer to a function | int (*op)(int, int) = add; op(2, 3); | |
| Name a function pointer type | typedef int (*binop)(int, int); | |
| Any number of arguments | int sum_all(int count, ...); | Read them with va_list from <stdarg.h> |
| Warn if the result is ignored | [[nodiscard]] int parse(const char *s); | C23 |
| Never returns | [[noreturn]] void die(const char *msg); | C23. _Noreturn in C11 |
| Suggest inlining | static inline int square(int x) { return x * x; } | C99+. Goes in a header |
Pointers and arrays
A pointer holds the address of something else. An array is a fixed block of values side by side, and in most expressions its name turns into a pointer to its first element.
| Task | Code | Notes |
|---|---|---|
| Declare an array | int nums[5] = {1, 2, 3, 4, 5}; | |
| Let the initialiser set the length | int nums[] = {1, 2, 3}; | |
| Zero every element | int counts[100] = {}; | C23. {0} works in every version |
| Set chosen elements | int a[10] = {[2] = 5, [9] = 1}; | The rest are zero |
| Count the elements | sizeof nums / sizeof nums[0] | Only on the array itself, not on a pointer to it |
| Two-dimensional array | int grid[3][4]; grid[row][col] = 1; | |
| Take an address | int *p = &x; | |
| Read or write through a pointer | *p = 10; | |
| Point at an array's first element | int *p = nums; | Same as &nums[0] |
| Index through a pointer | p[2] | Same as *(p + 2) |
| Move to the next element | p++; | Moves by sizeof(int) bytes, not 1 |
| Distance between two pointers | ptrdiff_t gap = end - start; | In elements. Same array only |
| Pointer to a pointer | int **pp = &p; | |
| Cannot change the value pointed to | const int *p = &x; | |
| Cannot point anywhere else | int *const p = &x; | |
| Pointer to anything | void *any = &x; | Cast it back before you dereference it |
| Check for null | if (p != nullptr) { } | Or if (p) |
| Array whose length is known at run time | int buf[n]; | Optional since C11. Avoid for a large or untrusted n |
| Unnamed array in an expression | sum((int[]){1, 2, 3}, 3) | Compound literal, C99+ |
Reading past the end of an array, through a null pointer or after free is undefined behaviour. Nothing checks it for you. The program might crash, carry on with garbage, or work until the day it ships. Build with -fsanitize=address while developing.
Strings
A C string is an array of char ending in a zero byte, written '\0'. Every function here finds the end by looking for that byte, so a string without one is a bug waiting to read the rest of memory.
| Task | Code | Notes |
|---|---|---|
| A string you can change | char name[] = "Ada"; | 4 bytes: A, d, a and the '\0' |
| A string you only read | const char *msg = "hello"; | String literals are read-only |
| A fixed-size buffer | char buf[64]; | |
| Length | strlen(s) | #include <string.h>. Does not count the '\0' |
| Are they equal | strcmp(a, b) == 0 | a == b compares addresses, not text |
| Compare the first n characters | strncmp(line, "GET ", 4) == 0 | |
| Copy into a buffer, safely | snprintf(dst, sizeof dst, "%s", src); | Always ends with '\0', cuts off what does not fit |
| Copy when you know it fits | strcpy(dst, src); | No bounds check at all |
| Append | strcat(dst, src); | dst needs room for both plus '\0' |
| Build a string from parts | snprintf(buf, sizeof buf, "%s-%d", name, id); | Returns the length it wanted, so you can spot truncation |
| Find a character | strchr(email, '@') | Pointer to it, or NULL |
| Find the last occurrence | strrchr(path, '/') | |
| Find a substring | strstr(s, "cat") | Pointer to the match, or NULL |
| Length of the part before any of these | strcspn(s, " \t") | |
| Remove the newline fgets kept | line[strcspn(line, "\n")] = '\0'; | |
| Text to a number, with error checks | long n = strtol(s, &end, 10); | end points past the digits. strtod for doubles |
| Text to an int, quick and trusting | atoi(s) | Returns 0 for bad input, with no way to tell |
| Copy into new memory | char *copy = strdup(s); | C23 in the standard, POSIX before. free it |
| Split on a separator | for (char *t = strtok(line, ","); t; t = strtok(nullptr, ",")) { } | Writes into line, and is not thread-safe |
| Test a character | isdigit(c) isalpha(c) isspace(c) | #include <ctype.h>. Pass an unsigned char value |
| Change case | toupper(c) tolower(c) | One character at a time |
Structs and unions
| Task | Code | Notes |
|---|---|---|
| Define a struct | struct point { int x; int y; }; | |
| Create one | struct point p = {1, 2}; | |
| Create one by field name | struct point p = {.x = 1, .y = 2}; | C99+. Fields you leave out are zero |
| Skip the struct keyword | typedef struct { int x; int y; } Point; Point p; | |
| Read a field | p.x | |
| Read a field through a pointer | ptr->x | Same as (*ptr).x |
| Copy one | struct point q = p; | Copies every field. Pointers inside are shared |
| Assign a whole struct at once | p = (struct point){.x = 5, .y = 6}; | Compound literal, C99+ |
| Compare two | a.x == b.x && a.y == b.y | == does not work on structs |
| Pass without copying | void move(struct point *p, int dx); | |
| Pass read-only without copying | double length(const struct point *p); | |
| Array of structs | struct point path[10]; path[0].x = 3; | |
| Struct that points to its own type | struct node { int value; struct node *next; }; | The building block of linked lists and trees |
| Variable-length data on the end | struct message { size_t len; char data[]; }; | Flexible array member. Allocate sizeof + len |
| Fields a few bits wide | struct flags { unsigned ready : 1; unsigned mode : 3; }; | Bit fields. Layout is up to the compiler |
| Where a field sits | offsetof(struct point, y) | #include <stddef.h> |
| Define a union | union number { int i; float f; }; | All fields share one piece of memory |
| Union that knows what it holds | struct value { enum { INT, FLOAT } kind; union { int i; float f; }; }; | Tagged union. Anonymous union is C11+ |
sizeof a struct can be bigger than its fields added up, because the compiler pads fields to their alignment. Ordering fields from largest to smallest usually shrinks it.
Dynamic memory (malloc and free)
Memory from malloc lives until you free it, not until the function returns. Everything here needs #include <stdlib.h>, apart from the mem functions, which are in string.h.
| Task | Code | Notes |
|---|---|---|
| Allocate an array | int *nums = malloc(count * sizeof *nums); | Contents are garbage until you set them |
| Check it worked | if (nums == nullptr) { /* out of memory */ } | malloc returns NULL on failure |
| Allocate and zero | int *nums = calloc(count, sizeof *nums); | Also checks count * size for overflow |
| Allocate one struct | struct node *n = malloc(sizeof *n); | |
| Room for a copy of a string | char *copy = malloc(strlen(s) + 1); | + 1 for the '\0' |
| Grow or shrink | int *tmp = realloc(nums, new_count * sizeof *nums); | May move the block. Old pointer is invalid if it did |
| Keep the old block if growing fails | if (tmp) { nums = tmp; } | Never nums = realloc(nums, ...) |
| Give it back | free(nums); nums = nullptr; | Nulling it stops a second free doing damage |
| Free nothing | free(nullptr); | Allowed, and does nothing |
| Copy bytes | memcpy(dst, src, count * sizeof *src); | The two must not overlap |
| Copy bytes that might overlap | memmove(dst, src, count * sizeof *src); | |
| Fill with a byte value | memset(buf, 0, sizeof buf); | Byte by byte, so only 0 or -1 make sense for ints |
| Compare bytes | memcmp(a, b, size) == 0 | |
| Allocate aligned memory | aligned_alloc(64, size) | C11. Release it with free |
Every malloc, calloc, realloc and strdup needs exactly one free. Two frees of the same pointer, or using it after the free, is undefined behaviour. Writing sizeof *nums rather than sizeof(int) keeps the size right if the type of nums ever changes.
Preprocessor
The preprocessor runs before the compiler and works on text. It knows nothing about types or scope, which is where most macro surprises come from.
| Task | Code | Notes |
|---|---|---|
| Include a standard header | #include <stdio.h> | |
| Include your own header | #include "list.h" | Searched for next to the current file first |
| Define a constant | #define MAX_LINE 256 | No semicolon and no = |
| Function-like macro | #define SQUARE(x) ((x) * (x)) | Bracket every use of x and the whole body |
| Macro with several statements | #define SWAP(a, b) do { int t = (a); (a) = (b); (b) = t; } while (0) | do-while(0) makes it safe after an if |
| Remove a definition | #undef MAX_LINE | |
| Compile only when defined | #ifdef DEBUG #endif | Pair with gcc -DDEBUG |
| Compile only when not defined | #ifndef NDEBUG #endif | |
| Else if defined | #elifdef _WIN32 | C23. #elif defined(_WIN32) before that |
| Test a value | #if LEVEL > 1 #elif LEVEL == 1 #else #endif | |
| Header guard | #ifndef LIST_H #define LIST_H ... #endif | Stops a header being included twice |
| Header guard, short form | #pragma once | Not in the standard, but GCC, Clang and MSVC all support it |
| Turn an argument into a string | #define STR(x) #x | STR(hello) becomes "hello" |
| Glue two tokens together | #define CAT(a, b) a##b | |
| Current file, line and function | __FILE__ __LINE__ __func__ | __func__ is a variable, not a macro |
| Which C standard is this | __STDC_VERSION__ | 202311L for C23, 201710L for C17 |
| Stop the build with a message | #error "Needs a 64-bit platform" | |
| Warn during the build | #warning "Deprecated header" | C23 |
| Check something at compile time | static_assert(sizeof(int) == 4); | C23 keyword. _Static_assert with a message in C11 |
| Embed a file's bytes in an array | #embed "icon.png" | C23, GCC 15+. On its own line inside the array's braces |
SQUARE(i++) expands to ((i++) * (i++)), which changes i twice and is undefined. A macro argument can be evaluated more than once, so never pass one anything with a side effect. A static inline function avoids the problem entirely.
Common standard library functions
| Task | Code | Notes |
|---|---|---|
| Print a line | puts("done"); | stdio.h. Adds the newline for you |
| Read a line of input | fgets(buf, sizeof buf, stdin) | stdio.h. NULL at end of input |
| Open a file | FILE *f = fopen("data.txt", "r"); | stdio.h. NULL on failure |
| File modes | "r" "w" "a" "r+" "rb" "wb" | w empties the file first. b for binary |
| Write to a file | fprintf(f, "%d\n", n); | |
| Read binary data | size_t got = fread(buf, 1, sizeof buf, f); | Returns how many items it read |
| Write binary data | fwrite(buf, 1, len, f); | |
| Close a file | fclose(f); | Flushes anything still buffered |
| Print why the last call failed | perror("fopen"); | stdio.h. Uses errno |
| Error message for an errno value | strerror(errno) | string.h and errno.h |
| Sort an array | qsort(arr, count, sizeof arr[0], compare); | stdlib.h. Example below |
| Search a sorted array | bsearch(&key, arr, count, sizeof arr[0], compare) | stdlib.h. Pointer to the match, or NULL |
| Absolute value | abs(n) labs(l) fabs(d) | stdlib.h for ints, math.h for fabs |
| Square root and power | sqrt(x) pow(x, 2) | math.h. Link with -lm |
| Round | floor(x) ceil(x) round(x) trunc(x) | math.h. Return doubles |
| Random number from 1 to 6 | rand() % 6 + 1 | stdlib.h. Seed once with srand. Not for security |
| Seed the random numbers | srand((unsigned)time(nullptr)); | time.h |
| Current time in seconds | time_t now = time(nullptr); | time.h |
| Time a piece of code | clock_t start = clock(); (double)(clock() - start) / CLOCKS_PER_SEC | time.h. CPU time, not wall-clock time |
| Read an environment variable | const char *home = getenv("HOME"); | stdlib.h. NULL if it is not set |
| Check an assumption while developing | assert(count > 0); | assert.h. Removed when built with -DNDEBUG |
| Exit from anywhere | exit(EXIT_FAILURE); | stdlib.h. Flushes and closes open files |
A growable array, start to finish
Structs, malloc through realloc, error checks and qsort in one program. It
grows the buffer by doubling, which is how most dynamic arrays work under the hood.
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int *items;
size_t len;
size_t cap;
} IntList;
// Returns false when memory runs out, and leaves the list as it was.
bool list_push(IntList *list, int value)
{
if (list->len == list->cap) {
size_t new_cap = list->cap ? list->cap * 2 : 8;
int *grown = realloc(list->items, new_cap * sizeof *grown);
if (grown == nullptr) {
return false;
}
list->items = grown;
list->cap = new_cap;
}
list->items[list->len++] = value;
return true;
}
void list_free(IntList *list)
{
free(list->items);
*list = (IntList){};
}
int compare_ints(const void *a, const void *b)
{
int x = *(const int *)a;
int y = *(const int *)b;
return (x > y) - (x < y); // not x - y, which can overflow
}
int main(void)
{
IntList scores = {}; // C23: empty braces set every member to zero
int input[] = {42, 7, 19, 88, 3};
for (size_t i = 0; i < sizeof input / sizeof input[0]; i++) {
if (!list_push(&scores, input[i])) {
fprintf(stderr, "out of memory\n");
list_free(&scores);
return EXIT_FAILURE;
}
}
qsort(scores.items, scores.len, sizeof scores.items[0], compare_ints);
for (size_t i = 0; i < scores.len; i++) {
printf("%d ", scores.items[i]);
}
printf("\n"); // 3 7 19 42 88
list_free(&scores);
return EXIT_SUCCESS;
}Build it with the sanitisers on while you are still changing it:
gcc -std=c23 -Wall -Wextra -g -fsanitize=address,undefined scores.c -o scores
./scoresrealloc(nullptr, size) behaves like malloc, which is why the first push needs
no special case. The standard does not say which algorithm qsort uses, or promise
that it is stable. C libraries have shipped variants of
quick sort and
merge sort, and both are on the
site as step-through visualisations.
Pointers, start to finish
A function can only change a caller's variable if it is given the variable's address. An array passed to a function arrives as a pointer to its first element, so the length has to travel with it.
#include <stdio.h>
void swap(int *a, int *b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
int sum(const int *nums, size_t count)
{
int total = 0;
for (size_t i = 0; i < count; i++) {
total += nums[i]; // same as *(nums + i)
}
return total;
}
int main(void)
{
int x = 1;
int y = 2;
swap(&x, &y);
printf("%d %d\n", x, y); // 2 1
int nums[] = {10, 20, 30};
size_t count = sizeof nums / sizeof nums[0]; // 3, only works on the array itself
printf("%d\n", sum(nums, count)); // 60
return 0;
}Inside sum, sizeof nums would give 8, the size of a pointer, not 12. That is
the most common way the element-count trick goes wrong.
Splitting a program across files
Declarations go in a header, definitions in a .c file, and every file that
uses them includes the header. The header guard stops it being pasted in twice.
// point.h
#ifndef POINT_H
#define POINT_H
typedef struct {
double x;
double y;
} Point;
double point_distance(Point a, Point b);
#endif// point.c
#include <math.h>
#include "point.h"
double point_distance(Point a, Point b)
{
return hypot(b.x - a.x, b.y - a.y);
}// main.c
#include <stdio.h>
#include "point.h"
int main(void)
{
Point a = {.x = 0, .y = 0};
Point b = {.x = 3, .y = 4};
printf("%.1f\n", point_distance(a, b)); // 5.0
return 0;
}Compile each .c file to an object file, then link them. Only files that changed
need compiling again, which is what make automates.
gcc -Wall -Wextra -c point.c
gcc -Wall -Wextra -c main.c
gcc main.o point.o -o app -lm
./appCommit the .c and .h files and add *.o and the program to .gitignore. The
Git cheat sheet covers the rest of that workflow.
Reading a file line by line
Open, check, loop with fgets, close. strerror(errno) turns the failure into a
message like "No such file or directory".
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[])
{
if (argc != 2) {
fprintf(stderr, "usage: %s FILE\n", argv[0]);
return EXIT_FAILURE;
}
FILE *f = fopen(argv[1], "r");
if (f == nullptr) {
fprintf(stderr, "%s: %s\n", argv[1], strerror(errno));
return EXIT_FAILURE;
}
char line[256];
int number = 0;
while (fgets(line, sizeof line, f) != nullptr) {
line[strcspn(line, "\n")] = '\0'; // fgets keeps the newline
printf("%4d %s\n", ++number, line);
}
if (ferror(f)) {
perror(argv[1]);
}
fclose(f);
return EXIT_SUCCESS;
}A line longer than 255 characters arrives in pieces, one per fgets call. On
Linux and macOS, POSIX getline allocates a buffer as big as each line needs. The
Linux commands cheat sheet and the
Bash cheat sheet cover running programs like this
one from the terminal.
Gotchas
The mistakes that turn up in almost every C codebase at some point.
| Looks right | What actually happens | Do this instead |
|---|---|---|
if (x = 5) | Assigns 5 to x, and the condition is always true | if (x == 5). -Wall warns about it |
if (a == b) on two strings | Compares addresses, so it is false for equal text | strcmp(a, b) == 0 |
sizeof arr inside a function | The size of a pointer, because the array decayed | Pass the length in as a separate argument |
char buf[5]; strcpy(buf, "hello"); | Writes six bytes, one past the end | Leave room for the '\0', or use snprintf |
double avg = sum / count; with two ints | Divides as whole numbers first, so 7 / 2 gives 3.0 | (double)sum / count |
scanf("%d", n) | Writes to whatever address n happens to hold | scanf("%d", &n) |
p = realloc(p, size) | Leaks the old block when realloc fails | Assign to a temporary, check it, then swap |
Using a pointer after free(p) | Undefined, and often works until it doesn't | Set p = nullptr straight after free |
int x; printf("%d", x); | Prints whatever was in that memory | Initialise every local variable |
for (unsigned i = n - 1; i >= 0; i--) | Never ends, because an unsigned value is always 0 or more | for (size_t i = n; i-- > 0;) |
int big = INT_MAX + 1; | Signed overflow is undefined, not a wrap to negative | Check before the add, or use a wider or unsigned type |
#define DOUBLE(x) x * 2 then DOUBLE(1 + 2) | Expands to 1 + 2 * 2, which is 5 | #define DOUBLE(x) ((x) * 2) |
Common questions
Which version of C does this cheat sheet cover?
C23, the current ISO standard (ISO/IEC 9899:2024), checked with GCC 15, which uses it by default. Most of the page works in any C from C99 onward, and anything that needs a newer standard says so in the notes column: nullptr, bool as a keyword, constexpr and binary literals need C23, and anonymous unions and aligned_alloc need C11. Pass -std=c99, -std=c11 or -std=c17 to gcc to build against an older standard.
What is the difference between char *s and char s[] in C?
char s[] = "hi" creates an array of three bytes that you own and can change. char *s = "hi" creates a pointer to a string literal, which lives in read-only memory, so writing to s[0] is undefined behaviour and usually crashes. sizeof also differs: on the array it gives the array's size, on the pointer it gives the pointer's size, 8 bytes on a 64-bit system. Write const char *s for literals so the compiler stops you writing to them.
Why does my C program crash with a segmentation fault?
A segmentation fault means the program touched memory it is not allowed to. The usual causes are dereferencing a NULL or uninitialised pointer, reading or writing past the end of an array, using memory after free, and a string with no terminating zero byte. Build with gcc -g -fsanitize=address and run it again: the sanitiser prints the exact line and what went wrong, which is far quicker than guessing.
Should I cast the result of malloc in C?
No. malloc returns void *, and C converts void * to any other object pointer type automatically, so the cast adds nothing. In very old C it could also hide a missing #include <stdlib.h>. The cast is required in C++, which is why you see it in code written to compile as both. Write int *p = malloc(n * sizeof *p) instead.
What is undefined behaviour in C?
Code whose result the C standard does not define at all, such as signed integer overflow, reading past the end of an array, using a pointer after free, or changing a variable twice in one expression like i = i++. The compiler is allowed to assume it never happens and optimise on that basis, so the program can crash, print garbage, or appear to work until a different compiler flag changes it. -fsanitize=undefined catches many cases at run time.
What is the difference between gcc and clang?
Both are free, standards-following C compilers that accept almost the same command-line flags, so everything on this page works with either. GCC is the default on most Linux distributions. Clang is the default on macOS, where typing gcc actually runs Apple's clang, and it is known for clear error messages. For learning, pick whichever is already installed.
What is the difference between C and C++?
C++ started as C with classes and has grown into a much larger language, with templates, references, exceptions, RAII and a big standard library of containers. C stays small: structs and functions, manual memory with malloc and free, and no function overloading. Most C code is close to valid C++, but not all of it, since C++ requires the malloc cast, and C has features like designated array initialisers that C++ does not.
Does main need to return 0?
main returns the program's exit status, 0 or EXIT_SUCCESS for success and anything else, usually EXIT_FAILURE, for an error. Since C99, reaching the closing brace of main without a return statement returns 0 automatically, so the return is optional there. It is not optional in any other function that declares a return type.
