C++ Pointers and Parameter Passing: Value vs Reference Deep Dive | Didaxa Blog
All Articles
C++ProgrammingPointersMemory ManagementComputer Science

C++ Pointers and Parameter Passing: Value vs Reference Deep Dive

Didaxa Team
10 min read

Share this article

C++ Pointers and Parameter Passing: Value vs Reference Deep Dive

Understanding pointers and how data is passed in C++ is fundamental to becoming a proficient programmer. These concepts affect performance, memory usage, and the behavior of your code in subtle but critical ways.

What is a Pointer?

A pointer is a variable that stores the memory address of another variable. Think of it as a "signpost" that points to where the actual data lives in memory.

cpp
int age = 25;        // A normal integer variable
int* ptr = &age;     // A pointer that stores the address of 'age'

In this example:

  • age contains the value 25
  • ptr contains the memory address where age is stored
  • • The & operator gets the address of a variable
  • • The * in the declaration means "this is a pointer"

Visualizing Memory

Let's see how this looks in memory:

text
Memory Address    |  Variable  |  Value
------------------|------------|--------
0x7ffd4a5b3c10   |   age      |   25
0x7ffd4a5b3c18   |   ptr      |   0x7ffd4a5b3c10

The pointer ptr doesn't contain 25—it contains the address where 25 is stored.

Dereferencing: Accessing the Value

To access the value that a pointer points to, we use the dereference operator *:

cpp
int age = 25;
int* ptr = &age;

cout << "Value of age: " << age << endl;        // Output: 25
cout << "Address of age: " << &age << endl;     // Output: 0x7ffd...
cout << "Value of ptr: " << ptr << endl;        // Output: 0x7ffd...
cout << "Dereferenced ptr: " << *ptr << endl;   // Output: 25

Notice how * serves two purposes:

  1. In declarations: declares a pointer type
  2. With existing pointers: dereferences to access the value

Pass by Value: The Copy Mechanism

When you pass a variable by value, C++ creates a copy of the data. The function works with the copy, not the original.

cpp
void increment(int num) {
    num = num + 1;  // This modifies the LOCAL copy
    cout << "Inside function: " << num << endl;  // Output: 11
}

int main() {
    int value = 10;
    increment(value);
    cout << "In main: " << value << endl;  // Output: 10 (unchanged!)
    return 0;
}
What happened?
  1. value (10) is copied into num
  2. num is incremented to 11
  3. When the function ends, num is destroyed
  4. value in main remains 10

Memory Diagram

text
Before function call:
main's stack    |  value: 10

During function call:
main's stack    |  value: 10
function stack  |  num: 10 (copy)

After increment:
main's stack    |  value: 10
function stack  |  num: 11 (copy modified)

After function ends:
main's stack    |  value: 10 (unchanged)

Pass by Reference: Sharing the Original

When you pass by reference, you're giving the function direct access to the original variable. Changes affect the original.

cpp
void increment(int& num) {  // Note the '&' - this is a reference
    num = num + 1;  // This modifies the ORIGINAL
    cout << "Inside function: " << num << endl;  // Output: 11
}

int main() {
    int value = 10;
    increment(value);
    cout << "In main: " << value << endl;  // Output: 11 (changed!)
    return 0;
}
What happened?
  • num is an alias for value
  • • They both refer to the same memory location
  • • Modifying num modifies value

Memory Diagram

text
During function call:
main's stack    |  value: 10
function stack  |  num: reference to 'value'
                    (both point to same memory)

After increment:
main's stack    |  value: 11 (modified!)
function stack  |  num: reference to 'value'

Pass by Pointer: Manual Dereferencing

Pointers can also be used to modify the original variable, but they require explicit dereferencing:

cpp
void increment(int* num) {  // Pointer parameter
    *num = *num + 1;  // Dereference to modify the value
    cout << "Inside function: " << *num << endl;  // Output: 11
}

int main() {
    int value = 10;
    increment(&value);  // Pass the address
    cout << "In main: " << value << endl;  // Output: 11 (changed!)
    return 0;
}

Notice three key differences from references:

  1. You must pass the address with &value
  2. Inside the function, you must dereference with *num
  3. Pointers can be nullptr, references cannot

Real-World Example: Swap Function

Let's implement a function that swaps two integers using all three approaches:

