Pointers and Memory Access in C++
A variable stores a value in memory. Along with that value, the variable occupies a specific memory location identified by an address. C++ pointers make it possible to store these addresses and access the corresponding objects indirectly.
Pointers are essential for working with arrays, dynamically allocated memory, efficient function parameters, linked data structures, and low-level memory operations. To use them safely, it is important to distinguish between an address, a pointer, and the object located at that address.
Memory Addresses
Whenever an object is created, storage is reserved for it. The address-of operator & retrieves the address of an object.
For example, if value is an integer, &value represents the address at which that integer is stored. The exact address is selected by the system and may differ between program executions.
Declaring and Initializing a Pointer
A pointer is declared by placing an asterisk after the pointed-to type:
int*stores the address of an integer.double*stores the address of a double.char*stores the address of a character.
The pointer type must be compatible with the object whose address is assigned to it. This type information tells the compiler how the pointed-to memory should be interpreted.
#include <iostream>using namespace std;int main() { int value = 10; int* pointer = &value; cout << "Value: " << value << '\n'; cout << "Address through &: " << &value << '\n'; cout << "Address through pointer: " << pointer << '\n'; cout << "Dereferenced value: " << *pointer << '\n'; cout << "Size of int: " << sizeof(int) << " bytes\n"; cout << "Size of pointer: " << sizeof(pointer) << " bytes\n"; return 0;}Both &value and pointer display the same address because the pointer was initialized with the address of value.
The expression *pointer performs dereferencing. It accesses the object stored at the address contained in the pointer. In this example, dereferencing the pointer produces 10.
Understanding the Two Uses of the Asterisk
The asterisk has two closely related uses in pointer syntax:
In a declaration such as
int* pointer, it declares a pointer to an integer.In an expression such as
*pointer, it dereferences the pointer and accesses the pointed-to integer.
A useful distinction is that pointer represents an address, whereas *pointer represents the object located at that address.
Reading and Modifying a Value Through a Pointer
Dereferencing can be used on either side of an assignment. Reading *pointer obtains the pointed-to value, while assigning to *pointer modifies that value.
The pointer itself can also be reassigned so that it points to a different compatible object. These are two separate operations:
*pointer = valuechanges the pointed-to object.pointer = addresschanges which object the pointer identifies.
#include <iostream>using namespace std;int main() { int firstValue = 10; int secondValue = 20; int* pointer = &firstValue; *pointer = 15; cout << "First value: " << firstValue << '\n'; pointer = &secondValue; *pointer = 25; cout << "First value: " << firstValue << '\n'; cout << "Second value: " << secondValue << '\n'; return 0;}The first assignment through *pointer changes firstValue from 10 to 15. After the pointer is redirected to secondValue, the next dereferenced assignment changes secondValue from 20 to 25.
Pointer Arithmetic
C++ allows limited arithmetic on pointers. Adding an integer to a pointer moves it by a number of elements rather than a raw number of bytes.
If pointer has type int*, then pointer + 1 advances by sizeof(int) bytes. Similarly, a double* advances by sizeof(double), and a char* advances by sizeof(char).
pointer + 1points to the next element.pointer + 2points two elements ahead.pointer - 1points to the previous element.pointer - 2points two elements behind.
This arithmetic is defined only when the pointer remains within the same array or reaches the special one-past-the-end position. A pointer may be formed at one position past an array for comparison and loop termination, but it must never be dereferenced there.
Null Pointers
A null pointer intentionally points to no object. Modern C++ represents this state with nullptr.
#include <iostream>using namespace std;int main() { int* pointer = nullptr; if (pointer != nullptr) { cout << *pointer << '\n'; } else { cout << "The pointer is null.\n"; } int value = 40; pointer = &value; if (pointer) { cout << "Value: " << *pointer << '\n'; } return 0;}The condition if (pointer) is a concise equivalent of checking pointer != nullptr.
Dereferencing a null pointer causes undefined behavior. The program is not guaranteed to report a clean runtime error; it may crash, corrupt data, or appear to work unpredictably.
Uninitialized Pointers
A declaration such as int* pointer; creates an uninitialized pointer when it is a local variable. Its value is indeterminate, so it must not be read or dereferenced. Initialize pointers immediately with a valid address or with nullptr.
The modern nullptr literal should be preferred over older representations such as 0 and NULL because it has a dedicated pointer-compatible type and avoids ambiguity.
Copying Pointers and Pointer Aliasing
Assigning one pointer to another copies the address. It does not create a copy of the pointed-to object. Consequently, multiple pointers can refer to the same object. This relationship is known as aliasing.
#include <iostream>using namespace std;int main() { int value = 40; int* first = &value; int* second = first; cout << "First address: " << first << '\n'; cout << "Second address: " << second << '\n'; *second = 27; cout << "Value through first: " << *first << '\n'; cout << "Original value: " << value << '\n'; return 0;}Both pointers contain the address of value. Modifying the object through second therefore changes what is observed through first and through the original variable name.
Constness and Pointers
With pointers, constness can apply to the pointed-to object, the pointer itself, or both. The position of const determines what may be changed.
Pointer to a Constant Value
A pointer to const permits reading the pointed-to object but prevents modification through that pointer:
const int* pointer;
The pointer can still be redirected to another integer. If the underlying object is not inherently constant, it may still be modified through another non-const access path.
Constant Pointer
A const pointer must continue holding the address with which it was initialized:
int* const pointer = &value;
The pointer cannot be redirected, but the pointed-to integer may be modified.
Constant Pointer to a Constant Value
The declaration const int* const pointer combines both restrictions. Neither the stored address nor the pointed-to value may be changed through that pointer.
#include <iostream>using namespace std;int main() { int firstValue = 10; int secondValue = 20; const int* readOnly = &firstValue; cout << *readOnly << '\n'; readOnly = &secondValue; cout << *readOnly << '\n'; int* const fixedPointer = &firstValue; *fixedPointer = 15; const int* const fixedReadOnly = &secondValue; cout << *fixedPointer << '\n'; cout << *fixedReadOnly << '\n'; return 0;}The following summary separates the three forms:
const int* pointer: the value is read-only through the pointer, but the pointer can move.int* const pointer: the pointer cannot move, but the value can be modified.const int* const pointer: neither the pointer nor the value can be changed through it.
Arrays and Pointers
Array elements are stored contiguously. In most expressions, an array name is automatically converted into a pointer to its first element. Therefore, if values is an integer array, the following initializations identify the same first element:
int* pointer = values;int* pointer = &values[0];
Array indexing and pointer arithmetic are closely related. For a valid index i, values[i] is equivalent to *(values + i).
#include <iostream>using namespace std;int main() { int values[] = {10, 20, 30, 40}; int size = sizeof(values) / sizeof(values[0]); int* pointer = values; for (int* current = pointer; current < values + size; ++current) { cout << *current << ' '; } cout << '\n'; cout << values[2] << '\n'; cout << *(pointer + 2) << '\n'; return 0;}The loop begins at the first element and advances one element at a time. The expression values + size produces the one-past-the-end pointer. It is useful as a stopping position but must not be dereferenced.
Passing Pointers to Functions
Function parameters are normally passed by value. When an integer is passed directly, the function receives a copy, so modifying the parameter does not modify the caller’s variable.
A pointer parameter receives an address. By dereferencing that address, the function can modify the original object.
Problem 1: Double a Value Through a Pointer
Given the address of an integer, modify the original integer so that it becomes twice its current value.
If the original value is
7, it should become14.If the original value is
-3, it should become-6.
#include <iostream>using namespace std;void doubleValue(int* pointer) { if (pointer != nullptr) { *pointer *= 2; }}int main() { int first = 7; int second = -3; doubleValue(&first); doubleValue(&second); cout << first << '\n'; cout << second << '\n'; return 0;}The function receives the address of the caller’s integer. The expression *pointer *= 2 updates the integer stored at that address. The null check protects the function from dereferencing an invalid null pointer.
This operation has constant time complexity, O(1), and constant auxiliary space complexity, O(1).
Problem 2: Swap Two Values Using Pointers
To swap two integers, the function receives the addresses of both values. Dereferencing the pointers gives direct access to the original objects.
For example, if the first value is 4 and the second value is 9, the result should be 9 and 4.
#include <iostream>using namespace std;void swapValues(int* first, int* second) { if (first == nullptr || second == nullptr) { return; } int temporary = *first; *first = *second; *second = temporary;}int main() { int first = 4; int second = 9; swapValues(&first, &second); cout << "First: " << first << '\n'; cout << "Second: " << second << '\n'; return 0;}The temporary variable preserves the first value before it is overwritten. All assignments operate on dereferenced pointers, so the original variables are changed.
The swap requires O(1) time and O(1) auxiliary space.
Problem 3: Sum an Array Using Pointer Traversal
An array can be traversed by moving a pointer from its first element to its one-past-the-end position. At every step, dereferencing the current pointer produces the current element.
For the array {2, 5, 8}, the sum is:
2 + 5 + 8 = 15
#include <iostream>using namespace std;int sumUsingPointers(const int* values, int size) { int sum = 0; for (const int* current = values; current < values + size; ++current) { sum += *current; } return sum;}int main() { int values[] = {2, 5, 8}; int size = sizeof(values) / sizeof(values[0]); cout << sumUsingPointers(values, size) << '\n'; return 0;}The parameter is a pointer to const because the function only reads the array. This prevents accidental modification of its elements.
Every element is visited exactly once, giving a time complexity of O(n). Only a sum and a traversal pointer are maintained, so the auxiliary space complexity is O(1).
Problem 4: Count Positive Elements Using Pointers
To count positive values, traverse the array through a pointer and increment a counter whenever the dereferenced value is greater than zero. Zero is neither positive nor negative.
For
{-5, -2, 4, 0, 7, -1}, the positive values are4and7, so the answer is2.For
{-3, 0, -8}, there are no positive values, so the answer is0.
#include <iostream>using namespace std;int countPositive(const int* values, int size) { int count = 0; for (const int* current = values; current < values + size; ++current) { if (*current > 0) { ++count; } } return count;}int main() { int values[] = {-5, -2, 4, 0, 7, -1}; int size = sizeof(values) / sizeof(values[0]); int result = countPositive(values, size); cout << result << '\n'; return 0;}The function performs one comparison for every element. Its time complexity is O(n), and its auxiliary space complexity is O(1).
Problem 5: Reverse an Array Using Two Pointers
An array can be reversed in place using two pointers:
The left pointer begins at the first element.
The right pointer begins at the last element.
The two pointed-to values are swapped.
The left pointer moves forward and the right pointer moves backward.
The process stops when the pointers meet or cross.
For example, the array {10, 20, 30, 40, 50} becomes {50, 40, 30, 20, 10}.
#include <iostream>using namespace std;void swapValues(int* first, int* second) { int temporary = *first; *first = *second; *second = temporary;}void reverseArray(int* values, int size) { if (values == nullptr || size <= 1) { return; } int* left = values; int* right = values + size - 1; while (left < right) { swapValues(left, right); ++left; --right; }}int main() { int values[] = {10, 20, 30, 40, 50}; int size = sizeof(values) / sizeof(values[0]); reverseArray(values, size); for (int value : values) { cout << value << ' '; } return 0;}Only half of the array needs to be processed because every swap places two elements into their final positions. The time complexity is O(n), while the auxiliary space complexity remains O(1).
Important Pointer Safety Rules
Initialize every pointer before reading or dereferencing it.
Use
nullptrwhen a pointer intentionally refers to no object.Check a potentially null pointer before dereferencing it.
Ensure the pointer type is compatible with the pointed-to object.
Perform pointer arithmetic only within the same array.
Do not dereference a one-past-the-end pointer.
Do not access an object after its lifetime has ended.
Use a pointer to const when a function only needs to read data.
Remember that copying a pointer copies its address, not its pointed-to object.
Summary
A pointer stores the address of an object, while dereferencing accesses the object at that address. Through pointers, a program can read or modify original variables, traverse contiguous array elements, share access to the same object, and implement efficient in-place operations.
Pointer arithmetic is scaled according to the pointed-to type and must remain within valid array boundaries. Null checks, immediate initialization, correct constness, and careful lifetime management are fundamental to avoiding undefined behavior. Once these rules are understood, pointers become a precise and powerful tool for controlling memory access in C++.
Be the first to add a comment.