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?
Well, we used to write
vals[i]
and now we're saying*(vals + i)
, sovals
doesn't look like an array any more. It looks like this sort-of ugly thing with a*
.Okay, can we build on that?
We're using the memory address of the first element of the array.
And that means…?
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 float
s, 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
”.
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?Context matters. Where the
*
appears governs what it means.How will I ever keep what it means straight in my head?
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 |
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?Let's think this one through…
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 float
s. 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);
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.
Can I write
firstValPtr[i]
instead? This syntax with the stars hurts my brain!You can, and it would work just the same. But we'll stick with the
*
version for now.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.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…
It's “destroyed” but probably nothing actually happens.
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.)
But the thing it pointed to will still be there, right?
Yes. (Just like we saw with references.)
When a pointer variable is destroyed, nothing (observable) happens.
Default-Initialized Pointers
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?
Yes.
A pointer that could point anywhere? Sounds scary!
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.So I should always remember to initialize pointer variables.
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”.)
Okay, detour over, back to the
normalize
function…
(When logged in, completion status appears here.)