Just after calling delete, your program writes a few characters on standard output. Possibly, just possibly, in order to perform this write() operation, the C++ I/O system had to allocate a few bytes, so it asked the C++ memory management system for a few bytes. Bad luck, the memory system gave to the I/O system precisely this little area that had just become available thanks to your delete operation.
The I/O system, acting fully within its rights, stores an useful pointer to some auxiliary structure into that little place. Then, by storing 20 into the location it had just "deleted", your program wrecks that useful pointer. After that point, the I/O system data structures are corrupt.
Note that there is nothing the hardware or operating system can do to protect the memory location from misuse. This is because memory write permissions are per process not per function, and the C++ I/O system is part of the same process as your main() function.
If, at a later point in time, the I/O system starts to delete or change files without notice, you may not complain to your C++ compiler vendor, because you wrote into memory that did not belong to you any more.
If your programming staff is prone to this sort of mistake, you have to insist that they write things like: "delete p ; p = nullptr;". That way, the crash caused by a subsequent misuse of the pointer occurs immediately and is very easy to debug, unlike a (possibly much further down the road) crash caused by a latent corruption of the I/O system data structures.
But the spirit of modern C++ is to replace "raw pointers", that is the sort of pointer you're using here, by objects called "smart pointers". So you might have to get acquainted with the std::shared_ptr and std::unique_ptr classes. Here is a small sample, where you can see the numerical value of the pointer has been reset to NULL:
#include <memory>
#include <iostream>
int main(void)
{
std::unique_ptr<int> uPtr = std::make_unique<int>(0);
*uPtr = 10;
std::cout << *uPtr << std::endl;
uPtr.reset();
auto ptrValue = reinterpret_cast<unsigned long>(uPtr.get());
std::cout << "uPtr is: " << ptrValue << std::endl;
std::cout << "So far so good ... " << std::endl;
// here, the program will crash :
*uPtr = 20;
std::cout << *uPtr << std::endl;
return EXIT_SUCCESS;
}
If you allow me a lame attempt at programmer humour: after the main function has written 20, the status of your program can be described as "so far so good". I don't know whether you are familiar with the financial services industry.
There is a classic joke about a legendary Wall Street trader who did a number of very bad deals with subprime financial instruments. So the trader decides to jump dive into the street below from the 94th floor of the building. Reaching the level of the 5th floor, he sees a secretary, who asks him: "How is it going ?". And the trader replies: "So far so good.".