Quick review of the first section of Pointer notes:
When we assign a data type to a variable, that tells us:
The amount of memory we’re looking at (size in bytes)
How to interpret the memory at that location
Recall that the following binary can be interpreted as 3:
1st byte
2nd byte
3rd byte
4th byte
00000000
00000000
00000000
00000011
And the following binary can be interpreted as 3.0f:
Sign (1 bit)
Exponent (8 bits)
Mantissa/Significand (23 bits)
0
10000000
10000000000000000000000
Also recall: integral values in C are:
char, short, int, long long
And that we can have signed and unsigned integral values.
True or False
An unsigned integer and a signed integer can represent the same number of values
An unsigned int and signed int are the same size in memory
An unsigned int and signed int can represent the same range of values (meaning the VALUES they represent are all THE SAME)
The conversion from binary to unsigned int is the same conversion used for binary to signed int
An unsigned int is intended to only represent positive values
The previous true/false answers are the same for chars, shorts, and long longs.
Answers
1.True (they’re the same size)
2. True (they’re the same size)
3. False (unsigned range is 0 on up, signed range includes negatives)
4. False (the range of values is different, so conversion is different)
5. True
6. True (same for the other 3 integral types)
Knowing data types, their sizes, and interpretations is important when trying to understand pointers. Integers are represented by 32 bits of binary, and we can convert those 32 bits into a whole-number integer value. Floats are represented by 32 bits of binary, and we can convert those 32 bits into a floating-point number (IEEE 754, a standard for floating point arithmetic, illuminates how to do this).
Similarly, a pointer is a variable whose value is a memory address (a number). We can convert the bits of the value stored in a pointer variable into a memory address.
We already know how to get a memory address value: we use the address operator on a variable. We can store that value in a pointer. The pointer’s type must match the type we used the address operator on (said another way: when we use the address operator on an int variable, we store the memory address in an int pointer):
int x = 825;int* pX = &x;
When the value of a pointer is a memory address of a variable, we say the pointer “points” to that variable. We can have more than one pointer “pointing” at a memory location:
int x = 825;//All of these point to the same place, //meaning the value stored in each of //these pointer variables is the sameint* pX = &x;int* p1 = &x;int* p2 = p1;int* p3 = p2;
We could even make an array full of pointers, all pointing to the same location in memory:
int myFace = 82;int* fingers[100];for(int i = 0; i < 100; ++i){ fingers[i] = &myFace;}
We also learned of two very powerful things pointers allow us to do: the indirection operator, and getting around C’s convention of “pass by value”.
The indirection operator allows us to treat a pointer as the thing it points to.
Question
What is the value of x after the code below runs?
int x = 10;int * pX = &x;(*pX) *= 10;
Answer
100
The value of x is 10. We then set a pointer, pX, equal to the address of x. Finally, we use the indirection operator to treat pX as if it were x, and then multiply it’s value by 10, storing that value back in x.
Remember not to be confused by the 3 different meanings behind the asterisk (*) in C!
Multiplication
Declaring a pointer variable
Dereferencing a pointer (indirection operator)
Question
What does it mean that C functions can only pass by value?
Answer
When parameters are passed to a function, the values are copied; we cannot “pass” a variable directly to a function
We get around this by passing the location of variables instead, and then altering the value at that location in memory via the dreference operator. You can see a non-working example below in BadSwap, and a working example in GoodSwap.
//Remember, this doesn't accomplish anything, and is total garbagevoid BadSwap(int a, int b);{ int swap = a; a = b; b = swap;}//Swaps values of whatever integers a and b point at, for realvoid GoodSwap(int* a, int* b);{ int swap = *a; *a = *b; *b = swap;}
Arrays and Pointers: Function Calls
There’s some interesting stuff we didn’t dive into when we originally talked about arrays. Remember, arrays are just lists of stuff:
//Create an arrayint myIntegers[10] = {0};//Use a loop to access the elementsfor(int i = 0; i < 10; ++i){ myIntegers[i] = i;}//access an individual elementmyIntegers[7] = 77777;
We didn’t talk about passing arrays to functions, but you can totally do that. Let’s complete the following function, for fun!
int SumArray(int myArray[], int size){ //What goes here?}
So, we have a function, we pass an array to it, and we sum the values and return that sum. Great! What if we did something a bit different? (a bit contrived, but roll with it)
void FillWithZeros(int myArray[], int size){ for(int i = 0; i < size; ++i) { myArray[i] = 0; }}int main(void){ int originalArray[5] = {1, 2, 3, 4, 5}; for(int i = 0; i < 5; ++i) { printf("%d ", originalArray[i]); } printf("\n"); FillWithZeros() for(int i = 0; i < 5; ++i) { printf("%d ", originalArray[i]); }}
Now, there shouldn’t be any real reason to use FillWithZeros. Remember, we’re passing by value! That means we theoretically would have copied our array into this function, set all the elements in the copy to zero, then just discarded that whole thing when we returned from the function (remember, the return type of FillWithZeros is void). If that’s true, then the two print loops in our main function will print the same thing …except they don’t?
1 2 3 4 50 0 0 0 0
What the heck is going on here? Why does it look like the original array is being altered? That shouldn’t be possible! Well, let’s think about this a bit.
We know that in C, we only pass arguments/parameters by value. What does this actually mean? Basically, we copy the arguments to some other place in memory, then run the instructions for the function we just called.
Say our function takes a float, and an int as arguments. How much memory do we need to copy?
float (4 bytes) + int (4 bytes) => 8 bytes
OK, that doesn’t seem too bad! So, what would happen if your function had to copy an array? Say it only has 5 integers:
int(4 bytes) * 5 => 20 bytes
Still not terrible! But what if you had an array of 100000 elements?
int(4 bytes) * 100000 => 400000 bytes, 0.4MB
Holy mega-bytes, Batman! It wouldn’t make much sense to copy this much memory around just for a function call. But then, how would we ever use arrays in functions at all?
Well we just confirmed via our code and printed output that we’re NOT copying arrays around when passing them into functions. So what’s actually happening? Turns out, when you pass an array to a function, you’re really just passing a pointer to the start of the array! Why copy 100000 elements-worth of data when you can just pass the address in memory where that data starts, and just access it directly?
So turns out, these are the exact same thing:
//Passing in an int pointerint SomeFunction( int* someAddress, int size);//Passing in an int arrayint SomeFunction( int someAddress[], int size);
But that raises some more questions… if the two function declarations above are essentially the same thing… can we treat pointers and arrays like they’re the same?
Array Subscript Notation
Recall, with arrays, that we use the square brackets (called subscript notation) to access individual elements:
int myIntegers[10] = {0};myIntegers[0] = 2749;int whatever = myIntegers[3];
Let’s think about what the subscript notation (square brackets) is actually DOING when we access array elements. With that last line of code above, we are looking at the 4th space in a chunk of memory that contains 10 integers. All of the elements in the array are right next to each other in memory.
Well, think about what we’d get if we did the following:
int myIntegers[10];int* firstElement = &(myIntegers[0]);
Don’t panic! This looks complicated and weird, but it isn’t so bad. We’ve seen all this stuff before. We’re just using a lot of operators at once. Let’s break it down below:
//Splitting up the stuff above//Create an array of 10 integers, all next to each other in memoryint myIntegers[10];//Create an integer pointer, whose value will be a memory addressint* firstElement;//Access the first element of the array. This represents an actual //location in memory, where we can store integer values.//For all intents and purposes, this is an integer variable.myIntegers[0];//The address operator gives us the address of this integer. //Namely, it's the address of the first element in our array.&(myIntegers[0]);//We're setting the value of our pointer to be the memory address of//the first element in our integer arrayfirstElement = &(myIntegers[0]);
So, putting it back together:
//Create an integer arrayint myIntegers[10];//Create a pointer, and set its value to be the address //of the first element in our integer arayint* firstElement = &(myIntegers[0]);
Sweet! Now we know how to describe what we’re doing above, and it makes sense.
OK, so… what if we did this?
//Create an integer arrayint myIntegers[10];//Create a pointer, and set its value to be the address //of the first element in our integer arayint* firstElement = &(myIntegers[0]);int* secondElement = &(myIntegers[1]);int* thirdElement = &(myIntegers[2]);
Let’s assume/contrive that the memory value of firstElement, after we set it above, is 200 (our int starts at byte 200). What would be the value of secondElement? Of thirdElement?
(Hint: remember that ints are 4 bytes in size, and we measure memory addresses in bytes).
You could think of myIntegers[1] as going to the memory address where the array starts, then moving forward 4 bytes to look at the next int in memory. Similarly, you could think of myIntegers[2] as going to the address where the array starts, then moving forward 8 bytes to look at that int in memory:
int myIntegers[10];myIntegers[0]; //Start of arraymyIntegers[1]; //Start of array + 4 bytesmyIntegers[2]; //Start of array + 8 bytesmyIntegers[3]; //Start of array + 12 bytes
We could also do the same thing for a char array:
char myChars[10];myChars[0]; //Start of arraymyChars[1]; //Start of array + 1 bytesmyChars[2]; //Start of array + 2 bytesmyChars[3]; //Start of array + 3 bytes
So, you could think of the subscript operator as something like:
myIntegers[i] ---------> Go to address of myIntegers + (i * size of data type)
Cool! Neat! What does this have to do with pointers?
Pointer Arithmetic
Well, it turns out we can do pointer addition, like so:
int x = 76;int* pX = &x;++pX;
WHAAAAAAAT? WHAT IS GOING ON? We’re using the ++ operator on a pointer? What the heck?
So, to break down what’s happening above: we’re creating an integer, x. Then, we create an int pointer pX and store the address of x in it. Cool, we’ve soon that before. Finally, we… increment the value of pX? What does this mean, and why would we even do it?
Well, in this context this behavior is actually worse than useless, it’s sorta harmful. We’ve changed the value of pX, and it is no longer pointing to something that makes any sense to point at. But what if there were a context in which this behavior were valid, and useful?
int myIntegers[10];//Create a pointer, and set its value to be the address //of the first element in our integer arayint* pElement = &(myIntegers[0]);++pElement;
So, we have an array, we set our pointer to point at the address of the first element. Then, we increment the pointer, which… we still haven’t explained what that’s actually doing. It changes the value of the pointer, meaning it is no longer pointing at the first element in the array. So… where is it pointing then? What does adding 1 to a pointer actually do?
Answer: it depends on the TYPE of the pointer!
Consider the following:
int myIntegers[10];char myChars[10];int* pInt = &(myIntegers[0]);++pInt;char* pChar = &(myChars[0]);++pChar;
Let’s assume/contrive that the memory value of pInt is 200, before we increment it.
What would be the value after we increment it?
Let’s further assume/contrive that the memory value of pChar is 500, before we increment it.
What would be the value after we increment it?
It turns out, incrementing a pointer will shift the memory address forward X bytes, where X is the size of the data type it points to.
For our integer example above, the value of pInt after incrementing would be 204.
For our char example above, the value of pChar after incrementing would be 501.
Hmm… where have we seen something like this before?
int myIntegers[10];myIntegers[0]; // Assume at address 200myIntegers[1]; // Address 204int* pInt = &(myIntegers[0]); //Address 200++pInt; //Address 204
So that means… incrementing a pointer is the same as using the square brackets? OK? Well it ALSO turns out, that we can do the following:
int myIntegers[10];myIntegers;
Wait, so myIntegers can just… hang out? As an expression? What’s the value of this expression?
Well earlier we mentioned that when we’re passing an array into a function, we’re actually passing the address of where the array starts (the first element). Which means myIntegers is just the address of the first element of the array! So the two lines below are the same:
int myIntegers[10];//THE TWO LINES BELOW ARE THE SAME!myIntegers;&(myIntegers[0]);
Which FURTHER means:
int myIntegers[10];myIntegers[1] = 365;*(myIntegers+1) = 100;
The value of the second element of the myIntegers array after this code runs will be… 100!
There will be situations where it is more convenient to use pointer arithmetic, and other situations where array subscript notation will be easier. You’ll run into these situations over time, but a good rule of thumb would be: if you already have what you KNOW to be an array, stick to subscript notation. If you have a chunk of memory that represents something that is NOT an array, but you need to navigate within those addresses, use pointer arithmetic. If you aren’t sure what the sentences above are trying to say, don’t worry too much; we’ll be encountering these situations soon!