Understanding the Differences Between Pointer and Reference Passing in C++

In C/C++ programming, function parameter passing is the core method for data interaction between the caller and the callee. Among them, pointer passing and reference passing are key means to “modify external variables through functions,” but there are significant differences in syntax characteristics, memory mechanisms, and usage scenarios. This article will start from the definition of concepts, combined with code examples, to comprehensively analyze the differences between pointer passing and reference passing, helping developers accurately grasp the applicable scenarios of the two parameter passing methods and avoid common pitfalls in programming.Video explanation: Search for “Coder Mark” on Bilibili

1. Basic Concepts: The Essential Differences Between Pointers and References

Before discussing the differences in parameter passing, it is necessary to clarify the essence of pointers and references — although both can achieve “indirect access to variables,” their underlying logic is completely different:

Comparison Dimension

Pointer

Reference

Essence

An independent variable that stores the memory address

A alias for a variable, sharing the same memory space as the original variable

Memory Usage

Occupies additional memory (e.g., 4 bytes on a 32-bit system, 8 bytes on a 64-bit system)

Does not occupy additional memory (only a syntactical alias at compile time, with no actual storage)

Null Value Validity

Supports null pointers (NULL or nullptr), can point to “meaningless addresses”

Does not support null references, must be bound to an existing variable at definition

Modifiability

The value of the pointer variable itself (i.e., the address it points to) can be modified

Once a reference is bound to a variable, it cannot be rebound to another variable

Syntax Form

Declared using * and accessed through dereferencing with *

Declared using & and accessed directly through the reference name

For a simple example, intuitively feel the differences between the two:

#include <stdio.h>int main() {    int a = 10;
    // Pointer: stores the address of a, is an independent variable    int* p = &a;      // Reference: alias for a, shares memory with a    int& r = a;   
    printf("a's address: %p, value: %d\n", &a, a);    // Output: a's address: 0x7ffeefbff4a4c, value: 10    printf("p's address: %p, points to address: %p, target value: %d\n", &p, p, *p);  // p has its own address, points to a's address    printf("r's address: %p, value: %d\n", &r, r);    // r's address is exactly the same as a's, value is consistent with a
    // Pointer can modify the pointed address (pointing to new variable b)    int b = 20;    p = &b;      // Reference cannot modify binding (if writing r = &b, it will be treated as "assigning b's address to the variable pointed to by r", compilation error)
    return 0;}

2. Function Parameter Passing: Core Differences Between Pointer Passing and Reference Passing

The essence of function parameter passing is “passing the value of the actual parameter to the formal parameter,” but pointer passing and reference passing achieve modification of external variables through the characteristic of “indirect access.” The differences mainly lie in the aspects of syntax usage, memory overhead, safety, and compiler handling across four levels.

1. Syntax Level: Differences in Parameter Passing and Access Methods

(1) Pointer Passing: Passes “address value,” requires dereferencing to access the target variable

The core of pointer passing is: passing the value of the actual parameter (pointer variable or variable address) to the formal parameter (pointer variable), where the formal and actual parameters are two independent pointer variables but point to the same memory. To modify the value of the target variable, dereferencing with * is required; to modify the pointer itself’s direction (i.e., to make the pointer point to a new address), the pointer variable is directly manipulated.

Example: Modifying an External Variable via Pointer Passing

