Understanding C++ Pointers and References: A Comprehensive Guide

In this section, we'll focus on basic pointers, excluding smart pointers. Pointers are crucial for memory management and manipulation in C++.

What is a Pointer?

A pointer is essentially an integer that stores a memory address. Consider an integer variable 'value' with the content 42:

int value = 42;
int* ptr = &value;

Here, we store the address of 'value' in the pointer 'ptr'. The pointer doesn't store the variable itself, but rather its memory location.

When we assign a type to a pointer, we're indicating that the address it holds should be interpreted as that specific data type.

Single-Level Pointer Definition

int* pointer; // Syntax: type * pointer_name;

The '&' operator retrieves the address, which can be stored in a pointer variable. The '*' operator, when used outside of pointer definition, dereferences the pointer, retrieving the value at the stored address.

For example, if 'address' is a pointer, 'address' represents a memory location, while '*address' represents the value stored at that location.

Double-Level Pointer Definition

Double-level pointers follow the same principle as single-level pointers. Note that all double-level pointers point to single-level pointer types, wich typically have a size of 4 bytes, making the step size for double-level pointers also 4 bytes.

Pointer Memory Size

  • In 32-bit operating systems, pointers occupy 4 bytes of memory, regardless of the data type they point to.
  • In 64-bit operating systems, pointers occupy 8 bytes of memory, regardless of the data type they point to.

Pointer Dangers

C++ allocates memory for storing addresses when creating pointers but doesn't allocate memory for the data they point to:

long* ptr;
*ptr = 12345;

Here, 'ptr' is indeed a pointer, but where does it point? The code doesn't assign an address to 'ptr'. Since 'ptr' is uninitialized, it could contain any value. The program might interpret this as the address where 12345 should be stored. If 'ptr' happens to contain 5000, the computer will place the data at address 5000, even if that's part of the program's code.

Important: Always initialize a pointer to a valid address before dereferencing it.

The new Operator

Pointers truly shine when allocating unnamed memory at runtime to store values. In this scenario, memory can only be accessed through pointers.

In C++, we use the 'new' operator to allocate heap memory. The programmer specifies the data type needed, 'new' finds an appropriately sized memory block, and returns its address. The programmer's responsibility is to assign this address to a pointer.

int* dynamicInt = new int;

'new int' requests memory suitable for storing an integer. It then finds such memory and returns its address. We assign this address to 'dynamicInt', and '*dynamicInt' represents the value stored there.

Compare this with assigning a variable's address to a pointer:

int regularVar;
int* ptr = &regularVar;

In both cases, we assign an integer variable's address to a pointer. The difference is that 'dynamicInt' points to memory without a name, storing only the address. This gives the program greater control over memory management.

The general format for allocating memory for a data object (which can be a structure or basic type) is:

typeName* pointer_name = new typeName;

Creating Dynamic Arrays with new

When creating arrays through declaration, memory is allocated at compile time. Regardless of whether the program uses the array, it occupies memory. Allocating memory at compile time is called static binding.

With 'new', arrays are created only when needed at runtime, and the array length can be determined during program execution. This is called dynamic binding.

With static binding, the array length must be specified when writing the program.

With dynamic binding, the program determines the array length at runtime.

int size = 10;
int* dynamicArray = new int[size];

The 'new' operator returns the address of the first element.

The general format for allocating array memory is:

type_name * pointer_name = new type_name[number_elements];

The delete Operator

The 'delete' operator allows returning memory to the memory pool after use. When using 'delete', you must follow it with a pointer to the memory block originally allocated with 'new'.

int * allocatedInt = new int;
// ...
delete allocatedInt;

Note: This releases the memory 'allocatedInt' points to but doesn't delete the pointer 'allocatedInt' itself.

Always use 'new' and 'delete' in pairs to avoid memory leaks.

Deleting Dynamic Arrays

For arrays created with 'new', use a different 'delete' format to release memory:

delete [] dynamicArray;

The brackets indicate that the entire array should be released, not just the element the pointer points to.

