Sandrino's WEBSITE

   ABOUT  BLOG  


Storing values in pointer slots in C

C · Pointers · Systems programming


Long time since my latest Blog post :) I am currently playing around with C and used a few times a trick (at least a trick for me)

In C, a pointer normally holds a memory address. But there is a lesser-known trick: you can store a small integer value directly inside the pointer slot, without allocating any memory at all. This is useful when an API hands you a void * context slot and you just want to pass a small integer through it.

The base example

Consider this program. ap is a normal pointer — it holds the address of a and can be dereferenced. bp is different — it holds the value of b (2020) cast directly into a pointer slot.

#include <stdio.h>

int main()
{
      int a  = 10;
      int *ap = &a;

      int b  = 2020;
      void *bp = (void *)(unsigned long)b;   // store value, not address

      int  c  = (int)(unsigned long)bp;      // cast back to read it

      printf("a      : %d at %p\n",  a,  &a);
      printf("*ap    : %d at %p pointing to %p\n", *ap, &ap, ap);

      printf("b      : %d at %p\n",  b,  &b);
      printf("bp     : %p at %p\n",  bp, &bp);  // NO dereference — use %p

      printf("c      : %d\n", c);
      return 0;
}

Output:

a      : 10 at 0x16efe2958
*ap    : 10 at 0x16efe2950 pointing to 0x16efe2958
b      : 2020 at 0x16efe294c
*bp    : 0x7e4 at 0x16efe2940
c      : 2020

Never dereference bp with *bp. The value 2020 is stored as-if it were an address (0x000007E4). Nothing lives at that address — dereferencing it causes an immediate segfault. This did happen to me several times and I guess because of muscle memory.

To print the stored value, skip the dereference and cast back instead:

printf("%d\n", (int)(unsigned long)bp);  // 2020
printf("%p\n", bp);                      // 0x00000000000007e4

The double cast through unsigned long (or uintptr_t from <stdint.h>) is required. Casting an integer directly to void * is technically undefined behaviour in C.


Normal pointer vs value-in-pointer

int *ap = &a;
// ap ──► [ 10 ]   dereference follows the arrow

void *bp = (void *)(unsigned long)b;
// bp = 0x000007E4   the value IS the pointer — no arrow to follow

With a normal pointer you follow the arrow to get the value. With a value stored in a pointer slot there is no arrow to follow — the value is the pointer itself, so you cast it directly.


Real-world use: pthread_create

pthread_create takes a void * argument for the thread function. When you only need to pass a small integer (like a thread index), you can avoid a heap allocation entirely by stuffing the value directly into that slot.

#include <stdio.h>
#include <pthread.h>

void *worker(void *arg)
{
      int index = (int)(unsigned long)arg;  // cast back — no dereference
      printf("thread %d running\n", index);
      return NULL;
}

int main()
{
      pthread_t threads[4];

      for (int i = 0; i < 4; i++)
      {
         // store i directly — no malloc needed
         pthread_create(&threads[i], NULL, worker,
         (void *)(unsigned long)i);
      }

      for (int i = 0; i < 4; i++)
      {
         pthread_join(threads[i], NULL);
      }

      return 0;
}

Compare this to the allocation-based approach, which requires a malloc per thread and a matching free inside the worker:

// with malloc — more code, easy to leak
int *idx = malloc(sizeof(int));
*idx = i;
pthread_create(&threads[i], NULL, worker, idx);

// worker must remember to free(arg)

The value must fit in a pointer — fine for thread indices, file descriptors, enum values, and small integers. For larger data structures, use the heap as normal.


Other common cases

The same pattern appears anywhere an API gives you a void * slot:

The rule is always the same — store the value with a double cast in, read it back with a double cast out, and never dereference.

C is more beautiful than C++