4.9. Arithmetic Operations With Pointers

Time: 00:06:05 | Download: Large, Large (CC), Small | Streaming Streaming (CC) | Slides (PDF)
Review

Pointers are variables storing addresses, and addresses are just integers that programs use in a specific way. This observation implies that pointers are subject to the same arithmetic and relational operators as other integers. However, many pointer operations are meaningless despite compiling and running without errors. Some operations are beneficial, and two operators, == and !=, and one constant, nullptr, are essential for implementing correct and secure programs using pointers.

Relational Operators

Applied to addresses, the range operators, <, <=, >, and >= produce meaningful results only when their operands lie within a contiguous memory block. However, programmers cannot control where the runtime system allocates data memory, so comparing the data's addresses with these operators is rarely meaningful. In contrast, programmers can use the equality and inequality operators, == and !=, to compare the addresses of data allocated anywhere within a program's address space (i.e., on the stack or heap).

int*	i1;
int*	i2;
Person*	p1;
Person*	p2;

if (i1 == i2) . . . .

if (i1 != i2) . . . .

if (p1 == p2) . . . .

if (p1 != p2) . . . .
Comparing pointers. Programs often use pointer comparisons with control statements (e.g., if and while). Pointer-based control-statement tests appear frequently in functions dealing with dynamic or linked data structures. Most computer science programs include a course, "Algorithms and Data Structures," where you will learn more about them and their behaviors.

nullptr

Every programming language using pointers includes a special pointer value indicating when a pointer does NOT point to anything. In C++, that value has three names. The preferred name is nullptr, but the older NULL and 0 (the single digit zero) still work and are still seen in older examples. Java programs have only one name available: null.

It is possible, and very useful, to link data together using pointers (see Dynamic Data Structures for examples). With this organization, nullptr indicates the end of the link. Control statements use tests incorporating the null pointer to manipulate these linked structures. In this context, the phrase "null pointer" is used in the conceptual or natural language sense. In a C++ program, a "null pointer" can be implemented with any of the three names stated previously: nullptr (preferred), NULL, or 0.

Person*	p0 = nullptr;
Person*	p1 = NULL;
Person*	p2 = 0;
while (p0 != nullptr) ....
while (p1 != NULL) ....
while (p2 != 0) ....
if (p0 == nullptr) ....
if (p1 == NULL) ....
if (p2 == 0) ....
if (p0 != nullptr) ....
if (p1 != NULL) ....
if (p2 != 0) ....
Using the null pointer. The null pointer allows programmers to test the content of a pointer variable to see if it points to valid data.

NULL is a symbolic constant or macro implemented with a #define directive. Consequently, programs using it must #include an appropriate header file. Many of the common header files (e.g., <iostream>), and some less common ones, are "appropriate." Alternatively, nullptr is a language-defined keyword recognized by the compiler, is easier to use, and does not require a specific header file.

Secure Programming With Pointers

Programming errors, informally but universally called bugs, involving pointers are notoriously difficult to find. They often lay dormant in large, complex programs for a considerable amount of time - often while the program is in full service. Pointer bugs cause frustration and embarrassment when the program eventually fails and are also a potential attack vector while the program is in service. Programmers must consider security throughout program development.

  1. Programs do not automatically initialize most pointer variables. So, until it explicitly initializes them, they contain a random bit pattern. The best practice is to initialize a pointer variable at the time of its definition:
    Person*	p = nullptr;
  2. It's impossible to access data or call a function through a null pointer. For example, if p is initialized as in (1), then
    cout << p->name << endl;
    is an error. The best practice is to always test the pointer before using it and to take an appropriate action if it is null. For example:
    • if (p == nullptr)
      {
      	cerr << "variable p is null" << endl;
      	return;	// or exit
      }
    • if (p != nullptr)
      	cout << p->name << endl;
  3. The behavior of uninitialized pointers is unpredictable! The behavior depends on whatever random value is currently in the pointer, which can vary between computers and even between two executions on the same computer. This error is similar to (2) but is typically much more challenging to locate and is an insecure bug that often hides in released code. The best practice is always to verify a pointer's initialization before using it:
    if (p == nullptr) . . .
    if (p != nullptr) . . .
    For these tests to function properly, programmers must appropriately initialize the pointer as in (1).
Steps for writing secure pointer programs. When writing programs that use pointers, programmers must take three concrete steps to minimize pointer errors and enhance program security.

Address Arithmetic

Pointers support a limited but essential set of arithmetic operations:

The operations do not change the values stored in the pointers but create an expression that points to a new location in memory.

The behavior of these operations, some of which are not completely intuitive, are most easily explored in terms of an abstract representation of main memory. Furthermore, the operations are only valid within a contiguous memory block without any "empty" space. An array is the most straightforward way to organize data this way. Adding an integer to or subtracting an integer from a pointer creates a new address that points to a different data item (i.e., a different array element). Taking the difference between two pointers calculates the number of data elements between the pointers. The following examples illustrate these operations.

An array illustrated as a large rectangle with small rectangles inside. The small rectangles represent the array elements, which, in this example, contain the letters from A to H in sequential order. A pointer named p1 points to A and another pointer, p2, points to E.
Arithmetic with 1-byte data elements. The array is defined and initialized as:
char data[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'};
The addresses next to each data item are arbitrary but increase by 1 as it would for a single-byte data element.
  1. p1 + 1 points to B
  2. p2 + 1 points to F
  3. p2 - 1 points to D
  4. p1 - 1 points to ?? (points to memory outside of the array)
  5. p2 + 4 points to ?? (points to memory outside of the array)
  6. p2 - p1 = 0x00ffaa04 - 0x00ffaa00 = 4, which implies that there are 4 elements between the pointers (includes the element pointed to by p1 but excludes the element pointed to by p2)
An array illustrated as a large rectangle containing smaller rectangles representing data items stored in the array. The illustration further divides each data-item-rectangles into four small squares, each representing the individual bytes within each element, as would be the case for a 4-byte integer. The data-item rectangles contain the integer values from 1 to 8 in order. A pointer named p1 points to 1, and another, named p2, points to 5.
Arithmetic with multi-byte data elements. The array is defined and initialized as:
int data[] = { 1, 2, 3, 4, 5, 6, 7, 8 };
In this example, individual data elements consist of four bytes (e.g., 4-byte ints). While the addresses adjacent to each element are arbitrary, they increase by 4 - appropriate for 4-byte elements stored in byte-addressable memory. Pointing into an element's interior is generally meaningless and isn't portable between different computer architectures. So, when a program adds and subtracts pointers and integers, it automatically performs an unseen operation: it multiplies the integer by the size of each data element - 4 in this example. The automatic operation ensures that the calculated address always points to the beginning of a complete data element. Similarly, when the program takes the difference between two pointers, the compiler automatically divides the result by the size of each element, or 4 in this example.

The C++ compiler allows these simple arithmetic operations on pointers but not on references, marking the primary difference between them. Pointer arithmetic is very powerful and beneficial when used correctly and with discretion. However, it is also very error-prone, historically providing a gateway for many potent computer viruses. The potential harm of address arithmetic caused the Java language designers to abandon C++ and create a new language.