13. Pointers#
A pointer is a variable that stores/contains a memory address. Often, these addresses are just those of other variables. However, in later lectures/pages, we will dynamically allocate memory and assign the initial address of that allocated memory to a pointer variable.
In some ways, we’ve already been using pointers through object references in Python. Consider this Python code:
primes = [1,2,3,5,7,11,13,17,19]
p = primes
primes
and p
both refer to the same underlying list object. We just have not had the capability to directly access or manipulate those address values. Notice that they have they same object ID:
//filename: python_id.py
//complile: not applicable
//execute: python python_id.py
primes = [1,2,3,5,7,11,13,17,19]
p = primes
print(id(primes));
print(id(p));
Output: (numbers may differ, but both lines will be the same)
4374464832
4374464832
In the center of the image below, we have an example of some arbitrary computer memory. We have defined a variable b
that’s stored at location 1008. We then define a pointer named a
. That variable’s contents contains the address of b
.
int b = 1842;
int *a = &b;
int *a
defines an int pointer. The unary operator &
(address of) gives the address of an object in memory. The address of operator can only be applied to variables and array elements. To access the contents of the memory location that a pointer references, we use the unary operator * (dereferences). Going back to the third Python notebook, you can equate this address to the address of a building in a city - it just references a location in memory.
Adding a *
after any type name changes that type declaration to a pointer of that type.
1//filename: pointer_1.cpp
2//complile: g++ -std=c++17 -o pointer_1 pointer_1.cpp
3//execute: ./pointer_1
4#include <iostream>
5using std::cout;
6
7int main(int argc, char *argv[]) {
8 int b = 1842; /* define a "regular" variable b */
9 int c = 1992;
10 int *a; /* declare a pointer variable a */
11
12 a = &b; /* store the address of b in a */
13
14 cout << "Value of b: " << b << "\n";
15 cout << "Value of a*: " << *a << "\n";
16 cout << "value of a: " << a << "\n"; /* a is a pointer, and contains the address of b */
17 cout << "Address of b: " << &b << "\n";
18 cout << "Address of a: " << &a << "\n";
19
20 cout << "Value of c: " << c << " (before assignment)\n";
21 c = *a; /* c now contains 1842 */
22 cout << "Value of c: " << c << " (after assignment)\n";
23
24 return 0;
25}
Another way to think about pointers is that a variable name (or a reference variable) directly refers to a value while a point indirectly refers to a value. The term indirection
means referencing a value through a pointer. &
creates a pointer value(address). *
“dereferences” a pointer value(address) to access the underlying value.
Remember that every variable is stored somewhere in memory, so we should then be able to store that address in another variable.
13.1. Initializing Pointers#
To initialize a pointer, assign it the address value of a variable/object generated with the &
address operator. If such a variable is not available when defining the pointer variable, use the value of nullptr
. Any pointer with this value “points to nothing”, and we generally refer to this as a “null pointer”. Prior to C++11, null pointers were specified by NULL
or 0
. However, these are both integer literals that can lead to type-related issues - is this a pointer value (i.e., an address) or an integer value?
13.2. Pointers and Functions#
Within C++, we can pass arguments to functions in three ways:
pass-by-value
pass-by-reference with a reference argument(s)
pass-by-reference with pointer argument(s)
The Functions notebook contained a bad implementation for a function swap
. By default, C++ passes parameters by value. As such, any changes made to the parameters will only be reflected within the function itself. Once the function exits, those parameters (variables) fall out of scope and their storage is released. In a prior Docable, we demonstrated using references with swap, which is preferred. However, due the legacy with C, we can use pointer argument(s) to implement pass-by-reference. This scenario is technically pass-by-value, but we pass the addresses of variables into functions and then dereference those pointers to access and manipulate those contents. Here is an implementation through using pointers for the two parameters:
1//filename: swap_pointer.cpp
2//complile: g++ -std=c++17 -o swap_pointer swap_pointer.cpp
3//execute: ./swap_pointer
4#include <iostream>
5using std::cout;
6
7void swap(int *a, int *b) {
8 int temp = *a;
9 *a = *b;
10 *b = temp;
11 cout << "in swap: a's value " << a << "\n";
12 cout << "in swap, a points to " << *a << "\n";
13}
14
15int main(int argc, char *argv[]) {
16 int a = 10;
17 int b = 15;
18 cout << "before swap: a address " << &a << "\n";
19 swap(&a,&b);
20 cout << "after swap: a address " << &a << "\n";
21 cout << "(a,b) = (" << a << "," << b << ")\n";
22
23 return 0;
24}
As another way to relate this back to Python, imagine that we are passing mutable objects into the function. However, instead of accessing those variables normally, we must access them through pointers.
13.3. Quick Review: Basic Usage#
Key operations with pointers:
Define a pointer variable:
_type_ *_pointerName_
Assign the address of another variable to that pointer:
_pointerName_ = &_variableName_
Access the the value at the address in the pointer:
*_pointerName_
_ _(dereferencing)Changing the contents of an address:
*pointerName = newValue
Note that we use the address-of operator &
to get the address of a variable and the dereferencing operator *
to access the value at that address.
1//filename: review.cpp
2//complile: g++ -std=c++17 -o review review.cpp
3//execute: ./review
4#include <iostream>
5using std::cout;
6
7int main () {
8 int var = 1842;
9 int *ip; /* declare pointer variable (item #1) */
10
11 ip = &var; /* store the address of var in ip (item #2) */
12
13 cout << "Address of var: " << &var << "\n";
14 cout << "Address stored in ip:" << ip << "\n";
15 cout << "dereferencing ip: " << *ip << "\n"; /* (item #3) */
16
17 *ip = 1992; /*changing the contents (value) stored at a particular location (item #4) */
18 cout << "Contents of var: " << var << "\n";
19
20 return 0;
21}
ip is said to “point to” var.
13.4. nullptr Pointer#
Introduced C++ 11, nullptr
is a keyword that represents the null pointer value. Prior to then, C++ (and C) used NULL
- a special constant that stands for 0 (zero). As mentioned above, you should use nullptr
as it is a type-safe and clear way to represent a null pointer.
Use nullptr
for these reasons:
To initialize a pointer variable that has not been assigned a value memory address (remember variables in C++ when uninitialized can hold any value).
To check for
nullptr
before accessing the variable.To pass a null pointer to a function when we do not want to pass a valid memory address (the function many alter its behavior based upon
nullptr
).To assign to a pointer variable after we have de-allocated memory that pointer referenced (presented in a later notebook).
1//filename: null.cpp
2//complile: g++ -std=c++17 -o null null.cpp
3//execute: ./null
4#include <iostream>
5
6int main(int argc, char *argv[]) {
7 int i = 10;
8 int *ip = nullptr;
9
10 std::cout << "ip value: " << ip << "\n";
11
12 return 0;
13}
Just because it prints 0
, don’t interchange nullptr
with NULL
or 0
.
13.5. Pointers and Classes / Structs#
We can also have pointers to class and structs. However, when referencing these pointers, we need to either use parenthesis around the pointer dereferencing or use the operator ->
. If p
is a pointer to a a class/structure, then p->_member-of-class/structure_
refers to the particular member. The reason for this is that the class/structure member operator .
has a higher precedence than *
.
1//filename: structure.cpp
2//complile: g++ -std=c++17 -o structure structure.cpp
3//execute: ./structure
4#include <iostream>
5using std::cout;
6
7class point {
8 public:
9 int x;
10 int y;
11};
12
13int main(int argc, char *argv[]) {
14 point p;
15
16 p.x = 1;
17 p.y = 5;
18
19 point *q = &p;
20
21 cout << "(x,y): (" << (*q).x << "," << q->y << ")\n";
22
23 return EXIT_SUCCESS;
24}
13.6. Common Mistakes#
Accessing a memory location that has been freed/deallocated
Trying to access a memory location when the pointer variable contains
nullptr
.In pointer arithmetic, accessing beyond the bounds of the allocated memory segment (e.g., going beyond the end of an array).
1//filename: mistakes.c
2//complile: gcc -std=gnu-99 -o mistakes mistakes.c
3//execute: ./mistakes
4int main(int argc, char *argv[]) {
5 int i = 10;
6 int *ip = &i;
7
8 ip = i; /* ip is an address, but i is just a value */
9 *ip = &i; /* &i is address but *ip is not */
10
11 return 0;
12}
Morale: don’t ignore compiler errors. (Note: the above is actually a C program. In C++, this would not compile.)
13.7. Pointer Usage in Modern C++#
While pointers can be exceedingly elegant and powerful, they can also be exceedingly difficult to work to properly program with nuanced defects difficult to find. New C++ projects should use reference variables and smart pointers. Where contiguous memory segments are needed, use std::array
and std:vector
objects from the STL instead of built-in arrays (or pointer-based arrays). Use C++ string objects rather than pointer-based (char *
) C-style strings. However, legacy code and certain data structures (e.g., trees and graphs) may necessitate the use of pointers.
13.8. Additional Tutorials#
Understanding and effectively using pointers is one of the more challenging aspects of C++ programming.
Here are two resources for C++:
https://www.learncpp.com/cpp-tutorial/introduction-to-pointers/ Learn CPP is an excellent resource for C++ in general.
Learning C++ Pointers for REAL Dummies Introduces pointers through the concept of houses and their addresses.
The following resources was created for C, but still broadly applies to C++: A Tutorial on Pointers and Arrays in C
13.9. Review Questions#
What advantages does
nullptr
provide overNULL
or using the value0
?