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:

  1. pass-by-value

  2. pass-by-reference with a reference argument(s)

  3. 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}

View the execution

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:

  1. Define a pointer variable: _type_ *_pointerName_

  2. Assign the address of another variable to that pointer: _pointerName_ = &_variableName_

  3. Access the the value at the address in the pointer: *_pointerName__ _(dereferencing)

  4. 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:

  1. To initialize a pointer variable that has not been assigned a value memory address (remember variables in C++ when uninitialized can hold any value).

  2. To check for nullptr before accessing the variable.

  3. 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 ).

  4. 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).

https://xkcd.com/371/

 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++:

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#

  1. What advantages does nullptr provide over NULL or using the value 0?