Skip to main content

C Syntax Speed Run

You know how to program. Let’s map your existing knowledge to C in record time.
Time: 4-6 hours
Goal: Write C code confidently
Assumed: You understand variables, functions, loops, arrays from another language

Types & Variables

Primitive Types

#include <stdio.h>
#include <stdint.h>  // Fixed-width integers (C99)
#include <stdbool.h> // bool type (C99)
#include <limits.h>  // Type limits

int main(void) {
    // Character types
    char c = 'A';           // At least 8 bits, may be signed or unsigned
    signed char sc = -128;  // Guaranteed signed
    unsigned char uc = 255; // Guaranteed unsigned (0-255)

    // Integer types (sizes are MINIMUM, vary by platform)
    short s = 32767;              // At least 16 bits
    int i = 2147483647;           // At least 16 bits (usually 32)
    long l = 2147483647L;         // At least 32 bits
    long long ll = 9223372036854775807LL; // At least 64 bits (C99)

    // Unsigned variants
    unsigned int ui = 4294967295U;
    unsigned long ul = 4294967295UL;

    // Fixed-width integers (USE THESE for portable code)
    int8_t   i8  = -128;
    int16_t  i16 = -32768;
    int32_t  i32 = -2147483648;
    int64_t  i64 = -9223372036854775807LL;
    uint8_t  u8  = 255;
    uint16_t u16 = 65535;
    uint32_t u32 = 4294967295U;
    uint64_t u64 = 18446744073709551615ULL;

    // Floating point
    float f = 3.14f;           // Usually 32 bits
    double d = 3.14159265359;  // Usually 64 bits
    long double ld = 3.14159265358979323846L; // At least 64 bits

    // Boolean (C99)
    bool flag = true;  // Actually just int, 0 = false, non-zero = true

    // Size type (for array indices, sizes)
    size_t size = sizeof(int);  // Unsigned, platform-specific

    // Pointer difference type
    ptrdiff_t diff = &i - &i;  // Signed, for pointer arithmetic

    return 0;
}
Critical: int is NOT always 32 bits. On embedded systems it might be 16 bits. Use int32_t when you need exactly 32 bits.

Type Sizes

#include <stdio.h>
#include <stdint.h>

int main(void) {
    printf("char:        %zu bytes\n", sizeof(char));      // Always 1
    printf("short:       %zu bytes\n", sizeof(short));     // >= 2
    printf("int:         %zu bytes\n", sizeof(int));       // >= 2
    printf("long:        %zu bytes\n", sizeof(long));      // >= 4
    printf("long long:   %zu bytes\n", sizeof(long long)); // >= 8
    printf("float:       %zu bytes\n", sizeof(float));     // Usually 4
    printf("double:      %zu bytes\n", sizeof(double));    // Usually 8
    printf("void*:       %zu bytes\n", sizeof(void*));     // 4 or 8 (32/64-bit)
    printf("size_t:      %zu bytes\n", sizeof(size_t));    // 4 or 8

    return 0;
}

Integer Promotion & Conversions

#include <stdio.h>
#include <stdint.h>

int main(void) {
    // Integer promotion: smaller types promoted to int in expressions
    char a = 100, b = 100;
    int result = a + b;  // a and b promoted to int before addition

    // This is why char arithmetic is safe (no overflow in expression)
    char c = a + b;  // Computed as int, then truncated to char (200)

    // DANGER: Implicit signed/unsigned conversion
    int negative = -1;
    unsigned int positive = 1;
    
    // -1 becomes UINT_MAX when converted to unsigned!
    if (negative < positive) {
        printf("Expected\n");
    } else {
        printf("Surprise! -1 > 1 in unsigned comparison\n"); // This prints!
    }

    // DANGER: Truncation
    int32_t big = 100000;
    int16_t small = big;  // Truncated! small = -31072 (undefined if out of range)

    return 0;
}

Control Flow (Quick Reference)

// If-else
if (condition) {
    // ...
} else if (other_condition) {
    // ...
} else {
    // ...
}

// Switch (MUST use break, fall-through is default)
switch (value) {
    case 1:
        // ...
        break;
    case 2:
    case 3:
        // handles both 2 and 3 (intentional fall-through)
        break;
    default:
        // ...
}

// Loops
for (int i = 0; i < n; i++) { }
while (condition) { }
do { } while (condition);

// Loop control
break;     // Exit loop
continue;  // Skip to next iteration
goto label; // Jump (use sparingly, but valid for error cleanup)

label:
    // ...

The Comma Operator

// Less common, but you'll see it
for (int i = 0, j = n; i < j; i++, j--) {
    // i increases, j decreases
}

