Intermediate C++ Systems Programming
Pointers and References
Memory Addresses and Pointers
Every variable you create in your program lives somewhere in your computer's memory. Think of memory as a vast street of houses, where each house has a unique address. A variable is like a person living in one of those houses. A pointer, then, is not the person itself, but a piece of paper with the person's home address written on it.
In C++, we use the ampersand symbol &, called the 'address-of' operator, to get the memory address of a variable. A pointer is a special type of variable that stores this address.
#include <iostream>
int main() {
int score = 95; // Our variable, living at some memory address
int* scorePtr; // A pointer, intended to hold the address of an integer
scorePtr = &score; // Assign the address of 'score' to the pointer
// Let's see the address itself
std::cout << "Address of score: " << scorePtr << std::endl;
// To get the value back from the address, we 'dereference' the pointer
std::cout << "Value at that address: " << *scorePtr << std::endl;
return 0;
}
In the code, int* scorePtr; declares a pointer named scorePtr that can hold the address of an integer. The * has two jobs. In a declaration, it signifies a pointer type. When used with an existing pointer variable, like *scorePtr, it becomes the 'dereference operator'. This operator follows the address stored in the pointer to access the actual value at that location.
References as Aliases
If a pointer is an address, a reference is a nickname. It's an alias for an existing variable. Once you create a reference, it acts exactly like the original variable because it is the original variable, just with a different name. You create a reference using the & symbol, but in the declaration itself.
int original = 100;
int& ref = original; // 'ref' is now another name for 'original'
ref = 200; // This changes 'original' to 200
std::cout << original << std::endl; // Prints 200
Unlike pointers, references have some strict rules:
- They must be initialized when they are created. You can't have an unattached reference.
- They cannot be changed to refer to another variable. A reference is a loyal alias for life.
- There is no such thing as a "null reference."
Because of these rules, references are often considered safer and easier to use than pointers.
Passing Data to Functions
The distinction between pointers and references becomes crucial when passing data to functions. The default method, pass-by-value, creates a complete copy of the variable for the function to use. For a simple integer, this is fine. But for a large data structure like a big object or a long list, making a copy is slow and wastes memory.
Imagine photocopying a 500-page book every time you wanted to show someone a single sentence. It's much more efficient to just give them the original book and a page number.
This is where pass-by-reference comes in. By passing a reference (or a pointer), you're not copying the data. You're just giving the function an alias (or the address) to the original data. This is faster and more memory-efficient.
// A large data structure
struct PlayerData {
// Imagine many fields here...
std::string name;
int health;
int mana;
};
// Pass-by-value (inefficient copy)
void processPlayerData_Value(PlayerData data) { /* ... */ }
// Pass-by-reference (efficient, no copy)
void processPlayerData_Reference(const PlayerData& data) { /* ... */ }
int main() {
PlayerData player_one = { "Arin", 100, 50 };
// Inefficient: A full copy of player_one is created
processPlayerData_Value(player_one);
// Efficient: The function just gets a reference to player_one
processPlayerData_Reference(player_one);
}
Notice the const keyword in the pass-by-reference example. This is a common practice. It makes the reference read-only, ensuring the function can't accidentally change the original data. It's a promise: "I'm only looking, not touching."
Stack Memory and Pointer Safety
When your program runs, local variables created inside functions are typically placed on a memory segment called the stack Each time you call a function, a new "stack frame" is added to the top, holding its local variables. When the function finishes, its frame is removed, and all its variables are destroyed automatically. This is fast and orderly.
A common and dangerous bug is a "dangling pointer." This happens when a pointer holds the address of a variable that has been destroyed, such as a local variable from a function that has already returned. Trying to use this pointer leads to undefined behavior because the memory it points to could now contain anything.
To improve safety, modern C++ introduced nullptr. It's a special keyword representing a pointer that points to nothing. Unlike its predecessor, NULL, which was just the integer 0, nullptr has its own distinct type. This prevents bugs where the compiler could confuse 0 the number with NULL the null pointer.
Always initialize your pointers to
nullptrif you don't have a valid address for them yet. Before using a pointer, check if it's notnullptr.
int* safePtr = nullptr; // Good practice: initialize to nullptr
// ... later in the code
if (safePtr != nullptr) {
// Only dereference if it's safe
std::cout << *safePtr << std::endl;
}
Ready to test your knowledge on pointers and references?
What is the primary purpose of a pointer in C++?
Consider the following C++ code snippet:
int score = 100;
int* scorePtr = &score;
What is the value of *scorePtr?
Mastering when to use a value, a pointer, or a reference is key to writing efficient and safe C++ code. Using references for passing large objects is a fundamental optimization you'll use constantly.