Pointers

Posted on Jul 21, 2023

What are pointers?

In programming, a pointer is a derived data type (like arrays, structs and unions).

Instead of storing values like normal types, pointers store memory addresses of another data objects. Hence the name pointer, because these addresses “point” to some data.

In short, a pointer is: a variable that contains a memory address (as its value).

We will see how they prove to be very useful and how to use them in the C programming language.

Before that, lets take a brief look at what memory and memory addresses are.

Memory

Computer memory refers to the hardware components that store data.

The basic unit of memory is a byte (8 binary digits); Each byte is uniquely addressable, meaning it has a specific location in memory. Similar how every house has a unique address in a city.

Memory chips are composed of memory cells, each capable of storing a single byte. When the CPU needs to read or write data, it sends the memory address over the memory bus, triggering the appropriate location to activate, retrieving or storing the data.

However, memory is not organized just as a raw sequence of bytes; It stores data in a structured manner, for example, data might be organized into variables, arrays, objects or executable code. Different memory locations are used for program code, stack, heap and other data structures.

Memory bus

You can think of the memory bus as a digital highway that connects the CPU and computer memory modules; It is like a high-speed road where data and instructions zoom back and forth.

When the CPU needs to fetch or store data, it sends a memory address on the memory bus; This address represents a unique ID or location on a memory chip. The data is then transferred through the memory bus to the CPU, where it can be processed of used for various tasks.

Types of memory

Computers have a memory structure hierarchy, each offering different characteristics such as capacity and speed.

The hierarchy goes as follows (from fastest to slowest):

  1. Registers

By far, the fastest memory units which are located within the CPU (Central Processing Unit)

  1. Cache

Acts as intermediaries between the CPU and RAM

  1. RAM (Random Access Memory)

Main memory used by the computer for running programs

  1. Secondary memory (NVMe, SSD, HDD, CD/DVD, …)

Large non-volatile storage

When programming in C, memory addresses typically refer to main memory (RAM).

Memory addresses

A memory address is a unique identifier assigned to each byte (8 bits) of memory in a computer; It represents a location in computer memory.

Memory addresses are usually represented in hexadecimal; Programmers like to use the hexadecimal numbering system because it offers a direct representation to binary: each 4 binary digits represent 1 hexadecimal digit; A byte can be represented with two hexadecimal ditits.

In a 32 bit system a memory address consists of 32 bits, or 4 bytes. In a 64 bit computer a memory address consists of 64 bits, or 8 bytes.

With a bit of math we can calculate how many different memory addresses we can have in a 32 bit system: 2^32 = 4,294,967,296 unique memory locations, which is 4GB; This is called address space.

Working with memory in programming languages involves memory allocation, reallocation and deallocation; Accessing memory (directly and inderectly); And using pointers.

What can pointers do?

Altough the definition of a pointer may seem simple, there are a lot of things you can do with pointers that are really useful. Some of those things are:

  • DYNAMIC MEMORY ALLOCATION

Pointers allow programs to request memory at runtime, this is specially useful when the size of the data is not known at compile time or when working with data structures that need to grow or shrink dynamically.

  • EFFICIENT PASSING OF LARGE DATA

When passing data to functions, rather then making copies of large data, you can just pass the pointer which points to the memory location of the data. This avoids unnecessary memory consumption and improves the performance of your program.

  • DATA STRUCTURES

To implement advanced data structures like linked lists, trees, graphs and hashtables pointers are essential. They are used to reference and connect elements efficiently.

  • FUNCTION POINTERS

Pointers to functions allow function to be treated as variables and passed as arguments to another functions.

  • OPERATING IN ARRAY ELEMENTS

Arrays and pointers are very related in C. Pointers can be use to move through, manipulate and access elements arrays more efficiently than using array indexing.

  • MEMORY MANIPULATION

Pointers provide direct access to memory, allowing memory manipulation operations, such as copying bocks of memory, comparing memory areas and so on.

I hope by now you are convinced that using pointers bring a lot of advantages and power to the programmer.

How to use them?

I will use the C programming language to demonstrate how to use pointers. I highly recommend you to play with this code examples and even try to do something new in your own. You will only learn by doing.

  • DECLARING POINTERS
void main() {
    int* ptr;
}

That is all you need!

