An all-in-one overview of what pointers are and how they operate.
Recently I’ve been introduced to pointers in C++. It took me a little while to gain an understanding of exactly what was going on. This is the first language where I’ve had any experience using pointers. Many of my peers still have questions regarding pointers and I’d like to use this as a concise resource of that information. I’m hoping this will not only help them but others who might have some questions as well.
Variables, Memory, and Addresses
First, to understand what is a pointer we must understand a little about variables and memory. When a variable is created such as char, short, and float that variable is allocated enough space to hold the value of that variables data type. For instance, one byte for chars, two bytes for shorts, and 4 bytes for int are common. Each byte of memory has a uniques address and a variable’s designated address is where the first byte of information is stored.
To get the address of a variable in C++, we use the &(ampersand) operator. Let’s say we have, int myInt = 4. To gain access to the address of myInt we would place the & operator before the variable name.. &myInt. Instead of giving you 4 which is what is found in memory, it will give you the location of where that variable is stored. You will be given an address such as 0x8f05.
Pass by Value and Pass by Reference
I believe it is very important to understand exactly what is happening when you are passing around variable or objects in the specific language you are working with. They are different depending on the language. In C++ variables and objects are passed by value. An easy way to think of this is when you pass a variable to a function you are actually passing a copy of that variable. That copy of the variable has no effect on the argument you passed into the function. They are now two completely different variables. The copy is only local to the function. That copy has an associated storage duration and will be deallocated after the function has run it’s course.
When something is passed by reference it is not copied but you are actually passing in a reference to the original data. So that data basically has two names but both “point” to the same piece of data. In the illustration below, myInt was the original variable that holds the int 4. When you pass myInt into a function you will then refer to that data by the parameter name. In this scenario, we use myReference. When you use myReference it will directly affect the data in myInt and they now both have access to that data. You will only refer to that data as myReference locally inside the function.
So if variables are passed by value why explain pass by reference? Well, for one it’s good to understand exactly how both of these work. The faster you can get a clear understanding of these concepts the easier your life will be. Secondly, although variables are passed by value in C++ there are ways you can explicitly pass variables by reference. Remember earlier I explained using the & operator before a variable names return the address? You can use that same operator in your parameters of a function to accept the address of an argument. Doing so will pass that value by reference and anything you do to that data will affect the data in the original one as well since they are both “pointing” to the same data. Although, pointers and reference variables seem similar there are some differences such as pointer arithmetic.
What Exactly is a Pointer?
Well, a pointer is actually a pointer variable. If you remember that it’s a variable it might make things a little easier to understand. A pointer is designed to hold memory addresses. The name pointer is fairly clear. It “points” to data that is stored in the computer's memory. This allows you to indirectly work with the data they point to.
The definition of a pointer is very similar to any other definition: int *ptr. The *(asterisk) in front of ptr indicates that ptr is a pointer variable and int indicates that ptr will be pointed towards an integer. It’s never a good idea to define a pointer variable without initializing it. If you have to you can assign it nullptr like int *ptr = nullptr. This will assign the pointer to the address 0. Now, referring back using the & operator to get an address, to assign a pointer to a variable you would use int *ptr = &myInt. This assigns the address of myInt to the ptr pointer variable. Both myInt and ptr both point to the same address that holds the data in myInt. So after you assign ptr to myInt if you were to use ptr = 5 you will be assigning 5 to myInt. To access the data a pointer is referencing we place an * before the pointer. This is known as an indirection operator. By using *ptr, it will dereference the pointer and return 5 instead of the address.
Consider an array. When an array is passed into a function you are not actually passing in the array but the array’s beginning address. An array variable named myArr passed into a function with a parameter name myReferemce will point to myArr array. In fact, you can not pass an array by value in C++ due to a C restriction(C++ is derived from C). Look at the example below, when you create a function with a formal parameter for an array it accepts the starting address of the address passed into it.
An array name without brackets or a subscript represents the starting address of the array. When you dereference myArray it will return what’s stored at the starting address of that array, *myArr == myArr. An array is stored together in consecutive order in memory. To access the contents of an array using the indirection operator we need to add or subtract to get the addresses of the other elements. For example, *(myArr + 1) would get you the data stored in myArr. What’s actually happening behinds the scenes is myArr + 1 * 4. When you add or subtract a number from a dereferenced array it will increment or decrement through that array by the size of what’s stored in there. So since our array is an int we can assume that the size allocated for each element will be 4. Each increment will move 4 bytes from the starting point to the next address where that int is stored. Few things to note:
- myArr[index] == *(myArr + index)
- *(myArr + 1) != *myArr + 1… Without the parenthesis, myArr will be dereferenced and 1 will be added to the value stored.
- Remember in C++ arrays doesn’t specifically deal with accessing invalid index. This is categorized as undefined behavior and anything can happen really so make sure to keep track of what you are doing.
Pointers as Function Parameters
When you create a function that uses a pointer parameter, you must write it similar to above. You must specify that this function’s parameter is a pointer to an int. You then can pass the address of number using the & operator. Inside the function, value is dereferenced and multiplied by 2. This is indirectly changing the data in number as well since they both point to the same data in memory.
This can seem confusing but I’ll break it down as simply as I can. There are 3 relationships:
- Pointers to Constants
In the function, const int defines what value points to. Const applies to the thing that value points to, not value itself(notice the definition is before the asterisk). Also, the asterisk indicates that value is a pointer. Like all constants, the compiler will not allow us to write code that changes the thing values point to. Although, the data the pointer points to can’t change the pointer itself can change. Also, a constants address can only be passed to a pointer to constant, a pointer to constant can receive the address of a nonconstant item.
- Constant Pointers
While previously we discussed a pointer that points to a constant, a constant pointer describe the pointer itself. When a constant pointer is initialized with an address it can not point to anything else. Notice the definition of const comes after the asterisk this time. This will help you know that the word const is defining the pointer not what the pointer is pointing to. While a constant pointer cannot be changed, the data it points to can be changed because number is not a constant.
- Constants Pointers to Constants
If you understand the previous relationships then this should be familiar. This is just a combination of the two. Const int(before the asterisk) describes what ptr points to. Const ptr(after the asterisk) describes the pointer itself. Remember that pointers to constants can accept the address of nonconstants. Even though number is not a constant, we’ve declared that this points to a constant so this pointer does not have permission to change the value it points to. Also, we’ve declared this a constant pointer so we can not change the address this pointer points to.
Dynamic Memory Allocation
So when writing your code you are familiar with creating variables you need during execution. Suppose you are now writing code for times where you don’t know how many variables you would need. What do you do? With arrays as an example, normally, you would have to provide a constant for the size of an array but with dynamic memory allocation, you allow the program to create its own variables. This is only possible with the use of pointers.
In simple terms, while the program is running it will ask the computer to allocate a specifically sized chunk of unused memory large enough to hold that data. The computer will seek for this unused space and return the starting address of that spot. The only way to access that allocated memory is through the address. That’s where the pointer is required.
You are already familiar with how to create a pointer: int *ptr = nullptr(remember to always initialize a pointer). To allocate space for a variable you will use the “new” keyword followed by the data type. For example, int *ptr = new int or if the pointer is already created you could just use, ptr = new int. That newly allocated address is now saved in the pointer and can be accessed easily by dereferencing the pointer with *.
Below is an example of getting a user’s input for the number of ID numbers to be entered into an array, Then, a pointer and a new array is created with the user’s input variable to specify the exact size of the array.
Without the pointer and dynamic memory allocation, this would not be possible. Typically an array created under normal circumstances must use a constant to declare the array size. Here, we were able to do it on the fly, A few things to note. If there isn’t enough free memory to accommodate the request, C++ will throw an exception and terminate the program.
It is important to delete or deallocate memory after use. This will help stop memory leaks from occurring. C++ does not have garbage collection. Garbage collection is a mechanism that will reclaim data on the heap which are no longer used. Since C++ does not offer this utility it is up to you. Using delete ptr(single variable) or delete ptr(array) will deallocate that memory and free up that space. C++ does also have smart pointers. They are objects that work similar to pointers but can automatically delete allocated memory when it’s no longer in use.
I hope this was able to provide some useful information and clear up any confusion. Pointers are an important part of C++ programming and having a firm grasp on this will be beneficial. I am no expert on the subject but this is just my functional understanding of the topic. Putting this information into my own words and thoughts allows me to reflect on what I actually understand. This is as much a learning tool for me as anyone else who needs it. If you have any questions or perhaps I need to make some corrections feel free to leave me a comment or send me a message.