With the powerful tool available with pointers -- the ability to communicate with the operating system to get allocate more memory -- comes a number of significant problems. We are now communicating directly with the OS and the OS is not very forgiving. Mistakes have a number of serious consequences (at least for the running program), as will be demonstrated here. You do not have to memorize these - just read them and make yourself aware of the problems.
If you try to access a pointer which has a value of 0, the operating system will terminate the program. If you try to compile Program 1, you will note that it successfully compiles (though a good compiler may give you a warning).
Program 1. Accessing a null pointer.
#include <iostream> using namespace std; int main() { int * ptr = 0; // pointing to nothing cout << "The pointer is storing the address " << ptr << endl; cout << "The value stored there is " << *ptr << endl; return 0; } |
However, when you try to run it in Unix, you will get the output
The pointer is pointing to 0 Segmentation fault (core dumped)
and the running program terminates. In Windows, you get the dialog shown in Figure 3 and the running program terminates. Don't bother reporting the problem to Microsoft.
Figure 3. Microsoft's response to you accessing the NULL pointer.
Neither error message is exceptionally informative...
Consider Program 2. In this program, we dynamically allocate memory, but then we give it back to the OS (with delete).
Program 2. Accessing a deleted pointer.
#include <iostream> using namespace std; int main() { int * ptr = new int( 42 ); // allocate memory cout << "The pointer is storing the address " << ptr << endl; cout << "The value stored there is " << *ptr << endl; delete ptr; // deallocate memory // okay cout << "The pointer is storing the address " << ptr << endl; // NOT OKAY cout << "The value stored there is " << *ptr << endl; return 0; } |
Once we call delete, the operating system takes the memory back and at that point has the option of allocating that memory to any other running program which may request memory. There are three things which may happen:
Which of these may actually occur is entirely random, so when you test your code, it may happen that only case 1 ever occurs. Unfortunately, it is exceptionally likely that as soon as your customer tries the code, you will run into case 2 or 3 which has potentially disastrous results.
Try running Program 3 and see what happens.
Program 3. Accessing a deleted pointer.
#include <iostream> using namespace std; int main() { int * ptr = new int( 42 ); // allocate memory cout << "The pointer is storing the address " << ptr << endl; cout << "The value stored there is " << *ptr << endl; cout << "Calling delete and new in quick succession..." << endl; delete ptr; // deallocate memory double * another_ptr = new double( 3.1415 ); // allocate 8 bytes cout << "The value stored at 'ptr' is (!!!) " << *ptr << endl; cout << "The value stored at 'another_ptr' is " << *another_ptr << endl; cout << "Assigning to the integer pointer *ptr..." << endl; *ptr = 1024; cout << "The value stored at 'ptr' is (!!!) " << *ptr << endl; cout << "The value stored at 'another_ptr' is " << *another_ptr << endl; delete another_ptr; return 0; } |
If you run this, you will find that, in all likelihood, the memory allocated to another_ptr happened to be the same memory, however, when you assign to *ptr, it considers only the first four bytes as storing an integer while assigning to *another_ptr considers all eight bytes as a double. This is shown in Figure 4.
Figure 4. Two pointers pointing to the same memory location.
Consider Program 4. Here we allocate new memory but then we deallocate it twice. It is sufficient to say that this has the potential to seriously confuse the OS.
Program 4. Calling delete twice.
#include <iostream> using namespace std; int main() { int * ptr = new int( 42 ); // allocate memory delete ptr; // deallocate memory: okay delete ptr; // potentially very bad return 0; } |
Consider Program 5 which calls new twice.
Program 5. Memory leaks.
#include <iostream> using namespace std; int main() { int * ptr = new int( 42 ); // allocate memory cout << "The pointer is has the address: " << ptr << endl; ptr = new int( 1024 ); // allocate more memory cout << "The pointer is now has the address: " << ptr << endl; delete ptr; return 0; } |
What happened to the memory allocated by the first call to new? The answer: absolutely nothing. It is still allocated to the running process, however, you can no longer access the initial location. You cannot call delete on that address because you have no means of telling delete which memory is no longer needed. For all intents and purposes, the memory which is currently storing 42 is lost. It exists, however it is useless. Diagrammatically, we have a situation which looks like Figure 5.
Figure 5. A memory leak.
We can access and modify the memory storing 1024, however, we can no longer access the memory storing 42. Thus, the running program has 4 bytes allocated to it which it cannot access.
While this may seem trivial (what's 4 bytes?), consider what happens if the memory leak is in the operating system itself: such OSs must be rebooted every-so-often in order to clean up these memory leaks. Imagine if, after closing a document in Microsoft® Word, it forgets to return the memory to the OS. This could potentially waste a significant amount of memory.
On your home computer, you may consider writing a while loop which always requests more memory, for example,
int * ptr; while ( true ) { ptr = new int( 42 ); }
What do you think may happen? If you try it out, make sure that everything else is saved. :-)
If you try this on a university computer, you may quickly find your computer privileges removed.
Why does the same problem not occur when you run the following code fragment?
int * ptr; while ( true ) { ptr = new int( 42 ); delete ptr; }
1. Each time you call new, determine where you should call delete, even if you only put something in a comment.
2. If you are not initializing a pointer with a reference to actual memory (either through & or new, set it to zero.
3. Each time you call delete, set the pointer to 0. Thus, if you accidentally try to access that pointer, your program will terminate and indicate that problem exists. If you don't, your program may continue to run, however, you will run into problems later.
1. Suppose that a call to new requires 600 instructions (cycles) on your computer. How long would it take to allocate 1 GiB of memory if with each cycle of a loop you request a new double (8 Bytes).
2. Test your assumption in Question 1 by requesting running the following for-loop on your home computer.
double * ptr; // 134217728 = 1024^3/8, i.e., one GiB of memory for ( int i = 0; i < 134217728; ++i ) { ptr = new double( 3.14 ); } return 0;