Working with pointers is a huge part of working with C++. While the specifics can get complicated, the key idea is simple:
So far, we've mainly been concerned with the values of variables, but every variable must occupy a specific place in memory. It is often useful to refer to the location where data is stored, rather than the data itself. Some of the reasons you might want to do that include:
So, given that it can be useful to refer to the address of a variable, how do we do it? For that, you'll want to use the address-of operator, or ampersand (&). This is tricky, since it's the same symbol that we use for both logical and bitwise AND. However, when placed directly in front of a variable, it can be used to get the address of that variable.
int MyNum = 23;
// outputs “23”
cout << MyNum << “\n”;
// outputs the address of MyNum, something like 0x7fff5092e73c
cout << &MyNum << “\n”;
Now that we've seen how to grab the address of a variable, now let's look at how to store it in a proper pointer. Creating a pointer variable is very similar to creating a normal variable, with the addition of an asterix. For example:
// create a normal integer variable
int MyNum = 23;
// create a pointer to the integer variable and set its value to the address of MyNum
int *MyNum_ptr = &MyNum;
// outputs “23”
cout << MyNum << “\n”;
// outputs something like 0x7fff5092e73c
cout << MyNum_ptr << “\n”;
It's important to note that pointers also have a type, and that an integer pointer is not the same as a float pointer. More on that in a bit, when we talk about pointer math.
So let's say you have a pointer, but you want to do something with the actual value of the variable it represents, rather than the memory address. For that, you'll need to dereference the pointer, which is done with the asterix.
// create an integer
int MyNum = 23;
// create a integer pointer
int *MyNum_ptr = &MyNum;
// output the address of MyNum, something like 0x7fff5092e73c
cout << MyNum_ptr << “\n”;
// output the value stored at the address in MyNum_ptr (“23”, in this case)
cout << *MyNum_ptr << “\n”;
Once you've dereferenced a pointer, you are essentially working with the variable directly, and you can do anything you would normally do with it. For example, if we wanted to change the value stored in MyNum via the pointer, we could do the following:
// dereference the pointer to get the variable it represents, and set it to 5
*MyNum_ptr = 5;
Now that we've seen how pointers work, we can dive a little deeper into how arrays function. All arrays are are pointers to the first element. Let's say we make an integer array:
int MyArray[] = {1,2,3,4,5};
If we look at the value of MyArray without adding an index, we'll get a memory address. If we examine the address of the first element in the array, we'll get exactly the same location.
// both lines would output something like 0x7fff524ad730
// and both would be the same address
cout << MyArray << “\n”; // note the lack of square brackets
// address of the first element is the same as the array itself
cout << &MyArray[0] << “\n”;
Where it gets interesting is that you can increment and decrement pointer values to step through arrays. So, if we add one to MyArray and dereference the result, we'll get the second element in the array.
// dereference the address of the first element- prints “1”
cout << *MyArray << “\n”;
// increment the pointer and dereference the new address- prints “2”
cout << *(MyArray + 1) << “\n”;
One important thing to note about pointer math is that it only works because pointers are aware of their type. When we add one to an integer pointer, the actual address is increased not by one, but by the size of an integer (usually 4 bytes). For example:
cout << MyArray << “\n”; // outputs something like 0x7fff55be5730
cout << MyArray + 1 << “\n”; // outputs the above value plus 4 (0x7fff55be5734)
Be careful with pointer math, since there is absolutely nothing to stop you from reading from (or worse yet, writing to) a bad location in memory.
One of the main reasons to use pointers is to be able to pass data around by reference rather than by value. Passing by value is what we've seen so far, and is fine for simple data types like integers. What happens when you pass by value is that a copy of the data is created. That can be what you want, but usually isn't for complex data types. Instead, you'll generally want to have a function receive a reference to the data in question.
To do that, you can pass in a pointer to the variable, rather than the variable itself. This can also allow the function to modify variables directly.
// a function that accepts two integers, and a pointer to a third
void addTwoAndStoreInPointer(int a, int b, int * result)
{
// dereference the result address, and set the value of the
// variable it represents to a + b
*result = a + b;
}
int main()
{
int MyNum; // make an integer variable
// call the function with two ints and the
// address of the place to store the result
addTwoAndStoreInPointer(2, 3, &MyNum);
cout << MyNum; // would output “5”
}
There's yet another way to use the & symbol. If it appears in an expression, it means “get the address of”. However, if it appears in a declaration of a new variable, it can be used to create an reference. A reference is another name for a variable, and is very similar to a pointer to that variable, except that it doesn't require the use of the dereference operator.
int MyNum = 23;
int &MyReference = MyNum;
MyNum = 5;
cout << MyReference << “\n”; // would output “5”;
There is very little reason to use references like the above, but they can be really helpful to create pass-by-reference functions without constantly having to use the dereference operator. Let's revisit the function we created above, but this time with reference arguments.
Void addTwoStoreByReference(int a, int b, int & result)
{
result = a + b;
}
int main()
{
int MyNum;
addTwoStoreByReference(2, 3, MyNum);
cout << MyNum; // outputs “5”
}
In the above, we get the same behavior as we saw with passing pointers, but the syntax looks a lot cleaner. What's really happening is that instead of copying the “MyNum” variable when it is passed to the function, a reference is created. That means that anything that is done to “result” will happen to “MyNum”, since they're actually the same variable (or rather, represent the same location in memory).
Passing data around by references is great, in that it allows you to avoid copying data all the time. However, it can also create situations where you accidentally change values that you didn't mean to. For example:
void someFunction(int & a)
{
a = 3;
}
int main()
{
int num = 5;
someFunction(num);
cout << num; // outputs “3”
}
If you're passing in a reference to something that shouldn't change within the function, it is good practice to declare the reference as const in the function definition, which looks like this:
void someFunction(const int & a)
{
a = 3;
}
Doing that will cause the compiler to throw an error, which is actually a good thing, since it will bring your attention to the fact that you're setting the value of something that shouldn't be changing.
For this week's assignment, take the class example as a starting point and create your own mini text adventure.
Your program should include multiple locations and the ability to pick up and drop objects.
The player should also be able to take at least two actions, such as looking around or using an object