#include <stdio.h>// Pointer passing: formal parameter is a pointer variable, receiving the address of the actual parametervoid modifyByPointer(int* p) {    // Dereference to modify the value of the target variable (modifying external variable a)    *p = 20;      // If modifying the pointer itself's direction (e.g., p = &b), only affects the formal parameter p, not the external actual parameter}
int main() {    int a = 10;    // Actual parameter: passing the address of a (&a)    modifyByPointer(&a);      printf("a's value: %d\n", a);  // Output: 20 (external variable modified)    return 0;}

(2) Reference Passing: Passes “variable alias,” directly accesses the target variable

The core of reference passing is: the formal parameter is an alias for the actual parameter, and they are bound to the same variable at compile time, so there is no need to explicitly pass the address during parameter passing (the compiler automatically handles the binding). Modifying the value of the formal parameter essentially modifies the value of the external actual parameter, without needing dereferencing.

Example: Modifying an External Variable via Reference Passing

#include <stdio.h>// Reference passing: formal parameter is a reference, bound to the actual parametervoid modifyByReference(int& r) {    // Directly modify the value of the reference (essentially modifying external variable a)    r = 20;      // Cannot modify the binding of the reference (e.g., r = b will be treated as "assignment" rather than "rebinding")}
int main() {    int a = 10;    // Actual parameter: directly passing variable a (the compiler automatically binds reference r to a)    modifyByReference(a);      printf("a's value: %d\n", a);  // Output: 20 (external variable modified)    return 0;}

Summary of Syntax Differences

Scenario

Pointer Passing

Reference Passing

Actual Parameter Form

Must pass variable address (&a) or pointer variable

Directly pass variable (a)

Formal Parameter Declaration

Use * (e.g., int* p)

Use & (e.g., int& r)

Accessing Target Variable

Requires dereferencing (*p)

Directly use reference name (r)

Modifying Target Variable’s Direction

Supported (p = &b)

Not supported (once bound, cannot change)

2. Memory Overhead: Pointer Passing Has Additional Overhead, Reference Passing Has No Overhead

In pointer passing, the formal parameter is an independent pointer variable, which occupies memory (e.g., 8 bytes on a 64-bit system), and the essence of parameter passing is “copying the address value of the actual parameter” into the memory space of the formal parameter; while in reference passing, the reference is merely a syntactical alias at compile time, with no actual memory storage, and during parameter passing, there is no need to copy data, only the compiler needs to record the binding relationship.

Example: Observing Overhead Differences via Memory Addresses

#include <stdio.h>// Pointer passing: formal parameter p is an independent variable, has its own addressvoid pointerParam(int* p) {    printf("Pointer formal parameter p's address: %p\n", &p);  // Output the address of formal parameter p (different from actual parameter address)}
// Reference passing: formal parameter r has no independent address, address is the same as actual parametervoid referenceParam(int& r) {    printf("Reference formal parameter r's address: %p\n", &r);  // Output the address of actual parameter (same as &a)}
int main() {    int a = 10;    printf("Actual parameter a's address: %p\n", &a);      // Output: 0x7ffeefbff4a4c (example address)
    pointerParam(&a);                      // Output: Pointer formal parameter p's address: 0x7ffeefbff4a28 (different from &a)    referenceParam(a);                     // Output: Reference formal parameter r's address: 0x7ffeefbff4a4c (same as &a)
    return 0;}

Conclusion: In frequently called functions (such as functions within loops) or when passing large objects (such as structures, class instances), reference passing has lower memory overhead and better performance; although the additional memory overhead of pointer passing is small, it can still affect performance in extreme scenarios.

3. Safety: Reference Passing is Safer, Pointer Passing is Prone to Null Pointer Issues

The syntactical characteristics of references determine that their safety is higher than that of pointers:

  • References cannot be null: References must be bound to an existing variable at definition, and cannot create “null references,” thus there is no risk of “null reference access” crashes;
  • Pointers can be null: Pointers support NULL or nullptr, and if the function does not check for null pointers, directly dereferencing (e.g., *p = 10) can lead to program crashes.

Example: Risks of Null Pointers in Pointer Passing and Safety of Reference Passing

#include <stdio.h>// Pointer passing: no null pointer check, risk of crashingvoid unsafePointer(int* p) {    *p = 20;  // If p is NULL, this will crash}
// Reference passing: no need to check for null value, reference must bind to a valid variablevoid safeReference(int& r) {    r = 20;  // No risk of null reference, compile-time guarantees r binds to a valid variable}
int main() {    int a = 10;    unsafePointer(&a);  // Normal: p points to a valid address    unsafePointer(NULL);  // Crash: null pointer dereference
    safeReference(a);  // Normal: r binds to a    // safeReference(NULL);  // Compile error: cannot bind NULL to reference
    return 0;}

Note: Although pointer passing has risks, it can be made safer by explicitly checking for null pointers (e.g., if (p == nullptr) return;) to enhance safety; the safety of reference passing relies on the syntactical rule that “references must bind to valid variables,” so developers do not need to handle null values additionally.

4. Compiler Handling: Pointers are “Variables,” References are “Aliases”

From the compiler’s perspective, the handling logic of pointers and references is completely different:

  • Pointers: The compiler treats them as independent variables, storing address values, and generates instructions to “access pointer address → dereference to get target address → access target variable” after compilation;
  • References: The compiler replaces the reference name with the original variable’s address at compile time, essentially “directly accessing the original variable,” with no additional instruction overhead, consistent with the efficiency of directly operating on the original variable.

For example, for r = 20 (reference assignment) and *p = 20 (pointer dereference assignment), the generated instructions by the compiler differ as follows:

  • Reference assignment: directly generates instructions to “write 20 to the address of variable a”;
  • Pointer dereference assignment: generates instructions to “first read the value of pointer p (i.e., the address of a) → then write 20 to that address.”

Conclusion: The efficiency of reference passing after compilation is slightly higher than that of pointer passing (no pointer address reading operation), but under modern compiler optimizations (such as -O2 level optimization), the efficiency differences between the two are usually negligible; only in non-optimized debug modes will the efficiency advantage of reference passing be evident.

3. Applicable Scenarios: When to Use Pointer Passing and When to Use Reference Passing?

Based on the above differences, the applicable scenarios for pointer passing and reference passing can be clearly delineated to avoid “abuse” or “misuse”:

1. Prefer Reference Passing in the Following Scenarios

  • Need to modify external variables, and the variables must be valid: For example, functions returning multiple values (modifying external variables through references), passing large objects (such as structures, class instances) to reduce memory overhead;
  • Pursuing syntactical simplicity and safety: References do not require dereferencing operations, making the code simpler, and there is no risk of null pointers, suitable for scenarios with high safety requirements.

Example: Modifying Multiple External Variables via Reference Passing

#include <stdio.h>// Modify two external variables via reference passingvoid calculate(int a, int b, int& sum, int& product) {    sum = a + b;    product = a * b;}
int main() {    int x = 3, y = 4;    int sum, product;    // Directly passing variables sum and product, modified via reference    calculate(x, y, sum, product);    printf("Sum: %d, Product: %d\n", sum, product);  // Output: Sum: 7, Product: 12    return 0;}

2. Prefer Pointer Passing in the Following Scenarios

  • Need to pass null values: For example, optional function parameters (if the parameter does not need to be passed, NULL can be passed), such as “finding an array element, returning the address if found, returning NULL if not found”;
  • Need to modify the direction of the pointer: For example, if the function needs to make the pointer point to new memory space (such as dynamic memory allocation, linked list node insertion);
  • C Language Compatibility Scenarios: C language does not support references, if the code needs to be used in a mixed C/C++ environment, pointer passing must be used.

Example: Using Pointer Passing to Implement “Optional Parameters”

#include <stdio.h>// Find the element in the array with value target, return address if found, return NULL if not foundint* findElement(int arr[], int size, int target) {    for (int i = 0; i < size; i++) {        if (arr[i] == target) {            return &arr[i];        }    }    return NULL;  // Not found, return null pointer}
int main() {    int arr[] = {10, 20, 30, 40};    int* p = findElement(arr, 4, 30);    if (p != NULL) {        printf("Found element: %d\n", *p);  // Output: Found element: 30    } else {        printf("Element not found\n");    }    return 0;}

4. Common Pitfalls and Avoidance Guide

In actual programming, developers often fall into pitfalls due to confusing the characteristics of pointer passing and reference passing. Here are two typical misconceptions and their solutions:

Misconception 1: Believing that “references can be rebound”

Once a reference is bound to a variable, it cannot be rebound to another variable. If you try to modify the binding through r = b, the actual effect is “assigning the value of b to the variable pointed to by r, rather than “rebinding.”

Incorrect Example:

#include <stdio.h>int main() {    int a = 10, b = 20;    int& r = a;  // r binds to a    r = b;       // Misunderstanding: thinking r is now bound to b; actual: assigns the value of b (20) to a    printf("a: %d, b: %d, r: %d\n", a, b, r);  // Output: a: 20, b: 20, r: 20    return 0;}

Misconception 2: Passing “reference of temporary variables”

Temporary variables (such as function return values, expression results) have a short lifespan. If you bind them to a reference, after the temporary variable is destroyed, the reference will become a “dangling reference,” and accessing it will result in undefined behavior (such as random values, program crashes).

Incorrect Example:

#include <stdio.h>// Function returns a temporary variable (lifetime only within the function)int getTemp() {    return 100;}
int main() {    // Incorrect: binding temporary variable (return value of getTemp()) to reference r    int& r = getTemp();      printf("r: %d\n", r);  // Undefined behavior: temporary variable has been destroyed, r is dangling    return 0;}

Avoidance Guide:

  • References should only bind to variables whose lifetimes are longer than the reference (such as global variables, local variables but the reference’s lifetime does not exceed the variable);
  • If you need to pass temporary variables, you can use const references (const int& r = getTemp()), the C++ standard will extend the lifetime of the temporary variable to match that of the const reference.

5. Conclusion

Pointer passing and reference passing are the core methods of function parameter passing in C/C++, and the essence of their differences is the distinction between “independent address variables” and “variable aliases.” The summary is as follows:

Comparison Dimension

Pointer Passing

Reference Passing

Syntax Characteristics

Requires dereferencing (*p), supports null pointers

Direct access, no null references

Memory Overhead

Occupies additional memory (stores address)

No additional memory (only syntactical alias)

Safety

Requires null pointer checks, otherwise prone to crashes

Compile-time guarantees binding to valid variables, safer

Modifiable Direction

Supported (p = &b)

Not supported (once bound, cannot change)

Applicable Scenarios

Optional parameters, modifying pointer direction, C language compatibility

Leave a Comment