int* ptr declares a pointer that can hold the memory address of an integer variable.

  • INITIALIZING POINTERS
void main() {
    int value = 13;
    int* ptr_value = &value;
}

In this example I created an integer variable, value, and a pointer, ptr_value, that points to its memory address. I assigned the address to the pointer using the “address-of” operator (&).

Simple right? The next step is accessing the variable that the pointer points to, ie: get a value through its memory address, also called reference. For that we must dereference the pointer.

  • DEREFERENCING POINTERS
void main() {
    int value = 13;
    int* ptr_value = &value;

    int value2 = *ptr_value;

    printf("value2 = %d\n", value2); // value2 = 13
}

In order to dereference a pointer I use the star operator (*) on ptr_value, creating a pointer to a pointer, which just equals to the value variable.

  • PRINTING A POINTER OR MEMORY ADDRESS
void main() {
    int value = 48;
    int* ptr_value = &value;
    
    printf("%p\n", ptr_value);
}

The printf function is called with the %p format specifier to indicate to the compiler that I am in fact trying to print a pointer variable. \n is new line.

For now, you have covered the basics of pointers, but there is more to explore!

Null pointers

void main() {
    int* ptr; // dont do this
    int* ptr_null = NULL; // do this
}

In this example, I declared two pointer variables ptr and ptr_null. ptr_null is initialized with the value NULL. This is called a null pointer.

When you don’t initilize a pointer, i.e., declare a pointer that does not point to a memory address of a known variable, you create uncertainty in your program. The ptr variable is not know at the beginning, and will be different everytime you re-run your program. In a hacker’s perspective, it may hold a random value that happened to be in the memory location assigned to that pointer.

A good practise is to ensure that a pointer has a well defined initial value, such as NULL. In the example I show you how to initialize a pointer variable with a NULL value. By doing this you are indicating the compiler that the pointer does not point to any valid memory address. Later in the program you may assign it the address of a valid memory location (variable).

Pointer arithmetic

void main() {
    int array_numbers[] = { 10, 20, 30, 40, 50 };
    int* ptr_number = &array_numbers;
    
    printf("%d\n", *ptr_number); // 10
    
    ptr++;
    printf("%d\n", *ptr_number); // 20
    printf("%d\n", *(ptr_number + 1)); // 30
    printf("%d\n", *(ptr_number + 2)); // 50
    printf("%d\n", *(ptr_number - 3)); // 20
}

When we set the ptr_number variable to point to the address of array_numbers we are assign the number pointer to point to the first element of the number array, i.e., array_numbers[0] = 10.

Pointers can be incremented or decremented using the ++ andd --operators respectively. When you increment/decrement a pointer it moves to the next/previous memory location of the same data type it points to.

For example, if you have a pointer to an integer (int*) incrementing the pointer will move it to the next memory location that can hold, in this case, an integer.

Pointer and array relationship

Previously we saw how pointer arithmetic allows us to interact with arrays, in fact arrays and pointers are closely related, since pointers allow us to manipulate arrays.

When we initialize a pointer with the address of an array the value at that address will be the first element.

Each position of an array can be manipulated not only through the built-in array square brackets notation but also with pointers since they represent the same thing.

void main() {
    int array_numbers[] = { 5, 10, 15, 20 };
    int* ptr_number = &array_numbers;
    
    printf("%d\n", array_numbers[0]); // this is the same as...
    printf("d\n", *ptr_number); // ...this, which is 5
    
    printf("%d\n", array_numbers[1]); // 10
    printf("%d\n", *(ptr_number + 1)); // 10
    
    printf("%d\n", array_numbers[3]); // 20
    printf("%d\n", *(ptr_number + 2)); // 20
}

char* str vs char str[]

A question I had when learning about pointers in C was: what is the difference between char* str and char str[]?

Both are used to represent a string of characters, but they each have different implications:

  • char str*

Here the declaration defines a pointer called str of type char*, which means it is a pointer to a character in a string (as we saw it points to the first element, in this case, a character).

It does not allocate any memory for the string itself. You will need to assign a valid memory location before use.

  • char str[]

This declaration defines a character array str, meaning it is a data type that can hold a sequence of characters.

When you use you use this representation you must declare a specific size to the array. Either by providing a number of characters during initialization or by define it in the declaration. This declaration allocates memory for the string and copies the provided characters into that memory.

