Module 3.4

References in C++

Learn C++ references—aliases to existing variables that provide a safer, cleaner alternative to pointers. Master pass by reference, const references, and modern rvalue references for efficient code.

35 min read
Intermediate
Hands-on Examples
What You'll Learn
  • Reference declaration & initialization
  • References vs pointers
  • Const references
  • Pass by reference
  • Rvalue references (C++11)
Contents
01

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.

Concept

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;
}
02

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
03

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:

  1. Function parameter that should never be null
  2. Iterating through array elements
  3. Optional output parameter that might not be used
  4. Alias for a long variable name
Show Solution
  1. Reference - Cannot be null, cleaner syntax
  2. Pointer - Needs arithmetic to move through array
  3. Pointer - Can be nullptr if not needed
  4. Reference - Simple alias, no null needed
04

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
05

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
06

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;
}
DANGER: Never return reference to local variable!
// 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)
07

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
  1. x - lvalue (named variable)
  2. x + 3 - rvalue (temporary result)
  3. 42 - rvalue (literal)
  4. arr[0] - lvalue (can take address)
  5. *p - lvalue (dereference gives lvalue)
08

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::move for 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::move on 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;
}
5 What does T&& represent in C++11?
6 What does std::move do?
Answer all questions to check your score