❌ By Value (Doesn't Work)

cpp
void swap(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
    // Only swaps the copies!
}

int main() {
    int x = 5, y = 10;
    swap(x, y);
    cout << "x: " << x << ", y: " << y << endl;  // Output: x: 5, y: 10
}

✅ By Reference (Works!)

cpp
void swap(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}

int main() {
    int x = 5, y = 10;
    swap(x, y);
    cout << "x: " << x << ", y: " << y << endl;  // Output: x: 10, y: 5
}

✅ By Pointer (Also Works!)

cpp
void swap(int* a, int* b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    int x = 5, y = 10;
    swap(&x, &y);
    cout << "x: " << x << ", y: " << y << endl;  // Output: x: 10, y: 5
}

Performance Implications

Small Types (int, float, char)

  • By value: Fast, minimal overhead
  • By reference/pointer: Slightly slower due to indirection

Large Types (arrays, structs, classes)

cpp
struct HugeData {
    int values[1000000];  // 4MB of data!
};

// ❌ BAD: Copies 4MB every time!
void processByValue(HugeData data) {
    // ...
}

// ✅ GOOD: Only passes an address (8 bytes)
void processByReference(const HugeData& data) {
    // ...
}

// ✅ GOOD: Only passes an address (8 bytes)
void processByPointer(const HugeData* data) {
    // ...
}
Rule of thumb: For anything larger than a few bytes, use references or pointers.

Const Correctness

When you want to pass by reference for efficiency but don't want to modify the original, use const:

cpp
void print(const string& text) {
    cout << text << endl;
    // text = "new value";  // ERROR: can't modify const reference
}

void display(const int* num) {
    cout << *num << endl;
    // *num = 42;  // ERROR: can't modify through const pointer
}

This is the best practice for passing large objects efficiently without allowing modifications.

Common Pitfalls

1. Dangling Pointers

cpp
int* createNumber() {
    int num = 42;
    return #  // ❌ DANGER! num will be destroyed
}

int main() {
    int* ptr = createNumber();
    cout << *ptr << endl;  // Undefined behavior!
}
Solution: Use dynamic allocation or return by value.

2. Null Pointer Dereferencing

cpp
int* ptr = nullptr;
cout << *ptr << endl;  // ❌ CRASH!
Solution: Always check before dereferencing:
cpp
if (ptr != nullptr) {
    cout << *ptr << endl;
}

3. Forgetting to Dereference

cpp
void increment(int* num) {
    num = num + 1;   // ❌ WRONG: moves the pointer, doesn't change value
    *num = *num + 1; // ✅ CORRECT: changes the value
}

When to Use What?

ScenarioUseWhy
Small types, no modificationBy valueSimple, safe
Small types, need modificationBy referenceClean syntax
Large types, no modificationConst referenceEfficient, safe
Large types, need modificationBy referenceEfficient
Optional parameterPointerCan be nullptr
Array manipulationPointerArrays decay to pointers
Low-level memory workPointerMaximum control

Advanced Example: Array Manipulation

Arrays in C++ are closely related to pointers:

cpp
void fillArray(int* arr, int size) {
    for (int i = 0; i < size; i++) {
        arr[i] = i * 10;  // arr[i] is equivalent to *(arr + i)
    }
}

int main() {
    int numbers[5];
    fillArray(numbers, 5);  // Array name decays to pointer

    for (int i = 0; i < 5; i++) {
        cout << numbers[i] << " ";  // Output: 0 10 20 30 40
    }
    return 0;
}
Note: When you pass an array to a function, you're actually passing a pointer to its first element!

Modern C++ Alternatives

C++11 and later provide safer alternatives:

Smart Pointers

cpp
#include <memory>

std::unique_ptr<int> ptr = std::make_unique<int>(42);
// Automatically deallocated, no memory leaks!

References in Range-Based Loops

cpp
vector<int> numbers = {1, 2, 3, 4, 5};

for (const auto& num : numbers) {  // Reference to avoid copying
    cout << num << " ";
}

Practice Exercise

Try implementing these functions:

cpp
// 1. Find the maximum of two numbers without modifying them
void findMax(int a, int b, int& result);

// 2. Double all elements in an array
void doubleArray(int* arr, int size);

// 3. Check if a pointer is valid and safe to use
bool isSafePointer(const int* ptr);

Summary

Pass by Value:
  • • Creates a copy
  • • Safe (original unchanged)
  • • Slower for large data
Pass by Reference:
  • • Works with the original
  • • Fast (no copy)
  • • Can modify original
  • • Cleaner syntax than pointers
Pass by Pointer:
  • • Works with the original
  • • Fast (no copy)
  • • Can modify original
  • • Can be nullptr
  • • Requires explicit dereferencing
Understanding these concepts is essential for writing efficient, correct C++ code. They affect everything from simple functions to complex data structures and algorithms.

Master C++ Programming with Didaxa

Ready to become a C++ expert? Didaxa's AI-powered platform helps you master pointers, memory management, and advanced programming concepts with interactive exercises and personalized feedback. Practice with real-world scenarios and build confidence in systems programming.

Because mastering C++ opens doors to performance-critical systems, game engines, and cutting-edge software development.
D

Written by

Didaxa Team

The Didaxa Team is dedicated to transforming education through AI-powered personalized learning experiences.

Related Articles

Continue your learning journey

Start Your Journey

Experience the Future of Learning

Join thousands of students already learning smarter with Didaxa's AI-powered platform.