Introduction to References
A reference in C++ is an alias—another name for an existing variable. Once initialized, a reference cannot be changed to refer to a different variable. References provide a safer, more intuitive way to work with indirect access compared to pointers.
Reference
A reference is an alias for another variable. It shares the same memory address as the original variable—any change through the reference affects the original, and vice versa.
Syntax: Type& refName = existingVar;
#include <iostream>
using namespace std;
int main() {
int original = 42;
int& ref = original; // ref is an alias for original
cout << "original = " << original << endl; // 42
cout << "ref = " << ref << endl; // 42
// Modifying through reference
ref = 100;
cout << "After ref = 100:" << endl;
cout << "original = " << original << endl; // 100
cout << "ref = " << ref << endl; // 100
// Modifying original
original = 200;
cout << "After original = 200:" << endl;
cout << "ref = " << ref << endl; // 200
// Same memory address
cout << "&original = " << &original << endl;
cout << "&ref = " << &ref << endl; // Same address!
return 0;
}
Reference Declaration & Initialization
References must be initialized when declared and cannot be reassigned to refer to different variables. They provide a cleaner syntax than pointers for many use cases.
#include <iostream>
using namespace std;
int main() {
// Reference declaration: Type& name = variable;
int num = 10;
int& ref = num; // ref is now an alias for num
// Style variations (all equivalent)
int &ref1 = num; // & next to name
int& ref2 = num; // & next to type (preferred)
int & ref3 = num; // & with spaces
// References to different types
double pi = 3.14159;
double& piRef = pi;
char letter = 'A';
char& letterRef = letter;
// MUST be initialized - this is an error:
// int& badRef; // Error: reference must be initialized
// Cannot be null - this is an error:
// int& nullRef = nullptr; // Error!
// Cannot rebind - assignment changes the value, not the binding
int x = 100, y = 200;
int& xRef = x;
xRef = y; // This assigns y's value to x, doesn't rebind!
cout << "x = " << x << endl; // 200, not 100!
return 0;
}
Reference Rules
Must Initialize
References must be initialized at declaration
Cannot Rebind
Once bound, cannot refer to another variable
Cannot Be Null
References always refer to valid objects
Practice Questions: Declaration
Task: Create a double variable with value 5.5, create a reference to it, and use the reference to change the value to 10.0.
Show Solution
double value = 5.5;
double& ref = value;
ref = 10.0;
cout << value << endl; // 10.0
Task: Predict the output:
int a = 10, b = 20;
int& ref = a;
ref = b;
b = 30;
cout << a << " " << ref << " " << b << endl;
Show Solution
// Output: 20 20 30
// Explanation:
// ref = b assigns b's VALUE (20) to a
// ref still refers to a, not b
// b = 30 doesn't affect a or ref
References vs Pointers
References and pointers both provide indirect access to variables, but they have important differences. Understanding when to use each is crucial for writing clean, safe C++ code.
| Feature | Reference | Pointer |
|---|---|---|
| Syntax | int& ref = var; |
int* ptr = &var; |
| Access | ref (direct) |
*ptr (dereference) |
| Must Initialize | Yes | No |
| Can Be Null | No | Yes |
| Can Rebind | No | Yes |
| Arithmetic | No | Yes |
| Memory | May not use extra memory | Uses memory for address |
#include <iostream>
using namespace std;
int main() {
int value = 42;
// Reference
int& ref = value;
cout << "Reference: " << ref << endl; // 42 (no dereference needed)
ref = 100; // Direct assignment
// Pointer
int* ptr = &value;
cout << "Pointer: " << *ptr << endl; // 100 (must dereference)
*ptr = 200; // Must dereference to assign
// Pointer can be null
int* nullPtr = nullptr; // OK
// int& nullRef; // Error! Must initialize
// Pointer can rebind
int other = 500;
ptr = &other; // Now points to other
cout << "*ptr = " << *ptr << endl; // 500
// Reference cannot rebind
// ref = other; // This ASSIGNS other's value to value!
// Pointer arithmetic
int arr[] = {10, 20, 30};
int* p = arr;
p++; // Move to next element
cout << *p << endl; // 20
// No reference arithmetic
// int& r = arr[0];
// r++; // This increments the VALUE, not the "position"
return 0;
}
When to Use Each
// Use REFERENCES when:
// 1. You don't need null
// 2. You don't need to rebind
// 3. You want cleaner syntax
void processData(const string& data); // Clean, no null check needed
// Use POINTERS when:
// 1. You need optional (nullable) parameters
// 2. You need to rebind to different objects
// 3. You need pointer arithmetic (arrays)
// 4. Working with dynamic memory
void processOptional(int* maybeNull); // Can be nullptr
int* findElement(int* arr, int size, int target); // Pointer arithmetic
Practice Questions: References vs Pointers
Task: For each scenario, decide if you should use a reference or pointer:
- Function parameter that should never be null
- Iterating through array elements
- Optional output parameter that might not be used
- Alias for a long variable name
Show Solution
- Reference - Cannot be null, cleaner syntax
- Pointer - Needs arithmetic to move through array
- Pointer - Can be nullptr if not needed
- Reference - Simple alias, no null needed
Const References
Const references (const T&) allow read-only access to a variable.
They're extensively used for function parameters to avoid copying while
preventing modification.
#include <iostream>
#include <string>
using namespace std;
int main() {
int value = 42;
// Const reference - read only
const int& constRef = value;
cout << constRef << endl; // OK - reading
// constRef = 100; // Error! Cannot modify through const ref
// Original can still be modified
value = 100;
cout << constRef << endl; // 100 - sees the change
// Const ref can bind to literals and temporaries!
const int& literalRef = 42; // OK!
const string& tempRef = "Hello"; // OK - binds to temporary
// Regular ref cannot bind to literals
// int& badRef = 42; // Error!
// Const ref extends lifetime of temporary
const string& extended = string("Temporary");
cout << extended << endl; // Safe - lifetime extended
return 0;
}
Const Reference with Functions
#include <iostream>
#include <string>
#include <vector>
using namespace std;
// Pass by const reference - efficient and safe
void printVector(const vector<int>& vec) {
// vec.push_back(1); // Error! Cannot modify
for (int val : vec) {
cout << val << " ";
}
cout << endl;
}
// Avoid copying large objects
void processString(const string& str) {
cout << "Length: " << str.length() << endl;
cout << "Content: " << str << endl;
// str[0] = 'X'; // Error! Cannot modify
}
// Compare: pass by value (copies!)
void inefficient(string str) { // Copies entire string
cout << str << endl;
}
// Compare: pass by const ref (no copy)
void efficient(const string& str) { // No copy
cout << str << endl;
}
int main() {
vector<int> numbers = {1, 2, 3, 4, 5};
printVector(numbers);
string longString = "This is a very long string...";
efficient(longString); // Preferred
// Can pass literals directly with const ref
efficient("Temporary string"); // OK!
return 0;
}
Practice Questions: Const References
int x = 10;
const int& ref = x;
cout << ref << endl; // Line 1
ref = 20; // Line 2
x = 30; // Line 3
cout << ref << endl; // Line 4
Show Solution
Line 2 causes an error - Cannot modify through const reference.
Lines 1, 3, 4 are valid. Line 3 modifies x directly (allowed), and Line 4 reads through const ref (allowed).
Task: This function doesn't modify the vector. Improve the signature:
int findMax(vector vec) {
int max = vec[0];
for (int v : vec) {
if (v > max) max = v;
}
return max;
}
Show Solution
int findMax(const vector& vec) {
int max = vec[0];
for (int v : vec) {
if (v > max) max = v;
}
return max;
}
// Benefits: No copying, clearly shows vec won't be modified
Pass by Reference
Pass by reference allows functions to modify caller's variables and avoid copying large objects. It's one of the most common uses of references in C++.
#include <iostream>
using namespace std;
// Pass by VALUE - copies the argument
void incrementByValue(int x) {
x++; // Modifies local copy only
cout << "Inside (value): " << x << endl;
}
// Pass by REFERENCE - modifies original
void incrementByRef(int& x) {
x++; // Modifies the original!
cout << "Inside (ref): " << x << endl;
}
int main() {
int num = 10;
cout << "Original: " << num << endl; // 10
incrementByValue(num);
cout << "After byValue: " << num << endl; // Still 10
incrementByRef(num);
cout << "After byRef: " << num << endl; // Now 11
return 0;
}
Classic Example: Swap Function
#include <iostream>
using namespace std;
// Won't work - swaps copies
void badSwap(int a, int b) {
int temp = a;
a = b;
b = temp;
}
// Works - swaps originals using references
void goodSwap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
// Also works - using pointers
void pointerSwap(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int x = 10, y = 20;
cout << "Before: x=" << x << ", y=" << y << endl;
badSwap(x, y);
cout << "After badSwap: x=" << x << ", y=" << y << endl; // No change
goodSwap(x, y);
cout << "After goodSwap: x=" << x << ", y=" << y << endl; // Swapped!
pointerSwap(&x, &y);
cout << "After pointerSwap: x=" << x << ", y=" << y << endl; // Swapped again
return 0;
}
Returning Multiple Values
#include <iostream>
#include <cmath>
using namespace std;
// Return multiple values through reference parameters
void getCircleProperties(double radius, double& area, double& circumference) {
area = M_PI * radius * radius;
circumference = 2 * M_PI * radius;
}
// Return min and max
void findMinMax(int arr[], int size, int& min, int& max) {
min = max = arr[0];
for (int i = 1; i < size; i++) {
if (arr[i] < min) min = arr[i];
if (arr[i] > max) max = arr[i];
}
}
int main() {
double area, circumference;
getCircleProperties(5.0, area, circumference);
cout << "Area: " << area << endl;
cout << "Circumference: " << circumference << endl;
int numbers[] = {3, 1, 4, 1, 5, 9, 2, 6};
int min, max;
findMinMax(numbers, 8, min, max);
cout << "Min: " << min << ", Max: " << max << endl;
return 0;
}
Practice Questions: Pass by Reference
Task: Write a function that takes an integer by reference and triples its value.
Show Solution
void triple(int& num) {
num *= 3;
}
// Usage:
int x = 5;
triple(x);
cout << x << endl; // 15
Task: Write a function that performs integer division and returns both quotient and remainder through reference parameters.
Show Solution
void divide(int dividend, int divisor, int& quotient, int& remainder) {
quotient = dividend / divisor;
remainder = dividend % divisor;
}
// Usage:
int q, r;
divide(17, 5, q, r);
cout << "17 / 5 = " << q << " R " << r << endl; // 3 R 2
Return by Reference
Functions can return references, allowing the returned value to be modified or avoiding copies of large objects. However, this must be used carefully to avoid returning references to local variables.
#include <iostream>
#include <vector>
using namespace std;
class IntArray {
private:
int data[10];
public:
IntArray() {
for (int i = 0; i < 10; i++) data[i] = 0;
}
// Return reference - allows modification
int& at(int index) {
return data[index];
}
// Return const reference - read only
const int& at(int index) const {
return data[index];
}
};
// Return reference to larger of two
int& larger(int& a, int& b) {
return (a > b) ? a : b;
}
int main() {
IntArray arr;
// Use returned reference as lvalue (left side of =)
arr.at(0) = 100; // Modifies data[0]
arr.at(1) = 200;
cout << arr.at(0) << endl; // 100
// Modify through returned reference
int x = 10, y = 20;
larger(x, y) = 50; // Modifies y (the larger one)
cout << "x=" << x << ", y=" << y << endl; // x=10, y=50
return 0;
}
// WRONG - returns dangling reference!
int& dangerous() {
int local = 42;
return local; // local dies when function returns!
}
// CORRECT - return reference to parameter or member
int& safe(int& param) {
return param; // param outlives the function
}
Common Use: Chained Operations
#include <iostream>
using namespace std;
class Counter {
private:
int value;
public:
Counter() : value(0) {}
// Return *this by reference enables chaining
Counter& increment() {
value++;
return *this;
}
Counter& add(int n) {
value += n;
return *this;
}
void print() const {
cout << "Value: " << value << endl;
}
};
int main() {
Counter c;
// Method chaining
c.increment().increment().add(10).increment();
c.print(); // Value: 13
return 0;
}
Practice Questions: Return by Reference
Task: Identify if each function is safe or dangerous:
// Function 1
int& func1(int& x) { return x; }
// Function 2
int& func2() { static int val = 10; return val; }
// Function 3
int& func3() { int temp = 5; return temp; }
// Function 4
int& func4(int arr[], int i) { return arr[i]; }
Show Solution
- func1: ✅ Safe - returns reference to parameter
- func2: ✅ Safe - static variable persists
- func3: ❌ Dangerous - returns reference to local
- func4: ✅ Safe - array outlives function (assuming valid index)
Rvalue References (C++11)
C++11 introduced rvalue references (T&&) to enable move semantics,
allowing efficient transfer of resources instead of copying. Understanding
lvalues and rvalues is key to modern C++.
Lvalues vs Rvalues
#include <iostream>
using namespace std;
int main() {
// LVALUE: Has a name, has an address, persists
int x = 10; // x is an lvalue
int* ptr = &x; // Can take address of lvalue
// RVALUE: Temporary, no persistent address
// 10 is an rvalue (literal)
// x + 5 is an rvalue (temporary result)
// func() return value is an rvalue (if not returning reference)
// Lvalue reference (&) binds to lvalues
int& lref = x; // OK - x is lvalue
// int& bad = 10; // Error - 10 is rvalue
// Const lvalue reference binds to both
const int& clref = x; // OK - lvalue
const int& clref2 = 10; // OK - rvalue (extends lifetime)
// Rvalue reference (&&) binds to rvalues
int&& rref = 10; // OK - 10 is rvalue
int&& rref2 = x + 5; // OK - expression result is rvalue
// int&& bad = x; // Error - x is lvalue
cout << "lref: " << lref << endl;
cout << "rref: " << rref << endl;
return 0;
}
Move Semantics
#include <iostream>
#include <string>
#include <utility> // for std::move
using namespace std;
class Buffer {
private:
int* data;
size_t size;
public:
// Constructor
Buffer(size_t s) : size(s), data(new int[s]) {
cout << "Constructed buffer of size " << size << endl;
}
// Destructor
~Buffer() {
delete[] data;
cout << "Destroyed buffer" << endl;
}
// Copy constructor (expensive)
Buffer(const Buffer& other) : size(other.size), data(new int[other.size]) {
copy(other.data, other.data + size, data);
cout << "Copied buffer (expensive!)" << endl;
}
// Move constructor (cheap) - C++11
Buffer(Buffer&& other) noexcept : size(other.size), data(other.data) {
other.data = nullptr; // Steal resources
other.size = 0;
cout << "Moved buffer (cheap!)" << endl;
}
};
Buffer createBuffer() {
return Buffer(1000); // Return triggers move
}
int main() {
Buffer b1(100);
// Copy (expensive)
Buffer b2 = b1;
// Move (cheap) - b3 steals b1's resources
Buffer b3 = move(b1); // b1 is now "empty"
// Move from temporary (automatic)
Buffer b4 = createBuffer();
return 0;
}
std::move
#include <iostream>
#include <string>
#include <vector>
#include <utility>
using namespace std;
int main() {
string str = "Hello, World!";
cout << "Before move: str = \"" << str << "\"" << endl;
// std::move casts lvalue to rvalue reference
// Enables move instead of copy
string str2 = move(str);
cout << "After move: str = \"" << str << "\"" << endl; // Empty or undefined
cout << "str2 = \"" << str2 << "\"" << endl; // "Hello, World!"
// Common use: moving into containers
vector<string> vec;
string s = "Large string data...";
vec.push_back(move(s)); // Move instead of copy
// s is now in moved-from state
return 0;
}
Practice Questions: Rvalue References
Task: Classify each expression as lvalue or rvalue:
int x = 5;
int y = x + 3;
int* p = &x;
int arr[3];
// Classify:
// 1. x
// 2. x + 3
// 3. 42
// 4. arr[0]
// 5. *p
Show Solution
x- lvalue (named variable)x + 3- rvalue (temporary result)42- rvalue (literal)arr[0]- lvalue (can take address)*p- lvalue (dereference gives lvalue)
Best Practices
Follow these guidelines to use references effectively and safely in your C++ code.
DO
- Use
const T&for read-only parameters - Use
T&when function needs to modify - Prefer references over pointers when null isn't needed
- Use
std::movefor expensive-to-copy objects - Return by reference from class member functions
DON'T
- Return reference to local variable
- Use reference when null is a valid state
- Forget to use const when not modifying
- Use
std::moveon const objects - Access moved-from objects (undefined state)
// Parameter passing guidelines:
// Small types (int, char, etc.) - pass by value
void process(int x);
// Large types, read-only - pass by const reference
void analyze(const vector<int>& data);
// Large types, needs modification - pass by reference
void modify(vector<int>& data);
// Optional parameter - use pointer
void optional(int* maybeNull);
// Taking ownership - pass by value and move
void takeOwnership(string str); // Call with std::move
// Returning:
// Small types - return by value
int calculate();
// Expensive to copy - return by value (RVO/move)
vector<int> generate(); // Compiler optimizes
// Chaining/member access - return by reference
class Builder {
Builder& addOption(string opt);
};
Key Takeaways
Reference = Alias
Another name for an existing variable
Must Initialize
Cannot be null, cannot rebind
Const Reference
Read-only access, efficient parameters
Pass by Reference
Modify caller's variables
Rvalue References
Enable move semantics (T&&)
Never Return Local Ref
Dangling reference is undefined behavior
Knowledge Check
Quick Quiz
Test what you have learned about C++ references
1 What is a C++ reference?
2 Which statement about references is TRUE?
3 What is the main benefit of const T& parameters?
4 What is wrong with this code?
int& getRef() {
int x = 10;
return x;
}