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.
This section covers the basic syntax and control flow necessary to write simple C programs.
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;
}
In C, you must declare the type of a variable before using it. Variables are mutable by default but their type cannot be changed.
int
: Integer values (e.g., 5
, -10
).float
: Floating-point (decimal) numbers (e.g., 3.14
).char
: A single character (e.g., 'a'
). Uses single quotes.char*
: A pointer to a character, used to represent strings. Uses double quotes.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 must declare their return type and the types of all arguments.
void
is a special type used to indicate that a function either returns nothing or takes no 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");
}
if/else
Statements: Used for conditional logic. Braces {}
are strongly recommended to define the scope of the body.
if (temp < 70) {
return "Too cold";
} else if (temp > 90) {
return "Too hot";
} else {
return "Just right";
}
for
Loops: Consist of three parts: initialization, condition, and final expression.
// Prints numbers from 0 to 9
for (int i = 0; i < 10; i++) {
printf("%d\n", i);
}
while
Loops: Execute as long as a condition is true.
int i = 0;
while (i < 10) {
printf("%d\n", i);
i++;
}
Header Files (.h
): Declare the “specification” of functions, structs, and types that can be shared across multiple .c
files. They are included using #include "filename.h"
.
Header Guards: Prevent a header file from being included multiple times in a single compilation unit, which would cause redefinition errors. The modern way to do this is with #pragma once
.
// In exercise.h
#pragma once
// Function declaration (prototype)
float get_average(int x, int y, int z);
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;
sizeof
Operator and Memory Paddingsizeof(type)
is an operator that returns the size of a type or variable in bytes.struct
is not always the sum of its members. The compiler may add padding between fields to ensure they align on word boundaries (e.g., a 4-byte int
starting at an address divisible by 4), which is more efficient for the CPU to access. To minimize padding, order struct
fields from largest to smallest.Pointers are the core of C’s memory management capabilities.
&
): Gets the memory address of a variable.*
): Gets the value at the address a pointer is pointing to.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
->
)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.
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
}
ptr
is an int*
, ptr + 1
automatically moves the pointer forward by sizeof(int)
bytes.int arr[3] = {10, 20, 30};
// These expressions are equivalent:
int second_val = arr[1];
int second_val_ptr = *(arr + 1);
\0
marks the end of the string.string.h
provides essential functions like:
strlen(str)
: Gets the length (excluding \0
).strcpy(dest, src)
: Copies a string. Unsafe, can cause buffer overflows.strcat(dest, src)
: Concatenates strings. Unsafe.strncpy
and strncat
: Safer versions that take a size limit.A C program’s memory is primarily divided into two regions.
malloc(size_t size)
: Memory allocate. Requests a block of size
bytes on the heap and returns a void*
pointer to it. The memory is uninitialized.free(void* ptr)
: Releases a block of memory previously allocated with malloc
, returning it to the system.free()
. Forgetting to do so causes a memory leak.**
)A pointer-to-pointer (or double pointer) is a pointer that stores the address of another pointer. This is useful for:
char*
) is represented as char**
.void*
)A void*
is a generic pointer that can point to data of any type.
void*
directly. You must first cast it to a specific pointer type (e.g., (int*)
) to tell the compiler how to interpret the memory.This project applies pointer techniques to build a dynamic, generic stack data structure.
Design: A struct
holds the stack’s state:
typedef struct {
size_t count; // Number of elements currently in the stack
size_t capacity; // Max number of elements before resizing
void** data; // An array of void pointers (generic elements)
} Stack;
stack_new(capacity)
: Allocates a Stack
struct and its data
array on the heap using malloc
.
stack_push(stack, element)
: Checks if count == capacity
. If so, it doubles the capacity and uses realloc()
to resize the data
array. Then it adds the new element.
stack_pop(stack)
: Returns the last element and decrements count
.
stack_free(stack)
: Frees the data
array first, then frees the Stack
struct itself.
This project simulates creating a dynamic object system for a custom language (“SnakeLang”).
Design (Tagged Union): A core SnakeObject
struct uses an enum
to “tag” what kind of data is currently stored in a union
.
typedef enum { INTEGER, FLOAT, STRING, ARRAY, ... } ObjectKind;
typedef union {
int v_int;
float v_float;
char* v_string;
// ... other types
} ObjectData;
typedef struct {
ObjectKind kind;
ObjectData data;
} SnakeObject;
Constructors: “Constructor” functions (e.g., new_snake_integer
, new_snake_string
) are created to safely allocate and initialize SnakeObject
s on the heap, setting the correct kind
and data
.
Container Types: Objects like arrays or vectors store pointers (SnakeObject*
) to other objects, allowing for nested data structures.
Polymorphic Functions: Functions like snake_add
or snake_length
use a switch
statement on the object’s kind
to perform different actions based on the type, mimicking polymorphic behavior.
This section builds two different garbage collectors for the SnakeObject
system.
ref_count
integer. The count is incremented whenever a new reference to it is created (e.g., assigned to a variable, added to a list). The count is decremented when a reference is destroyed. When the ref_count
reaches 0, the object is immediately freed.int ref_count;
field to the SnakeObject
struct.ref_count
to 1
in constructors.ref_count_inc(obj)
and ref_count_dec(obj)
functions.ref_count_dec
, if the count becomes 0, call a function to free the object and recursively decrement the counts of any objects it holds (e.g., in an array).bool is_marked;
field to the SnakeObject
struct.is_marked = true
.is_marked == false
, it is unreachable. Free it.is_marked == true
, it is live. Reset its flag to is_marked = false
for the next GC cycle.struct
, union
, pointers, and manual memory allocation.Enjoyed the article? I write about 1-2 a month. Subscribe via email or RSS feed.