C++ / REFERENCES, POINTERS, AND NULL
Memory addresses and the address-of operator
Take any object's address with unary &, print it without the char trap, and reason about what C++ actually guarantees about that address.
What you will learn
- Use unary & on a named object to get its address; the type of &x for int x is int*
- Read &x as the number of x's first byte, with sizeof x bytes following it
- Cast a char address to const void* before printing, or cout prints text instead
- Tell apart &'s three jobs: address-of, bitwise AND, and reference declarator
Understanding Memory addresses and the address-of operator
Memory in a running process behaves like one enormous array of bytes, each byte carrying a number. Every object your program creates occupies a run of consecutive bytes in that array, and its address is the number of the first of those bytes, while sizeof tells you how many bytes follow. Unary & hands you that number, but not as a bare integer: if x is an int, &x has type int*, so the value carries both the starting byte and the compiler's knowledge of how to read the bytes there.
The operand of & must be an lvalue, something that names storage, which is why &42 and &(a + b) do not compile. A literal or an arithmetic result may live only in a CPU register or be folded away entirely at compile time, and a register has no number in the byte-addressed sense. Taking the address of a local variable is in fact what forces the compiler to give that variable a real slot in memory instead of keeping it in a register.
The numeric value you see printed matters far less than beginners expect. It changes between runs because the loader randomises where stack, heap, and code are placed, so it is useless as a saved identifier and unsafe to order with < across unrelated objects. What the language does guarantee is worth memorising: two distinct objects alive at the same time have different addresses, an object's address is always a multiple of alignof for its type, elements of one array sit sizeof(T) bytes apart, and the size of an address is fixed by the platform rather than by the size of the thing it points at.
<cstdint>
<iostream>
int main() {
int n = 42;
double d = 3.5;
int arr[3] = {10, 20, 30};
int* pn = &n; // the type of &n is int*
double* pd = &d; // the type of &d is double*
auto as_number = [](const void* p) {
return reinterpret_cast<std::uintptr_t>(p);
};
std::cout << std::boolalpha;
std::cout << "sizeof n = " << sizeof n << '\n';
std::cout << "sizeof &n = " << sizeof &n << '\n';
std::cout << "sizeof &d = " << sizeof &d << '\n';
std::cout << "&n == pn = " << (&n == pn) << '\n';
std::cout << "&d aligned for double = "
<< (as_number(pd) % alignof(double) == 0) << '\n';
std::cout << "bytes from arr[0] to arr[1] = "
<< as_number(&arr[1]) - as_number(&arr[0]) << '\n';
std::cout << "&arr[0] == &arr[1] = " << (&arr[0] == &arr[1]) << '\n';
}
An object's address is simply the number of its first byte, and unary & delivers that number as a typed pointer value.
Worked examples
An object's address is the address of its first byte
Shows that a struct and its first member start at the same byte, and that later members have their own addresses further along.
<cstdint>
<iostream>
struct Point { int x; int y; };
int main() {
Point p{3, 4};
auto base = reinterpret_cast<std::uintptr_t>(&p);
std::cout << std::boolalpha;
std::cout << "offset of x: "
<< reinterpret_cast<std::uintptr_t>(&p.x) - base << '\n';
std::cout << "offset of y: "
<< reinterpret_cast<std::uintptr_t>(&p.y) - base << '\n';
std::cout << "&p and &p.x name the same byte: "
<< (static_cast<const void*>(&p) == static_cast<const void*>(&p.x))
<< '\n';
std::cout << "sizeof(Point): " << sizeof(Point) << '\n';
}
Example explained
Line 1reinterpret_cast<std::uintptr_t> turns an address into the plain byte number so it can be subtracted.
Line 2&p.x sits at offset 0 because a standard-layout struct begins with its first member, so the two addresses are the same byte with different static types (Point* and int*).
Line 3&p.y is 4 bytes further on because x occupies bytes 0 through 3.
Line 4The comparison needs both operands cast to const void*: comparing a Point* directly with an int* is a type error, not a runtime question.
Three different meanings of the & token
Distinguishes & as a reference declarator, & as bitwise AND, and & as the address-of operator in one program.
<iostream>
int main() {
int a = 12;
int b = 10;
int& r = a; // declarator: r is another name for a
std::cout << std::boolalpha;
std::cout << "a & b = " << (a & b) << '\n'; // binary operator
std::cout << "&r == &a = " << (&r == &a) << '\n'; // unary operator
std::cout << "&a == &b = " << (&a == &b) << '\n';
}
Example explained
Line 1In int& r = a the & is part of the type, not an operator; nothing is evaluated there.
Line 2a & b has two operands, so & is bitwise AND: 1100 AND 1010 gives 1000, which is 8.
Line 3&r has one operand, so & is address-of, and it yields a's address because r is not a separate object with storage of its own.
Line 4&a == &b is false because a and b are distinct objects alive at the same time, so they cannot share a byte.
When & is not the address-of operator you wanted
Demonstrates that a class can overload unary & and that std::addressof always gives the real address.
<iostream>
<memory>
struct Handle {
int id;
int operator&() const { return -1; } // hostile overload
};
int main() {
Handle h{7};
std::cout << std::boolalpha;
std::cout << "&h yields: " << (&h) << '\n';
std::cout << "addressof reaches the object: " << std::addressof(h)->id << '\n';
std::cout << "same byte as &h.id: "
<< (static_cast<const void*>(std::addressof(h))
== static_cast<const void*>(&h.id))
<< '\n';
}
Example explained
Line 1&h calls Handle::operator&, so the expression produces the int -1 and never touches memory layout at all.
Line 2std::addressof(h) from <memory> ignores any overload and returns the genuine Handle*, which is why generic library code uses it.
Line 3&h.id still means address-of because the overload belongs to Handle, not to int.
Line 4The final comparison confirms std::addressof(h) points at the same byte where the first member starts.
Important notes
The sizes shown assume a 64-bit build, where every address is 8 bytes; on a 32-bit target they are 4. Either way, sizeof of an address never depends on the size of the object it refers to.
The exact printed form of an address is implementation-defined: libstdc++ and libc++ print 0x-prefixed lowercase hex, while MSVC prints uppercase hex with no prefix.
Common mistakes
Writing std::cout << &myChar: the char* overload of operator<< treats the address as the start of a C string and prints bytes until it happens to hit a zero, giving garbage and possibly reading past the variable. Cast to const void* to print the address itself.
Trying int* p = &42; or &(a + b): both fail to compile with a message about needing an lvalue, because literals and arithmetic results are not objects with storage. Store the value in a named variable first, then take that variable's address.
Treating the printed hex number as stable or ordered: saving it, hard-coding it, or writing logic such as if (&a < &b) breaks because the value changes every run and the relative placement of unrelated objects is not specified.
Try it yourself
Change, predict, then run
Declare char c = 'Z';, int i = 7;, and double arr[3] = {}; then print sizeof of each object beside sizeof of its address, and print the byte gap between &arr[0] and &arr[2] using std::uintptr_t. Write down your predicted numbers before running it, and print &c only through a const void* cast.
Open the C++ workspaceCheck your understanding
Two locals are declared next to each other in the same function: int a = 1; int b = 2; Which statement about their addresses is guaranteed by C++?
- &b == &a + 1, because locals are laid out contiguously in declaration order
- &a < &b, because the compiler assigns increasing addresses as it reads declarations
- &a != &b, because two distinct objects alive at the same time occupy different bytes
- Nothing is defined about either address until you take the address of at least one of them
Show answer
Distinct objects that exist simultaneously must have distinct addresses, so &a != &b always holds. The contiguity option is tempting because array elements really do behave that way, but separate variables are not an array: the compiler may insert padding for alignment, order the stack slots however it likes, or keep one variable in a register until its address is needed, and relational comparison between pointers into unrelated objects is unspecified.