// Evaluates left-to-right, returns rightmost value
int x = (a = 5, b = 6, a + b);  // x = 11

Functions

Declaration vs Definition

// Declaration (prototype) - tells compiler function exists
int add(int a, int b);

// Definition - actual implementation
int add(int a, int b) {
    return a + b;
}

// Declaration can omit parameter names
int multiply(int, int);

// Old-style declarations (avoid, but you'll see in legacy code)
int old_style();  // Accepts ANY number of arguments!
int modern_style(void);  // Accepts NO arguments

Pass by Value (Always!)

void swap_broken(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
    // Original values unchanged - we modified copies!
}

void swap_working(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
    // Works because we passed addresses
}

int main(void) {
    int x = 5, y = 10;
    swap_broken(x, y);   // x=5, y=10 still
    swap_working(&x, &y); // x=10, y=5 now
    return 0;
}

Static Functions

// Only visible within this file (internal linkage)
static int helper_function(int x) {
    return x * 2;
}

// Can be called from other files (external linkage, default)
int public_function(int x) {
    return helper_function(x) + 1;
}

Inline Functions (C99)

// Hint to compiler: inline the code
static inline int square(int x) {
    return x * x;
}

Arrays

Basic Arrays

#include <stdio.h>
#include <string.h>

int main(void) {
    // Stack-allocated arrays (size must be constant in C89)
    int arr[5] = {1, 2, 3, 4, 5};
    int zeros[100] = {0};           // All elements zero
    int partial[5] = {1, 2};        // Rest are zero
    int inferred[] = {1, 2, 3};     // Size inferred as 3

    // Variable-length arrays (C99, optional in C11)
    int n = 10;
    int vla[n];  // Size determined at runtime (on stack!)

    // Array size
    size_t size = sizeof(arr) / sizeof(arr[0]); // 5

    // Array decay: arrays decay to pointers when passed
    void process(int *arr, size_t size);  // Receives pointer, not array

    return 0;
}
Array Decay: When you pass an array to a function, it decays to a pointer. You LOSE size information. Always pass size as a separate parameter.

Multi-dimensional Arrays

int matrix[3][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};

// Memory layout: row-major (rows are contiguous)
// matrix[0][0], matrix[0][1], ..., matrix[0][3], matrix[1][0], ...

// Passing to functions requires all dimensions except first
void process_matrix(int mat[][4], size_t rows);

Strings

Strings in C are just null-terminated character arrays.
#include <stdio.h>
#include <string.h>

int main(void) {
    // String literal (stored in read-only memory)
    const char *literal = "Hello";  // 6 bytes including '\0'

    // Mutable string (array, not pointer)
    char mutable[] = "Hello";       // Can modify
    mutable[0] = 'J';               // Now "Jello"

    // DANGER: This is undefined behavior
    // char *bad = "Hello";
    // bad[0] = 'J';  // UB! Modifying string literal

    // String functions
    size_t len = strlen("Hello");           // 5 (not counting '\0')
    char dest[20];
    strcpy(dest, "Hello");                  // Copy (DANGER: no bounds check)
    strncpy(dest, "Hello", sizeof(dest));   // Safer, but may not null-terminate!
    dest[sizeof(dest) - 1] = '\0';          // Ensure null termination

    strcat(dest, " World");                 // Concatenate (DANGER)
    strncat(dest, " World", sizeof(dest) - strlen(dest) - 1); // Safer

    int cmp = strcmp("abc", "abd");         // < 0 (a < a, b < b, c < d)
    char *found = strstr("Hello World", "World");  // Pointer to "World"

    // Safe string formatting
    char buffer[100];
    int written = snprintf(buffer, sizeof(buffer), "Value: %d", 42);
    // Returns number of chars that WOULD be written (for truncation detection)

    return 0;
}

Structs

Basic Structs

#include <stdio.h>
#include <string.h>

// Definition
struct Point {
    int x;
    int y;
};

// Typedef for cleaner syntax
typedef struct {
    char name[50];
    int age;
    float salary;
} Employee;

int main(void) {
    // Initialization
    struct Point p1 = {10, 20};
    struct Point p2 = {.y = 20, .x = 10};  // Designated initializers (C99)
    struct Point p3 = {0};                  // All fields zero

    Employee emp = {"John Doe", 30, 50000.0f};

    // Access
    p1.x = 15;
    printf("%s is %d years old\n", emp.name, emp.age);

    // Pointer access
    struct Point *ptr = &p1;
    ptr->x = 25;  // Equivalent to (*ptr).x = 25

    // Copy (entire struct, not pointer)
    struct Point p4 = p1;  // Deep copy of struct

    return 0;
}

Struct Memory Layout & Padding

#include <stdio.h>
#include <stddef.h>

