# C Cheat Sheet: C17 and C23 Syntax, Memory and gcc Flags

Source: https://srvscripts.com/guides/c-cheat-sheet/
Updated: 2026-10-09
Publisher: srvScripts (https://srvscripts.com/)

**Short answer:** Compile with `gcc -std=c17 -Wall -Wextra -O2 -o app app.c` (or `-std=c23` on GCC 14+ and Clang 18+). Use fixed-width types from `<stdint.h>`, `size_t` for sizes, `snprintf` instead of `sprintf`, and check every `malloc`, `fopen` and `strtol`. C23 adds `bool`, `nullptr`, `constexpr`, `typeof` and `auto` as keywords. Build test binaries with `-g -fsanitize=address,undefined` to catch memory bugs early.

The examples in this C cheat sheet were run with GCC 13.3, GCC 14.2 and Clang 18.1 on Ubuntu 24.04 on 9 October 2026. Notes about GCC 15 and 16 defaults and `#embed` were checked against the official documentation.

## Compile and run with gcc and clang

| Command | What it does |
| --- | --- |
| gcc -std=c17 -Wall -Wextra -O2 -o app app.c | Build one file with the usual warnings and optimisation |
| gcc -std=c23 ... / clang -std=c23 ... | C23 mode. GCC 13 only accepts the old name -std=c2x |
| -Wpedantic | Warn about GNU extensions that are not ISO C |
| -Werror | Turn warnings into errors (good in CI, risky in release tarballs) |
| -g -O0 | Debug build for gdb |
| -g -fsanitize=address,undefined | Catch buffer overflows, use-after-free and undefined behaviour at run time |
| -c util.c | Compile to util.o without linking |
| -I include -L lib -lssl | Header path, library path, link libssl |
| -lm | Link the maths library (needed for sqrt, pow on glibc) |
| -DDEBUG=1 | Define a macro from the command line |
| -E / -S | Stop after preprocessing / after generating assembly |
| $(pkg-config --cflags --libs libcurl) | Flags for an installed library |

GCC 15 changed the default C mode from `gnu17` to `gnu23`, so old code that uses `bool`, `true` or `false` as identifiers can stop compiling. Pin the standard with `-std=` in every build. `__STDC_VERSION__` is `201710L` for C17 and `202311L` for C23 (GCC 13 and 14 still report the draft value `202000L`).

## Types, variables and C23 additions

| Syntax | What it does | Example |
| --- | --- | --- |
| int, long, char, double | Basic types; sizes depend on the platform | long n = 10L; |
| int32_t, uint64_t | Fixed-width integers from  | uint16_t port = 443; |
| size_t | Unsigned type for sizes and indexes; print with %zu | size_t n = strlen(s); |
| const | Read-only through this name | const char *msg = "ok"; |
| static | File-local function, or a local that keeps its value | static int calls; |
| bool (C23 keyword) | Needs  before C23 | bool ok = true; |
| nullptr (C23) | Typed null pointer constant | int *p = nullptr; |
| constexpr (C23) | Compile-time constant object (GCC 13+, Clang 19+) | constexpr int MAX = 100; |
| auto (C23) | Infer the type from the initialiser | auto n = 42; |
| typeof (C23) | Type of an expression | typeof(n) m = 0; |
| 0b1010, 1'000'000 | C23 binary literals and digit separators | long big = 1'000'000; |
| static_assert(expr) | Compile-time check; C23 makes the message optional | static_assert(sizeof(int) == 4); |
| [[nodiscard]] | C23 attribute: warn if the return value is ignored | [[nodiscard]] int f(void); |

```
#include
#include

constexpr int MAX_USERS = 100;          // C23
static_assert(sizeof(int32_t) == 4);    // C23: message optional

int main(void) {
    bool ok = true;                     // no  needed in C23
    int *p = nullptr;
    auto n = 42;
    typeof(n) m = 0b1010;
    printf("%d %d %d %d %ld\n", ok, p == nullptr, n + MAX_USERS, m, __STDC_VERSION__);
}
```

## Pointers, arrays and strings

| Syntax | Meaning |
| --- | --- |
| int *p = &x; | p holds the address of x |
| *p = 5; | Write through the pointer |
| p[i] is *(p + i) | Indexing is pointer arithmetic |
| int a[10]; | Array; decays to int * when passed to a function, so pass the length too |
| sizeof a / sizeof a[0] | Element count. Works on the array, not on a pointer to it |
| const char *s vs char *const s | Pointer to read-only chars vs read-only pointer |
| strlen(s) | Length without the terminating \0 |
| strcmp(a, b) == 0 | Equal strings. Never compare strings with == |
| snprintf(buf, sizeof buf, ...) | Safe formatting; returns the length it wanted to write |
| memcpy(dst, src, n) / memmove | Copy bytes; use memmove when the areas overlap |
| strtol(s, &end, 10) | Parse an integer with error checking (atoi has none) |

```
char buf[16];
int need = snprintf(buf, sizeof buf, "host-%s.example", "web01");
if (need >= (int)sizeof buf)
    fprintf(stderr, "truncated, needed %d bytes\n", need);

char *end;
errno = 0;
long port = strtol(arg, &end, 10);
if (errno || *end != '\0' || port < 1 || port > 65535)
    fprintf(stderr, "bad port: %s\n", arg);
```

GCC also caught the truncation at compile time with `-Wformat-truncation`, which `-Wall` enables. Avoid `strcpy`, `strcat`, `sprintf` and `gets` (removed in C11) in new code.

## Structs, enums, unions and function pointers

```
typedef struct {
    char name[32];
    uint16_t port;
    bool tls;
} Service;

static int by_port(const void *a, const void *b) {
    const Service *x = a, *y = b;
    return (x->port > y->port) - (x->port < y->port);
}

Service svcs[] = {
    { .name = "https", .port = 443, .tls = true },   // designated initialisers
    { .name = "ssh",   .port = 22 },                  // other fields become 0
};
qsort(svcs, sizeof svcs / sizeof svcs[0], sizeof svcs[0], by_port);
```

| Syntax | What it does |
| --- | --- |
| s.port / ps->port | Member access on a struct / through a pointer |
| enum level { LOW, MEDIUM = 5, HIGH }; | Named integer constants (HIGH is 6) |
| union value { int i; float f; }; | Members share the same memory; read the one you last wrote |
| typedef int (*cmp_fn)(const void *, const void *); | Name for a function pointer type |
| cmp_fn f = by_port; f(a, b); | Call through a function pointer |

## Memory management: malloc, realloc and free

| Call | Notes |
| --- | --- |
| T *p = malloc(n * sizeof *p); | Uninitialised memory. Check for NULL |
| calloc(n, sizeof *p) | Zeroed memory, with an overflow check on n * size |
| realloc(p, new_size) | Grow or shrink. Assign to a temporary first |
| free(p); p = NULL; | Release, then clear the pointer to avoid a double free |
| sizeof *p | Size of what p points to; stays right if the type changes |

```
int *tmp = realloc(arr, cap * 2 * sizeof *arr);
if (!tmp) {            /* arr is still valid here */
    free(arr);
    return -1;
}
arr = tmp;
cap *= 2;
```

Writing `arr = realloc(arr, ...)` leaks the old block when `realloc` fails. Every `malloc` needs exactly one `free`. ASan reports a heap overflow like this:

```
$ gcc -g -fsanitize=address -o bug bug.c && ./bug
==9104==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x502000000020 ...
WRITE of size 4 at 0x502000000020 thread T0
    #0 ... in main bug.c:2
```

## Files, errors and a log-parsing example

| Call | What it does |
| --- | --- |
| FILE *fp = fopen(path, "r"); | Open for reading ("w" truncates, "a" appends) |
| fgets(buf, sizeof buf, fp) | Read one line into a fixed buffer |
| getline(&line, &cap, fp) | POSIX: read a line of any length; free line afterwards |
| fprintf(stderr, ...) | Errors go to stderr, not stdout |
| perror(path) / strerror(errno) | Print the reason a call failed |
| fclose(fp) | Flush and close; check its return value when writing |
| return EXIT_FAILURE; | Non-zero exit status for scripts and systemd |

For server admins: a small, fast counter for failed SSH logins in an auth log.

```
#define _POSIX_C_SOURCE 200809L
#include
#include
#include

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "usage: %s LOGFILE\n", argv[0]);
        return EXIT_FAILURE;
    }
    FILE *fp = fopen(argv[1], "r");
    if (!fp) { perror(argv[1]); return EXIT_FAILURE; }

    char *line = NULL;
    size_t cap = 0;
    long hits = 0;
    while (getline(&line, &cap, fp) != -1)
        if (strstr(line, "Failed password"))
            hits++;
    free(line);
    fclose(fp);
    printf("%ld failed SSH logins\n", hits);
    return EXIT_SUCCESS;
}
```

## Preprocessor, headers and Makefiles

| Directive | What it does |
| --- | --- |
| #include  / #include "util.h" | System header / project header |
| #ifndef UTIL_H #define UTIL_H … #endif | Include guard (or #pragma once, widely supported but not ISO) |
| #define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0])) | Function-like macro; bracket every argument |
| #if __STDC_VERSION__ >= 202311L | Compile only in C23 mode |
| #ifdef DEBUG / #ifndef DEBUG | Test whether a macro exists |
| #embed "cert.pem" | C23: include a file’s bytes as data (GCC 15+, Clang 19+) |

```
CC      = gcc
CFLAGS  = -std=c17 -Wall -Wextra -O2
LDLIBS  = -lm
OBJS    = main.o util.o

app: $(OBJS)
	$(CC) $(CFLAGS) -o $@ $(OBJS) $(LDLIBS)

%.o: %.c util.h
	$(CC) $(CFLAGS) -c $<

clean:
	rm -f app $(OBJS)

.PHONY: clean
```

Recipe lines must start with a tab. Override variables per run: `make CC=clang CFLAGS="-std=c23 -DDEBUG=1"`.

## Debugging and common undefined behaviour

| Tool or bug | What to do |
| --- | --- |
| gdb ./app then run, bt, print x | Backtrace after a crash; build with -g -O0 |
| valgrind --leak-check=full ./app | Leaks and invalid reads without recompiling (slower than ASan) |
| ulimit -c unlimited | Allow core dumps; on systemd hosts read them with coredumpctl |
| Signed integer overflow | Undefined. Check limits before adding, or use unsigned types |
| Reading uninitialised variables | Initialise at declaration; -Wall warns about many cases |
| Use after free | Set the pointer to NULL; ASan reports heap-use-after-free |
| Off-by-one on arrays | Loop i < n, not i
