Memory and Addresses
All working data is stored in your computer’s memory. Hence, you must have some method of storing and retrieving this data. Memory is accessed through addresses, which are essentially numbers (up to 2^64 – 1 for 64-bit machines) that sequentially correspond to locations in memory. Your program does not directly interface with the RAM in your computer—on a hardware level, this is managed by the operating system. This can be rather complicated; Windows uses entirely separate memory spaces for system and "user" processes (see types of kernels). However, managing memory within your own program is very straightforward.
The basic building block of memory management is the pointer. A pointer is simply a variable in your program, just like any other, except that instead of a value, it holds a memory address. This means that it can “point” to another variable in your program. Pointers are used extensively in all languages without automated memory management, so as with all these basic topics, they will be very important.
When using pointers, if you don’t quite understand the relationships between pointers, variables, and values, try to draw a diagram— they can be very helpful.
A very basic example:
This corresponds to this (simplified) memory layout:
Address | Type | Identifier | Value |
0 | - | - | - |
1 | Character | data | 'x' |
2 | - | - | - |
3 | - | - | - |
4 | Pointer to Character | dataPtr | 1 |
5 | - | - | - |
Note that "dataPtr" holds the address "1," which corresponds, or points, to the variable "data."
Using Pointers
When declaring a pointer, you must always (well, almost always) declare what data type it will point to. This is because when you use your pointer, your computer must know what kind of data it's receiving. To declare a pointer, first specify what type the pointer will point to, then add an asterisk (‘*’). This makes your new type a pointer to the type before the asterisk. Finally, add your pointer variable's identifier and you’ve declared a pointer.
char* characterPtr; double* dblPtr;
When assigning an address to a pointer, you can’t just use a literal value. Your program's memory space, or the range of available addresses will change with every execution. In fact, most compilers will error if you attempt this, except in the case of setting a pointer to 0 (or NULL). Hence, to set your pointer to the address of a certain other value in your program, you use the “address of” operator, a single ampersand.
char value; char* valuePtr = &value;
'valuePtr' now points to 'value.'
So far, you've learned how to assign addresses to pointers. That's all well and good, but if you try to use these pointers, you'll notice that they simply contain an address. If you were to output one to the console you'll simply get something like "0x237AF3." Pointers aren't very useful without a way to access the data they point to. Enter the dereference operator. The operator is an asterisk, and is followed by the pointer you want to dereference. Dereferencing a pointer tells your program to go to the address pointed to and retrieve that data. (This is why you need to know the "pointed to" type.) A deference statement will consequently act like a value of the data type your pointer points to.
Know that the data you get from dereferencing a pointer is the actual data at the pointed to address—not a copy. This means that if you change that data, you are actually changing it elsewhere in memory. For example, if you have a pointer to an integer, and you assign a value to the dereferenced pointer, you have changed the "pointed to" value elsewhere in memory.
char character = ‘x’; char* valuePtr = &character; *valuePtr = ‘y’;
This will change the value in “character.” It will hold ‘y’ after the last line.
cout << valuePtr << endl;
This will output the address of 'character.'
cout << *valuePtr << endl;
This will output the actual data of 'character.'
Finally, pointers are never necessarily pointing to a valid piece of data. Because of this, if you try to dereference an invalid pointer, your program will encounter a segmentation fault. This is why "NULL" pointers are so important. A NULL pointer is simply one that points to address zero, which is always invalid. If you know a pointer does not point to a valid address, always set it to NULL. This way, because all invalid pointers should be NULL, you can always test if a pointer is valid. Remember that pointers, like everything else, are initialized with garbage values. If you try to dereference a pointer you’ve just created without assignment, the program will most likely crash. It is good practice to initialize pointers to NULL.
char* valuePtr; cout << *valuePtr << endl;
This will probably crash. Don't do this.
char* valuePtr = NULL; if(valuePtr) cout << *valuePtr << endl;
This will not crash because the program makes sure that valuePtr is valid. This can only happen because the pointer is correctly set to NULL on initialization. Do this.
Pointers and Arrays
You’ve actually been using pointers ever since we learned about arrays—arrays are technically just pointers to data that cannot themselves be changed. The actual identifier of your array (say “arr”) holds the address of the first element in the array. To get a value from the array, you put the offset from that pointer in brackets. This is why arrays are 0-based: to get to the first element, you move 0 pieces of data away from the first address. Note that offset notation does NOT move the "head" pointer, it simply take a value relative to it.
Address | Type | Identifier | Value |
0 | Pointer to Character | array | 2 |
1 | - | - | - |
2 | Character | - | 's' |
3 | Character | - | 't' |
4 | Character | - | 'r' |
Here, “array” is the identifier of the array, holding the address of the first value. array[0] would go 0 places from the start of the array (memory address 2, the first element), then dereference that address to get the value (‘s’). Likewise, array[2] would go to address 4 and return ‘r.’
Because arrays are technically just pointers, you can assign pointers to point to arrays. You can use them exactly as you would use the original array identifier. You can use brackets on pointers&dmash;this is called offset notation. Remember that offset notation will go however many places after the first element in the array, and then dereference that address.
char charArray[10]; char* arrayPtr = charArray; for(int i = 0; i < 10; i++) { arrayPtr[i] = ‘a’; } cout << charArray[1] << endl;
This will output the letter ‘a,’ because when every value in “arrayPtr” was set to ‘a,’ it really set every value in “charArray” to ‘a.’
Pointers can be moved, but array identifiers cannot. This means that you can use a single pointer to switch between any number of arrays, but an array identifier will always point to one array.
Moving Pointers
One of the advantages to using pointers is that you can move them around. If you have a pointer that points to the first value in an array, if you increment that pointer, it will simply move up a place in memory (the size of one element), and is now be pointing to the second value. It’s that simple! However, you must be careful about doing pointer math, as it is easy to mistakenly send pointers into invalid memory. When you try to access them, you will get a segmentation fault.
For example, if you have a c-style string (which you know will be null terminated), you can loop through it by incrementing a pointer—without knowing its length.
char cstring[LENGTH]; cin >> cstring; char* strPtr = cstring; while(*strPtr) { cout << *strPtr << endl; strPtr++; }
This will output the string with each character on a new line—regardless of the length. The pointer will iterate through each value in the array until it comes to a value that is 0—the null terminator.
Pointer Parameters
As with all other variables, you can pass pointers to functions. They work just as you’d expect: the parameter will hold the address as passed in; you can access that value within your function. However, like I mentioned earlier, a pointer never creates a copy of the data it points to—if you modify data at an address within a function, it will be changed globally.
Because arrays can be used interchangeably with pointers, you can actually pass an array as a pointer parameter. If you do this, you’re of course going to want to design your function to use an array.
Remember leaning about how to pass normal parameters by reference? You can do the same with pointers. However, this means that the function can not only globally change the data pointed to by the pointer, but it can actually change where the pointer points to in the calling function. This concept is pretty useless right now, but it will come into play next lesson when we go over dynamic memory.
You can also pass pointers as constant. This can either prohibit your function from locally moving the pointer (you can’t set it to somewhere else, increment it, etc.), or it can prevent your function from editing the pointed-to data, or both. Also remember that passing a constant parameter does not change the constant status of the variable in the calling function. For more information on constants, see lesson 14.
void func1(char*& value);
This is totally unrestricted: the function can edit the pointed-to data, as well as reassign 'value,' which changes where the passed-in pointer is pointing to at func1's call site. (see lesson 07).
void func2(char* value);
Here, we can still edit the pointed-to data and reassign 'value,' but this cannot effect the caller.
void func3(const char* value);
Here, we can reassign 'value' within func3, but we cannot edit the pointed-to data. Note that this allows us to safely pass in literal strings, which are constant by default (their data is always read-only).
void func4(char* const value);
Here, we can edit the pointed-to data, but we cannot reassign 'value' within func4.
void func1(const char* const value);
Finally, we can neither reassign 'value' nor edit its data.
Pointer Pointers
The astute might at this point be wondering if you can make pointers that point to other pointers. Well, you can. It works largely how you'd expect. A pointer to a pointer is called a "double pointer." A pointer to a double pointer is called a "triple pointer," and so on.
The syntax is very logical. Consider what type you're pointing to, and add an asterisk. If you want a pointer to a pointer to an integer, your "pointed to" type is int*, and your pointer type is int**. A triple pointer would be int***, and so on.
int val = 5; int* valPtr = &val; int** valPtrPtr = &valPtr; int** nope = &&val; // This does not work.
It is likely obvious why the double "address of" operator does not work—you can't take an address of something not stored in memory. &val gives you the address of val—this intermediate value does not have an address for itself. Plus, an int** points to a pointer to an integer, not the integer itself. Even furthermore, your compiler may actually interpret && as the logical AND operator.
Dereferencing layered pointers works in the same way: dereferencing a double pointer gives you the single pointer, etc. However, this time you can chain dereferences.
cout << val << endl; cout << valPtr* << endl; cout << valPtrPtr** << endl;
All of these lines will output the value 5.
Multidimensional Arrays
The astute might also consider how this relates to multidimensional arrays. Intuitively, a two-dimensional array would be represented by a two-dimensional pointer. However, this is not the case. As I alluded to in lesson 09, static multidimensional arrays are stored in one dimension. What I mean by this is that they are literally an array of arrays, not an array of pointers to other arrays. If you have a 3 by 3 array, you can visualize it as...
0 | 1 | 2 | |
0 | array[0][0] | array[1][0] | array[2][0] |
1 | array[0][1] | array[1][1] | array[2][1] |
2 | array[0][2] | array[1][2] | array[2][2] |
...but the data is actually stored in memory as...
array[0][0] | array[0][1] | array[0][2] | array[1][0] | array[1][1] | array[1][2] | array[2][0] | array[2][1] | array[2][2] |
This is the reason why you must specify dimension sizes after the first—the program must know when it has reached the end of a dimension in the linear storage. For more information on accessing static multidimensional arrays, see lesson 11.
However, layered pointers are still relevant when considering multidimensional arrays. This is because you must use them to create dynamically allocated multidimensional arrays (see lesson 11). When using a layered pointer to represent a multidimensional array, the dimension "levels" are represented by arrays of pointers. For example, a 2D array is an array of pointers to arrays. A 3D array is an array of double pointers to 2D arrays, etc. Hence, unlike with static multidimensional arrays, specific dimension sizes are not needed—to pass a 2D array of this style, simply pass a double pointer. While the usage of layered pointer vs. static multidimensional arrays is the same, they are stored in fundamentally different formats.
This shows the layout of a multidimensional array using a double pointer.
Casting
Casting is another important part of pointer usage. While c-style "unsafe" casting is discouraged by modern C++ standards, it is still a necessary skill. Casting simply tells your program to interpret a value as one of a different type. For example, you can tell your program to see an int* as a float*—and if you dereference the cast, you will get an actual float value.
Casting syntax is extremely simple: simply put the desired type in parenthesis before your variable or value to be cast.
int val = 5; int* intPtr = &val; float* floatPtr; floatPtr = (float*)intPtr; float fval = *floatPtr;
This is all valid. However, the end result is not what you'd expect. This process is not the same as simply saying "float fval = val". Instead, the program interprets the data of "val" as a float at face value, without any change. Because floating point values are stored in a different format than integers, the end result is that "fval" is around 7*10^-45. Not quite 5. This strange process may seem completely broken and useless but it has been used effectively before.
Casting does not only work with pointers—you can use it to force any data type into any other. However, this can be very "type unsafe," or in layman's terms, it can completely screw up your data. In C++, casting is generally only used with pointers, as there are better ways of transmuting types in other situations. For example, you could cast a double to an integer—truncating the decimal—or you could use an actual floor or round function.
In any case, casting is not particularly useful with your current knowledge, but it will come in handy with void pointers, as well as with inheritance and polymorphism.
Void Pointers
Finally, void pointers. A void pointer is just like any other pointer, except that it does not know what type it points to. Void pointers do one thing and one thing only: store an address. They cannot be directly dereferenced. Void pointers are useful when you don’t know what exactly your type will be (or need to hide it). You can use a void pointer just like any other pointer in terms of addressing, but to access actual data, you must first cast the pointer to the correct type. Casting simply tells the compiler to treat the void pointer as a pointer to a certain data type. Your program then knows how to interpret the data.
void* voidPtr; int value; voidPtr = &value;
cout << *voidPtr << endl;
This does not work, because the program doesn’t know what type voidPtr points to.
cout << *((int*)voidPtr) << endl;
This does work, as you tell the program to treat voidPtr as an integer pointer before dereferencing it.