struct Padded {
    char a;      // 1 byte
                 // 3 bytes padding (to align int)
    int b;       // 4 bytes
    char c;      // 1 byte
                 // 3 bytes padding (to align struct to 4)
};  // Total: 12 bytes, not 6!

struct Packed {
    int b;       // 4 bytes
    char a;      // 1 byte
    char c;      // 1 byte
                 // 2 bytes padding
};  // Total: 8 bytes (better!)

// Check offsets
int main(void) {
    printf("Padded size: %zu\n", sizeof(struct Padded));
    printf("a offset: %zu\n", offsetof(struct Padded, a));
    printf("b offset: %zu\n", offsetof(struct Padded, b));
    printf("c offset: %zu\n", offsetof(struct Padded, c));
    return 0;
}
Optimization: Order struct members from largest to smallest to minimize padding. This is especially important in hot paths and large arrays.

Bit Fields

struct Flags {
    unsigned int active : 1;     // 1 bit
    unsigned int ready : 1;      // 1 bit
    unsigned int error_code : 4; // 4 bits (0-15)
    unsigned int : 2;            // 2 bits padding (unnamed)
    unsigned int mode : 3;       // 3 bits
};  // Typically 2-4 bytes depending on compiler

int main(void) {
    struct Flags f = {0};
    f.active = 1;
    f.error_code = 15;  // Max value for 4 bits
    // f.error_code = 16;  // UB! Overflow
    return 0;
}

Unions

Unions share memory between members—only one is valid at a time.
#include <stdio.h>
#include <stdint.h>

union Value {
    int32_t i;
    float f;
    char bytes[4];
};  // Size = max member size (4 bytes)

// Type-punning for byte inspection
void print_float_bytes(float f) {
    union {
        float f;
        uint8_t bytes[4];
    } u = {.f = f};

    printf("Float %f as bytes: ", f);
    for (int i = 0; i < 4; i++) {
        printf("%02x ", u.bytes[i]);
    }
    printf("\n");
}

// Tagged union (common pattern)
typedef enum { INT, FLOAT, STRING } ValueType;

typedef struct {
    ValueType type;
    union {
        int i;
        float f;
        char *s;
    } data;
} TaggedValue;

void print_value(TaggedValue *v) {
    switch (v->type) {
        case INT:    printf("%d\n", v->data.i); break;
        case FLOAT:  printf("%f\n", v->data.f); break;
        case STRING: printf("%s\n", v->data.s); break;
    }
}

Enums

typedef enum {
    RED,      // 0
    GREEN,    // 1
    BLUE      // 2
} Color;

typedef enum {
    SUCCESS = 0,
    ERROR_FILE_NOT_FOUND = -1,
    ERROR_PERMISSION_DENIED = -2,
    ERROR_OUT_OF_MEMORY = -3
} ErrorCode;

// Underlying type is int (can use flags)
typedef enum {
    FLAG_NONE = 0,
    FLAG_READ = 1 << 0,   // 1
    FLAG_WRITE = 1 << 1,  // 2
    FLAG_EXEC = 1 << 2    // 4
} Permissions;

int main(void) {
    Permissions p = FLAG_READ | FLAG_WRITE;  // 3
    if (p & FLAG_READ) {
        printf("Has read permission\n");
    }
    return 0;
}

Compilation Model

# The four stages:
# 1. Preprocessing (-E): Handle #include, #define, #ifdef
# 2. Compilation (-S): C code → Assembly
# 3. Assembly (-c): Assembly → Object file (.o)
# 4. Linking: Object files → Executable

# Full pipeline
gcc -E main.c -o main.i     # Preprocessor output
gcc -S main.c -o main.s     # Assembly output
gcc -c main.c -o main.o     # Object file
gcc main.o -o main          # Link to executable

# Or all at once
gcc main.c -o main

# Important flags
gcc -Wall -Wextra -Werror -std=c11 -pedantic main.c -o main
# -Wall: Enable common warnings
# -Wextra: Enable extra warnings
# -Werror: Treat warnings as errors
# -std=c11: Use C11 standard
# -pedantic: Strict ISO C compliance

Quick Exercises

1

Type Sizes

Write a program that prints the size of all basic types on your system. Note which are different from what you expected.
2

Struct Padding

Create a struct with 3 members of different sizes. Use sizeof and offsetof to visualize the padding.
3

String Manipulation

Implement a safe string_concat function that takes a destination buffer, its size, and two source strings, returning true if concatenation succeeded.
4

Tagged Union

Implement a JSON-like value type using a tagged union that can hold null, bool, int, double, string, or error.

Next Up

Build Systems & Toolchain

Master GCC, Make, CMake, and the compilation process