Raw Pointers, Dereferencing, and Typed Address Arithmetic
Deconstruct pointer mechanics: 64-bit addresses, dereferencing indirection (*), address-of (&), pointer scaling rules, and void* type erasure.
By the end, you’ll be able to…
- Inspect 64-bit virtual memory addresses using uintptr_t and hex formatting
- Calculate pointer arithmetic jumps scaled by sizeof(T) across primitive and struct types
- Manipulate untyped raw memory blocks using void* and reinterpret_cast
01Pointers as 64-Bit Memory Addresses
In modern 64-bit C++, a raw pointer is nothing more than an 8-byte unsigned integer value whose bits represent an address in the process's virtual memory space. Regardless of whether a pointer points to a 1-byte char, a 4-byte int, or a 1024-byte struct, sizeof(ptr) is universally 8 bytes on 64-bit architectures.
The address-of operator (&) extracts the memory location of an existing variable. The dereference operator (*) instructs the CPU to load or store the data residing at that memory address. Dereferencing an uninitialized pointer (wild pointer), a dangling pointer (pointing to freed memory), or a NULL/nullptr pointer results in undefined behavior (UB) and operating system segmentation faults.
Modern C++ deprecates the C-style NULL macro (which was merely an untyped 0) in favor of nullptr, a strongly typed keyword of type std::nullptr_t that prevents ambiguous overload resolutions between integers and pointers.
02Pointer Arithmetic and Type Scaling
Pointer arithmetic does not behave like standard scalar integer addition. When you add an integer offset n to a typed pointer T* p (e.g., p + 1), the memory address increases not by n bytes, but by n * sizeof(T) bytes!
For example, if int* p points to address 0x1000 (where sizeof(int) == 4): evaluating p + 1 advances the pointer to 0x1004. Evaluating p + 5 advances the pointer to 0x1000 + (5 * 4) = 0x1014. If double* dp points to 0x2000 (sizeof(double) == 8), evaluating dp + 2 advances to 0x2010.
This scaling rule ensures that pointer arithmetic seamlessly steps across contiguous elements in arrays. Subtracting two pointers (ptr2 - ptr1) yields an integer of type ptrdiff_t representing the exact count of elements between them.
03Void Pointers and Raw Byte Buffers
A void* pointer represents a generic memory address with no associated type information. Because the compiler does not know the size of the underlying data (sizeof(void) is invalid in standard C++), a void* cannot be directly dereferenced and arithmetic cannot be performed on it without casting.
Systems software—such as custom memory allocators, network serialization engines, and hardware drivers—uses void* or std::byte* to handle raw, untyped memory buffers. Converting between void* and concrete typed pointers is performed via static_cast or reinterpret_cast.
04Pointer Arithmetic Trace Table
Let us trace address jumps across different types starting from a base address of 0x1000:
Understanding pointer scaling is foundational for writing custom buffers, matrix strides, and fast memory copies.
+------------+-----------+--------------------+-------------------------+| Type T | sizeof(T) | Expression (p = 0) | Resulting Byte Address |+------------+-----------+--------------------+-------------------------+| char | 1 byte | p + 4 | 0x1000 + 4 = 0x1004 || short | 2 bytes | p + 4 | 0x1000 + 8 = 0x1008 || int | 4 bytes | p + 4 | 0x1000 + 16 = 0x1010 || double | 8 bytes | p + 4 | 0x1000 + 32 = 0x1020 || struct Vec3| 24 bytes | p + 2 | 0x1000 + 48 = 0x1030 |+------------+-----------+--------------------+-------------------------+The ideas worth keeping.
- Pointers are 64-bit virtual memory addresses that require 8 bytes of storage on 64-bit systems.
- Pointer arithmetic automatically scales offsets by sizeof(T).
- Always use nullptr rather than 0 or NULL to ensure strict type safety.
Make it yours.
Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.
Exercise 1: Given a struct Packet uint32 t id double p
Given a struct Packet { uint32_t id; double payload[4]; }; with sizeof(Packet) == 40 bytes, calculate the resulting address when adding 3 to Packet* ptr starting at 0x2000.
Explore a worked solution
// Pointer arithmetic formula: Address = Base + (Offset * sizeof(T))// Address = 0x2000 + (3 * 40 bytes)// 3 * 40 = 120 bytes = 0x78 in hexadecimal// Resulting Address = 0x2078Multiply offset 3 by 40 bytes, then convert to hexadecimal.
Exercise 2: Explain why standard C forbids pointer arit
Explain why standard C++ forbids pointer arithmetic on void* pointers, and how to safely compute byte offsets.
Explore a worked solution
// void is an incomplete type with no size (sizeof(void) is undefined),// so the compiler cannot determine how many bytes to scale the pointer by.// To safely perform byte arithmetic, cast void* to char* or std::byte*:// void* offset_ptr = static_cast<std::byte*>(raw_buffer) + byte_offset;Cast the pointer to std::byte* or char* before adding the offset.
What stayed with you?
Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.
Exercise 1: Given a struct Packet uint32 t id double p
Given a struct Packet { uint32_t id; double payload[4]; }; with sizeof(Packet) == 40 bytes, calculate the resulting address when adding 3 to Packet* ptr starting at 0x2000.
Build completed. Binary executed. [Exit code: 0]
Keep exploring.
The official references behind this lesson, when you want a second angle or the full details.
One lesson closer.
Reading is a start. Trying it yourself makes the connection.