Mastering C Pointers and Memory Management
Memory Addresses and Dereferencing
A Program's Place in Memory
When you run a C program, the operating system carves out a private space in RAM just for it. This isn't one big, messy drawer. It's a highly organized cabinet with different sections for different purposes. Understanding this layout is the first step to mastering how C interacts with memory.
Here's a quick rundown of the main sections:
- Text Segment: This is where the compiled machine code of your program resides. It's typically read-only to prevent a program from accidentally modifying its own instructions.
- Data Segment: This holds global and static variables. It's split into two parts: one for variables initialized with a specific value in your code, and another (often called BSS) for those that are uninitialized (or initialized to zero by default).
- Heap: This is a region of free memory available for your program to use at runtime. When you need to allocate memory dynamically (a topic for later), it comes from the heap.
- Stack: This area is used for local variables, function parameters, and return addresses. Each time a function is called, a new "stack frame" is pushed onto the stack to hold its local data. When the function returns, its frame is popped off.
Finding an Address
Every variable you create, whether it's on the stack or in the data segment, has a specific address—its unique location in memory. You can think of RAM as a vast grid of numbered mailboxes, and a variable's address is its mailbox number. The address-of operator (&) is how you ask the system for that number.
#include <stdio.h>
int main() {
int score = 95;
char grade = 'A';
// Use & to get the memory address
// %p is the format specifier for printing pointers
printf("Value of score: %d, Address of score: %p\n", score, &score);
printf("Value of grade: %c, Address of grade: %p\n", grade, &grade);
return 0;
}
The output will show the value stored in each variable and its corresponding memory address, printed as a hexadecimal number. These addresses are the raw information a pointer holds.
A pointer is simply a variable whose value is the memory address of another variable.
The size of a pointer variable itself depends on the computer's architecture. On a 32-bit system, an address is 32 bits (4 bytes) long, so a pointer needs 4 bytes of storage. On a 64-bit system, addresses are 64 bits (8 bytes), so a pointer needs 8 bytes. The size of a pointer is always the same, regardless of the type of data it points to, because it only needs to be large enough to hold a memory address.
Following the Address
Knowing an address is useful, but the real power comes from accessing the data stored there. This action is called dereferencing, and it's done with the indirection operator (*). When you place * before a pointer variable, you're telling the CPU: "Don't give me the value of this pointer (the address). Go to the address it holds and give me the value you find there."
#include <stdio.h>
int main() {
int price = 50;
int *price_ptr; // Declare a pointer to an integer
price_ptr = &price; // Store the address of 'price' in the pointer
printf("Address stored in price_ptr: %p\n", price_ptr);
// Dereference the pointer to get the value at that address
printf("Value at that address: %d\n", *price_ptr);
// You can also modify the original variable through the pointer
*price_ptr = 60;
printf("New value of price: %d\n", price);
return 0;
}
In the example, price_ptr holds the address of price. The expression *price_ptr follows that address and retrieves the value 50. We then use *price_ptr again to change the value at that memory location to 60, which directly modifies the original price variable.
Think of
&as creating a reference (getting an address) and*as following a reference (getting the value at an address).
This leads to an important distinction in C: l-values and r-values.
An l-value (locator value) represents a memory location. It's an expression that can appear on the left side of an assignment operator because it refers to a place where a value can be stored. A variable name like price is an l-value. A dereferenced pointer like *price_ptr is also an l-value, because it refers to the memory location of price.
An r-value (read value) is just a data value. It can appear on the right side of an assignment, but not the left. The literal number 50 is an r-value. The expression price + 10 is an r-value because it evaluates to a temporary value, not a storage location.
When we write *price_ptr = 60;, we are using *price_ptr as an l-value (a destination for the data) and 60 as an r-value (the data itself).
Where are local variables, function parameters, and return addresses typically stored in a C program's memory?
Which operator is used to retrieve the memory address of a variable?
Understanding how your program is laid out in memory and how pointers allow you to directly access it is fundamental to writing effective C code.