When using 'new' and 'delete', follow these rules:

  1. Don't use 'delete' to free memory not allocated with 'new'
  2. Don't use 'delete' to free the same memory block twice
  3. If you use 'new []' to allocate array memory, use 'delete []' to release it
  4. If you use 'new' to allocate a single entity's memory, use 'delete' to release it
  5. Using 'delete' on a null pointer is safe

After freeing a pointer with 'free' or 'delete', always set it to null. A pointer not set to null after being freed is often called a "dangling pointer". The following situations can also be considered "dangling pointers":

  1. Uninitialized pointers
  2. Pointers to temporary variables

Any pointer variable, when first created, contains random values and points to arbitrary memory locations. Therefore, always initialize pointers before use. Initialize or assign them at declaration to point to valid memory. For pointers with nowhere to point, assign them to null.

When a code block finishes execution, temporary variables (including function parameters) declared within that block are automatically destroyed, and their stack memory is reclaimed by the memory manager.

Pointers, Arrays, and Pointer Arithmetic

Pointers and arrays are largely equivalent due to pointer arithmetic and how C++ internally handles arrays.

Pointer Arithmetic

Consider arithmetic operations:

  • When an integer variable is incremented by 1, its value increases by 1.
  • When a pointer variable is incremented by 1, the increase equals the number of bytes of the type it points to.

For example, incrementing a pointer to 'double' by 1 increases its value by 8 if the system uses 8 bytes to store a 'double'.

sizeof()

Applying the 'sizeof' operator to an array returns the array's total size.

Applying the 'sizeof' operator to a pointer returns the pointer's size.

References

A reference isn't a new variable definition but rather an alias for an existing variable. The compiler doesn't allocate separate memory for a reference variable; it shares the same memory space as the variable it references.

The format for defining a reference type is:

type & reference_name = reference_entity;

In everyday life, aliases are easy to understand. For example, if Zhang San has an alias called San'er, both names refer to the same person. Whatever Zhang San does, San'er does too.

A reference is a variable, but it's merely an alias for another variable and doesn't occupy additional memory space.

Reference Characteristics

  • References must be initialized
  • References cannot be null
  • References cannot change their target
  • A variable can have multiple references (like having several aliases)

Reference vs Pointer Comparison

References Pointers
Must be initialized Can be uninitialized
Cannot be null Can be null
Cannot change target Can change target

Reference Forms

Variable Reference - Variable

int original = 10; // Read-write
int& ref = original; // Read-write

Variable Reference - Constant

const int original = 100; // Read-only
int& ref = original; // Compilation error - an alias cannot be a variable if the original is constant

Constant Reference - Variable

int original = 10; // Read-write
const int& ref = original; // Read-only

Constant Reference - Constant

const int original = 10; // Read-only
const int& ref = original; // Read-only

References in Functions

1. References as Function Parameters

Modifying parameters affects external arguments (since parameters are aliases for arguments).

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

int main()
{
    int x = 10;
    int y = 20;
    swap(x, y);
    // After swap, x = 20, y = 10
}

Since a reference is merely an alias, operations on parameters affect arguments. After 'swap(x, y)', x equals 20 and y equals 10.

Sometimes, you don't want to modify external arguments when using a function. In such cases, use constant references:

void process(const int& value)
{
    // ...
}

Generally, we use const reference parameters as read-only parameters. This approach avoids parameter copying while maintaining the same calling style as pass-by-value parameters.

void analyze(const std::vector<int>& data)
{
    // ...
}

int main()
{
    std::vector<int> dataset{1, 2, 3, 4, 5, 6, 7, 8};
    analyze(dataset);
}</int></int>

2. Reference Variables as Function Return Values

Note:

  1. When using a reference as a function return value, you must place '&' before the function name in the function definition.
  2. The greatest advantage of using a reference as a function return value is that no copy of the return value is created in memory.

Tags: C++ pointers References Memory Management

Posted on Mon, 22 Jun 2026 18:36:59 +0000 by WormTongue