Week 7: Memory Management - Pointers & References
Understand how C++ interacts with memory using pointers and references.
Explore Chapter 7Chapter 7: Pointers, References, and Memory
Memory Addresses.
Every variable you declare in your C++ program resides at a specific location in the computer's memory. This location has a unique memory address, often represented as a hexadecimal number.
You can get the memory address of a variable using the address-of operator (`&`).
#include <iostream>
int main() {
int score = 100;
double price = 19.99;
std::cout << "Address of score: " << &score << std::endl; // Prints the memory address (e.g., 0x7ffc1234abcd)
std::cout << "Address of price: " << &price << std::endl; // Prints a different memory address
return 0;
}
Understanding memory addresses is fundamental to understanding pointers.
Pointers: Variables Holding Addresses.
A pointer is a special type of variable that stores the memory address of another variable. Pointers "point to" the location where another value is stored.
Declaration
You declare a pointer using an asterisk (`*`) between the data type and the pointer variable name. The data type indicates the type of variable the pointer is intended to point to.
int* ptrToInt; // Declares a pointer that can hold the address of an integer
double* ptrToDouble; // Declares a pointer that can hold the address of a double
char* ptrToChar; // Declares a pointer that can hold the address of a char
Initialization and Assignment
You initialize or assign a pointer with the address of another variable using the address-of operator (`&`).
int age = 25;
int* ptrAge = &age; // ptrAge now holds the memory address of 'age'
std::cout << "Address stored in ptrAge: " << ptrAge << std::endl; // Prints the address of 'age'
std::cout << "Address of age variable: " << &age << std::endl; // Prints the same address
Dereferencing
To access the value at the memory address stored in a pointer, you use the dereference operator (`*`).
std::cout << "Value pointed to by ptrAge: " << *ptrAge << std::endl; // Output: 25
// You can also modify the original variable's value through the pointer
*ptrAge = 26;
std::cout << "New value of age: " << age << std::endl; // Output: 26
Null Pointers
A pointer that doesn't point to any valid memory location should ideally be set to `nullptr` (introduced in C++11) or `NULL` (older style) to indicate it's null.
int* safePtr = nullptr; // Recommended way to initialize a pointer that doesn't point anywhere yet
Dereferencing a null pointer leads to undefined behavior (often a crash).
Pointers and Arrays
The name of an array often decays into a pointer to its first element.
int numbers[] = {10, 20, 30};
int* ptrNumbers = numbers; // ptrNumbers points to the first element (numbers[0])
std::cout << *ptrNumbers << std::endl; // Output: 10
std::cout << *(ptrNumbers + 1) << std::endl; // Output: 20 (Pointer arithmetic)
std::cout << ptrNumbers[2] << std::endl; // Output: 30 (Array syntax can be used with pointers)
Dynamic Memory Allocation (`new` and `delete`).
So far, variables we've declared have their memory managed automatically (on the stack or in the static/global area). C++ also allows you to allocate memory dynamically at runtime from a memory pool called the heap (or free store).
This is essential when you don't know the amount of memory needed until the program is running, or when you need an object to exist beyond the scope where it was created.
`new` Operator
The `new` operator allocates memory on the heap and returns a pointer to the beginning of that allocated block.
int* ptrInt = new int; // Allocate memory for one integer
*ptrInt = 55; // Assign a value to the allocated memory
double* ptrDouble = new double(99.9); // Allocate and initialize
// Allocate an array dynamically
int size = 10;
int* ptrArray = new int[size]; // Allocate memory for an array of 10 integers
ptrArray[0] = 1; // Use it like a regular array
`delete` Operator
Memory allocated with `new` is not automatically managed. You are responsible for releasing it back to the system when it's no longer needed using the `delete` operator (for single objects) or `delete[]` (for arrays allocated with `new[]`).
Failure to `delete` dynamically allocated memory leads to memory leaks!
std::cout << *ptrInt << std::endl; // Use the allocated memory
delete ptrInt; // Deallocate the single integer
ptrInt = nullptr; // Good practice to nullify pointer after delete
delete ptrDouble; // Deallocate the single double
ptrDouble = nullptr;
delete[] ptrArray; // Deallocate the dynamically allocated array (use delete[])
ptrArray = nullptr;
Managing dynamic memory carefully is critical in C++. Modern C++ often uses smart pointers (like `std::unique_ptr`, `std::shared_ptr` from `
References: Aliases for Variables.
A reference is an alias or an alternative name for an existing variable. Once initialized, a reference always refers to the same variable it was initialized with; it cannot be reseated to refer to a different variable.
Declaration and Initialization
You declare a reference using an ampersand (`&`) between the data type and the reference variable name. References must be initialized when declared.
int original = 10;
int& refOriginal = original; // refOriginal is now an alias for 'original'
std::cout << "Original: " << original << std::endl; // Output: 10
std::cout << "Reference: " << refOriginal << std::endl; // Output: 10
// Modifying the reference modifies the original variable
refOriginal = 20;
std::cout << "Original after ref change: " << original << std::endl; // Output: 20
// Modifying the original modifies the reference
original = 30;
std::cout << "Reference after original change: " << refOriginal << std::endl; // Output: 30
// int& refUninitialized; // Error! References must be initialized.
// refOriginal = anotherVar; // Error! References cannot be reseated.
References vs. Pointers
- References must be initialized; pointers don't have to be (can be `nullptr`).
- References cannot be reassigned to refer to a different variable; pointers can point to different variables/addresses.
- References are generally considered safer and easier to use than pointers when an alias is needed, as they cannot be null and don't require dereferencing syntax.
Passing Arguments by Reference.
One of the primary uses of references is to allow functions to modify the original arguments passed to them. This is called pass by reference.
To pass by reference, you declare the function parameter as a reference type (`type& paramName`).
#include <iostream>
// Function takes an integer by reference
void increment(int& num) {
num++; // Modifies the original variable passed as argument
std::cout << "Inside function, num = " << num << std::endl;
}
int main() {
int value = 5;
std::cout << "Before call, value = " << value << std::endl; // Output: 5
increment(value); // Pass 'value'. The function receives a reference to it.
// Output: Inside function, num = 6
std::cout << "After call, value = " << value << std::endl; // Output: 6 (original value was modified)
return 0;
}
Pass by reference is more efficient than pass by value for large objects as it avoids copying the entire object. It's also necessary when a function needs to alter the state of the caller's variables.
You can also pass by const reference (`const type& paramName`) when you want the efficiency of avoiding a copy but also want to guarantee that the function will not modify the original argument.