Nick George
all/

Learning C in 2023

First published: December 3, 2023
Last updated: December 3, 2023

The more I learn about computers, the more I realize you can’t really understand much without understanding memory and the C programming language. As someone who grew up on garbage collected interpreted languages like python, I have very little experience in this area and need to improve.

So, while I don’t plan on becoming a C programmer, I’ve started working on some from-basics handmade-style projects to learn more about how the system works and develop a better intuition about memory and performance.

For my first project, I decided to build the classic snake game I played on my parent’s Nokia phones during my childhood. This is my first learning project using C (or graphics programming) and I used only SDL2 library and C for this implementation.

Thinking about memory on the stack and heap ¶

I had created a struct to initialize the game state, looking something like this:

struct Game {
  int window_w; 
  int window_h;
  int game_w;
  //...
  enum State state;
  SDL_Rect game_rect;
  SDL_Rect fruit;
  SDL_Window* window;
  SDL_Renderer* win_ren;
  SDL_Rect *body; // array representing the snake's body
};

This struct was initialized in main, just before the event loop. The snake body was represented as an array of SDL_Rect’s, and I pre-defined the max array size during compile time, thinking that reserving the space on the main stack frame during compile time meant I wouldn’t have to malloc (heap allocate) at all.

SDL_Rect* body[MAX_SNAKE_LEN];

This worked great, and I wrote most of the game like this. Eventually I decided I wanted a reset key, to start my game again without quitting. So I moved all the init code to a struct Game init() function, and set up a trigger in my event loop to call it when i pushed r.

struct Game init(void){
  SDL_Rect* body[MAX_SNAKE_LEN]; 
  //...
  struct Game game = {.window_w=WINDOW_W, 
    .window_h=WINDOW_H, 
    // ... 
    .head_x=GAME_SIZE/2,
    .head_y=GAME_SIZE/4,
    .body=body,
  };
  SDL_CreateWindowAndRenderer(WINDOW_W, WINDOW_H, SDL_WINDOW_RESIZABLE, &game.window, &game.win_ren);
  if (!game.window || !game.win_ren) {
    printf("Failed to create window or renderer. Error: %s", SDL_GetError());
    exit(EXIT_FAILURE);
  }
  SDL_SetWindowTitle(game.window, "snake");
  game.fruit = make_fruit(&game);
  return game;
}

And right away, I started getting segfaults or nonsense shapes on the screen. This was a great learning experience, and required the use of a debugger (lldb) to understand what was happening.

In short, my Game struct stored a pointer to the head of the snake array, which was pre-defined and allocated on the current stack frame. This was no problem in the original code, because I was on the main stack frame, and the memory remained valid for the life of the program. However, when I moved the definition to init(), the pointer no longer pointed to valid memory once init returned the game and the stack frame variables went out of scope.

While I’m sure there is a way to address this without allocating, I ended up changing my definition to heap allocate the snake instead:

SDL_Rect* body = malloc(MAX_SNAKE_LEN * sizeof(SDL_Rect));
if (body == NULL){
    puts("Failed to allocate snake");
    exit(EXIT_FAILURE);
}
body[0] = (SDL_Rect){.x=GAME_SIZE/2, .y=GAME_SIZE/4, .w=STEP_SIZE, .h=STEP_SIZE};
// ...

Now the pointer to the snake body will remain valid as long as I like.

Debuggers are great! ¶

I underestimated the value of even the command line interface of debuggers. When I was trying to figure out the array memory corruption problem, I don’t think I would have gotten anywhere without using lldb. By compiling with the -g flag and breaking on my init function, I was able to step through the code and see a correctly allocated snake body in init, then nonsense as soon as I stepped out of the frame! That let me narrow my search and learn about how to properly do this in C.

All I had to do was:

#clang `pkg-config sdl2 --libs --cflags` -Wall -g -O0 -pedantic -std=gnu11 main.c -o snake
lldb snake
break set init
// step with s
frame variable body
//inspect the addresses and values
step out
// Oh no!

This was much easier than adding my debug statements using %p everywhere.

While I’d like to use gdb in the future, lldb is much easier to use on MacOS and works by default so I stuck with that.

Arrays and pointers ¶

The most challenging part of C for me so far is groking arrays. While I can read the definition, and convinced myself that the array[i] syntax is really *(array + i) since array itself is a pointer to the 0ith element of the array by working through the following:

#include <stdio.h>
int main(){
  int array[3] = {1,2,3};
  printf("Address of array is %p\n", &array);     
  printf("Address of array[0] is %p\n", &array[0]);
  printf("Address of array+1 is %p\n", (array+1));     
  printf("Address of array[1] is %p\n", &array[1]);
  printf("Content of array+1 is %d\n", *(array+1));     
  printf("Content of array[1] is %d\n", array[1]);
}

// Address of array is 0x7ff7b4fb606c
// Address of array[0] is 0x7ff7b4fb606c
// Address of array+1 is 0x7ff7b4fb6070
// Address of array[1] is 0x7ff7b4fb6070
// Content of array+1 is 2
// Content of array[1] is 2

Using them in practice requires more thought and serious consideration of where things are stored and what you are actually doing, as I learned with my init function.

I have a lot to learn ¶

Obviously, there is a reason why many of us learn and use higher level languages. There are a lot of great features that make programming safer and nicer than C, and we’ve learned a lot since the creation of C.

However, there is also a lot of junk and bulk and unnecessary complexity. By better understanding these lower levels, I hope to be a better judge of the advantages and disadvantages of the tools I use, and learn how to create more resilient and higher performance software.

You can find my first attempt at making a snake here: https://github.com/nkicg6/csnake