CS 70

Past-the-End Pointers

There is an alternative to passing a pointer to the first element of an array and its size, we could instead pass a pointer to the first element of an array and a pointer to the “end” array.

If we have an array declared as:

float vals[TEMPERATURE_SAMPLE_COUNT];

// (and appropriately initialized by reading from a file)

Notice that

  • vals decays to a pointer to the first element of the array
  • vals + 0 also gives a pointer that points to the first element of the array
  • vals + TEMPERATURE_SAMPLE_COUNT - 1 gives a pointer that points to the last element of the array
  • vals + TEMPERATURE_SAMPLE_COUNT gives a pointer that points to just beyond the last element of the array
  • LHS Cow speaking

    It might seem counterintuitive, but it's almost always way more convenient to have “past-the-end” pointers than “last-element” pointers.

Let's look at a revised normalize function that uses this approach.

void normalize(float newmax, float* firstValPtr, float* pastEndPtr) {
    float maxval = *firstValPtr;
    for (float* p = firstValPtr; p < pastEndPtr; ++p) {
        if (*p > maxval) {
            maxval = *p;
        }
    }

    for (float* q = firstValPtr; q < pastEndPtr; ++q) {
        *q = *q / maxval * newmax;
    }
}

Notice that the code in the loop is simpler than before. Instead of saying *(firstValPtr+i) we're now just saying *p.

  • Duck speaking

    I think it's more efficient, too! We're avoiding two addition operations every time around the loop!

  • LHS Cow speaking

    Possibly. But compilers are good at optimizing code, so we can't really make judgements of that kind without experimental data to back up our claims.

  • Goat speaking

    Meh! It might not be more efficient for the generated code, but it's definitely more efficient for me, the programmer! It's less typing to say *p than *(firstValPtr+i) or even firstValPtr[i].

Calling Our Function

We'd call this function by writing

normalize(height, vals, vals+TEMPERATURE_SAMPLE_COUNT);
  • Bjarne speaking

    C++ also provides an alternative way of writing that code. We can write it as

    normalize(height, std::begin(vals), std::end(vals));
    
  • LHS Cow speaking

    Indeed. std::begin(vals) just returns vals (the address of the first element), and std::end(vals) returns the address that lies past-the-end of the array.

  • RHS Cow speaking

    Although how the compiler achieves that, for any array of any type, isn't something we'll delve into at this time.

Exercise

Trace through the execution of this code with a memory diagram, include the execution of the normalize function.

int main() {
    float stuff[4]{5.0f, 1.0f, 4.0f, 20.0f};
    float desired_max = 100.0f;

    normalize(desired_max, stuff, stuff+4);

    return 0;
}
  • Hedgehog speaking

    Before we do that, is

    float stuff[4]{5.0f, 1.0f, 4.0f, 20.0f};
    

    the same as

    float stuff[4] = {5.0f, 1.0f, 4.0f, 20.0f};
    

    or not? And also, what does that f mean at the end of the numbers?

  • LHS Cow speaking

    Yes, they're the same. The second version is the older C-style way of saying it. Both syntaxes are widely used in practice.

  • RHS Cow speaking

    Saying 5.0f means “this is a float literal for the number 5—if we'd just said 5.0 it would be a double literal, and it would have to be converted to a float to put it in the array.

What are all the values that p held, including its final value that triggered the loop to exit?

This video traces through the execution of the code and shows the memory diagram at each step.

Here's the final memory diagram:

memory diagram

(When logged in, completion status appears here.)