Learning C from TJ Devries


Jul 23, 2025 See all posts

This guide is a structured summary of TJ DeVries’ course on C and memory management. Its purpose is to use the C programming language as a vehicle to understand low-level memory management concepts. The guide progresses from C fundamentals to the practical implementation of complex systems, culminating in the construction of two distinct types of garbage collectors.


1. C Fundamentals: Getting Started

This section covers the basic syntax and control flow necessary to write simple C programs.

Compiled vs. Interpreted Languages

Your First C Program

A minimal C program requires a main function, which is the entry point for execution.

// Include the standard input/output library to use functions like printf
#include <stdio.h>

// The main function, which returns an integer status code
int main() {
    // printf prints a formatted string to the console.
    // \n is the newline character.
    printf("Hello, World!\n");

    // A return value of 0 indicates successful execution.
    return 0;
}

Basic Types and Variable Declaration

In C, you must declare the type of a variable before using it. Variables are mutable by default but their type cannot be changed.

To make a variable immutable, use the const keyword.

int age = 30;
float pi = 3.14f;
char initial = 'T';
char* name = "TJ";

// This variable cannot be changed later.
const int MEANING_OF_LIFE = 42;

Functions, Arguments, and Return Types

Functions must declare their return type and the types of all arguments.

// This function takes two integers and returns an integer.
int add(int x, int y) {
    return x + y;
}

// This function takes no arguments and returns nothing.
void print_message(void) {
    printf("A message!\n");
}

Control Flow

Header Files and Header Guards


2. Data Structures in C

C provides several ways to group and define custom data types.

struct (Structures)

A struct is a composite data type that groups related variables (fields) into a single unit. It is used for data only, not behavior (no methods).

struct Coordinate {
    int x;
    int y;
    int z;
};

// To use it:
struct Coordinate c;
c.x = 10;
c.y = 20;
c.z = 30;

enum (Enumerations)

An enum creates a set of named integer constants, making code more readable by avoiding “magic numbers.” By default, values start at 0 and auto-increment.

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

// You can also assign explicit values.
enum HttpStatusCode {
    OK = 200,
    NOT_FOUND = 404,
    SERVER_ERROR = 500
};

union (Unions)

A union can hold one of several possible types in the same memory location. The size of the union is determined by its largest member. All members share the same memory, so you can only use one at a time.

union Data {
    int i;
    float f;
    char* s;
};

Accessing a member that was not the one most recently set leads to undefined behavior, as you are reinterpreting the same bytes as a different type.

typedef

The typedef keyword creates an alias for an existing type, making code cleaner and easier to read. It’s commonly used with structs and enums.

typedef struct {
    int x;
    int y;
} Point;

// Now you can use "Point" directly instead of "struct Point"
Point p;
p.x = 5;

The sizeof Operator and Memory Padding


3. Pointers, Arrays, and Strings

Pointers are the core of C’s memory management capabilities.

Memory Addresses and Pointers

int x = 10;
int* ptr;      // ptr is a pointer to an integer.
ptr = &x;      // ptr now holds the memory address of x.

printf("Value of x: %d\n", x);        // Prints 10
printf("Value of x via ptr: %d\n", *ptr); // Prints 10

The Arrow Operator (->)

When working with a pointer to a struct, you use the arrow operator (->) as a shorthand to access its members.

Point p = {10, 20};
Point* p_ptr = &p;

// These two lines are equivalent:
(*p_ptr).x = 15;
p_ptr->x = 15; // Arrow operator is much cleaner.

Passing Arguments by Value (Copying)

In C, function arguments are passed by value. This means the function receives a copy of the argument’s data. Modifying the parameter inside the function does not affect the original variable.

To modify the original variable, you must pass a pointer to it.

void increment(int* num_ptr) {
    // Dereference the pointer to modify the original value
    (*num_ptr)++;
}

int main() {
    int a = 5;
    increment(&a); // Pass the address of 'a'
    // 'a' is now 6
}

Arrays and Pointers

int arr[3] = {10, 20, 30};

// These expressions are equivalent:
int second_val = arr[1];
int second_val_ptr = *(arr + 1);

Strings in C


4. Memory Layout: The Stack vs. The Heap

A C program’s memory is primarily divided into two regions.

The Stack

The Heap


5. Advanced Pointer Techniques

Pointers-to-Pointers (**)

A pointer-to-pointer (or double pointer) is a pointer that stores the address of another pointer. This is useful for:

  1. Modifying a pointer from within a function: If you want a function to change where an external pointer points, you must pass a pointer to that pointer.
  2. Creating arrays of pointers: An array of strings (char*) is represented as char**.

Void Pointers (void*)

A void* is a generic pointer that can point to data of any type.


6. Practical Application 1: Building a Generic Stack

This project applies pointer techniques to build a dynamic, generic stack data structure.


7. Practical Application 2: Building a Dynamic Object System

This project simulates creating a dynamic object system for a custom language (“SnakeLang”).


8. Practical Application 3: Implementing Garbage Collectors

This section builds two different garbage collectors for the SnakeObject system.

Reference Counting GC

Mark and Sweep GC


Key Takeaways


Enjoyed the article? I write about 1-2 a month. Subscribe via email or RSS feed.