You set end->next to null but p isn't null. p is whatever end->next was before you set it to null.
You'd need p to be a Node** if you want it to alias the value of end->next, Node **p = &end->next, then when you do *p != NULL you'd be following the p pointer back to the exact end->next, and if end->next had changed, *p would be able to see the updated value.
Here's a simple example:
#include <iostream>
struct Node {
struct Node *next;
};
int main() {
struct Node *end = new Node{new Node{nullptr}};
struct Node *p = end->next; // pointer to a Node
struct Node **pp = &end->next; // pointer to a pointer to a Node
std::cout << end->next << std::endl; // addr(end->next)
end->next = nullptr;
std::cout << p << std::endl; // still addr(end->next)
std::cout << *pp << std::endl; // 0
delete p;
delete end;
}
Here's a lines-and-boxes diagram:
After p = end->next:
.-------.
| end | .------.
| [next]-->| Node |
`-------` `------`
^
|
.------.
| p |
`------`
After end->next = NULL:
.-------------.
| end |
| [next=NULL] |
`-------------`
.------.
| Node |
`------`
^
|
.------.
| p |
`------`
You can see p is still not null just because end->next is.
With the double pointer **pp, after p = end->next, we have:
.-------.
| end | .------.
| [next]-->| Node |
`---^---` `------`
| ^
| |
.-------. .------.
| pp | | p |
`-------` `------`
Now we have a pointer to the pointer end->next. After end->next = NULL, we have:
.-------------.
| end | .------.
| [next=NULL] | | Node |
`---^---------` `------`
| ^
| |
.-------. .------.
| pp | | p |
`-------` `------`
and *pp gives us end->next's current value, NULL.