C Systems Programming Essentials
Pointers and Addresses
The What and Why of Pointers
Every time you declare a variable in your C program, the computer sets aside a small chunk of memory to store its value. Think of your computer's RAM as a vast, organized grid of mailboxes. Each mailbox has a unique address, allowing the system to find it. When you create an integer int score = 95;, you're essentially telling the computer: "Find an empty mailbox, label it 'score', and put the number 95 inside."
These memory addresses are typically represented as long hexadecimal numbers, like 0x7ffc9a8b45f4. Usually, we don't need to know the exact address; we just use the variable name 'score', and the compiler handles the rest. But C's most powerful feature is its ability to work directly with these addresses. This is where pointers come in.
Finding and Storing Addresses
A pointer is simply a variable that holds a memory address. Instead of storing a number like 95 or a character like 'A', it stores the location of another variable.
To get the address of a variable, you use the address-of operator, &. To declare a pointer, you use an asterisk * between the data type and the variable name. The type tells the compiler what kind of data the pointer will be pointing to. An int * points to an integer, while a char * points to a character.
#include <stdio.h>
int main() {
int score = 95;
int *score_ptr; // Declares a pointer to an integer
score_ptr = &score; // Assigns the address of 'score' to 'score_ptr'
printf("Value of score: %d\n", score);
printf("Address of score: %p\n", &score);
printf("Value of score_ptr (the address it holds): %p\n", score_ptr);
return 0;
}
Notice the %p format specifier in printf. It's used specifically for printing pointer addresses. If you run this code, you'll see that the address of score and the value stored in score_ptr are identical. The pointer is literally holding the location of the original variable.
Accessing Data Indirectly
Storing an address is useful, but the real power comes from accessing the data at that address. This is called dereferencing, and it's done with the dereference operator, which is also an asterisk *. This can be confusing at first.
- When used in a declaration (
int *p;), the*creates a pointer. - When used on an existing pointer variable (
*p = 100;), the*dereferences it, meaning it accesses the value at the stored address.
Think of & as "get address of" and * as "go to the value at this address."
#include <stdio.h>
int main() {
int score = 95;
int *score_ptr = &score; // Declare and initialize the pointer
printf("Initial score: %d\n", score);
// Use the dereference operator to change the value
*score_ptr = 100;
printf("New score: %d\n", score); // The original variable is changed!
printf("Value at address stored in score_ptr: %d\n", *score_ptr);
return 0;
}
By changing *score_ptr, we didn't change the pointer itself. The pointer still holds the same address. Instead, we reached through the pointer to the memory location it was pointing to and overwrote the value there. This is why the value of the original score variable changed.
Pointer Safety
What happens if a pointer doesn't point to anything? A pointer that hasn't been initialized contains a garbage address. Trying to dereference it can read from a random memory location or, worse, overwrite critical system data, leading to a program crash. This is a common bug called a "dangling pointer."
To prevent this, C provides the concept of a NULL pointers. NULL is a special value that represents "pointing to nothing." It's good practice to initialize pointers to NULL if you don't have a valid address for them yet. This way, you can check if a pointer is NULL before trying to use it.
#include <stdio.h>
#include <stdlib.h> // NULL is defined here
int main() {
int *my_ptr = NULL;
// This check prevents a crash
if (my_ptr != NULL) {
printf("The value is: %d\n", *my_ptr);
} else {
printf("The pointer is NULL and cannot be dereferenced.\n");
}
return 0;
}
Pointer types are also crucial for safety and correctness. When you declare int *p;, you're telling the compiler that p holds the address of an integer. The compiler knows an int takes up a certain amount of memory (e.g., 4 bytes). This information is essential for pointer arithmetic, which allows you to move between elements in an array, a topic we'll explore later.
Forcing a pointer of one type to point to data of another type can lead to unpredictable results, as the program might misinterpret the data stored in memory.
In C, what is a pointer?
Which operator is used to get the memory address of a variable?