Pointers are one of C++’s historical boogeymen, and a place where many aspiring C++ learners have gotten stuck. However, as you’ll see shortly, pointers are nothing to be scared of. In fact, pointers work very much like references. But before we explain that further, let’s do some setup.
Consider a normal variable, like this one:
1 |
int x {}; |
When this statement is executed by the CPU, a piece of memory from RAM will be set aside. For the sake of example, let’s say that the variable x
is assigned memory location 140
. Whenever the program sees the variable x
in an expression or statement, it knows that it should look in memory address 140
to get the value.
The nice thing about variables is that we don’t need to worry about what specific memory address is assigned. We just refer to the variable by its given identifier, and the compiler translates this name into the appropriately assigned memory address.
This address hiding is similarly true with references:
1 2 3 4 5 6 7 |
int main() { int x {} int& ref { x }; // when used with a type, the & denotes a reference return 0; } |
Whenever the compiler sees ref
, it knows that it should work identically to x
, so it can go look in memory address 140
to get the value. The memory addresses are still hidden to us.
The address-of operator (&)
Although the memory addresses used by variables aren’t exposed to us by default, we do have access to this information. When used outside of a type declaration, the address-of operator (&) returns the memory address of a variable. This is pretty straightforward:
1 2 3 4 5 6 7 8 9 10 |
#include <iostream> int main() { int x{ 5 }; std::cout << x << '\n'; // print the value of variable x std::cout << &x << '\n'; // print the memory address of variable x return 0; } |
On the author’s machine, the above program printed:
5 0027FEA0
Memory addresses are typically printed as hexadecimal values (we covered hex in lesson 4.13 -- Literals), sometimes without the 0x prefix.
Although the address-of
operator looks just like the bitwise-and
operator, you can distinguish them because the address-of
operator is unary, whereas the bitwise-and
operator is binary.
The dereference operator (*)
Getting the address of a variable isn’t very useful by itself.
The most useful thing we can do with an address is ask the program to fetch the value stored at that address. The dereference operator (*) returns the value at a particular address:
1 2 3 4 5 6 7 8 9 10 11 |
#include <iostream> int main() { int x{ 5 }; std::cout << x << '\n'; // print the value of variable x std::cout << &x << '\n'; // print the memory address of variable x std::cout << *(&x) << '\n'; /// print the value at the memory address of variable x (parenthesis not required, but make it easier to read) return 0; } |
On the author’s machine, the above program printed:
5 0027FEA0 5
This program is pretty simple. First we declare a variable x
and print it’s value. Then we print the address of variable x
. Then we use the dereference operator to print the value at the address of variable x
, which is just the value of x
.
Although the dereference operator looks just like the multiplication operator, you can distinguish them because the dereference operator is unary, whereas the multiplication operator is binary.
Key insight
Given a memory address, we can ask the program to fetch the value stored at that address.
However, getting the memory address of a variable and then immediately dereferencing that address to get a value isn’t that useful either. Why go through the hassle when we can just get a variable’s value by using the variable itself?
Pointers
With the address-of operator
and dereference operator
now added to our toolkits, we can now talk about pointers. A pointer is a variable that holds a memory address (typically of another variable) as its value.
Much like references are declared using an ampersand (&) character, pointers are declared using an asterisk (*) in the variable declaration:
1 2 3 4 |
int; // a normal int int&; // a reference to an int value int*; // a pointer to an int value (holds the address of an integer value) |
To create a pointer variable, we simply define a variable with a pointer type:
1 2 3 4 |
int x; // normal variable int& ref; // a reference to an integer int* ptr; // a pointer to an integer |
C++ will accept the asterisk next to the data type, next to the variable name, or with spaces on each side, but much like with references, its best to put the asterisk next to the type name. Note that this asterisk is part of the declaration syntax for pointers, not a use of the dereference operator.
Best practice
When declaring a pointer type, place the asterisk next to the type name.
Warning
Although you generally should not declare multiple variables on a single line, if you do, the asterisk has to be included with each variable.
1 2 |
int* ptr1, ptr2; // incorrect: ptr1 is a pointer to an int, but ptr2 is just a plain int! int* ptr3, * ptr4; // correct: ptr3 and p4 are pointers to an int |
Although this is sometimes used as an argument to not place the asterisk with the type name (instead attaching it to the variable), it’s a better argument for simply not defining multiple variables in the same statement.
Pointer initialization
Like normal variables, pointers are not initialized by default. A pointer that has not been initialized is sometimes called a wild pointer. Wild pointers contain a garbage address, and dereferencing a wild pointer will result in undefined behavior. Because of this, you should always initialize your pointers to a known value.
Rule
Always initialize your pointers.
1 2 3 4 5 6 7 8 9 |
int main() { int x{ 5 }; int* ptr; // uninitialized pointer, holds a garbage address int* ptr2{}; // a null pointer (we'll discuss this in the next lesson) int* ptr3{ &x }; // a pointer initialized to hold the address of variable x return 0; } |
Since pointers only hold addresses, when we initialize or assign a value to a pointer, that value has to be an address. Typically, pointers are used to hold the address of another variable (which we can get using the address-of operator
). We can then use the dereference operator
to access the value at the address that our pointer is holding.
For example:
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <iostream> int main() { int x{ 5 }; int* ptr{ &x }; // use address-of operator to get x's address, which we use to initialize ptr std::cout << x << '\n'; // print the value of variable x std::cout << *ptr << '\n'; // use dereference operator to print the value at the address that ptr is holding (which is x's address) return 0; } |
This prints:
5 5
Conceptually, you can think of the above snippet like this:
This is where pointers get their name from -- ptr
is holding the address of x
, so we say that ptr
is “pointing to” x
.
Author's note
A note on pointer nomenclature: “X pointer” (where X is some type) is a commonly used shorthand for “pointer to an X”. So when we say, “an integer pointer”, we really mean “a pointer to an integer”. This distinction will be valuable when we talk about const pointers.
The type of the pointer has to match the type of the variable being pointed to:
1 2 3 4 5 6 7 8 9 10 |
int main() { int i{ 5 }; double d{ 7.0 }; int* iPtr{ &i }; // ok int* iPtr2 { &d }; // not okay, types do not match double* dPtr{ &d }; // ok double* dPtr2{ &i }; // not okay, types do not match } |
With one exception that we’ll discuss next lesson, initializing a pointer with a literal value is also disallowed:
1 2 |
int* ptr{ 5 }; // not okay int* ptr{ 0x0012FF7C }; // not okay |
This is because pointers hold addresses, and integer literals (which are r-values) do not have a memory address. C++ will not recognize the hexadecimal integer number as a memory address. If you try this, the compiler will tell you it cannot convert an integer to an integer pointer.
Assignment to pointers and dereferenced pointers
We can use assignment with pointers for two purposes:
- To change the address that the pointer is holding (and thus, make it point at something else)
- To change the value at the address that the pointer is holding
First, let’s look at a case where a pointer is “re-pointed” at another object.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <iostream> int main() { int x{ 5 }; int* ptr{ &x }; // initialize ptr with address of variable x std::cout << *ptr; // print the value at the address that ptr is holding (which is x's address) int y{ 6 }; ptr = &y; // change address that ptr is pointing at (note no use of dereference operator here) std::cout << *ptr; // print the value at the address that ptr is holding (which is y's address) return 0; } |
The above prints:
56
Now, let’s look at how we can also use a pointer to change the value being pointed at:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <iostream> int main() { int x{ 5 }; int* ptr{ &x }; // initialize ptr with address of variable x std::cout << x; // print x's value std::cout << *ptr; // print the value at the address that ptr is holding (which is x's address) *ptr = 6; // change the value at the address that ptr is pointing to (note use of dereference operator here!) std::cout << x; std::cout << *ptr; return 0; } |
This program prints:
5566
As you can see, in the above program we used ptr
to change the value that ptr
was pointing at (which was x's
value). Thus, x is changed.
Key insight
We can assign a new address to the pointer to change what the pointer is pointing at.We can assign a new value to the dereferenced pointer to change the value of the object the pointer is pointing at.
Pointers vs references
Pointers work much like references. Consider the following program:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <iostream> int main() { int x{ 5 }; std::cout << x; int& ref { x }; // get a references to x std::cout << ref; // use the reference to print x's value int* ptr { &x }; // get a pointer to x std::cout << *ptr; // use the pointer to print x's value ref = 6; // use the reference to change the value of x std::cout << x; std::cout << ref; // use the reference to print x's value *ptr = 7; // use the pointer to change the value of x std::cout << x; std::cout << *ptr; // use the pointer to print x's value return 0; } |
This program prints:
5556677
In the above program, we first print the value of x
. Next we create reference to x
named ref
, change x's
value through the reference, and then print the value of x
through both x
and ref
. Finally, we create a pointer to x
named ptr
and we print x's
value through both x
and by dereferencing ptr
.
Thus, both pointers and references provide a way to indirectly access another object. The primary difference is that with pointers, we need to explicitly get the address to point at, and we have to explicitly dereference the pointer to get the value. With references, this all happens implicitly.
There are some other differences between pointers and references worth mentioning:
- References must be initialized, pointers are not required to be initialized (but should be).
- References are not objects, pointers are.
- References can not be reseated (changed to reference something else), pointers can change the address they hold.
- References must always point at an object, pointers can point to nothing (we’ll see an example of this in the next lesson).
- References are “safe” (outside of dangling references), pointers are inherently dangerous (we’ll also discuss this in the next lesson).
As an aside...
References are typically implemented using pointers. When you access a reference, the compiler does an implicit pointer dereference. This doesn’t apply in cases where the compiler can optimize away the reference entirely.
The address-of operator returns a pointer
It’s worth noting that the address-of operator (&) doesn’t return the address of its operand as a literal. Instead, it returns a pointer containing the address of the operand, whose type is derived from the argument (e.g. taking the address of an int will return the address in an int pointer).
We can see this in the following example:
1 2 3 4 5 6 7 8 9 10 |
#include <iostream> #include <typeinfo> int main() { int x{ 4 }; std::cout << typeid(&x).name() << '\n'; return 0; } |
On Visual Studio, this printed:
int *
(With gcc, this prints “pi” (pointer to int) instead).
The size of pointers
The size of a pointer is dependent upon the architecture the executable is compiled for -- a 32-bit executable uses 32-bit memory addresses -- consequently, a pointer on a 32-bit machine is 32 bits (4 bytes). With a 64-bit executable, a pointer would be 64 bits (8 bytes). Note that this is true regardless of the size of the object being pointed to:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <iostream> int main() // assume a 32-bit application { char *chPtr{}; // chars are 1 byte int *iPtr{}; // ints are usually 4 bytes long double *ldPtr{}; // long doubles are usually 8 or 12 bytes std::cout << sizeof(chPtr) << '\n'; // prints 4 std::cout << sizeof(iPtr) << '\n'; // prints 4 std::cout << sizeof(ldPtr) << '\n'; // prints 4 return 0; } |
The size of the pointer is always the same. This is because a pointer is just a memory address, and the number of bits needed to access a memory address on a given machine is always constant.
Dangling pointers
Much like dangling references, dangling pointers are pointers that are holding the address of an object that is no longer valid (e.g. because it has been destroyed). Dereferencing a dangling pointer will lead to undefined results.
Here’s an example of creating a dangling pointer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <iostream> int main() { int x{ 5 }; int* ptr{ &x }; std::cout << *ptr; // valid { int y{ 6 }; ptr = &y; std::cout << *ptr; // valid } // y goes out of scope, and ptr is now dangling std::cout << *ptr; // undefined behavior from dereferencing a dangling pointer return 0; } |
The above program will probably print:
566
But it may not, as the object that ptr
was pointing at went out of scope, leaving ptr
dangling.
Conclusion
Pointers are variables that hold a memory address. They can be dereferenced using the dereference operator (*) to retrieve the value at the address they are holding. Dereferencing a wild or dangling (or null) pointer will result in undefined behavior and will probably crash your application.
Pointers are both more flexible than references and more dangerous. We’ll continue to explore this in the upcoming lessons.
Quiz time
Question #1
What values does this program print? Assume a short is 2 bytes, and a 32-bit machine.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
short value{ 7 }; // &value = 0012FF60 short otherValue{ 3 }; // &otherValue = 0012FF54 short *ptr{ &value }; std::cout << &value << '\n'; std::cout << value << '\n'; std::cout << ptr << '\n'; std::cout << *ptr << '\n'; std::cout << '\n'; *ptr = 9; std::cout << &value << '\n'; std::cout << value << '\n'; std::cout << ptr << '\n'; std::cout << *ptr << '\n'; std::cout << '\n'; ptr = &otherValue; std::cout << &otherValue << '\n'; std::cout << otherValue << '\n'; std::cout << ptr << '\n'; std::cout << *ptr << '\n'; std::cout << '\n'; std::cout << sizeof(ptr) << '\n'; std::cout << sizeof(*ptr) << '\n'; |
Question #2
What’s wrong with this snippet of code?
1 2 3 |
int value{ 45 }; int *ptr{ &value }; // declare a pointer and initialize with address of value *ptr = &value; // assign address of value to ptr |
![]() |
![]() |
![]() |
What good are pointers?
Pointers are useful in many different cases:
// HERE
1) They are the only way you can dynamically allocate memory in C++ (covered in lesson 6.9). This is by far the most common use case for pointers.
3) They can be used to pass a large amount of data to a function in a way that doesn’t involve copying the data, which is inefficient (covered in lesson 7.4)
4) They can be used to pass a function as a parameter to another function (covered in lesson 7.8).
5) They can be used to achieve polymorphism when dealing with inheritance (covered in lesson 12.1).
6) They can be used to have one struct/class point at another struct/class, to form a chain. This is useful in some more advanced data structures, such as linked lists and trees.
So there are actually a surprising number of uses for pointers. But don’t worry if you don’t understand what most of these are yet. Now that you understand what pointers are at a basic level, we can start taking an in-depth look at the various cases in which they’re useful, which we’ll do in subsequent lessons.
Unless I missed something in the context or content of this section.
Why did they call (*) a "Dereferencing Operator"? I'd call it a "reference operator", it refers to (points to) the address of another variables location. That address does not go away (get dereferenced) until the close of the statement it's in. The closing brace (}). Did I miss something?
Text from lesson attached below in " ".
" int nValue = 5; int *pnPtr = &nValue; // pnPtr points to nValue *pnPtr = 7; // *pnPtr is the same as nValue, which is assigned 7 cout << nValue; // prints 7
Pointers can also be assigned and reassigned:"
The code above the last sentence should be moved somewhere below the Pointer sentence since it refers to the code that's written below it.
You say pointers can be assigned and reassigned. Does that mean that they are not always local but are used for global situations as well.
Sorry again for being your critic. We can see that you've done an outstanding job here.
This is a beautifully clear explanation - many thanks.
Hey, Alex. I just wanted to let you know I really appreciate you not only giving away all this knowledge without asking in return, but doing so in such a well structured way. In my book, this makes you an admirable person. On a sideline, with the knowledge I've acquired thus far, I started making a text-based dungeon crawler. I've already made functions which can generate a random map, and draw it using X's & whitespaces. I'm confident that it will turn out fairly nicely, and it's all thanks to you.
I am working on MS visual Studio 2010. OS is win7. What I know is win7 is 64 bit OS. yet I get pointer size as 4. Why it is not 8?
int* A;
cout<<"\nSize of memory address is"<<sizeof(A);
Output is:
Size of memory address is4
Thanks for answer.
Also my computer has 8gb of ram so if pointers are only 32-bit for some reason then surely only half the computers memory could be used? I don't get it.
on my computer (windows 7 64-bit) sizeof(pntr) returns 4 when I was expecting it to return 8 based on this tutorial. Does anyone know why this is?
I have a question ...i cannot understand something.
I have this simple code :
int *pValue;
*pValue = 4;
cout<<*pValue;
It's all ok..it prints 4;
I i declare another one my program crushes and i cannot understand why since is the same thing.
int *pValue, *nValue;
*pValue = 4;
*nValue = 5;
cout<<*pValue<<*nValue;
Something must be wrong, because the first code in which you declare only one pointer crashes too.
No memory allocated. That is why it crashes.