Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Understanding Memory Addresses and Pointers
Store memory addresses in pointer variables and access data through them.
Introduction to Pointers
Pointers have historically been one of C++'s most challenging concepts. However, they're not as intimidating as their reputation suggests. Pointers behave similarly to lvalue references, just with different syntax.
Related Content
If you're rusty on lvalue references, review the lessons on lvalue references, lvalue references to const, and pass by lvalue reference.
Consider a normal variable:
char symbol{}; // chars use 1 byte of memory
When this definition executes, a piece of RAM is assigned to this object. For example, symbol might get memory address 200. Whenever we use symbol in an expression, the program accesses memory address 200 to retrieve the stored value.
The beauty of variables is we don't worry about specific memory addresses. We reference the variable by its identifier, and the compiler translates this name into the assigned memory address.
This is also true with references:
int main()
{
char symbol{}; // Assume assigned memory address 200
char& alias{symbol}; // alias is lvalue reference to symbol
return 0;
}
Because alias acts as an alias for symbol, using alias accesses memory address 200. The compiler handles the addressing automatically.
The Address-of Operator (&)
Although variables' memory addresses aren't exposed by default, we can access this information. The address-of operator (&) returns its operand's memory address:
#include <iostream>
int main()
{
int health{100};
std::cout << health << '\n'; // Print health's value
std::cout << &health << '\n'; // Print health's memory address
return 0;
}
When tested, this printed:
100
0x7fff5fbff8ac
The address will differ on your machine. Memory addresses are typically printed as hexadecimal values. For objects using more than one byte, address-of returns the address of the first byte used.
Tip
The & symbol has different meanings depending on context:
- When following a type name, & denotes an lvalue reference:
int& alias - When used in a unary context, & is the address-of operator:
std::cout << &health - When used in a binary context, & is the Bitwise AND operator:
std::cout << a & b
The Dereference Operator (*)
Getting a variable's address isn't very useful by itself.
The dereference operator (*) (also called the indirection operator) returns the value at a given memory address as an lvalue:
#include <iostream>
int main()
{
int health{100};
std::cout << health << '\n'; // Print health's value
std::cout << &health << '\n'; // Print health's memory address
std::cout << *(&health) << '\n'; // Print value at health's memory address
return 0;
}
When tested, this printed:
100
0x7fff5fbff8ac
100
Core Understanding
Given a memory address, the dereference operator (*) gets the value at that address (as an lvalue).
The address-of operator (&) and dereference operator (*) work as opposites: address-of gets an object's address, and dereference gets the object at an address.
Tip
The dereference operator looks like multiplication, but dereference is unary while multiplication is binary.
Now that we have these operators in our toolkit, we're ready to discuss pointers.
Pointers
A pointer is an object holding a memory address (typically of another variable) as its value. This allows us to store another object's address for later use.
As an Aside
In modern C++, the pointers we're discussing are sometimes called "raw pointers" or "dumb pointers" to distinguish them from "smart pointers" introduced more recently. We cover smart pointers in a later chapter.
A type specifying a pointer (e.g., int*) is called a pointer type. Just as reference types use an ampersand (&), pointer types use an asterisk (*):
int; // Normal int
int&; // Lvalue reference to int value
int*; // Pointer to int value (holds an integer value's address)
To create a pointer variable, define a variable with a pointer type:
int main()
{
int health{100}; // Normal variable
int& alias{health}; // Reference to an integer (bound to health)
int* ptr; // Pointer to an integer
return 0;
}
The asterisk is part of the declaration syntax for pointers, not the dereference operator.
Best Practice
When declaring a pointer type, place the asterisk next to the type name.
Warning
When declaring multiple variables, the asterisk must be included with each variable:
int* ptr1, ptr2; // Incorrect: ptr1 is pointer to int, but ptr2 is plain int!
int* ptr3, * ptr4; // Correct: ptr3 and ptr4 are both pointers to int
This is a good argument for avoiding multiple variable definitions on the same line.
Pointer Initialization
Like normal variables, pointers are not initialized by default. An uninitialized pointer is called a wild pointer. Wild pointers contain garbage addresses, and dereferencing them causes undefined behavior.
Best Practice
Always initialize your pointers.
int main()
{
int health{100};
int* ptr; // Uninitialized pointer (holds garbage address)
int* ptr2{}; // Null pointer (covered next lesson)
int* ptr3{&health}; // Pointer initialized with health's address
return 0;
}
Since pointers hold addresses, we use the address-of operator (&) to obtain the address we want to store:
#include <iostream>
int main()
{
int health{100};
std::cout << health << '\n'; // Print health's value
int* ptr{&health}; // ptr holds health's address
std::cout << *ptr << '\n'; // Use dereference to print value at ptr's address
return 0;
}
This prints:
100
100
Conceptually, ptr is "pointing to" health.
"X pointer" (where X is some type) is shorthand for "pointer to an X". So "an integer pointer" means "a pointer to an integer".
Just as a reference's type must match the referred object's type, the pointer's type must match the pointed-to object's type:
int main()
{
int score{100};
double rating{4.5};
int* intPtr{&score}; // Valid: pointer to int points to int
int* intPtr2{&rating}; // Invalid: pointer to int can't point to double
double* dblPtr{&rating}; // Valid: pointer to double points to double
double* dblPtr2{&score}; // Invalid: pointer to double can't point to int
return 0;
}
With one exception (discussed next lesson), initializing a pointer with a literal value is disallowed:
int* ptr{7}; // Invalid
int* ptr{0x0012FF7C}; // Invalid: treated as integer literal
Pointers and Assignment
We can use assignment with pointers in two ways:
- To change what the pointer is pointing at (by assigning a new address)
- To change the value being pointed at (by assigning to the dereferenced pointer)
First, changing what a pointer points to:
#include <iostream>
int main()
{
int health{100};
int* ptr{&health}; // ptr points at health
std::cout << *ptr << '\n'; // Print value at ptr's address (100)
int mana{50};
ptr = &mana; // Change ptr to point at mana
std::cout << *ptr << '\n'; // Print value at ptr's address (50)
return 0;
}
This prints:
100
50
Now, using a pointer to change the value being pointed at:
#include <iostream>
int main()
{
int health{100};
int* ptr{&health}; // Initialize ptr with health's address
std::cout << health << '\n'; // Print health's value
std::cout << *ptr << '\n'; // Print value at ptr's address
*ptr = 75; // Object at ptr's address (health) assigned value 75
std::cout << health << '\n';
std::cout << *ptr << '\n';
return 0;
}
This prints:
100
100
75
75
Core Understanding
When we use a pointer without dereferencing (ptr), we access the address held by the pointer. Modifying this changes what the pointer points at.
When we dereference a pointer (*ptr), we access the object being pointed at. Modifying this changes the value of that object.
Pointers Behave Much Like Lvalue References
Pointers and lvalue references behave similarly:
#include <iostream>
int main()
{
int health{100};
int& alias{health}; // Get reference to health
int* ptr{&health}; // Get pointer to health
std::cout << health;
std::cout << alias; // Use reference to print health's value (100)
std::cout << *ptr << '\n'; // Use pointer to print health's value (100)
alias = 75; // Use reference to change health's value
std::cout << health;
std::cout << alias;
std::cout << *ptr << '\n';
*ptr = 50; // Use pointer to change health's value
std::cout << health;
std::cout << alias;
std::cout << *ptr << '\n';
return 0;
}
This prints:
100100100
757575
505050
Thus, pointers and references both provide indirect access to another object. The primary difference is that with pointers, we explicitly get the address and explicitly dereference. With references, this happens implicitly.
Other differences between pointers and references:
- References must be initialized, pointers are not required to be (but should be)
- References are not objects, pointers are
- References cannot be reseated, pointers can change what they point at
- References must always be bound to an object, pointers can point to nothing
- References are "safe" (outside of dangling references), pointers are inherently dangerous
The Address-of Operator Returns a Pointer
The address-of operator (&) doesn't return the operand's address as a literal. Instead, it returns a pointer to the operand (whose value is the operand's address):
#include <iostream>
#include <typeinfo>
int main()
{
int health{100};
std::cout << typeid(health).name() << '\n'; // Print health's type
std::cout << typeid(&health).name() << '\n'; // Print &health's type
return 0;
}
When tested, this printed:
int
int *
The Size of Pointers
Pointer size depends on the architecture. A 32-bit executable uses 32-bit memory addresses (4 bytes). A 64-bit executable uses 64-bit addresses (8 bytes). This is true regardless of the pointed-to object's size:
#include <iostream>
int main() // Assume 32-bit application
{
char* charPtr{}; // chars are 1 byte
int* intPtr{}; // ints are usually 4 bytes
long double* ldPtr{}; // long doubles are usually 8 or 12 bytes
std::cout << sizeof(charPtr) << '\n'; // Prints 4
std::cout << sizeof(intPtr) << '\n'; // Prints 4
std::cout << sizeof(ldPtr) << '\n'; // Prints 4
return 0;
}
Pointer size is constant because a pointer is just a memory address.
Dangling Pointers
A dangling pointer holds the address of an object that no longer exists.
Dereferencing a dangling pointer leads to undefined behavior. Any other use of an invalid pointer value has implementation-defined behavior.
Core Understanding
Dereferencing an invalid pointer leads to undefined behavior. Any other use of an invalid pointer value is implementation-defined.
Here's an example creating a dangling pointer:
#include <iostream>
int main()
{
int health{100};
int* ptr{&health};
std::cout << *ptr << '\n'; // Valid
{
int temp{75};
ptr = &temp;
std::cout << *ptr << '\n'; // Valid
} // temp goes out of scope, ptr is now dangling
std::cout << *ptr << '\n'; // Undefined behavior
return 0;
}
Conclusion
Pointers are variables holding memory addresses. They can be dereferenced using the dereference operator (*) to retrieve the value at the address they hold. Dereferencing a wild, dangling, or null pointer causes undefined behavior.
Pointers are both more flexible than references and more dangerous. We'll continue exploring this in upcoming lessons.
Quiz
Question 1
What values does this program print? Assume a short is 2 bytes, and a 32-bit machine.
#include <iostream>
int main()
{
short score{15}; // &score = 0x0012FF60
short bonus{5}; // &bonus = 0x0012FF54
short* ptr{&score};
std::cout << &score << '\n';
std::cout << score << '\n';
std::cout << ptr << '\n';
std::cout << *ptr << '\n';
std::cout << '\n';
*ptr = 25;
std::cout << &score << '\n';
std::cout << score << '\n';
std::cout << ptr << '\n';
std::cout << *ptr << '\n';
std::cout << '\n';
ptr = &bonus;
std::cout << &bonus << '\n';
std::cout << bonus << '\n';
std::cout << ptr << '\n';
std::cout << *ptr << '\n';
std::cout << '\n';
std::cout << sizeof(ptr) << '\n';
std::cout << sizeof(*ptr) << '\n';
return 0;
}
Answer:
0x0012FF60
15
0x0012FF60
15
0x0012FF60
25
0x0012FF60
25
0x0012FF54
5
0x0012FF54
5
4
2
A 32-bit machine means pointers are 4 bytes. Since ptr is a pointer to short, *ptr is a short, which is 2 bytes.
Question 2
What's wrong with this code snippet?
int hp{45};
int* ptr{&hp}; // Initialize ptr with address of hp
int mp{78};
*ptr = ∓ // Assign ptr to address of mp
Answer:
The last line doesn't compile.
On line two, the asterisk is part of pointer declaration syntax. This line assigns the address of hp to ptr.
On line five, the asterisk represents dereference. This line says "assign the address of mp to the value pointed to by ptr". The value pointed to is an integer, and you can't assign an address to an integer!
The fifth line should be:
ptr = ∓
This correctly assigns mp's address to the pointer.
Understanding Memory Addresses and Pointers - Quiz
Test your understanding of the lesson.
Practice Exercises
Introduction to Pointers
Practice using pointers to store and access memory addresses.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!