Dereferencing NULL does not guarantee a crash, actually. And this isn't one of those theoretical things where people say "that's undefined behavior!" but real-world systems always do a certain thing. Non-crashing NULL dereferences are real events that cause real problems. Just toss "NULL dereference exploit" into your favorite search engine for a vast list of examples.
Here's another potential problem. Let's say you write a little bit of code to copy an array into some dynamic storage, basically strdup for non-strings:
void *memdup(const void *source, size_t length) {
void *destination = malloc(length);
memcpy(destination, source, length);
return destination;
}
But then you're a little paranoid about a NULL dereference not crashing, and you want to call some special failure handler that will log more information about what happened, so you add this bit of code in the middle:
if(destination == NULL) {
log_failure();
abort();
}
Oops, you've written a bug! It's possible for malloc to return NULL when you
haven't run out of memory, specifically when you pass 0. The FreeBSD man page calls this "a silly response to a silly question." Zero-length arrays are a perfectly valid concept, and there's no reason this memdup function should have trouble with them, but now it does. Maybe. Depending on your malloc implementation.
Using an optional type would do away with all of these problems. It would guarantee that dereferencing a null pointer would actually halt execution, rather than just "probably," and it would highly encourage distinguishing between "an error occurred" and "no error occurred, but you didn't actually ask for any memory."
Yes, the underlying NULL value will always be there. Any reasonable system will represent the "null" value of an optional pointer type using the underlying NULL value. But in terms of the type system, NULL is terrible and optionals are much better. There's no real downside to it, and you can rest easy knowing that you're still working with 0x0 in the end.