Why this matters
A program often has two ways to reach the same data. A function might receive the list of students, while the main program also keeps a name for that list. A change made through one name is then visible through the other. Programmers call this situation aliasing, from the Latin word for "otherwise". Two names, one object, otherwise known as an alias.
Aliasing is not a bug. Most of the time it is what you want, because it lets a function update the caller's data without copying it. The bug comes from aliasing you did not expect. A variable gets changed in a place you did not look, and the program prints a wrong total. Beginners often meet this as a value that changes "by itself". The cause is almost always a second name pointing at the same object.
This lesson shows aliasing with ordinary variables, references and pointers, then shows the other side of the coin: a copy. A copy gives the new name its own object, so changes made through it stay separate. The lesson ends with a struct that copies by accident, which is a common source of crashes. Ownership, meaning which part of the program is responsible for freeing memory, is covered in the shared ownership lesson of this module. Here the focus is only on who sees a change.
Two names for one object
Imagine a notebook with a name on the cover, "Maya". Your friend calls the same notebook "the blue book". The notebook is one thing with two labels. If your friend writes a phone number in the blue book, you will see it when you open the notebook under the name Maya. Nothing was copied. The second label only points at the first.
C++ makes a second name with & in a declaration. The declaration std::vector<int>& b = a; says that b is another name for the vector a. Here is a program that uses the second name to change the vector.
#include <iostream>
#include <vector>
int main() {
std::vector<int> a{1, 2, 3};
std::vector<int>& b = a; // b is a second name for the same vector
b.push_back(4);
std::cout << "a = ";
for (int v : a) std::cout << v << ' ';
std::cout << "\nsize through a = " << a.size() << '\n';
std::cout << "same object: " << std::boolalpha << (&a == &b) << '\n';
int x = 10;
int& r = x; // r is another name for x
r = 20;
std::cout << "x = " << x << '\n';
int* p = &x; // p holds the address of x
*p += 5;
std::cout << "x = " << x << '\n';
return 0;
}Output:
a = 1 2 3 4
size through a = 4
same object: true
x = 20
x = 25Trace the first part. The vector a starts with three numbers. The name b is created as a second name for a. The call b.push_back(4) adds a number to the object, and that object is the same object a names. So when the program reads a, it sees four numbers. The line &a == &b asks whether the two names have the same address. It prints true, which confirms there is one object with two names.
The second part does the same with an int. The reference r is a name for x. Writing r = 20 writes to x. The third part uses a pointer. p holds the address of x, and *p += 5 changes the value at that address, which is x again.
The pointer form needs * to reach the value, and the reference form does not. The lesson on references and passing arguments in this module covers when to pick each form. Here, the only point is that all three names reach the same object.
Passing an argument creates a name
When you pass an argument to a function, C++ either copies the value or makes a name for the caller's object. The choice depends on how the parameter is written. The same vector is used in all three functions below, so the output shows the difference.
#include <iostream>
#include <vector>
void by_value(std::vector<int> v) {
v.push_back(99); // changes the copy only
}
void by_reference(std::vector<int>& v) {
v.push_back(99); // changes the caller's vector
}
void by_pointer(std::vector<int>* v) {
v->push_back(99); // same object, reached through an address
}
int main() {
std::vector<int> nums{1, 2};
by_value(nums);
std::cout << "after by_value: " << nums.size() << '\n';
by_reference(nums);
std::cout << "after by_reference: " << nums.size() << '\n';
by_pointer(&nums);
std::cout << "after by_pointer: " << nums.size() << '\n';
return 0;
}Output:
after by_value: 2
after by_reference: 3
after by_pointer: 4The parameter in by_value has no &. C++ makes a copy of the vector when the call happens, and the function's name v refers to that copy. The push inside the function adds a number to the copy, and the copy is thrown away when the function returns. The caller's nums still has two numbers.
The parameter in by_reference has &. Now v is a second name for nums. The push changes the caller's object, which is why the size grows to three.
by_pointer does the same job with an address. The caller writes &nums, and the function uses -> to reach the object. Both reference and pointer forms alias the caller's data, but the reference form needs no & or -> at the call site.
Copy on purpose
A copy gives the new name its own object. For a vector, the copy has its own array of numbers, so changing one vector does not affect the other. C++ makes a copy when you write = with a new name on the left. The copy is a separate object with the same values.
#include <iostream>
#include <vector>
int main() {
std::vector<int> original{1, 2, 3};
// Plain copy: a new vector with its own memory.
std::vector<int> copy = original;
copy[0] = 100;
std::cout << "original[0] = " << original[0] << '\n';
std::cout << "copy[0] = " << copy[0] << '\n';
std::cout << "same memory: " << std::boolalpha
<< (original.data() == copy.data()) << '\n';
// Alias: a second name, no copy.
std::vector<int>& alias = original;
alias[1] = 200;
std::cout << "original[1] = " << original[1] << '\n';
std::cout << "same memory: " << (original.data() == alias.data()) << '\n';
return 0;
}Output:
original[0] = 1
copy[0] = 100
same memory: false
original[1] = 200
same memory: trueThe vector data() method returns the address of the first number in the vector's memory. Comparing those addresses is a direct way to ask whether two names share memory. The first comparison is false because copy got its own memory. The second is true because alias is a name for original.
The rule is simple. A declaration such as auto copy = original; makes a copy. A declaration with &, such as auto& alias = original;, makes an alias. When you read a declaration, look for the & to see which one you have.
Aliasing in loops
A range-based loop is a common place where aliasing and copying get mixed up. The loop variable is either a copy of each element or a name for each element, depending on whether you write auto or auto&.
#include <iostream>
#include <vector>
int main() {
std::vector<int> prices{10, 20, 30};
for (auto p : prices) { // p is a copy of each element
p = p * 2;
}
std::cout << "after auto: ";
for (int p : prices) std::cout << p << ' ';
std::cout << '\n';
for (auto& p : prices) { // p is another name for each element
p = p * 2;
}
std::cout << "after auto&: ";
for (int p : prices) std::cout << p << ' ';
std::cout << '\n';
return 0;
}Output:
after auto: 10 20 30
after auto&: 20 40 60 In the first loop, p is a copy. Doubling it changes only the copy, so the vector is unchanged. In the second loop, p is a name for each element of prices. Doubling it changes the vector itself. The code inside each loop is almost the same, and the only difference is the &.
Copies cost time. For int, a copy is cheap. For a std::string with a thousand letters, a copy allocates new memory and copies every letter. A loop written as for (auto s : names) copies each string, even when the loop only reads it. Writing for (const auto& s : names) gives a name to each string without copying it, and it also stops the loop from changing the strings.
Copying into a function
Sometimes a function needs its own copy of the data, so it can change the data without affecting the caller. Taking the parameter by value does that. The function receives a copy, and the copy belongs to the function.
#include <algorithm>
#include <iostream>
#include <vector>
// Takes its argument by value, so sorting changes only this function's copy.
std::vector<int> sorted_copy(std::vector<int> v) {
std::sort(v.begin(), v.end());
return v;
}
int main() {
std::vector<int> scores{70, 95, 60};
std::vector<int> ranked = sorted_copy(scores);
std::cout << "scores: ";
for (int s : scores) std::cout << s << ' ';
std::cout << "\nranked: ";
for (int s : ranked) std::cout << s << ' ';
std::cout << '\n';
return 0;
}Output:
scores: 70 95 60
ranked: 60 70 95 The caller's scores is still in its original order. The sort happens to the copy inside the function, and the function returns that copy, which the caller stores in ranked. This pattern is useful when the function's input must not change. The price is one copy, so use it on purpose, not by reflex.
A copy that goes wrong
Copies of plain values and of std::vector do the right thing without help. A struct that holds a raw pointer does not. A raw pointer is a plain address, and C++ copies the address when it copies the struct. The two structs then point at one array. This is an alias you did not ask for.
#include <iostream>
struct Buffer {
explicit Buffer(int n) : data(new int[n]{}), size(n) {}
~Buffer() { delete[] data; }
int* data;
int size;
};
int main() {
Buffer first(3);
first.data[0] = 7;
Buffer second = first;
second.data[0] = 99;
std::cout << "first.data[0] = " << first.data[0] << '\n';
std::cout << "same array: " << std::boolalpha << (first.data == second.data) << '\n';
return 0;
}Run without any extra options, the program stops with this message. In a terminal, the two lines print first, and they show that both names share one array:
first.data[0] = 99
same array: true
free(): double free detected in tcache 2If the output goes to a file or a pipe, the two lines may be missing, because the program stops before the buffered text is written.
The message comes from the C runtime. The problem sits in the two objects. first and second both hold the same address in data, and the struct has no way to know that. When the program ends, each object's destructor calls delete[] on that address. The second call frees memory that was already freed, and the runtime stops the program.
With AddressSanitizer, a memory checker that g++ turns on with -fsanitize=address, the report is longer. The first lines of it are below, with the long source path shortened to main.cpp:
==758657==ERROR: AddressSanitizer: attempting double-free on 0x78ec147e0010 in thread T0:
#0 0x7ccc15d2d46f in operator delete[](void*) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:179
#1 0x5fd6f2e03aab in Buffer::~Buffer() main.cpp:5
#2 0x5fd6f2e036f8 in main main.cpp:21The process number and the addresses differ on every run. The lines that matter are attempting double-free and the two frames that point at main.cpp:5 (the destructor) and main.cpp:21 (the end of main). Line 21 is where the second object is destroyed.
The fix is to let a standard type own the memory. A std::vector copies its contents when it is copied, so the two structs get separate arrays.
#include <iostream>
#include <vector>
struct Buffer {
explicit Buffer(int n) : data(n, 0) {}
std::vector<int> data; // the vector owns its memory and copies it on copy
};
int main() {
Buffer first(3);
first.data[0] = 7;
Buffer second = first; // a real copy: a new array
second.data[0] = 99;
std::cout << "first.data[0] = " << first.data[0] << '\n';
std::cout << "second.data[0] = " << second.data[0] << '\n';
std::cout << "same array: " << std::boolalpha
<< (first.data.data() == second.data.data()) << '\n';
return 0;
}Output:
first.data[0] = 7
second.data[0] = 99
same array: falseThe copy made by Buffer second = first; now copies the vector, which allocates a new array and copies the three numbers. Changing second no longer touches first. A struct that owns a raw new[] array would need its own copy constructor and assignment operator. The lessons on dangling and null bugs and on shared ownership cover how raw memory goes wrong and what the standard types do instead.
Aliasing inside one call
The last hard case happens when a function receives the same object twice, once as a place to write and once as the data to read. The two names are different, but the object is the same.
#include <iostream>
#include <vector>
// dst and src may be the same vector.
void append_copy(std::vector<int>& dst, const std::vector<int>& src) {
std::size_t n = src.size(); // read the size once, before any push_back
for (std::size_t i = 0; i < n; ++i) {
dst.push_back(src[i]);
}
}
int main() {
std::vector<int> v{1, 2};
append_copy(v, v); // the same object is both dst and src
std::cout << "after append_copy(v, v): ";
for (int x : v) std::cout << x << ' ';
std::cout << '\n';
// Naive version, shown with a guard so it stops after 6 steps.
std::vector<int> w{1, 2};
for (std::size_t i = 0; i < w.size(); ++i) {
if (i >= 6) break;
w.push_back(w[i]);
std::cout << "step " << i << ": size " << w.size() << '\n';
}
return 0;
}Output:
after append_copy(v, v): 1 2 1 2
step 0: size 3
step 1: size 4
step 2: size 5
step 3: size 6
step 4: size 7
step 5: size 8The function reads src and writes to dst. When both names are the same vector, each push_back makes the vector longer, so the source grows while the loop reads it. The first version avoids the trap by reading src.size() once, into n, before the loop starts. The loop then copies exactly the original numbers.
The second loop in main shows the trap. It is written as i < w.size(), so the test is run again on every pass. Each push_back makes w.size() larger, so the test is never false. Without the guard if (i >= 6) break;, the loop does not end until the program runs out of memory. The guard only stops the demonstration. It is not part of a fix. The fix is the single read of src.size() in the first version.
Common mistakes
These mistakes come up often in beginner code. Each one has a short fix.
Mistake 1: thinking auto makes an alias
auto b = a; creates a copy. auto& b = a; creates an alias. A loop written as for (auto p : items) p = 0; does not change items, because p is a copy. The fix is to write auto& when the loop needs to change the elements.
Mistake 2: copying a struct with a raw pointer
A struct that holds int* gets a copy of the address when it is copied. Two objects then share one array, and when both are destroyed the array is freed twice. The runtime reports free(): double free detected in tcache 2 or the sanitizer reports attempting double-free. The fix is to store a std::vector or another standard type, which copies its contents.
Mistake 3: expecting a copy when the parameter has &
void f(std::vector<int>& v) does not copy. A change inside f is a change to the caller's vector. If the function must not change the caller's data, write const std::vector<int>& for a read-only function, or take the parameter by value for a function that works on its own copy.
Mistake 4: a loop whose end moves
A loop that reads and writes the same vector must not keep asking the vector for its size while it grows. Read size() once before the loop, as in append_copy, or copy the source first. The naive loop above shows what happens without this step: the condition stays true, so the loop does not end.
Practice
Each exercise has its answer under it. Try the code in a file before you read the answer.
Exercise 1: predict the output
What does this program print? Write both lines before you check.
#include <iostream>
#include <vector>
int main() {
std::vector<int> a{5, 6};
auto b = a;
auto& c = b;
c.push_back(7);
std::cout << a.size() << ' ' << b.size() << ' ' << c.size() << '\n';
int n = 4;
int& alias = n;
int copy = n;
alias = 9;
std::cout << n << ' ' << copy << ' ' << alias << '\n';
return 0;
}Show the answer
The program prints 2 3 3 on the first line and 9 4 9 on the second. b is a copy of a, so push_back on b leaves a at two numbers. c is a name for b, so it sees the three numbers. For the integers, alias is a name for n, so writing alias = 9 changes n. copy was made before the change, so it still holds 4.
Exercise 2: predict the output of a struct copy
What does this program print?
#include <iostream>
#include <string>
struct Player {
std::string name;
int score;
};
int main() {
Player s{"Ana", 10};
Player t = s;
t.score = 50;
Player& u = s;
u.name = "Ben";
std::cout << s.name << ' ' << s.score << '\n';
std::cout << t.name << ' ' << t.score << '\n';
return 0;
}Show the answer
It prints Ben 10 and then Ana 50. t is a copy of s made before the change, so changing t.score does not affect s. u is a name for s, so u.name = "Ben" changes s.name. The copy t keeps the name "Ana", because it was copied before the rename. Both std::string and int members are copied by value, so this struct needs no extra work.
Exercise 3: change the caller's data
Write a function double_all that doubles every number in a std::vector<int> that the caller passes in. Then call it on {1, 2, 3} and print the result.
Show the answer
#include <iostream>
#include <vector>
void double_all(std::vector<int>& values) {
for (int& v : values) {
v *= 2;
}
}
int main() {
std::vector<int> nums{1, 2, 3};
double_all(nums);
for (int v : nums) std::cout << v << ' ';
std::cout << '\n';
return 0;
}The program prints 2 4 6. The parameter is a reference, so the function works on the caller's vector. The loop variable is int&, so each v is a name for one number in the vector.
Exercise 4: fix the loop
This loop is meant to add one to every number, but it does not. Find the mistake and fix it.
#include <iostream>
#include <vector>
int main() {
std::vector<int> nums{1, 2, 3};
for (auto x : nums) {
x += 1;
}
for (int x : nums) std::cout << x << ' ';
std::cout << '\n';
return 0;
}Show the answer
The loop variable is auto x, which makes a copy of each number, so x += 1 changes only the copy. The fix is auto& x, which makes x a name for each element. With the fix, the program prints 2 3 4.
Exercise 5: find the shared memory
A colleague writes a struct with an int* array, and copies it to make a backup before a change. After the change, the backup also shows the change, and the program crashes at the end with a double free. Explain why, and say what change to the struct removes the problem.
Show the answer
The default copy copies the pointer, not the array. The backup and the original both point at one array, so a change through either one shows in both. At the end, each struct's destructor frees the same array, and the second free is reported as a double free. The fix is to store the numbers in a std::vector<int> member. The vector copies its own array when the struct is copied, so the backup and the original each own separate memory.
Summary
- Two names for one object share every change. A reference (
&) or a pointer reaches the same object as the original name. - A new name written with
=makes a copy, and changes to the copy stay in the copy. - A parameter taken by value gets a copy. A parameter taken by reference or pointer changes the caller's object.
- In a range-based loop,
automakes a copy of each element andauto&makes a name for each element. - A struct with a raw pointer copies the address, so two objects share one array and the array is freed twice. Store a
std::vectorinstead. - When a function reads and writes the same vector, read the size once before the loop so the loop does not chase its own growth.