void main() {
    char str[] = "Hello";
    
    char* other_str = "World!";
    
    printf("%s %s\n", str, other_str);
}

So which one should I choose? It depends on your requirements. Using a pointer is useful for scenarios where you want dynamic memory allocation and change the string during runtime, an array is better for cases where you have fixed size strings. In arrays the memory is automatically allocated and deallocated, where as if you use pointers you will need to carefully manage your memory.

Pointer data type

What is the difference between int* char* and void*?

The difference between these pointers is the variable type they point to. However what is void*? Void is a data type in C used to indicate for example, that a function does not return anything. However, void* is a special type of pointer, it is a generic pointer type that represents a pointer to an unspecified type. It is often used when you do not know the specific data type at compile time.

Pointers to pointers

Also known as double pointers, pointers to pointers are variables that store the memory address of other pointers. It is declared using an additional star (**).

This stategy provides some nice features:

  • MULTIPLE LEVELS OF INDIRECTION

Double pointers introduce an additional level of indirection, allowing to indirectly access and modify the value of a pointer variable.

  • MULTIDIMENSIONAL ARRAYS

Pointers to pointers are used for representing and accessing elements of multidimensional arrays.

Pointers to functions

Pointers to functions allow a pointer variable to store the memory address of a function. This provides flexibility to call different functions dynamically at runtime.

int numbers_add(int a, int b) {
    return a + b;
}

int numbers_subtract(int a, int b) {
    return a - b;
}

void main() {
    int (*ptr_fn)(int, int);
    
    ptr_fn = &numbers_add;
    int result1 = ptr_fn(5, 7);
    
    ptr_fn = &numbers_subtract;
    int result2 = ptr_fn(11, 5);

    printf("Result 1: %d\n", result1); // 12
    printf("Result 2: %d\n", result2); // 6
}

Pointers to structures

Pointers to structures allow you to work with structures indirectly by using their memory addresses.

typedef struct {
    char name[20];
    int age;
} Human;

void main() {
    Human h1;
    Human* ptr_h1;

    ptr_h1 = &h1;
    ptr_h1->age = 25;

    printf("Age: %d\n", ptr->age); // 25
}

Memory allocation and deallocation

Memory allocation means reserving a block of memory for storing data. Memory deallocation means releasing previously allocated memory so that it can be reused by the system or the program.

In C, memory allocation and deallocation are usually performed using funtions like: malloc, calloc, realloc and free.

Memory allocation is extremely important and related to pointers because the second you start working with dynamically allocated memory you will both pointers and memory allocation and deallocation funtions.

Direct vs indirect access to memory

There are two ways to manipulate data stored in memory:

  • DIRECT ACCESS

Accessing and modifying memory locations directly using memory addresses or variable names. For example, when assigning a value to a variable.

  • INDIRECT ACCESS

Accessing and modifying memory through the use of pointers or references. Instead of working directly with memory addresses or variables, you work with pointers that hold the memory addresses. It is specially useful when working with dynamic memory allocation, passing values by reference, dynamically selecting functions or data structures at runtime.

Aliasing

Aliasing refers when multiple variables or pointers refer to the same memory address. It is important to be aware of aliasing issues in your code. Handle it properly to ensure correct behavior:

  • Use const whenever possible
  • Use the restrict keyword (C99)
  • Avoid casting pointers
  • Avoid global variables

Bit manipulation

Bit manipulation with pointers involves doing bitwise operations on the binary representation of the memory address (held by the pointers). These operations can be useful for low-level manipulation of data. Here are a few techniques:

  1. Bitwise AND (&)

Performs bit masking. Extract specific bits or set certain bits to zero while preserving other bits.

  1. Bitwise OR (|)

Set specific bits to 1.

  1. Bitwise XOR (^)

Toggle specific bits.

  1. Bitwise Shift (« or »)

Shift the bits left or right by some number of positions.

Pointers can help us do these techniques more clearly.

Pointer casting

Pointer casting is when you change the type of a pointer to another type. It allows to treat a memory location as if it stored a different type of data.

void main() {
    int num = 10;
    int* ptr = #

    char* ptr_char = (char*)ptr;
}

TODO

This page is a work in progress. Some topics may contain errors. I will be working S&S on fixing them!