Applications of Pointers
Imagine your friend needs to go to a grocery store but doesn’t know where one is. In helping them, you can do either:
- Tell them where the nearest grocery story is (perhaps by giving its address)
-OR- - You tell them to wait while you create a brand new grocery store right where they’re standing
Analogy aside, pointers offer the same flexibility. You pass the address of an entity around as a layer of indirection because addresses are easy to copy and notate where reconstructing and managing entire entities is expensive. Toss in a note about dynamically sized data structures and heap memory management, and that’s pretty much the entirety of it.
Most common use cases of pointers in C:
Strings
In C, strings are null terminated (end with ‘\0’ character). Therefore, if you know the location of the first character and you know what the last character will be, you can represent strings as an array of characters:
char complicated_string[] = {‘H’, ‘e’, ‘l’, ‘l’, ‘o’, ‘!’, ‘\0’};
char *easy_string = “Hello!”;
// easy_string is a pointer to the first character ‘H’ in a null-terminated string
// complicated_string shows the equivalent array
Pass-by-reference
We can have functions that modify variables that exist in a different scope. Consider this example:
int x = 5; // x = 5
int *p = &x; // p -> x
printf(“%d\n”, x); // prints 5
void modify_x(int *pointer_to_x) {
*pointer_to_x = 2;
}
modify_x(p);
printf(“%d\n”, x); // prints 2
We just changed the value of x by passing a reference to it instead of using x as a global variable. Passing the address of a variable to a function instead of the variable itself is also really important if you’re using really large variables and need to manage memory efficiently:
struct my_struct = { // A really big struct (208 bytes!)
double a;
double b;
double c;
…
double z;
};
void use_copy_of_struct(struct my_struct x) {
// Makes a copy of the entirety of x and
// puts it on the stack, using lots of memory
}
void use_reference_of_struct(struct my_struct *x) {
// Makes a copy of the address of x and
// puts it on the stack, using much less memory
// (usually 4 or 8 bytes)
}
struct my_struct x;
use_copy_of_struct(x); // Bad for memory
use_reference_of_struct(&x) // Good for memory
Data structures
Pointers are also used to implement more abstract data structures like linked lists, trees, graphs, etc. Here’s a common linked list example:
struct my_struct = {
int some_int_data;
char some_char_data;
struct my_struct *next;
};
struct my_struct first;
struct my_struct second;
// First struct now has a link to the second struct
first.next = &second;
Dynamic memory allocation
If you don’t know ahead of time how much memory your program will need, you can dynamically allocate memory as you go and access it with a pointer:
unsigned int bytes_needed = get_input_from_user();
my_data_structure *new_data = (my_data_structure *)malloc(bytes_needed * sizeof(my_data_structure));
// Use dynamically allocated memory here…
free(new_data);
Malloc returns a void pointer to the block of memory you just allocated which you can then cast to the appropriate data type pointer. Don’t forget to free it when you’re done to avoid memory leaks!
Memory-mapped I/O
MMIO is a technique used for accessing and modifying data on an I/O device (such as a sensor or motor) from your program. Basically, the I/O device has a reserved “address” in the computer’s memory and if the I/O device sees an access to that address on the memory bus, it will intervene and respond to the request. This is especially useful in embedded systems. For example, suppose you have a complex sensor attached to a microcontroller and you want to retrieve the latest reading from a register inside the sensor:
unsigned *char sensor_base_addr = (unsigned char *)0x80000000;
unsigned *char result_reg_offset = (unsigned char *)0x0000123;
// Tries to access 0x80000123 in memory, sensor sees this and
// responds instead with the value
int sensor_val = *(sensor_base_addr + result_reg_offset);