Dangling Pointers and References into a Vector
If a variable is destroyed while a pointer still holds its address, the pointer is called a dangling pointer. A reference can end up in the same state, and is then called a dangling reference. Using either one is undefined behavior.
Two common ways to get one
Here are two common ways for this to happen:
- The variable
maxRetriesis declared in a block on lines 12–15, and is destroyed at the closing brace. But the pointerretryLimitcontinues holding its address beyond that, making it a dangling pointer. - On line 17, the function
buildGreetingreturns a dangling reference to a local variable that’s destroyed when the function returns. Returning a pointer togreetingwould dangle the same way.
Why it sometimes seems to work: If you run this, lines 19–20 might print the correct value, an empty line, or garbage, depending on the compiler. When a variable is destroyed, its memory is not erased and it’s only marked as free to be reused. Until something else uses that memory, the old bytes still remain there. The next function you call or variable you declare is likely to reuse that same space, and then the old value is overwritten. This is what makes dangling pointers hard to find. The bug often passes testing and fails later, somewhere else.
How a vector stores its elements
There’s one surprising way to get a dangling pointer or reference involving vectors.
An std::vector keeps its elements side by side in a single block of memory, the same way an array does. That’s what makes a read or write using tasks[i] fast. The vector finds element i by starting at the first element and stepping forward i places, using pointer arithmetic.
A vector usually keeps some spare room at the end of its block, so adding an element is normally just a matter of writing into the next free spot.
What goes wrong when a vector grows
When the spare room runs out, the vector can’t just make its block bigger, because the memory right after it may already be in use by something else. So push_back does three things in such a case:
- Gets a new, larger block of memory somewhere else.
- Moves every element into the new block.
- Releases the old block.
Any pointer or reference you took to an element before this still points into the old block.
This might print the right task, print garbage, or crash, for the same reason discussed above.
The safe rule: Once you add elements to a vector, treat every pointer and reference into it as dangling. You can’t easily predict which
push_backwill cause a move, because it depends on how much spare room the vector has left.
The same problem inside a loop
The same problem shows up inside range-based for loops:
for (const std::string& task : tasks) { if (task == "Fix bug") { tasks.push_back("Test the fix"); // Undefined behavior }}The loop keeps track of the current location inside the vector’s memory. If push_back moves the elements, the loop keeps reading from the old, released block. Don’t add elements to a vector while a range-based for loop is going through it.
If you need to add while looping, use a classic for loop with an index. The loop checks tasks.size() each time, so it will also visit the elements you add.
for (std::size_t i = 0; i < tasks.size(); ++i) { if (tasks[i] == "Fix bug") { tasks.push_back("Test the fix"); }}Why a nullptr check doesn’t help
Checking for nullptr won’t catch a dangling pointer, because it still holds a normal-looking address, which isn’t equal to nullptr.
References don’t have a state like nullptr, so there isn’t even a check to write.
Fixing it with an index
For vector elements, the simplest fix is to not keep a pointer or reference at all. Store the element’s position instead, as an std::size_t:
An index works because every time you write tasks[urgentIndex], the vector looks up that position in whatever block it’s using right now.
An index stays correct as long as the element stays at the same position within the vector, which is true when you only add to the end. It also has to be less than tasks.size() when you use it.
Checkpoint
This function returns a pointer to the highest score, and main passes it a vector that stays alive the whole time. Is top safe to use?
const int* findHighScore(std::vector<int> scores) {
const int* best = &scores[0];
for (std::size_t i = 1; i < scores.size(); ++i) {
if (scores[i] > *best) {
best = &scores[i];
}
}
return best;
}
int main() {
std::vector<int> results = {72, 91, 85};
const int* top = findHighScore(results);
std::cout << *top;
}Is host safe to use on the last line?
std::vector<std::string> guests = {"Amir", "Bea"};
guests.push_back("Chen");
guests.push_back("Dana");
std::string& host = guests[0];
std::cout << host;This loop splits any order larger than 5 units into two orders. What’s the problem?
std::vector<int> orders = {3, 7, 2};
for (int& qty : orders) {
if (qty > 5) {
orders.push_back(qty - 5);
qty = 5;
}
}