CS 70

What's the C++ Type for a Memory Address?

Let's look back at the code for normalizing the data values from Homework 2. In this version, we've used the more long-winded syntax for array access so that we can better see what's going on.

float biggest_val = *vals;
for (int i = 0; i < TEMPERATURE_SAMPLE_COUNT; ++i) {
    if (*(vals + i) > biggest_val) {
        biggest_val = *(vals + i);
    }
}

for (int i = 0; i < TEMPERATURE_SAMPLE_COUNT; ++i) {
    *(vals + i) = *(vals + i) / biggest_val * height;
}

We wanted to be able to write a helper function. Does this version of the code give us any ideas?

  • Hedgehog speaking

    Well, we used to write vals[i] and now we're saying *(vals + i), so vals doesn't look like an array any more. It looks like this sort-of ugly thing with a *.

  • LHS Cow speaking

    Okay, can we build on that?

  • Cat speaking

    We're using the memory address of the first element of the array.

  • LHS Cow speaking

    And that means…?

  • Duck speaking

    We don't need to pass whole arrays around, we could pass addresses! Only I don't know how.

Pointer Types

When we have the memory address of the first element of an array of floats, we call that a pointer to a float, and we write the type as

float* start = vals;

Notice the * in the type.

We read this type (from right to left!!) pronouncing * as “is a pointer to”, so in this case “start is a pointer to a float”. You can also think of it as saying “start is a memory address for a location in memory where we'll find a float”.

  • Horse speaking

    Hay! Hold your horses! I thought * meant something else, to read the value from that memory, now it means a type thing. How can it mean two things?

  • LHS Cow speaking

    Context matters. Where the * appears governs what it means.

  • Hedgehog speaking

    How will I ever keep what it means straight in my head?

  • LHS Cow speaking

    It's not as bad as the ambiguity in English! The rules for understanding which context we're in are fairly simple. Let's remind ourselves a bit about types and values.

Types and Values

Types and values are different things, and used in different places.

Types

For example, this declaration is a context where we've written a type three times, the type int.

int area(int width, int height);

Values

But the code below has an expression where we compute a value (and send it to the std::cout output stream).

std::cout << area(40,30);

We can actually say there are five values in this expression, 40, 30, the result of the function call area(40,30), the value of std::cout, and the result of the << operator (which is actually returned by << and then just thrown away unused). But we'll focus on the first three.

Types and Values Together

So, when reading code, we need to keep in mind whether we're looking at a type, or looking at a value. Check out this code:

int  triangle[3] = {3,4,5};
int* secondVertexPtr   = triangle+1;    // * is in a type
int  secondVertexValue = *(triangle+1); // * is in an expression

This code has two disinct types, int and int*. And several values, including 3, triangle+1 and *(triangle+1).

In summary,

In a Type Context In a Value Context
* means pointer to indirection operator
* written after the type in front of the address
  • Duck speaking

    Okay, I think I get it. So now I can write a function

    void normalize(float* firstValPtr);
    

    and then I can call normalize in each of my two functions?

  • LHS Cow speaking

    Let's think this one through…

If we had a normalize function as given above, could we pass in vals as an argument, and would the function have enough information to do its job?

A Usable Normalize Function

In the code below, we've created a normalize helper function that takes in all the information it needs to do its job.

void normalize(float newmax, float* firstValPtr, size_t valCount) {
    float maxval = *firstValPtr;
    for (int i = 0; i < valCount; ++i) {
        if (*(firstValPtr + i) > maxval) {
            maxval = *(firstValPtr + i);
        }
    }

    for (int i = 0; i < valCount; ++i) {
        *(firstValPtr + i) = *(firstValPtr + i) / maxval * newmax;
    }
}

So now, suppose we have an array of floats. We can run

float vals[TEMPERATURE_SAMPLE_COUNT];

// ... fill in vals with data ...

// Normalize vals to fit in the range 0 to graph height
normalize(height, vals, TEMPERATURE_SAMPLE_COUNT);

If you can, explain in your words why this approach will work to let us normalize any array of floats no matter how big it is. If you're a bit confused, we'll go over it after this question.

Overall, how do you feel about this code, does it make sense to you? Anything seem confusing?

Let's break down why this code works.

  • The first parameter, newmax, is the new maximum value we want to scale to.
  • The second parameter, float* firstValPtr, is a pointer to the first element of the array of floats we want to normalize.
  • The third parameter, size_t valCount, is the number of elements in the array (as we mention when we talked about numeric types, size_t is the type used for sizes of things in memory).

When we call normalize, we can pass in the array name for the second argument, and it will automatically decay to a pointer to the first element of the array. We also pass in the size of the array as the third argument.

Our loop uses the pointer arithmetic and indirection operator to access each element of the array, just like we did in the earlier version of the code.

  • Hedgehog speaking

    Can I write firstValPtr[i] instead? This syntax with the stars hurts my brain!

  • LHS Cow speaking

    You can, and it would work just the same. But we'll stick with the * version for now.

  • Dog speaking

    What about what happens when firstValPtr goes away? I remember that with references, when the reference variable goes away, nothing happens to the thing it referred to.

  • LHS Cow speaking

    It's actually pretty similar with pointers. Let's detour to look at that next and come back to the normalize function on the next lesson page.

When a Pointer Variable Goes Away

Pointers are a primitive type, like integers and floats, so it's going to behave similarly to those types in terms of what happens when the variable goes away. Let's remind ourselves what happens when a primitive type variable goes away…

Suppose inside a function I have this code:

int x = 29;
if (x > 0) {
    int y = x + 13;
    std::cout << "The meaning of life is " << y << "\n";>>
}

What happens when y goes away at the closing brace of the if statement?

Remember, as far as object lifetime goes, we have two stages, destruction and deallocation. When y goes away at the closing brace of the if statement, what happens for the destruction phase, and what happens for deallocation phase?

For destruction, what happens?

For deallocation, what happens?

  • Cat speaking

    It's “destroyed” but probably nothing actually happens.

  • LHS Cow speaking

    Exactly. (But, you can't be sure the old value will still be there—at least in theory, it could set all the bits to zero or something.)

  • Hedgehog speaking

    But the thing it pointed to will still be there, right?

  • LHS Cow speaking

    Yes. (Just like we saw with references.)

When a pointer variable is destroyed, nothing (observable) happens.

Default-Initialized Pointers

  • Horse speaking

    Hay, wait a minute! If a pointer is a primitive type, if we don't specify how to initialize it, will it have an indeterminate value?

  • LHS Cow speaking

    Yes.

  • Hedgehog speaking

    A pointer that could point anywhere? Sounds scary!

  • LHS Cow speaking

    Yes, in practice, yes. C++ says that default-initialized pointers are considered “invalid” and you aren't allowed to apply * to them. If you break this rule, it's undefined behavior.

  • Cat speaking

    So I should always remember to initialize pointer variables.

  • LHS Cow speaking

    That's a good rule of thumb!

If a pointer variable is default initialized, it will have an indeterminate value. C++ says that all indeterminate pointer values should be considered invalid, and it is “against the rules” to try to access the memory that an invalid pointer points to (via the * operator). As we'd expect for C++, if you write a buggy program that breaks this rule, the result is undefined behavior. (In practice, an invalid pointer often points to a memory area that doesn't even belong to our program, which causes the operating system to kill it with a “Segmentation fault”, or is improperly aligned for the kind of value we're reading and causes a “Bus error”.)

  • RHS Cow speaking

    Okay, detour over, back to the normalize function…

(When logged in, completion status appears here.)